Merge branch 'develop' for release v3.43.0

This commit is contained in:
emeric
2023-11-04 22:09:46 +01:00
137 changed files with 9477 additions and 9165 deletions
+4 -2
View File
@@ -4,7 +4,7 @@
_LMS_ is a self-hosted music streaming software: access your music collection from anywhere using a web interface! _LMS_ is a self-hosted music streaming software: access your music collection from anywhere using a web interface!
A [demo instance](http://lms-demo.poupon.dev) is available. Note the administration panel is not available. A [demo instance](http://lms-demo.poupon.dev) is available. Note the administration settings are not available.
## Main features ## Main features
* Recommendation engine * Recommendation engine
@@ -34,12 +34,14 @@ _LMS_ provides several ways to help you find the music you like:
* Starred _Jazz_ albums * Starred _Jazz_ albums
* ... * ...
__Note__: depending on your database size and/or your hardware, the tag-based recommendation engine may significantly slow down the user interface. You can disable it in the administration settings.
## About tags ## About tags
_LMS_ relies exclusively on tags to organize your music collection. _LMS_ relies exclusively on tags to organize your music collection.
### Filtering ### Filtering
You can specify the tags you want to use to filter your collection. By default, `genre`, `albumgrouping`, `mood` and `albummood` tags are used. You can specify the tags you want to use to filter your collection. By default, `genre`, `albumgrouping`, `mood` and `albummood` tags are used.
In the administration panel, you can set whatever tags you want, even custom tags. In the administration settings, you can set whatever tags you want, even custom tags.
__Note__: you can use the `lms-metadata` tool to have an idea of the tags parsed by _LMS_ using [TagLib](https://github.com/taglib/taglib). __Note__: you can use the `lms-metadata` tool to have an idea of the tags parsed by _LMS_ using [TagLib](https://github.com/taglib/taglib).
+5 -7
View File
@@ -34,17 +34,15 @@
</div> </div>
</div> </div>
<legend>${tr:Lms.Admin.Database.scan-options}</legend> <legend>${tr:Lms.Admin.Database.scan-options}</legend>
${<if-has-recommendation-engine>} <div class="col-12">
<div class="col-lg-6"> <label class="form-label" for="${id:similarity-engine-type}">
<label class="form-label" for="${id:recommendation-engine-type}"> ${tr:Lms.Admin.Database.similarity-engine-type}
${tr:Lms.Admin.Database.recommendation-engine-type}
</label> </label>
${recommendation-engine-type class="form-control"} ${similarity-engine-type class="form-control"}
<div class="invalid-feedback"> <div class="invalid-feedback">
${recommendation-engine-type-info} ${similarity-engine-type-info}
</div> </div>
</div> </div>
${</if-has-recommendation-engine>}
<div class="col-12"> <div class="col-12">
<label class="form-label" for="${id:clusters}"> <label class="form-label" for="${id:clusters}">
${tr:Lms.clusters} ${tr:Lms.clusters}
+8 -7
View File
@@ -53,13 +53,13 @@
<message id="Lms.Admin.Database.never">Never</message> <message id="Lms.Admin.Database.never">Never</message>
<message id="Lms.Admin.Database.path">Media root directory</message> <message id="Lms.Admin.Database.path">Media root directory</message>
<message id="Lms.Admin.Database.path-help">Directories containing a <code>.lmsignore</code> file are skipped</message> <message id="Lms.Admin.Database.path-help">Directories containing a <code>.lmsignore</code> file are skipped</message>
<message id="Lms.Admin.Database.recommendation-engine-type">Recommendation engine</message>
<message id="Lms.Admin.Database.recommendation-engine-type.clusters">Tags based</message>
<message id="Lms.Admin.Database.recommendation-engine-type.features">Audio analysis based</message>
<message id="Lms.Admin.Database.scan-complete">Scan complete: {1} total files, {2} additions, {3} updates, {4} deletions, {5} duplicates, {6} errors</message> <message id="Lms.Admin.Database.scan-complete">Scan complete: {1} total files, {2} additions, {3} updates, {4} deletions, {5} duplicates, {6} errors</message>
<message id="Lms.Admin.Database.scan-launched">Scan launched!</message> <message id="Lms.Admin.Database.scan-launched">Scan launched!</message>
<message id="Lms.Admin.Database.scan-options">Scan options</message> <message id="Lms.Admin.Database.scan-options">Scan options</message>
<message id="Lms.Admin.Database.settings-saved">New settings saved!</message> <message id="Lms.Admin.Database.settings-saved">New settings saved!</message>
<message id="Lms.Admin.Database.similarity-engine-type">Similarity engine</message>
<message id="Lms.Admin.Database.similarity-engine-type.clusters">Tag-based</message>
<message id="Lms.Admin.Database.similarity-engine-type.none">None</message>
<message id="Lms.Admin.Database.update-period">Update period</message> <message id="Lms.Admin.Database.update-period">Update period</message>
<message id="Lms.Admin.Database.update-start-time">Update start time</message> <message id="Lms.Admin.Database.update-start-time">Update start time</message>
<message id="Lms.Admin.Database.weekly">Weekly</message> <message id="Lms.Admin.Database.weekly">Weekly</message>
@@ -240,10 +240,14 @@
<!--Settings--> <!--Settings-->
<message id="Lms.Settings.audio">Audio</message> <message id="Lms.Settings.audio">Audio</message>
<message id="Lms.Settings.audio-settings-are-local">These audio settings are local to your browser!</message> <message id="Lms.Settings.audio-settings-are-local">These audio settings are local to your browser!</message>
<message id="Lms.Settings.backend.internal">Internal</message>
<message id="Lms.Settings.backend.listenbrainz">ListenBrainz</message>
<message id="Lms.Settings.backend.listenbrainz-token">ListenBrainz API token</message>
<message id="Lms.Settings.change-password">Change password</message> <message id="Lms.Settings.change-password">Change password</message>
<message id="Lms.Settings.default-transcode-bitrate">Default transcode bitrate</message> <message id="Lms.Settings.default-transcode-bitrate">Default transcode bitrate</message>
<message id="Lms.Settings.default-transcode-format">Default transcode format</message> <message id="Lms.Settings.default-transcode-format">Default transcode format</message>
<message id="Lms.Settings.demo-cannot-save">Cannot save using a demo account!</message> <message id="Lms.Settings.demo-cannot-save">Cannot save using a demo account!</message>
<message id="Lms.Settings.feedback">Feedback</message>
<message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> Settings</message> <message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> Settings</message>
<message id="Lms.Settings.password-bad">Bad password</message> <message id="Lms.Settings.password-bad">Bad password</message>
<message id="Lms.Settings.password-must-fill-old-password">Old password must be filled in</message> <message id="Lms.Settings.password-must-fill-old-password">Old password must be filled in</message>
@@ -255,10 +259,7 @@
<message id="Lms.Settings.replaygain-preamp">ReplayGain preamp</message> <message id="Lms.Settings.replaygain-preamp">ReplayGain preamp</message>
<message id="Lms.Settings.replaygain-preamp-no-rg-info">ReplayGain preamp (if no info)</message> <message id="Lms.Settings.replaygain-preamp-no-rg-info">ReplayGain preamp (if no info)</message>
<message id="Lms.Settings.scrobbling">Scrobbling</message> <message id="Lms.Settings.scrobbling">Scrobbling</message>
<message id="Lms.Settings.scrobbling.scrobbler">Scrobbler</message> <message id="Lms.Settings.services">Services</message>
<message id="Lms.Settings.scrobbling.scrobbler.internal">Internal</message>
<message id="Lms.Settings.scrobbling.scrobbler.listenbrainz">ListenBrainz</message>
<message id="Lms.Settings.scrobbling.listenbrainz-token">ListenBrainz token</message>
<message id="Lms.Settings.settings">Settings</message> <message id="Lms.Settings.settings">Settings</message>
<message id="Lms.Settings.subsonic-artist-list-mode">Artist list mode</message> <message id="Lms.Settings.subsonic-artist-list-mode">Artist list mode</message>
<message id="Lms.Settings.subsonic-artist-list-mode.all-artists">All artists</message> <message id="Lms.Settings.subsonic-artist-list-mode.all-artists">All artists</message>
+8 -7
View File
@@ -53,13 +53,13 @@
<message id="Lms.Admin.Database.never">Jamais</message> <message id="Lms.Admin.Database.never">Jamais</message>
<message id="Lms.Admin.Database.path">Dossier racine des fichiers de musique</message> <message id="Lms.Admin.Database.path">Dossier racine des fichiers de musique</message>
<message id="Lms.Admin.Database.path-help">Les dossiers contenant un fichier <code>.lmsignore</code> sont ignorés</message> <message id="Lms.Admin.Database.path-help">Les dossiers contenant un fichier <code>.lmsignore</code> sont ignorés</message>
<message id="Lms.Admin.Database.recommendation-engine-type">Moteur de recommandation</message>
<message id="Lms.Admin.Database.recommendation-engine-type.clusters">Basé sur les tags</message>
<message id="Lms.Admin.Database.recommendation-engine-type.features">Basé sur l'analyse audio</message>
<message id="Lms.Admin.Database.scan-complete">Scan terminé : {1} fichiers, {2} ajouts, {3} mises à jour, {4} suppressions, {5} duplicatas, {6} erreurs</message> <message id="Lms.Admin.Database.scan-complete">Scan terminé : {1} fichiers, {2} ajouts, {3} mises à jour, {4} suppressions, {5} duplicatas, {6} erreurs</message>
<message id="Lms.Admin.Database.scan-launched">Scan lancé !</message> <message id="Lms.Admin.Database.scan-launched">Scan lancé !</message>
<message id="Lms.Admin.Database.scan-options">Options </message> <message id="Lms.Admin.Database.scan-options">Options </message>
<message id="Lms.Admin.Database.settings-saved">Nouveaux paramètres sauvegardés !</message> <message id="Lms.Admin.Database.settings-saved">Nouveaux paramètres sauvegardés !</message>
<message id="Lms.Admin.Database.similarity-engine-type">Moteur de similarité</message>
<message id="Lms.Admin.Database.similarity-engine-type.clusters">Basé sur les tags</message>
<message id="Lms.Admin.Database.similarity-engine-type.none">Aucun</message>
<message id="Lms.Admin.Database.update-period">Périodicité des mises à jour</message> <message id="Lms.Admin.Database.update-period">Périodicité des mises à jour</message>
<message id="Lms.Admin.Database.update-start-time">Heure de départ de la mise à jour</message> <message id="Lms.Admin.Database.update-start-time">Heure de départ de la mise à jour</message>
<message id="Lms.Admin.Database.weekly">Toutes les semaines</message> <message id="Lms.Admin.Database.weekly">Toutes les semaines</message>
@@ -240,10 +240,14 @@
<!--Settings--> <!--Settings-->
<message id="Lms.Settings.audio">Audio</message> <message id="Lms.Settings.audio">Audio</message>
<message id="Lms.Settings.audio-settings-are-local">Ces paramètres audio sont locaux à votre navigateur !</message> <message id="Lms.Settings.audio-settings-are-local">Ces paramètres audio sont locaux à votre navigateur !</message>
<message id="Lms.Settings.backend.internal">Interne</message>
<message id="Lms.Settings.backend.listenbrainz">ListenBrainz</message>
<message id="Lms.Settings.backend.listenbrainz-token">Jeton d'API ListenBrainz</message>
<message id="Lms.Settings.change-password">Changement de mot de passe</message> <message id="Lms.Settings.change-password">Changement de mot de passe</message>
<message id="Lms.Settings.default-transcode-bitrate">Bitrate par defaut du transcodage</message> <message id="Lms.Settings.default-transcode-bitrate">Bitrate par defaut du transcodage</message>
<message id="Lms.Settings.default-transcode-format">Format par defaut du transcodage</message> <message id="Lms.Settings.default-transcode-format">Format par defaut du transcodage</message>
<message id="Lms.Settings.demo-cannot-save">Impossible de sauvegarder en utilisant un compte de démo !</message> <message id="Lms.Settings.demo-cannot-save">Impossible de sauvegarder en utilisant un compte de démo !</message>
<message id="Lms.Settings.feedback">Feedback</message>
<message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> Paramètres</message> <message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> Paramètres</message>
<message id="Lms.Settings.password-bad">Mauvais mot de passe</message> <message id="Lms.Settings.password-bad">Mauvais mot de passe</message>
<message id="Lms.Settings.password-must-fill-old-password">L'ancien mot de passe doit être renseigné</message> <message id="Lms.Settings.password-must-fill-old-password">L'ancien mot de passe doit être renseigné</message>
@@ -255,10 +259,7 @@
<message id="Lms.Settings.replaygain-preamp">Pre-amplification ReplayGain</message> <message id="Lms.Settings.replaygain-preamp">Pre-amplification ReplayGain</message>
<message id="Lms.Settings.replaygain-preamp-no-rg-info">Pre-amplification ReplayGain (si pas d'info)</message> <message id="Lms.Settings.replaygain-preamp-no-rg-info">Pre-amplification ReplayGain (si pas d'info)</message>
<message id="Lms.Settings.scrobbling">Scrobbling</message> <message id="Lms.Settings.scrobbling">Scrobbling</message>
<message id="Lms.Settings.scrobbling.scrobbler">Scrobbler</message> <message id="Lms.Settings.services">Services</message>
<message id="Lms.Settings.scrobbling.scrobbler.internal">Interne</message>
<message id="Lms.Settings.scrobbling.scrobbler.listenbrainz">ListenBrainz</message>
<message id="Lms.Settings.scrobbling.listenbrainz-token">Jeton ListenBrainz</message>
<message id="Lms.Settings.settings">Paramètres</message> <message id="Lms.Settings.settings">Paramètres</message>
<message id="Lms.Settings.subsonic-artist-list-mode">Mode de listage des artistes</message> <message id="Lms.Settings.subsonic-artist-list-mode">Mode de listage des artistes</message>
<message id="Lms.Settings.subsonic-artist-list-mode.all-artists">Tous les artistes</message> <message id="Lms.Settings.subsonic-artist-list-mode.all-artists">Tous les artistes</message>
+8 -7
View File
@@ -9,7 +9,7 @@
<message id="Lms.cancel">Annulla</message> <message id="Lms.cancel">Annulla</message>
<message id="Lms.clusters">Tags</message> <message id="Lms.clusters">Tags</message>
<message id="Lms.create">Crea</message> <message id="Lms.create">Crea</message>
<message id="Lms.del">Elimina</message> <message id="Lms.delete">Elimina</message>
<message id="Lms.discard">Annulla</message> <message id="Lms.discard">Annulla</message>
<message id="Lms.edit">Modifica</message> <message id="Lms.edit">Modifica</message>
<message id="Lms.loading">Caricamento...</message> <message id="Lms.loading">Caricamento...</message>
@@ -53,13 +53,13 @@
<message id="Lms.Admin.Database.never">Mai</message> <message id="Lms.Admin.Database.never">Mai</message>
<message id="Lms.Admin.Database.path">Cartella dei file multimediali</message> <message id="Lms.Admin.Database.path">Cartella dei file multimediali</message>
<message id="Lms.Admin.Database.recommendation-engine-type">Modalità di raccomandazione brani consigliati</message>
<message id="Lms.Admin.Database.recommendation-engine-type.clusters">Basata sui tag</message>
<message id="Lms.Admin.Database.recommendation-engine-type.features">Basata sull'analisi acustica</message>
<message id="Lms.Admin.Database.scan-complete">Scansione completata: {1} file totali, {2} aggiunti, {3} aggiornati, {4} eliminati, {5} duplicati, {6} errati</message> <message id="Lms.Admin.Database.scan-complete">Scansione completata: {1} file totali, {2} aggiunti, {3} aggiornati, {4} eliminati, {5} duplicati, {6} errati</message>
<message id="Lms.Admin.Database.scan-launched">Scansione avviata!</message> <message id="Lms.Admin.Database.scan-launched">Scansione avviata!</message>
<message id="Lms.Admin.Database.scan-options">Impostazioni scansione</message> <message id="Lms.Admin.Database.scan-options">Impostazioni scansione</message>
<message id="Lms.Admin.Database.settings-saved">Impostazioni salvate!</message> <message id="Lms.Admin.Database.settings-saved">Impostazioni salvate!</message>
<message id="Lms.Admin.Database.update-period">Frequenza di aggiornamento</message> <message id="Lms.Admin.Database.update-period">Frequenza di aggiornamento</message>
<message id="Lms.Admin.Database.update-start-time">Orario di aggiornamento</message> <message id="Lms.Admin.Database.update-start-time">Orario di aggiornamento</message>
<message id="Lms.Admin.Database.weekly">Settimanale</message> <message id="Lms.Admin.Database.weekly">Settimanale</message>
@@ -240,10 +240,14 @@
<!--Settings--> <!--Settings-->
<message id="Lms.Settings.audio">Audio</message> <message id="Lms.Settings.audio">Audio</message>
<message id="Lms.Settings.audio-settings-are-local">Queste impostazioni sono salvate localmente su questo dispositivo!</message> <message id="Lms.Settings.audio-settings-are-local">Queste impostazioni sono salvate localmente su questo dispositivo!</message>
<message id="Lms.Settings.change-password">Cambia password</message> <message id="Lms.Settings.change-password">Cambia password</message>
<message id="Lms.Settings.demo-cannot-save">Non posso salvare le impostazioni utilizzando l'account demo!</message> <message id="Lms.Settings.demo-cannot-save">Non posso salvare le impostazioni utilizzando l'account demo!</message>
<message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> Impostazioni</message> <message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> Impostazioni</message>
<message id="Lms.Settings.password-bad">Password errata</message> <message id="Lms.Settings.password-bad">Password errata</message>
<message id="Lms.Settings.password-must-fill-old-password">Devi inserire la password attuale</message> <message id="Lms.Settings.password-must-fill-old-password">Devi inserire la password attuale</message>
@@ -257,9 +261,6 @@
<message id="Lms.Settings.subsonic-artist-list-mode">Modalità di elencazione artisti</message> <message id="Lms.Settings.subsonic-artist-list-mode">Modalità di elencazione artisti</message>
<message id="Lms.Settings.subsonic-artist-list-mode.all-artists">Tutti gli artisti</message> <message id="Lms.Settings.subsonic-artist-list-mode.all-artists">Tutti gli artisti</message>
<message id="Lms.Settings.subsonic-artist-list-mode.release-artists">Artisti album</message> <message id="Lms.Settings.subsonic-artist-list-mode.release-artists">Artisti album</message>
+7 -6
View File
@@ -53,13 +53,13 @@
<message id="Lms.Admin.Database.never">从不</message> <message id="Lms.Admin.Database.never">从不</message>
<message id="Lms.Admin.Database.path">媒体根目录</message> <message id="Lms.Admin.Database.path">媒体根目录</message>
<message id="Lms.Admin.Database.path-help">包含 <code>.lmsignore</code> 文件的目录将会被跳过</message> <message id="Lms.Admin.Database.path-help">包含 <code>.lmsignore</code> 文件的目录将会被跳过</message>
<message id="Lms.Admin.Database.recommendation-engine-type">推荐引擎</message>
<message id="Lms.Admin.Database.recommendation-engine-type.clusters">基于标签</message>
<message id="Lms.Admin.Database.recommendation-engine-type.features">基于音频分析</message>
<message id="Lms.Admin.Database.scan-complete">扫描完成:总文件 {1},添加文件 {2},更新文件 {3},删除文件 {4},重复文件 {5},错误文件 {6}</message> <message id="Lms.Admin.Database.scan-complete">扫描完成:总文件 {1},添加文件 {2},更新文件 {3},删除文件 {4},重复文件 {5},错误文件 {6}</message>
<message id="Lms.Admin.Database.scan-launched">扫描已完成!</message> <message id="Lms.Admin.Database.scan-launched">扫描已完成!</message>
<message id="Lms.Admin.Database.scan-options">扫描选项</message> <message id="Lms.Admin.Database.scan-options">扫描选项</message>
<message id="Lms.Admin.Database.settings-saved">新设置已保存!</message> <message id="Lms.Admin.Database.settings-saved">新设置已保存!</message>
<message id="Lms.Admin.Database.update-period">更新周期</message> <message id="Lms.Admin.Database.update-period">更新周期</message>
<message id="Lms.Admin.Database.update-start-time">更新开始时间</message> <message id="Lms.Admin.Database.update-start-time">更新开始时间</message>
<message id="Lms.Admin.Database.weekly">每周</message> <message id="Lms.Admin.Database.weekly">每周</message>
@@ -240,10 +240,14 @@
<!--Settings--> <!--Settings-->
<message id="Lms.Settings.audio">音频</message> <message id="Lms.Settings.audio">音频</message>
<message id="Lms.Settings.audio-settings-are-local">这些音频设置仅保存在您的本地浏览器!</message> <message id="Lms.Settings.audio-settings-are-local">这些音频设置仅保存在您的本地浏览器!</message>
<message id="Lms.Settings.change-password">更改密码</message> <message id="Lms.Settings.change-password">更改密码</message>
<message id="Lms.Settings.demo-cannot-save">使用演示账号时不能保存!</message> <message id="Lms.Settings.demo-cannot-save">使用演示账号时不能保存!</message>
<message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> 设置</message> <message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> 设置</message>
<message id="Lms.Settings.password-bad">无效密码</message> <message id="Lms.Settings.password-bad">无效密码</message>
<message id="Lms.Settings.password-must-fill-old-password">必须填写旧密码</message> <message id="Lms.Settings.password-must-fill-old-password">必须填写旧密码</message>
@@ -257,9 +261,6 @@
<message id="Lms.Settings.subsonic-artist-list-mode">歌手列表模式</message> <message id="Lms.Settings.subsonic-artist-list-mode">歌手列表模式</message>
<message id="Lms.Settings.subsonic-artist-list-mode.all-artists">所有歌手</message> <message id="Lms.Settings.subsonic-artist-list-mode.all-artists">所有歌手</message>
<message id="Lms.Settings.subsonic-artist-list-mode.release-artists">专辑歌手</message> <message id="Lms.Settings.subsonic-artist-list-mode.release-artists">专辑歌手</message>
+22 -22
View File
@@ -3,29 +3,29 @@
<!--FORMS message blocks--> <!--FORMS message blocks-->
<message id="Lms.Explore.Search.template"> <message id="Lms.Explore.Search.template">
<ul class="nav nav-pills mb-3" id="myTab" role="tablist"> <div class="mb-3">
<li class="nav-item" role="presentation"> <ul class="nav nav-pills mb-3">
<button class="nav-link active" id="releases-tab" data-bs-toggle="tab" data-bs-target="#releases" type="button" role="tab" aria-controls="releases" aria-selected="true">${tr:Lms.Explore.releases}</button> <li class="nav-item">
</li> ${releases class="nav-link"}
<li class="nav-item" role="presentation"> </li>
<button class="nav-link" id="artists-tab" data-bs-toggle="tab" data-bs-target="#artists" type="button" role="tab" aria-controls="artists" aria-selected="false">${tr:Lms.Explore.artists}</button> <li class="nav-item">
</li> ${artists class="nav-link"}
<li class="nav-item" role="presentation"> </li>
<button class="nav-link" id="tracks-tab" data-bs-toggle="tab" data-bs-target="#tracks" type="button" role="tab" aria-controls="tracks" aria-selected="false">${tr:Lms.Explore.tracks}</button> <li class="nav-item">
</li> ${tracks class="nav-link"}
</ul> </li>
<div class="tab-content" id="myTabContent"> </ul>
<div class="tab-pane show active" id="releases" role="tabpanel" aria-labelledby="releases-tab">${releases}</div>
<div class="tab-pane" id="artists" role="tabpanel" aria-labelledby="artists-tab">
<div class="row mb-3">
<div class="col-lg-3">
${link-type class="form-select"}
</div>
</div>
${artists}
</div>
<div class="tab-pane" id="tracks" role="tabpanel" aria-labelledby="tracks-tab">${tracks}</div>
</div> </div>
${results}
</message>
<message id="Lms.Explore.Search.template.artists">
<div class="row mb-3">
<div class="col-lg-3">
${link-type class="form-select"}
</div>
</div>
${artists}
</message> </message>
</messages> </messages>
+17 -8
View File
@@ -28,9 +28,9 @@
</div> </div>
</div> </div>
<div class="col-lg-4"> <div class="col-lg-4">
<label class="form-label" for="${id:transcode-bitrate}"> <label class="form-label" for="${id:transcode-bitrate}">
${tr:Lms.Settings.transcode-bitrate} ${tr:Lms.Settings.transcode-bitrate}
; </label> </label>
<div class="input-group mb-3"> <div class="input-group mb-3">
${transcode-bitrate class="form-control"} ${transcode-bitrate class="form-control"}
<span class="input-group-text">kbps</span> <span class="input-group-text">kbps</span>
@@ -105,19 +105,28 @@
</div> </div>
</div> </div>
${</if-has-subsonic-api>} ${</if-has-subsonic-api>}
<legend>${tr:Lms.Settings.scrobbling}</legend> <legend>${tr:Lms.Settings.services}</legend>
<div class="col-lg-6"> <div class="col-lg-6">
<label class="form-label" for="${id:scrobbler}"> <label class="form-label" for="${id:scrobbling-backend}">
${tr:Lms.Settings.scrobbling.scrobbler} ${tr:Lms.Settings.scrobbling}
</label> </label>
${scrobbler class="form-control"} ${scrobbling-backend class="form-control"}
<div class="invalid-feedback"> <div class="invalid-feedback">
${scrobbler-info class="help-block"} ${scrobbling-backend-info class="help-block"}
</div> </div>
</div> </div>
<div class="col-lg-6"> <div class="col-lg-6">
<label class="form-label" for="${id:feedback-backend}">
${tr:Lms.Settings.feedback}
</label>
${feedback-backend class="form-control"}
<div class="invalid-feedback">
${feedback-backend-info class="help-block"}
</div>
</div>
<div class="col-lg-12">
<label class="form-label" for="${id:listenbrainz-token}"> <label class="form-label" for="${id:listenbrainz-token}">
${tr:Lms.Settings.scrobbling.listenbrainz-token} ${tr:Lms.Settings.backend.listenbrainz-token}
</label> </label>
${listenbrainz-token class="form-control"} ${listenbrainz-token class="form-control"}
<div class="invalid-feedback"> <div class="invalid-feedback">
+4 -1
View File
@@ -63,6 +63,9 @@ api-subsonic = true;
# Main usage is to make auto detections for the 'p' (password) parameter work # Main usage is to make auto detections for the 'p' (password) parameter work
api-subsonic-report-old-server-protocol = ("DSub"); api-subsonic-report-old-server-protocol = ("DSub");
# List of clients for whom open subsonic extensions and extra fields are disabled
api-open-subsonic-disabled-clients = ("DSub");
# Turn on this option to allow the demo account creation/use # Turn on this option to allow the demo account creation/use
demo = false; demo = false;
@@ -85,4 +88,4 @@ playqueue-max-entry-count = 1000;
scanner-skip-duplicate-mbid = false; scanner-skip-duplicate-mbid = false;
# Scanner read style for metadata, maybe be 'fast', 'average' or 'accurate' # Scanner read style for metadata, maybe be 'fast', 'average' or 'accurate'
scanner-parser-read-style = "accurate"; scanner-parser-read-style = "average";
@@ -56,10 +56,12 @@ namespace Av
if (_estimatedContentLength) if (_estimatedContentLength)
response.setContentLength(*_estimatedContentLength); response.setContentLength(*_estimatedContentLength);
response.setMimeType(_transcoder.getOutputMimeType()); response.setMimeType(_transcoder.getOutputMimeType());
LMS_LOG(TRANSCODE, DEBUG) << "Set mime type to " << _transcoder.getOutputMimeType(); LMS_LOG(TRANSCODE, DEBUG) << "Transcoder finished = " << _transcoder.finished() << ", total served bytes = " << _totalServedByteCount << ", mime type = " << _transcoder.getOutputMimeType();
if (_bytesReadyCount > 0) if (_bytesReadyCount > 0)
{ {
LMS_LOG(TRANSCODE, DEBUG) << "Writing " << _bytesReadyCount << " bytes back to client";
response.out().write(reinterpret_cast<const char *>(&_buffer[0]), _bytesReadyCount); response.out().write(reinterpret_cast<const char *>(&_buffer[0]), _bytesReadyCount);
_totalServedByteCount += _bytesReadyCount; _totalServedByteCount += _bytesReadyCount;
_bytesReadyCount = 0; _bytesReadyCount = 0;
@@ -71,6 +73,8 @@ namespace Av
continuation->waitForMoreData(); continuation->waitForMoreData();
_transcoder.asyncRead(_buffer.data(), _buffer.size(), [=](std::size_t nbBytesRead) _transcoder.asyncRead(_buffer.data(), _buffer.size(), [=](std::size_t nbBytesRead)
{ {
LMS_LOG(TRANSCODE, DEBUG) << "Have " << nbBytesRead << " more bytes to send back";
assert(_bytesReadyCount == 0); assert(_bytesReadyCount == 0);
_bytesReadyCount = nbBytesRead; _bytesReadyCount = nbBytesRead;
continuation->haveMoreData(); continuation->haveMoreData();
+1
View File
@@ -1,6 +1,7 @@
add_subdirectory(auth) add_subdirectory(auth)
add_subdirectory(cover) add_subdirectory(cover)
add_subdirectory(database) add_subdirectory(database)
add_subdirectory(feedback)
add_subdirectory(recommendation) add_subdirectory(recommendation)
add_subdirectory(scanner) add_subdirectory(scanner)
add_subdirectory(scrobbling) add_subdirectory(scrobbling)
+396 -416
View File
@@ -34,445 +34,425 @@
#include "utils/String.hpp" #include "utils/String.hpp"
#include "utils/Utils.hpp" #include "utils/Utils.hpp"
namespace namespace Cover
{ {
struct TrackInfo
{
bool hasCover {};
bool isMultiDisc {};
std::filesystem::path trackPath;
std::optional<Database::ReleaseId> releaseId;
};
std::optional<TrackInfo> namespace
getTrackInfo(Database::Session& dbSession, Database::TrackId trackId) {
{ struct TrackInfo
std::optional<TrackInfo> res; {
bool hasCover{};
bool isMultiDisc{};
std::filesystem::path trackPath;
std::optional<Database::ReleaseId> releaseId;
};
auto transaction {dbSession.createSharedTransaction()}; std::optional<TrackInfo> getTrackInfo(Database::Session& dbSession, Database::TrackId trackId)
{
std::optional<TrackInfo> res;
const Database::Track::pointer track {Database::Track::find(dbSession, trackId)}; auto transaction{ dbSession.createSharedTransaction() };
if (!track)
return res;
res = TrackInfo {}; const Database::Track::pointer track{ Database::Track::find(dbSession, trackId) };
if (!track)
return res;
res->hasCover = track->hasCover(); res = TrackInfo{};
res->trackPath = track->getPath();
if (const Database::Release::pointer& release {track->getRelease()}) res->hasCover = track->hasCover();
{ res->trackPath = track->getPath();
res->releaseId = release->getId();
if (release->getTotalDisc() > 1)
res->isMultiDisc = true;
}
return res; if (const Database::Release::pointer & release{ track->getRelease() })
} {
res->releaseId = release->getId();
if (release->getTotalDisc() > 1)
res->isMultiDisc = true;
}
std::vector<std::string> constructPreferredFileNames() return res;
{ }
std::vector<std::string> res;
Service<IConfig>::get()->visitStrings("cover-preferred-file-names", std::vector<std::string> constructPreferredFileNames()
[&res](std::string_view fileName) {
{ std::vector<std::string> res;
res.emplace_back(fileName);
}, {"cover", "front"});
return res; Service<IConfig>::get()->visitStrings("cover-preferred-file-names",
} [&res](std::string_view fileName)
} {
res.emplace_back(fileName);
}, { "cover", "front" });
namespace Cover { return res;
}
using namespace Image; bool isFileSupported(const std::filesystem::path& file, const std::vector<std::filesystem::path>& extensions)
{
return (std::find(std::cbegin(extensions), std::cend(extensions), file.extension()) != std::cend(extensions));
}
}
static std::unique_ptr<ICoverService> createCoverService(Database::Db& db, const std::filesystem::path& execPath, const std::filesystem::path& defaultCoverPath)
bool {
isFileSupported(const std::filesystem::path& file, const std::vector<std::filesystem::path>& extensions) return std::make_unique<CoverService>(db, execPath, defaultCoverPath);
{ }
return (std::find(std::cbegin(extensions), std::cend(extensions), file.extension()) != std::cend(extensions));
}
std::unique_ptr<ICoverService> using namespace Image;
createCoverService(Database::Db& db, const std::filesystem::path& execPath, const std::filesystem::path& defaultCoverPath)
{
return std::make_unique<CoverService>(db, execPath, defaultCoverPath);
}
CoverService::CoverService(Database::Db& db, CoverService::CoverService(Database::Db& db,
const std::filesystem::path& execPath, const std::filesystem::path& execPath,
const std::filesystem::path& defaultCoverPath) const std::filesystem::path& defaultCoverPath)
: _db {db} : _db{ db }
, _defaultCoverPath {defaultCoverPath} , _defaultCoverPath{ defaultCoverPath }
, _maxCacheSize {Service<IConfig>::get()->getULong("cover-max-cache-size", 30) * 1000 * 1000} , _maxCacheSize{ Service<IConfig>::get()->getULong("cover-max-cache-size", 30) * 1000 * 1000 }
, _maxFileSize {Service<IConfig>::get()->getULong("cover-max-file-size", 10) * 1000 * 1000} , _maxFileSize{ Service<IConfig>::get()->getULong("cover-max-file-size", 10) * 1000 * 1000 }
, _preferredFileNames {constructPreferredFileNames()} , _preferredFileNames{ constructPreferredFileNames() }
{ {
setJpegQuality(Service<IConfig>::get()->getULong("cover-jpeg-quality", 75)); setJpegQuality(Service<IConfig>::get()->getULong("cover-jpeg-quality", 75));
LMS_LOG(COVER, INFO) << "Default cover path = '" << _defaultCoverPath.string() << "'"; LMS_LOG(COVER, INFO) << "Default cover path = '" << _defaultCoverPath.string() << "'";
LMS_LOG(COVER, INFO) << "Max cache size = " << _maxCacheSize; LMS_LOG(COVER, INFO) << "Max cache size = " << _maxCacheSize;
LMS_LOG(COVER, INFO) << "Max file size = " << _maxFileSize; LMS_LOG(COVER, INFO) << "Max file size = " << _maxFileSize;
LMS_LOG(COVER, INFO) << "Preferred file names: " << StringUtils::joinStrings(_preferredFileNames, ","); LMS_LOG(COVER, INFO) << "Preferred file names: " << StringUtils::joinStrings(_preferredFileNames, ",");
#if LMS_SUPPORT_IMAGE_GM #if LMS_SUPPORT_IMAGE_GM
GraphicsMagick::init(execPath); GraphicsMagick::init(execPath);
#else #else
(void)execPath; (void)execPath;
#endif #endif
try try
{ {
getDefault(512); getDefault(512);
} }
catch (const Image::ImageException& e) catch (const Image::ImageException& e)
{ {
throw LmsException("Cannot read default cover file '" + _defaultCoverPath.string() + "': " + e.what()); throw LmsException("Cannot read default cover file '" + _defaultCoverPath.string() + "': " + e.what());
} }
} }
std::unique_ptr<IEncodedImage> std::unique_ptr<IEncodedImage> CoverService::getFromAvMediaFile(const Av::IAudioFile& input, ImageSize width) const
CoverService::getFromAvMediaFile(const Av::IAudioFile& input, ImageSize width) const {
{ std::unique_ptr<IEncodedImage> image;
std::unique_ptr<IEncodedImage> image;
input.visitAttachedPictures([&](const Av::Picture& picture)
input.visitAttachedPictures([&](const Av::Picture& picture) {
{ if (image)
if (image) return;
return;
try
try {
{ std::unique_ptr<IRawImage> rawImage{ decodeImage(picture.data, picture.dataSize) };
std::unique_ptr<IRawImage> rawImage {decodeImage(picture.data, picture.dataSize)}; rawImage->resize(width);
rawImage->resize(width); image = rawImage->encodeToJPEG(_jpegQuality);
image = rawImage->encodeToJPEG(_jpegQuality); }
} catch (const Image::ImageException& e)
catch (const Image::ImageException& e) {
{ LMS_LOG(COVER, ERROR) << "Cannot read embedded cover: " << e.what();
LMS_LOG(COVER, ERROR) << "Cannot read embedded cover: " << e.what(); }
} });
});
return image;
return image; }
}
std::unique_ptr<IEncodedImage> CoverService::getFromCoverFile(const std::filesystem::path& p, ImageSize width) const
std::unique_ptr<IEncodedImage> {
CoverService::getFromCoverFile(const std::filesystem::path& p, ImageSize width) const std::unique_ptr<IEncodedImage> image;
{
std::unique_ptr<IEncodedImage> image; try
{
try std::unique_ptr<IRawImage> rawImage{ decodeImage(p) };
{ rawImage->resize(width);
std::unique_ptr<IRawImage> rawImage {decodeImage(p)}; image = rawImage->encodeToJPEG(_jpegQuality);
rawImage->resize(width); }
image = rawImage->encodeToJPEG(_jpegQuality); catch (const ImageException& e)
} {
catch (const ImageException& e) LMS_LOG(COVER, ERROR) << "Cannot read cover in file '" << p.string() << "': " << e.what();
{ }
LMS_LOG(COVER, ERROR) << "Cannot read cover in file '" << p.string() << "': " << e.what();
} return image;
}
return image;
} std::shared_ptr<IEncodedImage> CoverService::getDefault(ImageSize width)
{
std::shared_ptr<IEncodedImage> {
CoverService::getDefault(ImageSize width) std::shared_lock lock{ _cacheMutex };
{
{ if (auto it{ _defaultCoverCache.find(width) }; it != std::cend(_defaultCoverCache))
std::shared_lock lock {_cacheMutex}; return it->second;
}
if (auto it {_defaultCoverCache.find(width)}; it != std::cend(_defaultCoverCache))
return it->second; {
} std::unique_lock lock{ _cacheMutex };
{ if (auto it{ _defaultCoverCache.find(width) }; it != std::cend(_defaultCoverCache))
std::unique_lock lock {_cacheMutex}; return it->second;
if (auto it {_defaultCoverCache.find(width)}; it != std::cend(_defaultCoverCache)) std::shared_ptr<IEncodedImage> image{ getFromCoverFile(_defaultCoverPath, width) };
return it->second; _defaultCoverCache[width] = image;
LMS_LOG(COVER, DEBUG) << "Default cache entries = " << _defaultCoverCache.size();
std::shared_ptr<IEncodedImage> image {getFromCoverFile(_defaultCoverPath, width)};
_defaultCoverCache[width] = image; return image;
LMS_LOG(COVER, DEBUG) << "Default cache entries = " << _defaultCoverCache.size(); }
}
return image;
} std::unique_ptr<IEncodedImage> CoverService::getFromDirectory(const std::filesystem::path& directory, ImageSize width) const
} {
const std::multimap<std::string, std::filesystem::path> coverPaths{ getCoverPaths(directory) };
std::unique_ptr<IEncodedImage>
CoverService::getFromDirectory(const std::filesystem::path& directory, ImageSize width) const auto tryLoadImageFromFilename = [&](std::string_view fileName)
{ {
const std::multimap<std::string, std::filesystem::path> coverPaths {getCoverPaths(directory)}; std::unique_ptr<IEncodedImage> image;
auto tryLoadImageFromFilename = [&](std::string_view fileName) auto range{ coverPaths.equal_range(std::string {fileName}) };
{ for (auto it{ range.first }; it != range.second; ++it)
std::unique_ptr<IEncodedImage> image; {
image = getFromCoverFile(it->second, width);
auto range {coverPaths.equal_range(std::string {fileName})}; if (image)
for (auto it {range.first}; it != range.second; ++it) break;
{ }
image = getFromCoverFile(it->second, width); return image;
if (image) };
break;
} std::unique_ptr<IEncodedImage> image;
return image;
}; for (std::string_view filename : _preferredFileNames)
{
std::unique_ptr<IEncodedImage> image; image = tryLoadImageFromFilename(filename);
if (image)
for (std::string_view filename : _preferredFileNames) return image;
{ }
image = tryLoadImageFromFilename(filename);
if (image) // Just pick one
return image; for (const auto& [filename, coverPath] : coverPaths)
} {
image = getFromCoverFile(coverPath, width);
// Just pick one if (image)
for (const auto& [filename, coverPath] : coverPaths) return image;
{ }
image = getFromCoverFile(coverPath, width);
if (image) return image;
return image; }
}
std::unique_ptr<IEncodedImage> CoverService::getFromSameNamedFile(const std::filesystem::path& filePath, ImageSize width) const
return image; {
} std::unique_ptr<IEncodedImage> res;
std::unique_ptr<IEncodedImage> std::filesystem::path coverPath{ filePath };
CoverService::getFromSameNamedFile(const std::filesystem::path& filePath, ImageSize width) const for (const std::filesystem::path& extension : _fileExtensions)
{ {
std::unique_ptr<IEncodedImage> res; coverPath.replace_extension(extension);
std::filesystem::path coverPath {filePath}; if (!checkCoverFile(coverPath))
for (const std::filesystem::path& extension : _fileExtensions) continue;
{
coverPath.replace_extension(extension); res = getFromCoverFile(coverPath, width);
if (res)
if (!checkCoverFile(coverPath)) break;
continue; }
res = getFromCoverFile(coverPath, width); return res;
if (res) }
break;
} bool CoverService::checkCoverFile(const std::filesystem::path& filePath) const
{
return res; std::error_code ec;
}
if (!isFileSupported(filePath, _fileExtensions))
bool return false;
CoverService::checkCoverFile(const std::filesystem::path& filePath) const
{ if (!std::filesystem::exists(filePath, ec))
std::error_code ec; return false;
if (!isFileSupported(filePath, _fileExtensions)) if (!std::filesystem::is_regular_file(filePath, ec))
return false; return false;
if (!std::filesystem::exists(filePath, ec)) if (std::filesystem::file_size(filePath, ec) > _maxFileSize && !ec)
return false; {
LMS_LOG(COVER, INFO) << "Cover file '" << filePath.string() << " is too big (" << std::filesystem::file_size(filePath, ec) << "), limit is " << _maxFileSize;
if (!std::filesystem::is_regular_file(filePath, ec)) return false;
return false; }
if (std::filesystem::file_size(filePath, ec) > _maxFileSize && !ec) return true;
{ }
LMS_LOG(COVER, INFO) << "Cover file '" << filePath.string() << " is too big (" << std::filesystem::file_size(filePath, ec) << "), limit is " << _maxFileSize;
return false; std::multimap<std::string, std::filesystem::path> CoverService::getCoverPaths(const std::filesystem::path& directoryPath) const
} {
std::multimap<std::string, std::filesystem::path> res;
return true; std::error_code ec;
}
std::filesystem::directory_iterator itPath(directoryPath, ec);
std::multimap<std::string, std::filesystem::path> std::filesystem::directory_iterator itEnd;
CoverService::getCoverPaths(const std::filesystem::path& directoryPath) const while (!ec && itPath != itEnd)
{ {
std::multimap<std::string, std::filesystem::path> res; const std::filesystem::path& path{ *itPath };
std::error_code ec;
if (checkCoverFile(path))
std::filesystem::directory_iterator itPath(directoryPath, ec); res.emplace(std::filesystem::path{ path }.filename().replace_extension("").string(), path);
std::filesystem::directory_iterator itEnd;
while (!ec && itPath != itEnd) itPath.increment(ec);
{ }
const std::filesystem::path& path {*itPath};
return res;
if (checkCoverFile(path)) }
res.emplace(std::filesystem::path{ path }.filename().replace_extension("").string(), path);
std::unique_ptr<IEncodedImage> CoverService::getFromTrack(const std::filesystem::path& p, ImageSize width) const
itPath.increment(ec); {
} std::unique_ptr<IEncodedImage> image;
return res; try
} {
image = getFromAvMediaFile(*Av::parseAudioFile(p), width);
std::unique_ptr<IEncodedImage> }
CoverService::getFromTrack(const std::filesystem::path& p, ImageSize width) const catch (Av::Exception& e)
{ {
std::unique_ptr<IEncodedImage> image; LMS_LOG(COVER, ERROR) << "Cannot get covers from track " << p.string() << ": " << e.what();
}
try
{ return image;
image = getFromAvMediaFile(*Av::parseAudioFile(p), width); }
}
catch (Av::Exception& e) std::shared_ptr<IEncodedImage> CoverService::getFromTrack(Database::TrackId trackId, ImageSize width)
{ {
LMS_LOG(COVER, ERROR) << "Cannot get covers from track " << p.string() << ": " << e.what(); return getFromTrack(_db.getTLSSession(), trackId, width, true /* allow release fallback*/);
} }
return image; std::shared_ptr<IEncodedImage> CoverService::getFromTrack(Database::Session& dbSession, Database::TrackId trackId, ImageSize width, bool allowReleaseFallback)
} {
using namespace Database;
std::shared_ptr<IEncodedImage>
CoverService::getFromTrack(Database::TrackId trackId, ImageSize width) const CacheEntryDesc cacheEntryDesc{ trackId, width };
{
return getFromTrack(_db.getTLSSession(), trackId, width, true /* allow release fallback*/); std::shared_ptr<IEncodedImage> cover{ loadFromCache(cacheEntryDesc) };
} if (cover)
return cover;
std::shared_ptr<IEncodedImage>
CoverService::getFromTrack(Database::Session& dbSession, Database::TrackId trackId, ImageSize width, bool allowReleaseFallback) if (const std::optional<TrackInfo> trackInfo{ getTrackInfo(dbSession, trackId) })
{ {
using namespace Database; if (trackInfo->hasCover)
cover = getFromTrack(trackInfo->trackPath, width);
const CacheEntryDesc cacheEntryDesc {trackId, width};
if (!cover)
std::shared_ptr<IEncodedImage> cover {loadFromCache(cacheEntryDesc)}; cover = getFromSameNamedFile(trackInfo->trackPath, width);
if (cover)
return cover; if (!cover && trackInfo->releaseId && allowReleaseFallback)
cover = getFromRelease(*trackInfo->releaseId, width);
if (const std::optional<TrackInfo> trackInfo {getTrackInfo(dbSession, trackId)})
{ if (!cover && trackInfo->isMultiDisc)
if (trackInfo->hasCover) {
cover = getFromTrack(trackInfo->trackPath, width); if (trackInfo->trackPath.parent_path().has_parent_path())
cover = getFromDirectory(trackInfo->trackPath.parent_path().parent_path(), width);
if (!cover) }
cover = getFromSameNamedFile(trackInfo->trackPath, width); }
if (!cover && trackInfo->releaseId && allowReleaseFallback) if (!cover)
cover = getFromRelease(*trackInfo->releaseId, width); cover = getDefault(width);
if (!cover && trackInfo->isMultiDisc) if (cover)
{ saveToCache(cacheEntryDesc, cover);
if (trackInfo->trackPath.parent_path().has_parent_path())
cover = getFromDirectory(trackInfo->trackPath.parent_path().parent_path(), width); return cover;
} }
}
std::shared_ptr<IEncodedImage> CoverService::getFromRelease(Database::ReleaseId releaseId, ImageSize width)
if (!cover) {
cover = getDefault(width); using namespace Database;
const CacheEntryDesc cacheEntryDesc{ releaseId, width };
if (cover)
saveToCache(cacheEntryDesc, cover); std::shared_ptr<IEncodedImage> cover{ loadFromCache(cacheEntryDesc) };
if (cover)
return cover; return cover;
}
struct ReleaseInfo
std::shared_ptr<IEncodedImage> {
CoverService::getFromRelease(Database::ReleaseId releaseId, ImageSize width) TrackId firstTrackId;
{ std::filesystem::path releaseDirectory;
using namespace Database; };
const CacheEntryDesc cacheEntryDesc {releaseId, width};
Session& session{ _db.getTLSSession() };
std::shared_ptr<IEncodedImage> cover {loadFromCache(cacheEntryDesc)};
if (cover) auto getReleaseInfo{ [&]
return cover; {
std::optional<ReleaseInfo> res;
struct ReleaseInfo
{ auto transaction{ session.createSharedTransaction() };
TrackId firstTrackId;
std::filesystem::path releaseDirectory; const auto tracks{ Track::find(session, Track::FindParameters {}.setRelease(releaseId).setRange({0, 1}).setSortMethod(TrackSortMethod::Release)) };
};
if (!tracks.results.empty())
Session& session {_db.getTLSSession()}; {
const Track::pointer& track{ tracks.results.front() };
auto getReleaseInfo {[&] res = ReleaseInfo{};
{ res->firstTrackId = track->getId();
std::optional<ReleaseInfo> res; res->releaseDirectory = track->getPath().parent_path();
}
auto transaction {session.createSharedTransaction()};
return res;
const auto tracks {Track::find(session, Track::FindParameters {}.setRelease(releaseId).setRange({0, 1}).setSortMethod(TrackSortMethod::Release))}; } };
if (!tracks.results.empty()) if (const std::optional<ReleaseInfo> releaseInfo{ getReleaseInfo() })
{ {
if (const Track::pointer track {Track::find(session, tracks.results.front())}) cover = getFromDirectory(releaseInfo->releaseDirectory, width);
{ if (!cover)
res = ReleaseInfo {}; cover = getFromTrack(session, releaseInfo->firstTrackId, width, false /* no release fallback */);
res->firstTrackId = track->getId(); }
res->releaseDirectory = track->getPath().parent_path();
} if (!cover)
} cover = getDefault(width);
return res; if (cover)
}}; saveToCache(cacheEntryDesc, cover);
if (const std::optional<ReleaseInfo> releaseInfo {getReleaseInfo()}) return cover;
{ }
cover = getFromDirectory(releaseInfo->releaseDirectory, width);
if (!cover) void CoverService::flushCache()
cover = getFromTrack(session, releaseInfo->firstTrackId, width, false /* no release fallback */); {
} std::unique_lock lock{ _cacheMutex };
if (!cover) LMS_LOG(COVER, DEBUG) << "Cache stats: hits = " << _cacheHits << ", misses = " << _cacheMisses << ", nb entries = " << _cache.size() << ", size = " << _cacheSize;
cover = getDefault(width); _cacheHits = 0;
_cacheMisses = 0;
if (cover) _cacheSize = 0;
saveToCache(cacheEntryDesc, cover); _cache.clear();
}
return cover;
} void CoverService::setJpegQuality(unsigned quality)
{
void _jpegQuality = Utils::clamp<unsigned>(quality, 1, 100);
CoverService::flushCache()
{ LMS_LOG(COVER, INFO) << "JPEG export quality = " << _jpegQuality;
std::unique_lock lock {_cacheMutex}; }
LMS_LOG(COVER, DEBUG) << "Cache stats: hits = " << _cacheHits << ", misses = " << _cacheMisses << ", nb entries = " << _cache.size() << ", size = " << _cacheSize; void CoverService::saveToCache(const CacheEntryDesc& entryDesc, std::shared_ptr<IEncodedImage> image)
_cacheHits = 0; {
_cacheMisses = 0; std::unique_lock lock{ _cacheMutex };
_cacheSize = 0;
_cache.clear(); while (_cacheSize + image->getDataSize() > _maxCacheSize && !_cache.empty())
} {
auto itRandom{ Random::pickRandom(_cache) };
void _cacheSize -= itRandom->second->getDataSize();
CoverService::setJpegQuality(unsigned quality) _cache.erase(itRandom);
{ }
_jpegQuality = Utils::clamp<unsigned>(quality, 1, 100);
_cacheSize += image->getDataSize();
LMS_LOG(COVER, INFO) << "JPEG export quality = " << _jpegQuality; _cache[entryDesc] = image;
} }
void std::shared_ptr<IEncodedImage> CoverService::loadFromCache(const CacheEntryDesc& entryDesc)
CoverService::saveToCache(const CacheEntryDesc& entryDesc, std::shared_ptr<IEncodedImage> image) {
{ std::shared_lock lock{ _cacheMutex };
std::unique_lock lock {_cacheMutex};
auto it{ _cache.find(entryDesc) };
while (_cacheSize + image->getDataSize() > _maxCacheSize && !_cache.empty()) if (it == std::cend(_cache))
{ {
auto itRandom {Random::pickRandom(_cache)}; ++_cacheMisses;
_cacheSize -= itRandom->second->getDataSize(); return nullptr;
_cache.erase(itRandom); }
}
++_cacheHits;
_cacheSize += image->getDataSize(); return it->second;
_cache[entryDesc] = image; }
}
std::shared_ptr<IEncodedImage>
CoverService::loadFromCache(const CacheEntryDesc& entryDesc)
{
std::shared_lock lock {_cacheMutex};
auto it {_cache.find(entryDesc)};
if (it == std::cend(_cache))
{
++_cacheMisses;
return nullptr;
}
++_cacheHits;
return it->second;
}
} // namespace Cover } // namespace Cover
+243 -223
View File
@@ -33,278 +33,298 @@
namespace Database namespace Database
{ {
namespace
{
template <typename ResultType>
Wt::Dbo::Query<ResultType> createQuery(Session& session, std::string_view itemToSelect, const Artist::FindParameters& params)
{
session.checkSharedLocked();
Artist::Artist(const std::string& name, const std::optional<UUID>& MBID) auto query{ session.getDboSession().query<ResultType>("SELECT DISTINCT " + std::string{ itemToSelect } + " FROM artist a") };
: _name {std::string(name, 0 , _maxNameLength)}, if (params.sortMethod == ArtistSortMethod::LastWritten
_sortName {_name}, || params.writtenAfter.isValid()
_MBID {MBID ? MBID->getAsString() : ""} || params.linkType
{ || params.track.isValid()
} || params.release.isValid()
|| params.clusters.size() == 1)
{
query.join("track t ON t.id = t_a_l.track_id");
query.join("track_artist_link t_a_l ON t_a_l.artist_id = a.id");
}
Artist::pointer if (params.linkType)
Artist::create(Session& session, const std::string& name, const std::optional<UUID>& MBID) query.where("t_a_l.type = ?").bind(*params.linkType);
{
return session.getDboSession().add(std::unique_ptr<Artist> {new Artist {name, MBID}});
}
std::size_t if (params.writtenAfter.isValid())
Artist::getCount(Session& session) query.where("t.file_last_write > ?").bind(params.writtenAfter);
{
session.checkSharedLocked();
return session.getDboSession().query<int>("SELECT COUNT(*) FROM artist"); if (!params.keywords.empty())
} {
std::vector<std::string> clauses;
std::vector<std::string> sortClauses;
std::vector<Artist::pointer> for (std::string_view keyword : params.keywords)
Artist::find(Session& session, const std::string& name) {
{ clauses.push_back("a.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'");
session.checkSharedLocked(); query.bind("%" + Utils::escapeLikeKeyword(keyword) + "%");
}
Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> res = session.getDboSession().find<Artist>() for (std::string_view keyword : params.keywords)
.where("name = ?").bind(std::string {name, 0, _maxNameLength}) {
.orderBy("LENGTH(mbid) DESC"); // put mbid entries first sortClauses.push_back("a.sort_name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'");
query.bind("%" + Utils::escapeLikeKeyword(keyword) + "%");
}
return std::vector<Artist::pointer>(res.begin(), res.end()); query.where("(" + StringUtils::joinStrings(clauses, " AND ") + ") OR (" + StringUtils::joinStrings(sortClauses, " AND ") + ")");
} }
Artist::pointer if (params.starringUser.isValid())
Artist::find(Session& session, const UUID& mbid) {
{ assert(params.feedbackBackend);
session.checkSharedLocked(); query.join("starred_artist s_a ON s_a.artist_id = a.id")
return session.getDboSession().find<Artist>().where("mbid = ?").bind(std::string {mbid.getAsString()}).resultValue(); .where("s_a.user_id = ?").bind(params.starringUser)
} .where("s_a.backend = ?").bind(*params.feedbackBackend)
.where("s_a.sync_state <> ?").bind(SyncState::PendingRemove);
}
Artist::pointer if (params.clusters.size() == 1)
Artist::find(Session& session, ArtistId id) {
{ query.join("track_cluster t_c ON t_c.track_id = t.id")
session.checkSharedLocked(); .where("t_c.cluster_id = ?").bind(params.clusters.front());
return session.getDboSession().find<Artist>().where("id = ?").bind(id).resultValue(); }
} else if (params.clusters.size() > 1)
{
std::ostringstream oss;
oss << "a.id IN (SELECT DISTINCT a.id FROM artist a"
" INNER JOIN track t ON t.id = t_a_l.track_id"
" INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id"
" INNER JOIN cluster c ON c.id = t_c.cluster_id"
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
bool WhereClause clusterClause;
Artist::exists(Session& session, ArtistId id) for (const ClusterId clusterId : params.clusters)
{ {
session.checkSharedLocked(); clusterClause.Or(WhereClause("c.id = ?"));
return session.getDboSession().query<int>("SELECT 1 FROM artist").where("id = ?").bind(id).resultValue() == 1; query.bind(clusterId);
} }
static oss << " " << clusterClause.get();
Wt::Dbo::Query<ArtistId> oss << " GROUP BY t.id,a.id HAVING COUNT(DISTINCT c.id) = " << params.clusters.size() << ")";
createQuery(Session& session, const Artist::FindParameters& params)
{
session.checkSharedLocked();
auto query {session.getDboSession().query<ArtistId>("SELECT DISTINCT a.id FROM artist a")}; query.where(oss.str());
if (params.sortMethod == ArtistSortMethod::LastWritten }
|| params.writtenAfter.isValid()
|| params.linkType
|| params.track.isValid()
|| params.release.isValid())
{
query.join("track t ON t.id = t_a_l.track_id");
query.join("track_artist_link t_a_l ON t_a_l.artist_id = a.id");
}
if (params.linkType) if (params.track.isValid())
query.where("t_a_l.type = ?").bind(*params.linkType); query.where("t.id = ?").bind(params.track);
if (params.writtenAfter.isValid()) if (params.release.isValid())
query.where("t.file_last_write > ?").bind(params.writtenAfter); query.where("t.release_id = ?").bind(params.release);
if (!params.keywords.empty()) switch (params.sortMethod)
{ {
std::vector<std::string> clauses; case ArtistSortMethod::None:
std::vector<std::string> sortClauses; break;
case ArtistSortMethod::ByName:
query.orderBy("a.name COLLATE NOCASE");
break;
case ArtistSortMethod::BySortName:
query.orderBy("a.sort_name COLLATE NOCASE");
break;
case ArtistSortMethod::Random:
query.orderBy("RANDOM()");
break;
case ArtistSortMethod::LastWritten:
query.orderBy("t.file_last_write DESC");
break;
case ArtistSortMethod::StarredDateDesc:
assert(params.starringUser.isValid());
query.orderBy("s_a.date_time DESC");
break;
}
for (std::string_view keyword : params.keywords) return query;
{ }
clauses.push_back("a.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'");
query.bind("%" + Utils::escapeLikeKeyword(keyword) + "%");
}
for (std::string_view keyword : params.keywords) template <typename ResultType>
{ Wt::Dbo::Query<ResultType> createQuery(Session& session, const Artist::FindParameters& params)
sortClauses.push_back("a.sort_name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'"); {
query.bind("%" + Utils::escapeLikeKeyword(keyword) + "%"); std::string_view itemToSelect;
}
if constexpr (std::is_same_v<ResultType, ArtistId>)
itemToSelect = "a.id";
else if constexpr (std::is_same_v<ResultType, Wt::Dbo::ptr<Artist>>)
itemToSelect = "a";
else
static_assert("Unhandled type");
query.where("(" + StringUtils::joinStrings(clauses, " AND ") + ") OR (" + StringUtils::joinStrings(sortClauses, " AND ") + ")"); return createQuery<ResultType>(session, itemToSelect, params);
} }
}
if (params.starringUser.isValid()) Artist::Artist(const std::string& name, const std::optional<UUID>& MBID)
{ : _name{ std::string(name, 0 , _maxNameLength) },
assert(params.scrobbler); _sortName{ _name },
query.join("starred_artist s_a ON s_a.artist_id = a.id") _MBID{ MBID ? MBID->getAsString() : "" }
.where("s_a.user_id = ?").bind(params.starringUser) {
.where("s_a.scrobbler = ?").bind(*params.scrobbler) }
.where("s_a.scrobbling_state <> ?").bind(ScrobblingState::PendingRemove);
}
if (!params.clusters.empty()) Artist::pointer Artist::create(Session& session, const std::string& name, const std::optional<UUID>& MBID)
{ {
std::ostringstream oss; return session.getDboSession().add(std::unique_ptr<Artist> {new Artist{ name, MBID }});
oss << "a.id IN (SELECT DISTINCT a.id FROM artist a" }
" INNER JOIN track t ON t.id = t_a_l.track_id"
" INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id"
" INNER JOIN cluster c ON c.id = t_c.cluster_id"
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
WhereClause clusterClause; std::size_t Artist::getCount(Session& session)
for (const ClusterId clusterId : params.clusters) {
{ session.checkSharedLocked();
clusterClause.Or(WhereClause("c.id = ?"));
query.bind(clusterId);
}
oss << " " << clusterClause.get(); return session.getDboSession().query<int>("SELECT COUNT(*) FROM artist");
oss << " GROUP BY t.id,a.id HAVING COUNT(DISTINCT c.id) = " << params.clusters.size() << ")"; }
query.where(oss.str()); std::vector<Artist::pointer> Artist::find(Session& session, const std::string& name)
} {
session.checkSharedLocked();
if (params.track.isValid()) Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> res = session.getDboSession().find<Artist>()
query.where("t.id = ?").bind(params.track); .where("name = ?").bind(std::string{ name, 0, _maxNameLength })
.orderBy("LENGTH(mbid) DESC"); // put mbid entries first
if (params.release.isValid()) return std::vector<Artist::pointer>(res.begin(), res.end());
query.where("t.release_id = ?").bind(params.release); }
switch (params.sortMethod) Artist::pointer Artist::find(Session& session, const UUID& mbid)
{ {
case ArtistSortMethod::None: session.checkSharedLocked();
break; return session.getDboSession().find<Artist>().where("mbid = ?").bind(std::string{ mbid.getAsString() }).resultValue();
case ArtistSortMethod::ByName: }
query.orderBy("a.name COLLATE NOCASE");
break;
case ArtistSortMethod::BySortName:
query.orderBy("a.sort_name COLLATE NOCASE");
break;
case ArtistSortMethod::Random:
query.orderBy("RANDOM()");
break;
case ArtistSortMethod::LastWritten:
query.orderBy("t.file_last_write DESC");
break;
case ArtistSortMethod::StarredDateDesc:
assert(params.starringUser.isValid());
query.orderBy("s_a.date_time DESC");
break;
}
return query; Artist::pointer Artist::find(Session& session, ArtistId id)
} {
session.checkSharedLocked();
return session.getDboSession().find<Artist>().where("id = ?").bind(id).resultValue();
}
RangeResults<ArtistId> bool Artist::exists(Session& session, ArtistId id)
Artist::findAllOrphans(Session& session, Range range) {
{ session.checkSharedLocked();
session.checkSharedLocked(); return session.getDboSession().query<int>("SELECT 1 FROM artist").where("id = ?").bind(id).resultValue() == 1;
auto query {session.getDboSession().query<ArtistId>("SELECT DISTINCT a.id FROM artist a WHERE NOT EXISTS(SELECT 1 FROM track t INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id WHERE t.id = t_a_l.track_id)")}; }
return Utils::execQuery(query, range);
}
RangeResults<ArtistId> RangeResults<ArtistId> Artist::findOrphanIds(Session& session, Range range)
Artist::find(Session& session, const FindParameters& params) {
{ session.checkSharedLocked();
session.checkSharedLocked(); auto query{ session.getDboSession().query<ArtistId>("SELECT DISTINCT a.id FROM artist a WHERE NOT EXISTS(SELECT 1 FROM track t INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id WHERE t.id = t_a_l.track_id)") };
auto query {createQuery(session, params)}; return Utils::execQuery(query, range);
return Utils::execQuery(query, params.range); }
}
RangeResults<ArtistId> RangeResults<ArtistId> Artist::findIds(Session& session, const FindParameters& params)
Artist::findSimilarArtists(EnumSet<TrackArtistLinkType> artistLinkTypes, Range range) const {
{ session.checkSharedLocked();
assert(session());
std::ostringstream oss; auto query{ createQuery<ArtistId>(session, params) };
oss << return Utils::execQuery(query, params.range);
"SELECT a.id FROM artist a" }
" INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id"
" INNER JOIN track t ON t.id = t_a_l.track_id"
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
" WHERE "
" t_c.cluster_id IN (SELECT c.id from cluster c"
" INNER JOIN track t ON c.id = t_c.cluster_id"
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
" INNER JOIN artist a ON a.id = t_a_l.artist_id"
" INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id"
" WHERE a.id = ?)"
" AND a.id <> ?";
if (!artistLinkTypes.empty()) RangeResults<Artist::pointer> Artist::find(Session& session, const FindParameters& params)
{ {
oss << " AND t_a_l.type IN ("; session.checkSharedLocked();
bool first {true}; auto query{ createQuery<Wt::Dbo::ptr<Artist>>(session, params) };
for (TrackArtistLinkType type : artistLinkTypes) return Utils::execQuery(query, params.range);
{ }
(void) type;
if (!first)
oss << ", ";
oss << "?";
first = false;
}
oss << ")";
}
auto query {session()->query<ArtistId>(oss.str()) RangeResults<ArtistId> Artist::findSimilarArtistIds(EnumSet<TrackArtistLinkType> artistLinkTypes, Range range) const
.bind(getId()) {
.bind(getId()) assert(session());
.groupBy("a.id")
.orderBy("COUNT(*) DESC, RANDOM()")};
for (TrackArtistLinkType type : artistLinkTypes) std::ostringstream oss;
query.bind(type); oss <<
"SELECT a.id FROM artist a"
" INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id"
" INNER JOIN track t ON t.id = t_a_l.track_id"
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
" WHERE "
" t_c.cluster_id IN (SELECT DISTINCT c.id from cluster c"
" INNER JOIN track t ON c.id = t_c.cluster_id"
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
" INNER JOIN artist a ON a.id = t_a_l.artist_id"
" INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id"
" WHERE a.id = ?)"
" AND a.id <> ?";
return Utils::execQuery(query, range); if (!artistLinkTypes.empty())
} {
oss << " AND t_a_l.type IN (";
std::vector<std::vector<Cluster::pointer>> bool first{ true };
Artist::getClusterGroups(std::vector<ClusterType::pointer> clusterTypes, std::size_t size) const for (TrackArtistLinkType type : artistLinkTypes)
{ {
assert(session()); (void)type;
if (!first)
oss << ", ";
oss << "?";
first = false;
}
oss << ")";
}
WhereClause where; auto query{ session()->query<ArtistId>(oss.str())
.bind(getId())
.bind(getId())
.groupBy("a.id")
.orderBy("COUNT(*) DESC, RANDOM()") };
std::ostringstream oss; for (TrackArtistLinkType type : artistLinkTypes)
oss << "SELECT c FROM cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN cluster_type c_type ON c.cluster_type_id = c_type.id INNER JOIN artist a ON t_a_l.artist_id = a.id INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id"; query.bind(type);
where.And(WhereClause("a.id = ?")).bind(getId().toString()); return Utils::execQuery(query, range);
{ }
WhereClause clusterClause;
for (auto clusterType : clusterTypes)
clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterType->getId().toString());
where.And(clusterClause); std::vector<std::vector<Cluster::pointer>> Artist::getClusterGroups(std::vector<ClusterType::pointer> clusterTypes, std::size_t size) const
} {
oss << " " << where.get(); assert(session());
oss << "GROUP BY c.id ORDER BY COUNT(DISTINCT c.id) DESC";
Wt::Dbo::Query<Wt::Dbo::ptr<Cluster>> query = session()->query<Wt::Dbo::ptr<Cluster>>( oss.str() ); WhereClause where;
for (const std::string& bindArg : where.getBindArgs()) std::ostringstream oss;
query.bind(bindArg); oss << "SELECT c FROM cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN cluster_type c_type ON c.cluster_type_id = c_type.id INNER JOIN artist a ON t_a_l.artist_id = a.id INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id";
Wt::Dbo::collection<Wt::Dbo::ptr<Cluster>> queryRes = query; where.And(WhereClause("a.id = ?")).bind(getId().toString());
{
WhereClause clusterClause;
for (auto clusterType : clusterTypes)
clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterType->getId().toString());
std::map<ClusterTypeId, std::vector<Cluster::pointer>> clustersByType; where.And(clusterClause);
for (Cluster::pointer cluster : queryRes) }
{ oss << " " << where.get();
if (clustersByType[cluster->getType()->getId()].size() < size) oss << "GROUP BY c.id ORDER BY COUNT(DISTINCT c.id) DESC";
clustersByType[cluster->getType()->getId()].push_back(cluster);
}
std::vector<std::vector<Cluster::pointer>> res; Wt::Dbo::Query<Wt::Dbo::ptr<Cluster>> query = session()->query<Wt::Dbo::ptr<Cluster>>(oss.str());
for (const auto& [clusterTypeId, clusters] : clustersByType)
res.push_back(clusters);
return res; for (const std::string& bindArg : where.getBindArgs())
} query.bind(bindArg);
void Wt::Dbo::collection<Wt::Dbo::ptr<Cluster>> queryRes = query;
Artist::setSortName(const std::string& sortName)
{ std::map<ClusterTypeId, std::vector<Cluster::pointer>> clustersByType;
_sortName = std::string(sortName, 0 , _maxNameLength); for (Cluster::pointer cluster : queryRes)
} {
if (clustersByType[cluster->getType()->getId()].size() < size)
clustersByType[cluster->getType()->getId()].push_back(cluster);
}
std::vector<std::vector<Cluster::pointer>> res;
for (const auto& [clusterTypeId, clusters] : clustersByType)
res.push_back(clusters);
return res;
}
void Artist::setSortName(const std::string& sortName)
{
_sortName = std::string(sortName, 0, _maxNameLength);
}
} // namespace Database } // namespace Database
+23 -4
View File
@@ -40,13 +40,25 @@ namespace Database
prepare(); prepare();
} }
Connection(const Connection& other)
: Wt::Dbo::backend::Sqlite3{ other }
, _dbPath{ other._dbPath }
{
prepare();
}
~Connection()
{
// make use of per-connection usage stats to optimize
optimize();
}
private: private:
Connection(const Connection&) = delete;
Connection& operator=(const Connection&) = delete; Connection& operator=(const Connection&) = delete;
std::unique_ptr<SqlConnection> clone() const override std::unique_ptr<SqlConnection> clone() const override
{ {
return std::make_unique<Connection>(_dbPath); return std::make_unique<Connection>(*this);
} }
void prepare() void prepare()
@@ -54,10 +66,17 @@ namespace Database
LMS_LOG(DB, DEBUG) << "Setting per-connection settings..."; LMS_LOG(DB, DEBUG) << "Setting per-connection settings...";
executeSql("pragma journal_mode=WAL"); executeSql("pragma journal_mode=WAL");
executeSql("pragma synchronous=normal"); executeSql("pragma synchronous=normal");
executeSql("pragma analysis_limit=1000"); // to help make analyze command faster executeSql("pragma analysis_limit=2000"); // to help make analyze command faster, 1000 does not seem to be enough to speed up all queries
LMS_LOG(DB, DEBUG) << "Setting per-connection settings done!"; LMS_LOG(DB, DEBUG) << "Setting per-connection settings done!";
} }
void optimize()
{
LMS_LOG(DB, DEBUG) << "connection close: Running pragma optimize...";
executeSql("pragma optimize");
LMS_LOG(DB, DEBUG) << "connection close: pragma optimize complete";
}
std::filesystem::path _dbPath; std::filesystem::path _dbPath;
}; };
} }
@@ -68,7 +87,7 @@ namespace Database
LMS_LOG(DB, INFO) << "Creating connection pool on file " << dbPath.string(); LMS_LOG(DB, INFO) << "Creating connection pool on file " << dbPath.string();
auto connection{ std::make_unique<Connection>(dbPath.string()) }; auto connection{ std::make_unique<Connection>(dbPath.string()) };
// connection->setProperty("show-queries", "true"); // connection->setProperty("show-queries", "true");
auto connectionPool{ std::make_unique<Wt::Dbo::FixedSqlConnectionPool>(std::move(connection), connectionCount) }; auto connectionPool{ std::make_unique<Wt::Dbo::FixedSqlConnectionPool>(std::move(connection), connectionCount) };
connectionPool->setTimeout(std::chrono::seconds{ 10 }); connectionPool->setTimeout(std::chrono::seconds{ 10 });
+71 -34
View File
@@ -29,14 +29,14 @@ namespace
{ {
using namespace Database; using namespace Database;
Wt::Dbo::Query<ArtistId> createArtistsQuery(Wt::Dbo::Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType) Wt::Dbo::Query<ArtistId> createArtistsQuery(Wt::Dbo::Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType)
{ {
auto query{ session.query<ArtistId>("SELECT a.id from artist a") auto query{ session.query<ArtistId>("SELECT a.id from artist a")
.join("track t ON t.id = t_a_l.track_id") .join("track t ON t.id = t_a_l.track_id")
.join("track_artist_link t_a_l ON t_a_l.artist_id = a.id") .join("track_artist_link t_a_l ON t_a_l.artist_id = a.id")
.join("listen l ON l.track_id = t.id") .join("listen l ON l.track_id = t.id")
.where("l.user_id = ?").bind(userId) .where("l.user_id = ?").bind(userId)
.where("l.scrobbler = ?").bind(scrobbler) }; .where("l.backend = ?").bind(backend) };
if (linkType) if (linkType)
query.where("t_a_l.type = ?").bind(*linkType); query.where("t_a_l.type = ?").bind(*linkType);
@@ -66,13 +66,13 @@ namespace
return query; return query;
} }
Wt::Dbo::Query<ReleaseId> createReleasesQuery(Wt::Dbo::Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds) Wt::Dbo::Query<ReleaseId> createReleasesQuery(Wt::Dbo::Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds)
{ {
auto query{ session.query<ReleaseId>("SELECT r.id from release r") auto query{ session.query<ReleaseId>("SELECT r.id from release r")
.join("track t ON t.release_id = r.id") .join("track t ON t.release_id = r.id")
.join("listen l ON l.track_id = t.id") .join("listen l ON l.track_id = t.id")
.where("l.user_id = ?").bind(userId) .where("l.user_id = ?").bind(userId)
.where("l.scrobbler = ?").bind(scrobbler) }; .where("l.backend = ?").bind(backend) };
if (!clusterIds.empty()) if (!clusterIds.empty())
{ {
@@ -98,12 +98,12 @@ namespace
return query; return query;
} }
Wt::Dbo::Query<TrackId> createTracksQuery(Wt::Dbo::Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds) Wt::Dbo::Query<TrackId> createTracksQuery(Wt::Dbo::Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds)
{ {
auto query{ session.query<TrackId>("SELECT t.id from track t") auto query{ session.query<TrackId>("SELECT t.id from track t")
.join("listen l ON l.track_id = t.id") .join("listen l ON l.track_id = t.id")
.where("l.user_id = ?").bind(userId) .where("l.user_id = ?").bind(userId)
.where("l.scrobbler = ?").bind(scrobbler) }; .where("l.backend = ?").bind(backend) };
if (!clusterIds.empty()) if (!clusterIds.empty())
{ {
@@ -131,16 +131,17 @@ namespace
namespace Database namespace Database
{ {
Listen::Listen(ObjectPtr<User> user, ObjectPtr<Track> track, Scrobbler scrobbler, const Wt::WDateTime& dateTime) Listen::Listen(ObjectPtr<User> user, ObjectPtr<Track> track, ScrobblingBackend backend, const Wt::WDateTime& dateTime)
: _dateTime{ Wt::WDateTime::fromTime_t(dateTime.toTime_t()) } : _dateTime{ Wt::WDateTime::fromTime_t(dateTime.toTime_t()) }
, _scrobbler{ scrobbler } , _backend{ backend }
, _user{ getDboPtr(user) } , _user{ getDboPtr(user) }
, _track{ getDboPtr(track) } , _track{ getDboPtr(track) }
{} {}
Listen::pointer Listen::create(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track, Scrobbler scrobbler, const Wt::WDateTime& dateTime) Listen::pointer Listen::create(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track, ScrobblingBackend backend, const Wt::WDateTime& dateTime)
{ {
return session.getDboSession().add(std::unique_ptr<Listen> {new Listen{ user, track, scrobbler, dateTime }}); session.checkUniqueLocked();
return session.getDboSession().add(std::unique_ptr<Listen> {new Listen{ user, track, backend, dateTime }});
} }
std::size_t Listen::getCount(Session& session) std::size_t Listen::getCount(Session& session)
@@ -165,30 +166,31 @@ namespace Database
if (parameters.user.isValid()) if (parameters.user.isValid())
query.where("user_id = ?").bind(parameters.user); query.where("user_id = ?").bind(parameters.user);
if (parameters.scrobbler) if (parameters.backend)
query.where("scrobbler = ?").bind(*parameters.scrobbler); query.where("backend = ?").bind(*parameters.backend);
if (parameters.scrobblingState) if (parameters.syncState)
query.where("scrobbling_state = ?").bind(*parameters.scrobblingState); query.where("sync_state = ?").bind(*parameters.syncState);
return Utils::execQuery(query, parameters.range); return Utils::execQuery(query, parameters.range);
} }
Listen::pointer Listen::find(Session& session, UserId userId, TrackId trackId, Scrobbler scrobbler, const Wt::WDateTime& dateTime) Listen::pointer Listen::find(Session& session, UserId userId, TrackId trackId, ScrobblingBackend backend, const Wt::WDateTime& dateTime)
{ {
session.checkSharedLocked(); session.checkSharedLocked();
return session.getDboSession().find<Listen>() return session.getDboSession().find<Listen>()
.where("user_id = ?").bind(userId) .where("user_id = ?").bind(userId)
.where("track_id = ?").bind(trackId) .where("track_id = ?").bind(trackId)
.where("scrobbler = ?").bind(scrobbler) .where("backend = ?").bind(backend)
.where("date_time = ?").bind(Wt::WDateTime::fromTime_t(dateTime.toTime_t())) .where("date_time = ?").bind(Wt::WDateTime::fromTime_t(dateTime.toTime_t()))
.resultValue(); .resultValue();
} }
RangeResults<ArtistId> Listen::getTopArtists(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range) RangeResults<ArtistId> Listen::getTopArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range)
{ {
auto query{ createArtistsQuery(session.getDboSession(), userId, scrobbler, clusterIds, linkType) }; session.checkSharedLocked();
auto query{ createArtistsQuery(session.getDboSession(), userId, backend, clusterIds, linkType) };
auto collection{ query auto collection{ query
.orderBy("COUNT(a.id) DESC") .orderBy("COUNT(a.id) DESC")
@@ -197,72 +199,107 @@ namespace Database
return Utils::execQuery(query, range); return Utils::execQuery(query, range);
} }
RangeResults<ReleaseId> Listen::getTopReleases(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, Range range) RangeResults<ReleaseId> Listen::getTopReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, Range range)
{ {
auto query{ createReleasesQuery(session.getDboSession(), userId, scrobbler, clusterIds) session.checkSharedLocked();
auto query{ createReleasesQuery(session.getDboSession(), userId, backend, clusterIds)
.orderBy("COUNT(r.id) DESC") .orderBy("COUNT(r.id) DESC")
.groupBy("r.id") }; .groupBy("r.id") };
return Utils::execQuery(query, range); return Utils::execQuery(query, range);
} }
RangeResults<TrackId> Listen::getTopTracks(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, Range range) RangeResults<TrackId> Listen::getTopTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, Range range)
{ {
auto query{ createTracksQuery(session.getDboSession(), userId, scrobbler, clusterIds) session.checkSharedLocked();
auto query{ createTracksQuery(session.getDboSession(), userId, backend, clusterIds)
.orderBy("COUNT(t.id) DESC") .orderBy("COUNT(t.id) DESC")
.groupBy("t.id") }; .groupBy("t.id") };
return Utils::execQuery(query, range); return Utils::execQuery(query, range);
} }
RangeResults<ArtistId> Listen::getRecentArtists(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range) RangeResults<ArtistId> Listen::getRecentArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range)
{ {
auto query{ createArtistsQuery(session.getDboSession(), userId, scrobbler, clusterIds, linkType) session.checkSharedLocked();
auto query{ createArtistsQuery(session.getDboSession(), userId, backend, clusterIds, linkType)
.groupBy("a.id").having("l.date_time = MAX(l.date_time)") .groupBy("a.id").having("l.date_time = MAX(l.date_time)")
.orderBy("l.date_time DESC") }; .orderBy("l.date_time DESC") };
return Utils::execQuery(query, range); return Utils::execQuery(query, range);
} }
RangeResults<ReleaseId> Listen::getRecentReleases(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, Range range) RangeResults<ReleaseId> Listen::getRecentReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, Range range)
{ {
auto query{ createReleasesQuery(session.getDboSession(), userId, scrobbler, clusterIds) session.checkSharedLocked();
auto query{ createReleasesQuery(session.getDboSession(), userId, backend, clusterIds)
.groupBy("r.id").having("l.date_time = MAX(l.date_time)") .groupBy("r.id").having("l.date_time = MAX(l.date_time)")
.orderBy("l.date_time DESC") }; .orderBy("l.date_time DESC") };
return Utils::execQuery(query, range); return Utils::execQuery(query, range);
} }
RangeResults<TrackId> Listen::getRecentTracks(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, Range range) RangeResults<TrackId> Listen::getRecentTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, Range range)
{ {
auto query{ createTracksQuery(session.getDboSession(), userId, scrobbler, clusterIds) session.checkSharedLocked();
auto query{ createTracksQuery(session.getDboSession(), userId, backend, clusterIds)
.groupBy("t.id").having("l.date_time = MAX(l.date_time)") .groupBy("t.id").having("l.date_time = MAX(l.date_time)")
.orderBy("l.date_time DESC") }; .orderBy("l.date_time DESC") };
return Utils::execQuery(query, range); return Utils::execQuery(query, range);
} }
Listen::pointer Listen::getMostRecentListen(Session& session, UserId userId, Scrobbler scrobbler, ReleaseId releaseId) std::size_t Listen::getCount(Session& session, UserId userId, ScrobblingBackend backend, TrackId trackId)
{ {
session.checkSharedLocked();
return session.getDboSession().query<int>("SELECT COUNT(*) from listen l")
.where("l.track_id = ?").bind(trackId)
.where("l.user_id = ?").bind(userId)
.where("l.backend = ?").bind(backend)
.resultValue();
}
std::size_t Listen::getCount(Session& session, UserId userId, ScrobblingBackend backend, ReleaseId releaseId)
{
session.checkSharedLocked();
return session.getDboSession().query<int>("SELECT IFNULL(MIN(count_result), 0)"
" FROM ("
" SELECT COUNT(l.track_id) AS count_result"
" FROM track t"
" LEFT JOIN listen l ON t.id = l.track_id AND l.backend = ? AND l.user_id = ?"
" WHERE t.release_id = ?"
" GROUP BY t.id)")
.bind(backend)
.bind(userId)
.bind(releaseId)
.resultValue();
}
Listen::pointer Listen::getMostRecentListen(Session& session, UserId userId, ScrobblingBackend backend, ReleaseId releaseId)
{
session.checkSharedLocked();
// TODO not pending remove? // TODO not pending remove?
return session.getDboSession().query<Wt::Dbo::ptr<Listen>>("SELECT l from listen l") return session.getDboSession().query<Wt::Dbo::ptr<Listen>>("SELECT l from listen l")
.join("track t ON l.track_id = t.id") .join("track t ON l.track_id = t.id")
.where("t.release_id = ?").bind(releaseId) .where("t.release_id = ?").bind(releaseId)
.where("l.user_id = ?").bind(userId) .where("l.user_id = ?").bind(userId)
.where("l.scrobbler = ?").bind(scrobbler) .where("l.backend = ?").bind(backend)
.orderBy("l.date_time DESC") .orderBy("l.date_time DESC")
.limit(1) .limit(1)
.resultValue(); .resultValue();
} }
Listen::pointer Listen::getMostRecentListen(Session& session, UserId userId, Scrobbler scrobbler, TrackId trackId) Listen::pointer Listen::getMostRecentListen(Session& session, UserId userId, ScrobblingBackend backend, TrackId trackId)
{ {
session.checkSharedLocked();
// TODO not pending remove? // TODO not pending remove?
return session.getDboSession().query<Wt::Dbo::ptr<Listen>>("SELECT l from listen l") return session.getDboSession().query<Wt::Dbo::ptr<Listen>>("SELECT l from listen l")
.join("track t ON track_id = t.id") .where("l.track_id = ?").bind(trackId)
.where("t.id = ?").bind(trackId)
.where("l.user_id = ?").bind(userId) .where("l.user_id = ?").bind(userId)
.where("l.scrobbler = ?").bind(scrobbler) .where("l.backend = ?").bind(backend)
.orderBy("l.date_time DESC") .orderBy("l.date_time DESC")
.limit(1) .limit(1)
.resultValue(); .resultValue();
+28 -435
View File
@@ -72,414 +72,6 @@ namespace Database::Migration
Db& _db; Db& _db;
}; };
static std::string dateTimeToDbFormat(const Wt::WDateTime& dateTime)
{
return dateTime.toString("yyyy'-'MM'-'dd'T'hh':'mm':'ss'.000'", false).toUTF8();
}
static void migrateFromV5(Session& session)
{
session.getDboSession().execute("DELETE FROM auth_token"); // format has changed
}
static void migrateFromV6(Session& session)
{
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
ScanSettings::get(session).modify()->incScanVersion();
}
static void migrateFromV7(Session& session)
{
session.getDboSession().execute("DROP TABLE similarity_settings");
session.getDboSession().execute("DROP TABLE similarity_settings_feature");
session.getDboSession().execute("ALTER TABLE scan_settings ADD similarity_engine_type INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(ScanSettings::RecommendationEngineType::Clusters)) + ")");
}
static void migrateFromV8(Session& session)
{
// Better cover handling, need to rescan the whole files
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
ScanSettings::get(session).modify()->incScanVersion();
}
static void migrateFromV9(Session& session)
{
session.getDboSession().execute(R"(
CREATE TABLE IF NOT EXISTS "track_bookmark" (
"id" integer primary key autoincrement,
"version" integer not null,
"offset" integer,
"comment" text not null,
"track_id" bigint,
"user_id" bigint,
constraint "fk_track_bookmark_track" foreign key ("track_id") references "track" ("id") on delete cascade deferrable initially deferred,
constraint "fk_track_bookmark_user" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred
);)");
}
static void migrateFromV10(Session& session)
{
ScanSettings::get(session).modify()->addAudioFileExtension(".m4b");
ScanSettings::get(session).modify()->addAudioFileExtension(".alac");
}
static void migrateFromV11(Session& session)
{
// Sanitize bad MBID, need to rescan the whole files
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
ScanSettings::get(session).modify()->incScanVersion();
}
static void migrateFromV12(Session& session)
{
// Artist and release that have a badly parsed name but a MBID had no chance to updat the name
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
ScanSettings::get(session).modify()->incScanVersion();
}
static void migrateFromV13(Session& session)
{
// Always store UUID in lower case + better WMA parsing
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
ScanSettings::get(session).modify()->incScanVersion();
}
static void migrateFromV14(Session& session)
{
// SortName now set from metadata
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
ScanSettings::get(session).modify()->incScanVersion();
}
static void migrateFromV15(Session& session)
{
session.getDboSession().execute("ALTER TABLE user ADD ui_theme INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(User::defaultUITheme)) + ")");
}
static void migrateFromV16(Session& session)
{
session.getDboSession().execute("ALTER TABLE track ADD total_disc INTEGER NOT NULL DEFAULT(0)");
session.getDboSession().execute("ALTER TABLE track ADD total_track INTEGER NOT NULL DEFAULT(0)");
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
ScanSettings::get(session).modify()->incScanVersion();
}
static void migrateFromV17(Session& session)
{
// Drop colums total_disc/total_track from release
session.getDboSession().execute(R"(
CREATE TABLE "release_backup" (
"id" integer primary key autoincrement,
"version" integer not null,
"name" text not null,
"mbid" text not null
))");
session.getDboSession().execute("INSERT INTO release_backup SELECT id,version,name,mbid FROM release");
session.getDboSession().execute("DROP TABLE release");
session.getDboSession().execute("ALTER TABLE release_backup RENAME TO release");
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
ScanSettings::get(session).modify()->incScanVersion();
}
static void migrateFromV18(Session& session)
{
session.getDboSession().execute(R"(
CREATE TABLE IF NOT EXISTS "subsonic_settings" (
"id" integer primary key autoincrement,
"version" integer not null,
"api_enabled" boolean not null,
"artist_list_mode" integer not null
))");
}
static void migrateFromV19(Session& session)
{
session.getDboSession().execute(R"(
CREATE TABLE "user_backup" (
"id" integer primary key autoincrement,
"version" integer not null,
"type" integer not null,
"login_name" text not null,
"password_salt" text not null,
"password_hash" text not null,
"last_login" text,
"subsonic_transcode_enable" boolean not null,
"subsonic_transcode_format" integer not null,
"subsonic_transcode_bitrate" integer not null,
"subsonic_artist_list_mode" integer not null,
"ui_theme" integer not null,
"cur_playing_track_pos" integer not null,
"repeat_all" boolean not null,
"radio" boolean not null
))");
session.getDboSession().execute(std::string{ "INSERT INTO user_backup SELECT id, version, type, login_name, password_salt, password_hash, last_login, " }
+ "1" // default enable transcode
+ ", " + std::to_string(static_cast<int>(User::defaultSubsonicTranscodeFormat))
+ ", " + std::to_string(User::defaultSubsonicTranscodeBitrate)
+ ", " + std::to_string(static_cast<int>(User::defaultSubsonicArtistListMode))
+ ", ui_theme, cur_playing_track_pos, repeat_all, radio FROM user");
session.getDboSession().execute("DROP TABLE user");
session.getDboSession().execute("ALTER TABLE user_backup RENAME TO user");
}
static void migrateFromV20(Session& session)
{
session.getDboSession().execute("DROP TABLE subsonic_settings");
}
static void migrateFromV21(Session& session)
{
session.getDboSession().execute("ALTER TABLE track ADD track_replay_gain REAL");
session.getDboSession().execute("ALTER TABLE track ADD release_replay_gain REAL");
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
ScanSettings::get(session).modify()->incScanVersion();
}
static void migrateFromV22(Session& session)
{
session.getDboSession().execute("ALTER TABLE track ADD disc_subtitle TEXT NOT NULL DEFAULT ''");
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
ScanSettings::get(session).modify()->incScanVersion();
}
static void migrateFromV23(Session& session)
{
// Better cover detection
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
ScanSettings::get(session).modify()->incScanVersion();
}
static void migrateFromV24(Session& session)
{
// User's AuthMode
session.getDboSession().execute("ALTER TABLE user ADD auth_mode INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(/*User::defaultAuthMode*/0)) + ")");
}
static void migrateFromV25(Session& session)
{
// Better cover detection
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
ScanSettings::get(session).modify()->incScanVersion();
}
static void migrateFromV26(Session& session)
{
// Composer, mixer, etc. support
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
ScanSettings::get(session).modify()->incScanVersion();
}
static void migrateFromV27(Session& session)
{
// Composer, mixer, etc. support, now fallback on MBID tagged entries as there is no mean to provide MBID by tags for these kinf od artists
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
ScanSettings::get(session).modify()->incScanVersion();
}
static void migrateFromV28(Session& session)
{
// Drop Auth mode
session.getDboSession().execute(R"(
CREATE TABLE "user_backup" (
"id" integer primary key autoincrement,
"version" integer not null,
"type" integer not null,
"login_name" text not null,
"password_salt" text not null,
"password_hash" text not null,
"last_login" text,
"subsonic_transcode_enable" boolean not null,
"subsonic_transcode_format" integer not null,
"subsonic_transcode_bitrate" integer not null,
"subsonic_artist_list_mode" integer not null,
"ui_theme" integer not null,
"cur_playing_track_pos" integer not null,
"repeat_all" boolean not null,
"radio" boolean not null
))");
session.getDboSession().execute("INSERT INTO user_backup SELECT id, version, type, login_name, password_salt, password_hash, last_login, subsonic_transcode_enable, subsonic_transcode_format, subsonic_transcode_bitrate, subsonic_artist_list_mode, ui_theme, cur_playing_track_pos, repeat_all, radio FROM user");
session.getDboSession().execute("DROP TABLE user");
session.getDboSession().execute("ALTER TABLE user_backup RENAME TO user");
}
static void migrateFromV29(Session& session)
{
session.getDboSession().execute("ALTER TABLE tracklist_entry ADD date_time TEXT");
session.getDboSession().execute("ALTER TABLE user ADD listenbrainz_token TEXT");
session.getDboSession().execute("ALTER TABLE user ADD scrobbler INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(User::defaultScrobbler)) + ")");
session.getDboSession().execute("ALTER TABLE track ADD recording_mbid TEXT");
session.getDboSession().execute("DELETE from tracklist WHERE name = ?").bind("__played_tracks__");
// MBID changes
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
ScanSettings::get(session).modify()->incScanVersion();
}
static void migrateFromV30(Session& session)
{
// drop "year" and "original_year" (rescan needed to convert them into dates)
session.getDboSession().execute(R"(
CREATE TABLE "track_backup" (
"id" integer primary key autoincrement,
"version" integer not null,
"scan_version" integer not null,
"track_number" integer not null,
"disc_number" integer not null,
"name" text not null,
"duration" integer,
"date" integer text,
"original_date" integer text,
"file_path" text not null,
"file_last_write" text,
"file_added" text,
"has_cover" boolean not null,
"mbid" text not null,
"copyright" text not null,
"copyright_url" text not null,
"release_id" bigint, total_disc INTEGER NOT NULL DEFAULT(0), total_track INTEGER NOT NULL DEFAULT(0), track_replay_gain REAL, release_replay_gain REAL, disc_subtitle TEXT NOT NULL DEFAULT '', recording_mbid TEXT,
constraint "fk_track_release" foreign key ("release_id") references "release" ("id") on delete cascade deferrable initially deferred
))");
session.getDboSession().execute("INSERT INTO track_backup SELECT id, version, scan_version, track_number, disc_number, name, duration, \"1900-01-01\", \"1900-01-01\", file_path, file_last_write, file_added, has_cover, mbid, copyright, copyright_url, release_id, total_disc, total_track, track_replay_gain, release_replay_gain, disc_subtitle, recording_mbid FROM track");
session.getDboSession().execute("DROP TABLE track");
session.getDboSession().execute("ALTER TABLE track_backup RENAME TO track");
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
ScanSettings::get(session).modify()->incScanVersion();
}
static void migrateFromV31(Session& session)
{
// new star system, using dedicated entries per scrobbler and date time
session.getDboSession().execute(R"(
CREATE TABLE "starred_artist" (
"id" integer primary key autoincrement,
"version" integer not null,
"scrobbler" integer not null,
"date_time" text,
"artist_id" bigint,
"user_id" bigint,
constraint "fk_starred_artist_artist" foreign key ("artist_id") references "artist" ("id") on delete cascade deferrable initially deferred,
constraint "fk_starred_artist_user" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred
))");
session.getDboSession().execute(R"(
CREATE TABLE "starred_release" (
"id" integer primary key autoincrement,
"version" integer not null,
"scrobbler" integer not null,
"date_time" text,
"release_id" bigint,
"user_id" bigint,
constraint "fk_starred_release_release" foreign key ("release_id") references "release" ("id") on delete cascade deferrable initially deferred,
constraint "fk_starred_release_user" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred
))");
session.getDboSession().execute(R"(
CREATE TABLE "starred_track" (
"id" integer primary key autoincrement,
"version" integer not null,
"scrobbler" integer not null,
"date_time" text,
"track_id" bigint,
"user_id" bigint,
constraint "fk_starred_track_track" foreign key ("track_id") references "track" ("id") on delete cascade deferrable initially deferred,
constraint "fk_starred_track_user" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred
))");
// Can't migrate using class mapping as mapping may evolve in the future
// use time_t to avoid rounding issues later
const std::string now{ dateTimeToDbFormat(Wt::WDateTime::fromTime_t(Wt::WDateTime::currentDateTime().toTime_t())) };
std::map<IdType::ValueType, Scrobbler> userScrobblers;
auto getScrobbler{ [&](IdType::ValueType userId)
{
auto itScrobbler {userScrobblers.find(userId)};
if (itScrobbler != std::cend(userScrobblers))
return itScrobbler->second;
auto query {session.getDboSession().query<Scrobbler>("SELECT scrobbler FROM user WHERE id = ?").bind(userId)};
[[maybe_unused]] auto [itInserted, inserted] {userScrobblers.emplace(userId, query.resultValue())};
assert(inserted);
return itInserted->second;
} };
auto migrateStarEntries{ [&session, &getScrobbler, &now](const std::string& colName, const std::string& oldTableName, const std::string& newTableName)
{
using UserIdObjectId = std::tuple<IdType::ValueType /* userId */, IdType::ValueType /* entryId */>;
std::vector<UserIdObjectId> starredEntries;
auto query {session.getDboSession().query<UserIdObjectId>("SELECT user_id, " + colName + " from " + oldTableName)};
auto results {query.resultList()};
LMS_LOG(DB, INFO) << "Found " << results.size() << " " << colName << " to migrate";
for (const auto& [userId, entryId] : results)
{
session.getDboSession().execute("INSERT INTO " + newTableName + " ('version', 'scrobbler', 'date_time', '" + colName + "', 'user_id') VALUES (?, ?, ?, ?, ?)")
.bind(0)
.bind(getScrobbler(userId))
.bind(now)
.bind(entryId)
.bind(userId);
}
session.getDboSession().execute("DROP TABLE " + oldTableName);
} };
migrateStarEntries("artist_id", "user_artist_starred", "starred_artist");
migrateStarEntries("release_id", "user_release_starred", "starred_release");
migrateStarEntries("track_id", "user_track_starred", "starred_track");
// new listen system, no longer using tracklists
session.getDboSession().execute(R"(
CREATE TABLE "listen" (
"id" integer primary key autoincrement,
"version" integer not null,
"date_time" text,
"scrobbler" integer not null,
"scrobbling_state" integer not null,
"track_id" bigint,
"user_id" bigint,
constraint "fk_listen_track" foreign key ("track_id") references "track" ("id") on delete cascade deferrable initially deferred,
constraint "fk_listen_user" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred
))");
auto migrateListens{ [&session](const std::string& trackListName, Scrobbler scrobbler)
{
using UserIdObjectId = std::tuple<IdType::ValueType /* userId */, IdType::ValueType /* trackId */, Wt::WDateTime>;
std::vector<UserIdObjectId> listens;
auto query {session.getDboSession().query<UserIdObjectId>("SELECT t_l.user_id, t_l_e.track_id, t_l_e.date_time FROM tracklist t_l")
.join("tracklist_entry t_l_e ON t_l_e.tracklist_id = t_l.id")
.where("t_l.name = ?").bind(trackListName)};
auto results {query.resultList()};
listens.reserve(results.size());
LMS_LOG(DB, INFO) << "Found " << results.size() << " listens in " << trackListName;
for (const auto& [userId, trackId, dateTime] : results)
{
session.getDboSession().execute("INSERT INTO listen ('version', 'date_time', 'scrobbler', 'scrobbling_state', 'track_id', 'user_id') VALUES (?, ?, ?, ?, ?, ?)")
.bind(0)
.bind(dateTimeToDbFormat(dateTime))
.bind(scrobbler)
.bind(ScrobblingState::Synchronized) // consider sync is done to avoid duplicate submissions
.bind(trackId)
.bind(userId);
}
} };
migrateListens("__scrobbler_internal_history__", Scrobbler::Internal);
migrateListens("__scrobbler_listenbrainz_history__", Scrobbler::ListenBrainz);
}
static void migrateFromV32(Session& session) static void migrateFromV32(Session& session)
{ {
ScanSettings::get(session).modify()->addAudioFileExtension(".wv"); ScanSettings::get(session).modify()->addAudioFileExtension(".wv");
@@ -605,6 +197,33 @@ CREATE TABLE IF NOT EXISTS "track_backup" (
session.getDboSession().execute("ALTER TABLE user DROP COLUMN subsonic_transcode_enable"); session.getDboSession().execute("ALTER TABLE user DROP COLUMN subsonic_transcode_enable");
} }
static void migrateFromV42(Session& session)
{
session.getDboSession().execute("DROP INDEX IF EXISTS listen_scrobbler_idx");
session.getDboSession().execute("DROP INDEX IF EXISTS listen_user_scrobbler_idx");
session.getDboSession().execute("DROP INDEX IF EXISTS listen_user_track_scrobbler_date_time_idx");
session.getDboSession().execute("DROP INDEX IF EXISTS starred_artist_user_scrobbler_idx");
session.getDboSession().execute("DROP INDEX IF EXISTS starred_artist_artist_user_scrobbler_idx");
session.getDboSession().execute("DROP INDEX IF EXISTS starred_release_user_scrobbler_idx");
session.getDboSession().execute("DROP INDEX IF EXISTS starred_release_release_user_scrobbler_idx");
session.getDboSession().execute("DROP INDEX IF EXISTS starred_track_user_scrobbler_idx");
session.getDboSession().execute("DROP INDEX IF EXISTS starred_track_track_user_scrobbler_idx");
// New feedback service that now handles the star/unstar stuff (that was previously handled by the scrobbling service)
session.getDboSession().execute("ALTER TABLE user RENAME COLUMN scrobbler TO scrobbling_backend");
session.getDboSession().execute("ALTER TABLE user ADD feedback_backend INTEGER");
session.getDboSession().execute("ALTER TABLE listen RENAME COLUMN scrobbler TO backend");
session.getDboSession().execute("ALTER TABLE listen RENAME COLUMN scrobbling_state TO sync_state");
session.getDboSession().execute("ALTER TABLE starred_artist RENAME COLUMN scrobbler TO backend");
session.getDboSession().execute("ALTER TABLE starred_artist RENAME COLUMN scrobbling_state TO sync_state");
session.getDboSession().execute("ALTER TABLE starred_release RENAME COLUMN scrobbler TO backend");
session.getDboSession().execute("ALTER TABLE starred_release RENAME COLUMN scrobbling_state TO sync_state");
session.getDboSession().execute("ALTER TABLE starred_track RENAME COLUMN scrobbler TO backend");
session.getDboSession().execute("ALTER TABLE starred_track RENAME COLUMN scrobbling_state TO sync_state");
session.getDboSession().execute("UPDATE user SET feedback_backend = scrobbling_backend");
}
void doDbMigration(Session& session) void doDbMigration(Session& session)
{ {
static const std::string outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" }; static const std::string outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
@@ -615,33 +234,6 @@ CREATE TABLE IF NOT EXISTS "track_backup" (
const std::map<unsigned, MigrationFunction> migrationFunctions const std::map<unsigned, MigrationFunction> migrationFunctions
{ {
{5, migrateFromV5},
{6, migrateFromV6},
{7, migrateFromV7},
{8, migrateFromV8},
{9, migrateFromV9},
{10, migrateFromV10},
{11, migrateFromV11},
{12, migrateFromV12},
{13, migrateFromV13},
{14, migrateFromV14},
{15, migrateFromV15},
{16, migrateFromV16},
{17, migrateFromV17},
{18, migrateFromV18},
{19, migrateFromV19},
{20, migrateFromV20},
{21, migrateFromV21},
{22, migrateFromV22},
{23, migrateFromV23},
{24, migrateFromV24},
{25, migrateFromV25},
{26, migrateFromV26},
{27, migrateFromV27},
{28, migrateFromV28},
{29, migrateFromV29},
{30, migrateFromV30},
{31, migrateFromV31},
{32, migrateFromV32}, {32, migrateFromV32},
{33, migrateFromV33}, {33, migrateFromV33},
{34, migrateFromV34}, {34, migrateFromV34},
@@ -652,6 +244,7 @@ CREATE TABLE IF NOT EXISTS "track_backup" (
{39, migrateFromV39}, {39, migrateFromV39},
{40, migrateFromV40}, {40, migrateFromV40},
{41, migrateFromV41}, {41, migrateFromV41},
{42, migrateFromV42},
}; };
{ {
@@ -26,7 +26,7 @@ namespace Database
class Session; class Session;
using Version = std::size_t; using Version = std::size_t;
static constexpr Version LMS_DATABASE_VERSION{ 42 }; static constexpr Version LMS_DATABASE_VERSION{ 43 };
class VersionInfo class VersionInfo
{ {
public: public:
+159 -123
View File
@@ -34,147 +34,170 @@
namespace Database namespace Database
{ {
namespace
Wt::Dbo::Query<ReleaseId> createQuery(Session& session, const Release::FindParameters& params)
{ {
auto query{ session.getDboSession().query<ReleaseId>("SELECT DISTINCT r.id from release r") }; template <typename ResultType>
Wt::Dbo::Query<ResultType> createQuery(Session& session, std::string_view itemToSelect, const Release::FindParameters& params)
if (params.sortMethod == ReleaseSortMethod::LastWritten
|| params.sortMethod == ReleaseSortMethod::Date
|| params.sortMethod == ReleaseSortMethod::OriginalDate
|| params.sortMethod == ReleaseSortMethod::OriginalDateDesc
|| params.writtenAfter.isValid()
|| params.dateRange
|| params.artist.isValid())
{ {
query.join("track t ON t.release_id = r.id"); auto query{ session.getDboSession().query<ResultType>("SELECT DISTINCT " + std::string{ itemToSelect } + " from release r") };
}
if (params.writtenAfter.isValid()) if (params.sortMethod == ReleaseSortMethod::LastWritten
query.where("t.file_last_write > ?").bind(params.writtenAfter); || params.sortMethod == ReleaseSortMethod::Date
|| params.sortMethod == ReleaseSortMethod::OriginalDate
if (params.dateRange) || params.sortMethod == ReleaseSortMethod::OriginalDateDesc
{ || params.writtenAfter.isValid()
query.where("t.date >= ?").bind(params.dateRange->begin); || params.dateRange
query.where("t.date <= ?").bind(params.dateRange->end); || params.artist.isValid()
} || params.clusters.size() == 1)
for (std::string_view keyword : params.keywords)
query.where("r.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + Utils::escapeLikeKeyword(keyword) + "%");
if (params.starringUser.isValid())
{
assert(params.scrobbler);
query.join("starred_release s_r ON s_r.release_id = r.id")
.where("s_r.user_id = ?").bind(params.starringUser)
.where("s_r.scrobbler = ?").bind(*params.scrobbler)
.where("s_r.scrobbling_state <> ?").bind(ScrobblingState::PendingRemove);
}
if (params.artist.isValid())
{
query.join("artist a ON a.id = t_a_l.artist_id")
.join("track_artist_link t_a_l ON t_a_l.track_id = t.id")
.where("a.id = ?").bind(params.artist);
if (!params.trackArtistLinkTypes.empty())
{ {
std::ostringstream oss; query.join("track t ON t.release_id = r.id");
bool first{ true };
for (TrackArtistLinkType linkType : params.trackArtistLinkTypes)
{
if (!first)
oss << " OR ";
oss << "t_a_l.type = ?";
query.bind(linkType);
first = false;
}
query.where(oss.str());
} }
if (!params.excludedTrackArtistLinkTypes.empty()) if (params.writtenAfter.isValid())
query.where("t.file_last_write > ?").bind(params.writtenAfter);
if (params.dateRange)
{
query.where("t.date >= ?").bind(params.dateRange->begin);
query.where("t.date <= ?").bind(params.dateRange->end);
}
for (std::string_view keyword : params.keywords)
query.where("r.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + Utils::escapeLikeKeyword(keyword) + "%");
if (params.starringUser.isValid())
{
assert(params.feedbackBackend);
query.join("starred_release s_r ON s_r.release_id = r.id")
.where("s_r.user_id = ?").bind(params.starringUser)
.where("s_r.backend = ?").bind(*params.feedbackBackend)
.where("s_r.sync_state <> ?").bind(SyncState::PendingRemove);
}
if (params.artist.isValid())
{
query.join("artist a ON a.id = t_a_l.artist_id")
.join("track_artist_link t_a_l ON t_a_l.track_id = t.id")
.where("a.id = ?").bind(params.artist);
if (!params.trackArtistLinkTypes.empty())
{
std::ostringstream oss;
bool first{ true };
for (TrackArtistLinkType linkType : params.trackArtistLinkTypes)
{
if (!first)
oss << " OR ";
oss << "t_a_l.type = ?";
query.bind(linkType);
first = false;
}
query.where(oss.str());
}
if (!params.excludedTrackArtistLinkTypes.empty())
{
std::ostringstream oss;
oss << "r.id NOT IN (SELECT DISTINCT r.id FROM release r"
" INNER JOIN artist a ON a.id = t_a_l.artist_id"
" INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id"
" INNER JOIN track t ON t.release_id = r.id"
" WHERE (a.id = ? AND (";
query.bind(params.artist);
bool first{ true };
for (const TrackArtistLinkType linkType : params.excludedTrackArtistLinkTypes)
{
if (!first)
oss << " OR ";
oss << "t_a_l.type = ?";
query.bind(linkType);
first = false;
}
oss << ")))";
query.where(oss.str());
}
}
if (params.clusters.size() == 1)
{
query.join("track_cluster t_c ON t_c.track_id = t.id")
.where("t_c.cluster_id = ?").bind(params.clusters.front());
}
else if (params.clusters.size() > 1)
{ {
std::ostringstream oss; std::ostringstream oss;
oss << "r.id NOT IN (SELECT DISTINCT r.id FROM release r" oss << "r.id IN (SELECT DISTINCT r.id FROM release r"
" INNER JOIN artist a ON a.id = t_a_l.artist_id"
" INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id"
" INNER JOIN track t ON t.release_id = r.id" " INNER JOIN track t ON t.release_id = r.id"
" WHERE (a.id = ? AND ("; " INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
query.bind(params.artist); WhereClause clusterClause;
for (const ClusterId clusterId : params.clusters)
bool first{ true };
for (const TrackArtistLinkType linkType : params.excludedTrackArtistLinkTypes)
{ {
if (!first) clusterClause.Or(WhereClause("t_c.cluster_id = ?"));
oss << " OR "; query.bind(clusterId);
oss << "t_a_l.type = ?";
query.bind(linkType);
first = false;
} }
oss << ")))";
oss << " " << clusterClause.get();
oss << " GROUP BY t.id HAVING COUNT(*) = " << params.clusters.size() << ")";
query.where(oss.str()); query.where(oss.str());
} }
}
if (!params.clusters.empty()) if (params.primaryType)
{ query.where("primary_type = ?").bind(*params.primaryType);
std::ostringstream oss; if (!params.secondaryTypes.empty())
oss << "r.id IN (SELECT DISTINCT r.id FROM release r" query.where("secondary_type = ?").bind(params.secondaryTypes);
" INNER JOIN track t ON t.release_id = r.id"
" INNER JOIN cluster c ON c.id = t_c.cluster_id"
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
WhereClause clusterClause; switch (params.sortMethod)
for (const ClusterId clusterId : params.clusters)
{ {
clusterClause.Or(WhereClause("c.id = ?")); case ReleaseSortMethod::None:
query.bind(clusterId); break;
case ReleaseSortMethod::Name:
query.orderBy("r.name COLLATE NOCASE");
break;
case ReleaseSortMethod::Random:
query.orderBy("RANDOM()");
break;
case ReleaseSortMethod::LastWritten:
query.orderBy("t.file_last_write DESC");
break;
case ReleaseSortMethod::Date:
query.orderBy("t.date, r.name COLLATE NOCASE");
break;
case ReleaseSortMethod::OriginalDate:
query.orderBy("CASE WHEN t.original_date IS NULL THEN t.date ELSE t.original_date END, t.date, r.name COLLATE NOCASE");
break;
case ReleaseSortMethod::OriginalDateDesc:
query.orderBy("CASE WHEN t.original_date IS NULL THEN t.date ELSE t.original_date END DESC, t.date, r.name COLLATE NOCASE");
break;
case ReleaseSortMethod::StarredDateDesc:
assert(params.starringUser.isValid());
query.orderBy("s_r.date_time DESC");
break;
} }
oss << " " << clusterClause.get(); return query;
oss << " GROUP BY t.id HAVING COUNT(*) = " << params.clusters.size() << ")";
query.where(oss.str());
} }
if (params.primaryType) template <typename ResultType>
query.where("primary_type = ?").bind(*params.primaryType); Wt::Dbo::Query<ResultType> createQuery(Session& session, const Release::FindParameters& params)
if (!params.secondaryTypes.empty())
query.where("secondary_type = ?").bind(params.secondaryTypes);
switch (params.sortMethod)
{ {
case ReleaseSortMethod::None: std::string_view itemToSelect;
break;
case ReleaseSortMethod::Name: if constexpr (std::is_same_v<ResultType, ReleaseId>)
query.orderBy("r.name COLLATE NOCASE"); itemToSelect = "r.id";
break; else if constexpr (std::is_same_v<ResultType, Wt::Dbo::ptr<Release>>)
case ReleaseSortMethod::Random: itemToSelect = "r";
query.orderBy("RANDOM()"); else
break; static_assert("Unhandled type");
case ReleaseSortMethod::LastWritten:
query.orderBy("t.file_last_write DESC");
break;
case ReleaseSortMethod::Date:
query.orderBy("t.date, r.name COLLATE NOCASE");
break;
case ReleaseSortMethod::OriginalDate:
query.orderBy("CASE WHEN t.original_date IS NULL THEN t.date ELSE t.original_date END, t.date, r.name COLLATE NOCASE");
break;
case ReleaseSortMethod::OriginalDateDesc:
query.orderBy("CASE WHEN t.original_date IS NULL THEN t.date ELSE t.original_date END DESC, t.date, r.name COLLATE NOCASE");
break;
case ReleaseSortMethod::StarredDateDesc:
assert(params.starringUser.isValid());
query.orderBy("s_r.date_time DESC");
break;
}
return query; return createQuery<ResultType>(session, itemToSelect, params);
}
} }
Release::Release(const std::string& name, const std::optional<UUID>& MBID) Release::Release(const std::string& name, const std::optional<UUID>& MBID)
@@ -233,7 +256,7 @@ namespace Database
return session.getDboSession().query<int>("SELECT COUNT(*) FROM release"); return session.getDboSession().query<int>("SELECT COUNT(*) FROM release");
} }
RangeResults<ReleaseId> Release::findOrderedByArtist(Session& session, Range range) RangeResults<ReleaseId> Release::findIdsOrderedByArtist(Session& session, Range range)
{ {
session.checkSharedLocked(); session.checkSharedLocked();
@@ -248,7 +271,7 @@ namespace Database
return Utils::execQuery(query, range); return Utils::execQuery(query, range);
} }
RangeResults<ReleaseId> Release::findOrphans(Session& session, Range range) RangeResults<ReleaseId> Release::findOrphanIds(Session& session, Range range)
{ {
session.checkSharedLocked(); session.checkSharedLocked();
@@ -256,12 +279,19 @@ namespace Database
return Utils::execQuery(query, range); return Utils::execQuery(query, range);
} }
RangeResults<ReleaseId> Release::find(Session& session, const FindParameters& params) RangeResults<Release::pointer> Release::find(Session& session, const FindParameters& params)
{ {
session.checkSharedLocked(); session.checkSharedLocked();
auto query{ createQuery(session, params) }; auto query{ createQuery<Wt::Dbo::ptr<Release>>(session, params) };
return Utils::execQuery(query, params.range);
}
RangeResults<ReleaseId> Release::findIds(Session& session, const FindParameters& params)
{
session.checkSharedLocked();
auto query{ createQuery<ReleaseId>(session, params) };
return Utils::execQuery(query, params.range); return Utils::execQuery(query, params.range);
} }
@@ -382,12 +412,18 @@ namespace Database
{ {
assert(session()); assert(session());
// Select the similar releases using the 5 most used clusters of the release
auto res{ session()->query<Wt::Dbo::ptr<Release>>( auto res{ session()->query<Wt::Dbo::ptr<Release>>(
"SELECT r FROM release r" "SELECT r FROM release r"
" INNER JOIN track t ON t.release_id = r.id" " INNER JOIN track t ON t.release_id = r.id"
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id" " INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
" WHERE " " WHERE "
" t_c.cluster_id IN (SELECT c.id from cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN release r ON r.id = t.release_id WHERE r.id = ?)" " t_c.cluster_id IN "
"(SELECT DISTINCT c.id FROM cluster c"
" INNER JOIN track t ON c.id = t_c.cluster_id"
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
" INNER JOIN release r ON r.id = t.release_id"
" WHERE r.id = ?)"
" AND r.id <> ?" " AND r.id <> ?"
) )
.bind(getId()) .bind(getId())
+21 -10
View File
@@ -140,13 +140,13 @@ namespace Database
_session.execute("CREATE INDEX IF NOT EXISTS release_name_idx ON release(name)"); _session.execute("CREATE INDEX IF NOT EXISTS release_name_idx ON release(name)");
_session.execute("CREATE INDEX IF NOT EXISTS release_name_nocase_idx ON release(name COLLATE NOCASE)"); _session.execute("CREATE INDEX IF NOT EXISTS release_name_nocase_idx ON release(name COLLATE NOCASE)");
_session.execute("CREATE INDEX IF NOT EXISTS release_mbid_idx ON release(mbid)"); _session.execute("CREATE INDEX IF NOT EXISTS release_mbid_idx ON release(mbid)");
_session.execute("CREATE INDEX IF NOT EXISTS track_file_last_write_idx ON track(file_last_write)");
_session.execute("CREATE INDEX IF NOT EXISTS track_path_idx ON track(file_path)"); _session.execute("CREATE INDEX IF NOT EXISTS track_path_idx ON track(file_path)");
_session.execute("CREATE INDEX IF NOT EXISTS track_name_idx ON track(name)"); _session.execute("CREATE INDEX IF NOT EXISTS track_name_idx ON track(name)");
_session.execute("CREATE INDEX IF NOT EXISTS track_name_nocase_idx ON track(name COLLATE NOCASE)"); _session.execute("CREATE INDEX IF NOT EXISTS track_name_nocase_idx ON track(name COLLATE NOCASE)");
_session.execute("CREATE INDEX IF NOT EXISTS track_mbid_idx ON track(mbid)"); _session.execute("CREATE INDEX IF NOT EXISTS track_mbid_idx ON track(mbid)");
_session.execute("CREATE INDEX IF NOT EXISTS track_recording_mbid_idx ON track(recording_mbid)"); _session.execute("CREATE INDEX IF NOT EXISTS track_recording_mbid_idx ON track(recording_mbid)");
_session.execute("CREATE INDEX IF NOT EXISTS track_release_idx ON track(release_id)"); _session.execute("CREATE INDEX IF NOT EXISTS track_release_idx ON track(release_id)");
_session.execute("CREATE INDEX IF NOT EXISTS track_file_last_write_idx ON track(file_last_write)");
_session.execute("CREATE INDEX IF NOT EXISTS track_date_idx ON track(date)"); _session.execute("CREATE INDEX IF NOT EXISTS track_date_idx ON track(date)");
_session.execute("CREATE INDEX IF NOT EXISTS track_original_date_idx ON track(original_date)"); _session.execute("CREATE INDEX IF NOT EXISTS track_original_date_idx ON track(original_date)");
_session.execute("CREATE INDEX IF NOT EXISTS tracklist_name_idx ON tracklist(name)"); _session.execute("CREATE INDEX IF NOT EXISTS tracklist_name_idx ON tracklist(name)");
@@ -158,15 +158,16 @@ namespace Database
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_artist_type_idx ON track_artist_link(artist_id,type)"); _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_artist_type_idx ON track_artist_link(artist_id,type)");
_session.execute("CREATE INDEX IF NOT EXISTS track_bookmark_user_idx ON track_bookmark(user_id)"); _session.execute("CREATE INDEX IF NOT EXISTS track_bookmark_user_idx ON track_bookmark(user_id)");
_session.execute("CREATE INDEX IF NOT EXISTS track_bookmark_user_track_idx ON track_bookmark(user_id,track_id)"); _session.execute("CREATE INDEX IF NOT EXISTS track_bookmark_user_track_idx ON track_bookmark(user_id,track_id)");
_session.execute("CREATE INDEX IF NOT EXISTS listen_scrobbler_idx ON listen(scrobbler)"); _session.execute("CREATE INDEX IF NOT EXISTS listen_backend_idx ON listen(backend)");
_session.execute("CREATE INDEX IF NOT EXISTS listen_user_scrobbler_idx ON listen(user_id,scrobbler)"); _session.execute("CREATE INDEX IF NOT EXISTS listen_user_backend_idx ON listen(user_id,backend)");
_session.execute("CREATE INDEX IF NOT EXISTS listen_user_track_scrobbler_date_time_idx ON listen(user_id,track_id,scrobbler,date_time)"); _session.execute("CREATE INDEX IF NOT EXISTS listen_track_user_backend_idx ON listen(track_id,user_id,backend)");
_session.execute("CREATE INDEX IF NOT EXISTS starred_artist_user_scrobbler_idx ON starred_artist(user_id,scrobbler)"); _session.execute("CREATE INDEX IF NOT EXISTS listen_user_track_backend_date_time_idx ON listen(user_id,track_id,backend,date_time)");
_session.execute("CREATE INDEX IF NOT EXISTS starred_artist_artist_user_scrobbler_idx ON starred_artist(artist_id,user_id,scrobbler)"); _session.execute("CREATE INDEX IF NOT EXISTS starred_artist_user_backend_idx ON starred_artist(user_id,backend)");
_session.execute("CREATE INDEX IF NOT EXISTS starred_release_user_scrobbler_idx ON starred_release(user_id,scrobbler)"); _session.execute("CREATE INDEX IF NOT EXISTS starred_artist_artist_user_backend_idx ON starred_artist(artist_id,user_id,backend)");
_session.execute("CREATE INDEX IF NOT EXISTS starred_release_release_user_scrobbler_idx ON starred_release(release_id,user_id,scrobbler)"); _session.execute("CREATE INDEX IF NOT EXISTS starred_release_user_backend_idx ON starred_release(user_id,backend)");
_session.execute("CREATE INDEX IF NOT EXISTS starred_track_user_scrobbler_idx ON starred_track(user_id,scrobbler)"); _session.execute("CREATE INDEX IF NOT EXISTS starred_release_release_user_backend_idx ON starred_release(release_id,user_id,backend)");
_session.execute("CREATE INDEX IF NOT EXISTS starred_track_track_user_scrobbler_idx ON starred_track(track_id,user_id,scrobbler)"); _session.execute("CREATE INDEX IF NOT EXISTS starred_track_user_backend_idx ON starred_track(user_id,backend)");
_session.execute("CREATE INDEX IF NOT EXISTS starred_track_track_user_backend_idx ON starred_track(track_id,user_id,backend)");
} }
// Initial settings tables // Initial settings tables
@@ -187,4 +188,14 @@ namespace Database
LMS_LOG(DB, INFO) << "Database Analyze complete"; LMS_LOG(DB, INFO) << "Database Analyze complete";
} }
void Session::optimize()
{
LMS_LOG(DB, INFO) << "Optimizing database...";
{
auto uniqueTransaction{ createUniqueTransaction() };
_session.execute("PRAGMA optimize");
}
LMS_LOG(DB, INFO) << "Database optimizing complete";
}
} // namespace Database } // namespace Database
@@ -29,47 +29,42 @@
namespace Database namespace Database
{ {
StarredArtist::StarredArtist(ObjectPtr<Artist> artist, ObjectPtr<User> user, Scrobbler scrobbler) StarredArtist::StarredArtist(ObjectPtr<Artist> artist, ObjectPtr<User> user, FeedbackBackend backend)
: _scrobbler {scrobbler} : _backend{ backend }
, _artist {getDboPtr(artist)} , _artist{ getDboPtr(artist) }
, _user {getDboPtr(user)} , _user{ getDboPtr(user) }
{ {
} }
StarredArtist::pointer StarredArtist::pointer StarredArtist::create(Session& session, ObjectPtr<Artist> artist, ObjectPtr<User> user, FeedbackBackend backend)
StarredArtist::create(Session& session, ObjectPtr<Artist> artist, ObjectPtr<User> user, Scrobbler scrobbler) {
{ return session.getDboSession().add(std::unique_ptr<StarredArtist> {new StarredArtist{ artist, user, backend }});
return session.getDboSession().add(std::unique_ptr<StarredArtist> {new StarredArtist {artist, user, scrobbler}}); }
}
std::size_t std::size_t StarredArtist::getCount(Session& session)
StarredArtist::getCount(Session& session) {
{ session.checkSharedLocked();
session.checkSharedLocked(); return session.getDboSession().query<int>("SELECT COUNT(*) FROM starred_artist");
return session.getDboSession().query<int>("SELECT COUNT(*) FROM starred_artist"); }
}
StarredArtist::pointer StarredArtist::pointer StarredArtist::find(Session& session, StarredArtistId id)
StarredArtist::find(Session& session, StarredArtistId id) {
{ session.checkSharedLocked();
session.checkSharedLocked(); return session.getDboSession().find<StarredArtist>().where("id = ?").bind(id).resultValue();
return session.getDboSession().find<StarredArtist>().where("id = ?").bind(id).resultValue(); }
}
StarredArtist::pointer StarredArtist::pointer StarredArtist::find(Session& session, ArtistId artistId, UserId userId, FeedbackBackend backend)
StarredArtist::find(Session& session, ArtistId artistId, UserId userId, Scrobbler scrobbler) {
{ session.checkSharedLocked();
session.checkSharedLocked(); return session.getDboSession().find<StarredArtist>()
return session.getDboSession().find<StarredArtist>() .where("artist_id = ?").bind(artistId)
.where("artist_id = ?").bind(artistId) .where("user_id = ?").bind(userId)
.where("user_id = ?").bind(userId) .where("backend = ?").bind(backend)
.where("scrobbler = ?").bind(scrobbler) .resultValue();
.resultValue(); }
}
void void StarredArtist::setDateTime(const Wt::WDateTime& dateTime)
StarredArtist::setDateTime(const Wt::WDateTime& dateTime) {
{ _dateTime = Utils::normalizeDateTime(dateTime);
_dateTime = Utils::normalizeDateTime(dateTime); }
}
} }
@@ -29,47 +29,42 @@
namespace Database namespace Database
{ {
StarredRelease::StarredRelease(ObjectPtr<Release> release, ObjectPtr<User> user, Scrobbler scrobbler) StarredRelease::StarredRelease(ObjectPtr<Release> release, ObjectPtr<User> user, FeedbackBackend backend)
: _scrobbler {scrobbler} : _backend{ backend }
, _release {getDboPtr(release)} , _release{ getDboPtr(release) }
, _user {getDboPtr(user)} , _user{ getDboPtr(user) }
{ {
} }
StarredRelease::pointer StarredRelease::pointer StarredRelease::create(Session& session, ObjectPtr<Release> release, ObjectPtr<User> user, FeedbackBackend backend)
StarredRelease::create(Session& session, ObjectPtr<Release> release, ObjectPtr<User> user, Scrobbler scrobbler) {
{ return session.getDboSession().add(std::unique_ptr<StarredRelease>{new StarredRelease{ release, user, backend }});
return session.getDboSession().add(std::unique_ptr<StarredRelease>{new StarredRelease{release, user, scrobbler}}); }
}
std::size_t std::size_t StarredRelease::getCount(Session& session)
StarredRelease::getCount(Session& session) {
{ session.checkSharedLocked();
session.checkSharedLocked(); return session.getDboSession().query<int>("SELECT COUNT(*) FROM starred_release");
return session.getDboSession().query<int>("SELECT COUNT(*) FROM starred_release"); }
}
StarredRelease::pointer StarredRelease::pointer StarredRelease::find(Session& session, StarredReleaseId id)
StarredRelease::find(Session& session, StarredReleaseId id) {
{ session.checkSharedLocked();
session.checkSharedLocked(); return session.getDboSession().find<StarredRelease>().where("id = ?").bind(id).resultValue();
return session.getDboSession().find<StarredRelease>().where("id = ?").bind(id).resultValue(); }
}
StarredRelease::pointer StarredRelease::pointer StarredRelease::find(Session& session, ReleaseId releaseId, UserId userId, FeedbackBackend backend)
StarredRelease::find(Session& session, ReleaseId releaseId, UserId userId, Scrobbler scrobbler) {
{ session.checkSharedLocked();
session.checkSharedLocked(); return session.getDboSession().find<StarredRelease>()
return session.getDboSession().find<StarredRelease>() .where("release_id = ?").bind(releaseId)
.where("release_id = ?").bind(releaseId) .where("user_id = ?").bind(userId)
.where("user_id = ?").bind(userId) .where("backend = ?").bind(backend)
.where("scrobbler = ?").bind(scrobbler) .resultValue();
.resultValue(); }
}
void void StarredRelease::setDateTime(const Wt::WDateTime& dateTime)
StarredRelease::setDateTime(const Wt::WDateTime& dateTime) {
{ _dateTime = Utils::normalizeDateTime(dateTime);
_dateTime = Utils::normalizeDateTime(dateTime); }
}
} }
@@ -29,64 +29,58 @@
namespace Database namespace Database
{ {
StarredTrack::StarredTrack(ObjectPtr<Track> track, ObjectPtr<User> user, Scrobbler scrobbler) StarredTrack::StarredTrack(ObjectPtr<Track> track, ObjectPtr<User> user, FeedbackBackend backend)
: _scrobbler {scrobbler} : _backend{ backend }
, _track {getDboPtr(track)} , _track{ getDboPtr(track) }
, _user {getDboPtr(user)} , _user{ getDboPtr(user) }
{ {
} }
StarredTrack::pointer StarredTrack::pointer StarredTrack::create(Session& session, ObjectPtr<Track> track, ObjectPtr<User> user, FeedbackBackend backend)
StarredTrack::create(Session& session, ObjectPtr<Track> track, ObjectPtr<User> user, Scrobbler scrobbler) {
{ return session.getDboSession().add(std::unique_ptr<StarredTrack> {new StarredTrack{ track, user, backend }});
return session.getDboSession().add(std::unique_ptr<StarredTrack> {new StarredTrack{track, user, scrobbler}}); }
}
std::size_t std::size_t StarredTrack::getCount(Session& session)
StarredTrack::getCount(Session& session) {
{ session.checkSharedLocked();
session.checkSharedLocked(); return session.getDboSession().query<int>("SELECT COUNT(*) FROM starred_track");
return session.getDboSession().query<int>("SELECT COUNT(*) FROM starred_track"); }
}
StarredTrack::pointer StarredTrack::pointer StarredTrack::find(Session& session, StarredTrackId id)
StarredTrack::find(Session& session, StarredTrackId id) {
{ session.checkSharedLocked();
session.checkSharedLocked(); return session.getDboSession().find<StarredTrack>().where("id = ?").bind(id).resultValue();
return session.getDboSession().find<StarredTrack>().where("id = ?").bind(id).resultValue(); }
}
StarredTrack::pointer StarredTrack::pointer StarredTrack::find(Session& session, TrackId trackId, UserId userId, FeedbackBackend backend)
StarredTrack::find(Session& session, TrackId trackId, UserId userId, Scrobbler scrobbler) {
{ session.checkSharedLocked();
session.checkSharedLocked(); return session.getDboSession().find<StarredTrack>()
return session.getDboSession().find<StarredTrack>() .where("track_id = ?").bind(trackId)
.where("track_id = ?").bind(trackId) .where("user_id = ?").bind(userId)
.where("user_id = ?").bind(userId) .where("backend = ?").bind(backend)
.where("scrobbler = ?").bind(scrobbler) .resultValue();
.resultValue(); }
}
RangeResults<StarredTrackId> RangeResults<StarredTrackId> StarredTrack::find(Session& session, const FindParameters& params)
StarredTrack::find(Session& session, const FindParameters& params) {
{ session.checkSharedLocked();
session.checkSharedLocked();
auto query {session.getDboSession().query<StarredTrackId>("SELECT DISTINCT s_t.id FROM starred_track s_t")}; auto query{ session.getDboSession().query<StarredTrackId>("SELECT DISTINCT s_t.id FROM starred_track s_t") };
if (params.scrobbler) if (params.backend)
query.where("s_t.scrobbler = ?").bind(*params.scrobbler); query.where("s_t.backend = ?").bind(*params.backend);
if (params.scrobblingState) if (params.syncState)
query.where("s_t.scrobbling_state = ?").bind(*params.scrobblingState); query.where("s_t.sync_state = ?").bind(*params.syncState);
if (params.user.isValid()) if (params.user.isValid())
query.where("s_t.user_id = ?").bind(params.user); query.where("s_t.user_id = ?").bind(params.user);
return Utils::execQuery(query, params.range); return Utils::execQuery(query, params.range);
} }
void void StarredTrack::setDateTime(const Wt::WDateTime& dateTime)
StarredTrack::setDateTime(const Wt::WDateTime& dateTime) {
{ _dateTime = Utils::normalizeDateTime(dateTime);
_dateTime = Utils::normalizeDateTime(dateTime); }
}
} }
File diff suppressed because it is too large Load Diff
@@ -538,7 +538,7 @@ TrackList::getSimilarTracks(std::optional<std::size_t> offset, std::optional<std
"SELECT t FROM track t" "SELECT t FROM track t"
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id" " INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
" WHERE " " WHERE "
" (t_c.cluster_id IN (SELECT c.id from cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN tracklist_entry p_e ON p_e.track_id = t.id INNER JOIN tracklist p ON p.id = p_e.tracklist_id WHERE p.id = ?)" " (t_c.cluster_id IN (SELECT DISTINCT c.id from cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN tracklist_entry p_e ON p_e.track_id = t.id INNER JOIN tracklist p ON p.id = p_e.tracklist_id WHERE p.id = ?)"
" AND t.id NOT IN (SELECT tracklist_t.id FROM track tracklist_t INNER JOIN tracklist_entry t_e ON t_e.track_id = tracklist_t.id WHERE t_e.tracklist_id = ?))" " AND t.id NOT IN (SELECT tracklist_t.id FROM track tracklist_t INNER JOIN tracklist_entry t_e ON t_e.track_id = tracklist_t.id WHERE t_e.tracklist_id = ?))"
) )
.bind(getId()) .bind(getId())
+4 -2
View File
@@ -53,8 +53,10 @@ namespace Database {
auto query{ session.getDboSession().query<UserId>("SELECT id FROM user") }; auto query{ session.getDboSession().query<UserId>("SELECT id FROM user") };
if (params.scrobbler) if (params.scrobblingBackend)
query.where("scrobbler = ?").bind(*params.scrobbler); query.where("scrobbling_backend = ?").bind(*params.scrobblingBackend);
if (params.feedbackBackend)
query.where("feedback_backend = ?").bind(*params.feedbackBackend);
return Utils::execQuery(query, params.range); return Utils::execQuery(query, params.range);
} }
@@ -40,96 +40,97 @@
namespace Database namespace Database
{ {
class Cluster; class Cluster;
class ClusterType; class ClusterType;
class Release; class Release;
class Session; class Session;
class StarredArtist; class StarredArtist;
class Track; class Track;
class TrackArtistLink; class TrackArtistLink;
class User; class User;
class Artist final : public Object<Artist, ArtistId> class Artist final : public Object<Artist, ArtistId>
{ {
public: public:
struct FindParameters struct FindParameters
{ {
std::vector<ClusterId> clusters; // if non empty, at least one artist that belongs to these clusters std::vector<ClusterId> clusters; // if non empty, at least one artist that belongs to these clusters
std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords (on either name field OR sort name field) std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords (on either name field OR sort name field)
std::optional<TrackArtistLinkType> linkType; // if set, only artists that have produced at least one track with this link type std::optional<TrackArtistLinkType> linkType; // if set, only artists that have produced at least one track with this link type
ArtistSortMethod sortMethod {ArtistSortMethod::None}; ArtistSortMethod sortMethod{ ArtistSortMethod::None };
Range range; Range range;
Wt::WDateTime writtenAfter; Wt::WDateTime writtenAfter;
UserId starringUser; // only artists starred by this user UserId starringUser; // only artists starred by this user
std::optional<Scrobbler> scrobbler; // and for this scrobbler std::optional<FeedbackBackend> feedbackBackend; // and for this feedback backend
TrackId track; // artists involved in this track TrackId track; // artists involved in this track
ReleaseId release; // artists involved in this release ReleaseId release; // artists involved in this release
FindParameters& setClusters(const std::vector<ClusterId>& _clusters) { clusters = _clusters; return *this; } FindParameters& setClusters(const std::vector<ClusterId>& _clusters) { clusters = _clusters; return *this; }
FindParameters& setKeywords(const std::vector<std::string_view>& _keywords) { keywords = _keywords; return *this; } FindParameters& setKeywords(const std::vector<std::string_view>& _keywords) { keywords = _keywords; return *this; }
FindParameters& setLinkType(std::optional<TrackArtistLinkType> _linkType) { linkType = _linkType; return *this; } FindParameters& setLinkType(std::optional<TrackArtistLinkType> _linkType) { linkType = _linkType; return *this; }
FindParameters& setSortMethod(ArtistSortMethod _sortMethod) {sortMethod = _sortMethod; return *this; } FindParameters& setSortMethod(ArtistSortMethod _sortMethod) { sortMethod = _sortMethod; return *this; }
FindParameters& setRange(Range _range) {range = _range; return *this; } FindParameters& setRange(Range _range) { range = _range; return *this; }
FindParameters& setWrittenAfter(const Wt::WDateTime& _after) { writtenAfter = _after; return *this; } FindParameters& setWrittenAfter(const Wt::WDateTime& _after) { writtenAfter = _after; return *this; }
FindParameters& setStarringUser(UserId _user, Scrobbler _scrobbler) { starringUser = _user; scrobbler = _scrobbler; return *this; } FindParameters& setStarringUser(UserId _user, FeedbackBackend _feedbackBackend) { starringUser = _user; feedbackBackend = _feedbackBackend; return *this; }
FindParameters& setTrack(TrackId _track) { track = _track; return *this; } FindParameters& setTrack(TrackId _track) { track = _track; return *this; }
FindParameters& setRelease(ReleaseId _release) { release = _release; return *this; } FindParameters& setRelease(ReleaseId _release) { release = _release; return *this; }
}; };
Artist() = default; Artist() = default;
// Accessors // Accessors
static std::size_t getCount(Session& session); static std::size_t getCount(Session& session);
static pointer find(Session& session, const UUID& MBID); static pointer find(Session& session, const UUID& MBID);
static pointer find(Session& session, ArtistId id); static pointer find(Session& session, ArtistId id);
static std::vector<pointer> find(Session& session, const std::string& name); // exact match on name field static std::vector<pointer> find(Session& session, const std::string& name); // exact match on name field
static RangeResults<ArtistId> find(Session& session, const FindParameters& parameters); static RangeResults<pointer> find(Session& session, const FindParameters& parameters);
static RangeResults<ArtistId> findAllOrphans(Session& session, Range range); // No track related static RangeResults<ArtistId> findIds(Session& session, const FindParameters& parameters);
static bool exists(Session& session, ArtistId id); static RangeResults<ArtistId> findOrphanIds(Session& session, Range range); // No track related
static bool exists(Session& session, ArtistId id);
// Accessors // Accessors
const std::string& getName() const { return _name; } const std::string& getName() const { return _name; }
const std::string& getSortName() const { return _sortName; } const std::string& getSortName() const { return _sortName; }
std::optional<UUID> getMBID() const { return UUID::fromString(_MBID); } std::optional<UUID> getMBID() const { return UUID::fromString(_MBID); }
// No artistLinkTypes means get them all // No artistLinkTypes means get them all
RangeResults<ArtistId> findSimilarArtists(EnumSet<TrackArtistLinkType> artistLinkTypes = {}, Range range = {}) const; RangeResults<ArtistId> findSimilarArtistIds(EnumSet<TrackArtistLinkType> artistLinkTypes = {}, Range range = {}) const;
// Get the cluster of the tracks made by this artist // Get the cluster of the tracks made by this artist
// Each clusters are grouped by cluster type, sorted by the number of occurence // Each clusters are grouped by cluster type, sorted by the number of occurence
// size is the max number of cluster per cluster type // size is the max number of cluster per cluster type
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(std::vector<ObjectPtr<ClusterType>> clusterTypes, std::size_t size) const; std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(std::vector<ObjectPtr<ClusterType>> clusterTypes, std::size_t size) const;
void setName(std::string_view name) { _name = name; } void setName(std::string_view name) { _name = name; }
void setMBID(const std::optional<UUID>& mbid) { _MBID = mbid ? mbid->getAsString() : ""; } void setMBID(const std::optional<UUID>& mbid) { _MBID = mbid ? mbid->getAsString() : ""; }
void setSortName(const std::string& sortName); void setSortName(const std::string& sortName);
template<class Action> template<class Action>
void persist(Action& a) void persist(Action& a)
{ {
Wt::Dbo::field(a, _name, "name"); Wt::Dbo::field(a, _name, "name");
Wt::Dbo::field(a, _sortName, "sort_name"); Wt::Dbo::field(a, _sortName, "sort_name");
Wt::Dbo::field(a, _MBID, "mbid"); Wt::Dbo::field(a, _MBID, "mbid");
Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "artist"); Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "artist");
Wt::Dbo::hasMany(a, _starredArtists, Wt::Dbo::ManyToMany, "user_starred_artists", "", Wt::Dbo::OnDeleteCascade); Wt::Dbo::hasMany(a, _starredArtists, Wt::Dbo::ManyToMany, "user_starred_artists", "", Wt::Dbo::OnDeleteCascade);
} }
private: private:
static constexpr std::size_t _maxNameLength {128}; static constexpr std::size_t _maxNameLength{ 128 };
friend class Session; friend class Session;
// Create // Create
Artist(const std::string& name, const std::optional<UUID>& MBID = {}); Artist(const std::string& name, const std::optional<UUID>& MBID = {});
static pointer create(Session& session, const std::string& name, const std::optional<UUID>& UUID = {}); static pointer create(Session& session, const std::string& name, const std::optional<UUID>& UUID = {});
std::string _name; std::string _name;
std::string _sortName; std::string _sortName;
std::string _MBID; // Musicbrainz Identifier std::string _MBID; // Musicbrainz Identifier
Wt::Dbo::collection<Wt::Dbo::ptr<TrackArtistLink>> _trackArtistLinks; // Tracks involving this artist Wt::Dbo::collection<Wt::Dbo::ptr<TrackArtistLink>> _trackArtistLinks; // Tracks involving this artist
Wt::Dbo::collection<Wt::Dbo::ptr<StarredArtist>> _starredArtists; // starred entries for this artist Wt::Dbo::collection<Wt::Dbo::ptr<StarredArtist>> _starredArtists; // starred entries for this artist
}; };
} // namespace Database } // namespace Database
@@ -46,47 +46,50 @@ namespace Database
struct FindParameters struct FindParameters
{ {
UserId user; UserId user;
std::optional<Scrobbler> scrobbler; std::optional<ScrobblingBackend> backend;
std::optional<ScrobblingState> scrobblingState; std::optional<SyncState> syncState;
Range range; Range range;
FindParameters& setUser(UserId _user) { user = _user; return *this; } FindParameters& setUser(UserId _user) { user = _user; return *this; }
FindParameters& setScrobbler(Scrobbler _scrobbler) { scrobbler = _scrobbler; return *this; } FindParameters& setScrobblingBackend(ScrobblingBackend _backend) { backend = _backend; return *this; }
FindParameters& setScrobblingState(ScrobblingState _scrobblingState) { scrobblingState = _scrobblingState; return *this; } FindParameters& setSyncState(SyncState _syncState) { syncState = _syncState; return *this; }
FindParameters& setRange(Range _range) { range = _range; return *this; } FindParameters& setRange(Range _range) { range = _range; return *this; }
}; };
// Accessors // Accessors
static std::size_t getCount(Session& session); static std::size_t getCount(Session& session);
static pointer find(Session& session, ListenId id); static pointer find(Session& session, ListenId id);
static pointer find(Session& session, UserId userId, TrackId trackId, Scrobbler scrobbler, const Wt::WDateTime& dateTime); static pointer find(Session& session, UserId userId, TrackId trackId, ScrobblingBackend backend, const Wt::WDateTime& dateTime);
static RangeResults<ListenId> find(Session& session, const FindParameters& parameters); static RangeResults<ListenId> find(Session& session, const FindParameters& parameters);
// Stats // Stats
static RangeResults<ArtistId> getTopArtists(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range = {}); static RangeResults<ArtistId> getTopArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range = {});
static RangeResults<ReleaseId> getTopReleases(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, Range range = {}); static RangeResults<ReleaseId> getTopReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, Range range = {});
static RangeResults<TrackId> getTopTracks(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, Range range = {}); static RangeResults<TrackId> getTopTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, Range range = {});
static RangeResults<ArtistId> getRecentArtists(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range = {}); static RangeResults<ArtistId> getRecentArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range = {});
static RangeResults<ReleaseId> getRecentReleases(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, Range range = {}); static RangeResults<ReleaseId> getRecentReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, Range range = {});
static RangeResults<TrackId> getRecentTracks(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, Range range = {}); static RangeResults<TrackId> getRecentTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, Range range = {});
static pointer getMostRecentListen(Session& session, UserId userId, Scrobbler scrobbler, ReleaseId releaseId); static std::size_t getCount(Session& session, UserId userId, ScrobblingBackend backend, TrackId trackId);
static pointer getMostRecentListen(Session& session, UserId userId, Scrobbler scrobbler, TrackId releaseId); static std::size_t getCount(Session& session, UserId userId, ScrobblingBackend backend, ReleaseId trackId);
ScrobblingState getScrobblingState() const { return _scrobblingState; } static pointer getMostRecentListen(Session& session, UserId userId, ScrobblingBackend backend, ReleaseId releaseId);
static pointer getMostRecentListen(Session& session, UserId userId, ScrobblingBackend backend, TrackId releaseId);
SyncState getSyncState() const { return _syncState; }
ObjectPtr<User> getUser() const { return _user; } ObjectPtr<User> getUser() const { return _user; }
ObjectPtr<Track> getTrack() const { return _track; } ObjectPtr<Track> getTrack() const { return _track; }
const Wt::WDateTime& getDateTime() const { return _dateTime; } const Wt::WDateTime& getDateTime() const { return _dateTime; }
void setScrobblingState(ScrobblingState state) { _scrobblingState = state; } void setSyncState(SyncState state) { _syncState = state; }
template<class Action> template<class Action>
void persist(Action& a) void persist(Action& a)
{ {
Wt::Dbo::field(a, _dateTime, "date_time"); Wt::Dbo::field(a, _dateTime, "date_time");
Wt::Dbo::field(a, _scrobbler, "scrobbler"); Wt::Dbo::field(a, _backend, "backend"); // TODO rename
Wt::Dbo::field(a, _scrobblingState, "scrobbling_state"); Wt::Dbo::field(a, _syncState, "sync_state"); // TODO rename
Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade); Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade); Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
@@ -94,12 +97,12 @@ namespace Database
private: private:
friend class Session; friend class Session;
Listen(ObjectPtr<User> user, ObjectPtr<Track> track, Scrobbler scrobbler, const Wt::WDateTime& dateTime); Listen(ObjectPtr<User> user, ObjectPtr<Track> track, ScrobblingBackend backend, const Wt::WDateTime& dateTime);
static pointer create(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track, Scrobbler scrobbler, const Wt::WDateTime& dateTime); static pointer create(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track, ScrobblingBackend backend, const Wt::WDateTime& dateTime);
Wt::WDateTime _dateTime; Wt::WDateTime _dateTime;
Scrobbler _scrobbler; ScrobblingBackend _backend;
ScrobblingState _scrobblingState{ ScrobblingState::PendingAdd }; SyncState _syncState{ SyncState::PendingAdd };
Wt::Dbo::ptr<User> _user; Wt::Dbo::ptr<User> _user;
Wt::Dbo::ptr<Track> _track; Wt::Dbo::ptr<Track> _track;
@@ -57,7 +57,7 @@ namespace Database
Wt::WDateTime writtenAfter; Wt::WDateTime writtenAfter;
std::optional<DateRange> dateRange; std::optional<DateRange> dateRange;
UserId starringUser; // only releases starred by this user UserId starringUser; // only releases starred by this user
std::optional<Scrobbler> scrobbler; // and for this scrobbler std::optional<FeedbackBackend> feedbackBackend; // and for this backend
ArtistId artist; // only releases that involved this user ArtistId artist; // only releases that involved this user
EnumSet<TrackArtistLinkType> trackArtistLinkTypes; // and for these link types EnumSet<TrackArtistLinkType> trackArtistLinkTypes; // and for these link types
EnumSet<TrackArtistLinkType> excludedTrackArtistLinkTypes; // but not for these link types EnumSet<TrackArtistLinkType> excludedTrackArtistLinkTypes; // but not for these link types
@@ -70,7 +70,7 @@ namespace Database
FindParameters& setRange(Range _range) { range = _range; return *this; } FindParameters& setRange(Range _range) { range = _range; return *this; }
FindParameters& setWrittenAfter(const Wt::WDateTime& _after) { writtenAfter = _after; return *this; } FindParameters& setWrittenAfter(const Wt::WDateTime& _after) { writtenAfter = _after; return *this; }
FindParameters& setDateRange(const std::optional<DateRange>& _dateRange) { dateRange = _dateRange; return *this; } FindParameters& setDateRange(const std::optional<DateRange>& _dateRange) { dateRange = _dateRange; return *this; }
FindParameters& setStarringUser(UserId _user, Scrobbler _scrobbler) { starringUser = _user; scrobbler = _scrobbler; return *this; } FindParameters& setStarringUser(UserId _user, FeedbackBackend _feedbackBackend) { starringUser = _user; feedbackBackend = _feedbackBackend; return *this; }
FindParameters& setArtist(ArtistId _artist, EnumSet<TrackArtistLinkType> _trackArtistLinkTypes = {}, EnumSet<TrackArtistLinkType> _excludedTrackArtistLinkTypes = {}) FindParameters& setArtist(ArtistId _artist, EnumSet<TrackArtistLinkType> _trackArtistLinkTypes = {}, EnumSet<TrackArtistLinkType> _excludedTrackArtistLinkTypes = {})
{ {
artist = _artist; artist = _artist;
@@ -88,9 +88,10 @@ namespace Database
static pointer find(Session& session, const UUID& MBID); static pointer find(Session& session, const UUID& MBID);
static std::vector<pointer> find(Session& session, const std::string& name); static std::vector<pointer> find(Session& session, const std::string& name);
static pointer find(Session& session, ReleaseId id); static pointer find(Session& session, ReleaseId id);
static RangeResults<ReleaseId> find(Session& session, const FindParameters& parameters); static RangeResults<pointer> find(Session& session, const FindParameters& parameters);
static RangeResults<ReleaseId> findOrphans(Session& session, Range range); // no track related static RangeResults<ReleaseId> findIds(Session& session, const FindParameters& parameters);
static RangeResults<ReleaseId> findOrderedByArtist(Session& session, Range range); static RangeResults<ReleaseId> findOrphanIds(Session& session, Range range); // not track related
static RangeResults<ReleaseId> findIdsOrderedByArtist(Session& session, Range range);
// Get the cluster of the tracks that belong to this release // Get the cluster of the tracks that belong to this release
// Each clusters are grouped by cluster type, sorted by the number of occurence (max to min) // Each clusters are grouped by cluster type, sorted by the number of occurence (max to min)
@@ -104,7 +105,7 @@ namespace Database
std::optional<std::string> getCopyrightURL() const; std::optional<std::string> getCopyrightURL() const;
// Accessors // Accessors
const std::string& getName() const { return _name; } const std::string& getName() const { return _name; }
std::optional<UUID> getMBID() const { return UUID::fromString(_MBID); } std::optional<UUID> getMBID() const { return UUID::fromString(_MBID); }
std::optional<std::size_t> getTotalDisc() const { return _totalDisc; } std::optional<std::size_t> getTotalDisc() const { return _totalDisc; }
std::size_t getDiscCount() const; // may not be total disc (if incomplete for example) std::size_t getDiscCount() const; // may not be total disc (if incomplete for example)
@@ -32,72 +32,74 @@ LMS_DECLARE_IDTYPE(ScanSettingsId)
namespace Database { namespace Database {
class ClusterType; class ClusterType;
class Session; class Session;
class ScanSettings final : public Object<ScanSettings, ScanSettingsId> class ScanSettings final : public Object<ScanSettings, ScanSettingsId>
{ {
public: public:
// Do not modify values (just add) // Do not modify values (just add)
enum class UpdatePeriod enum class UpdatePeriod
{ {
Never = 0, Never = 0,
Daily, Daily,
Weekly, Weekly,
Monthly, Monthly,
Hourly, Hourly,
}; };
// Do not modify values (just add) // Do not modify values (just add)
enum class RecommendationEngineType enum class SimilarityEngineType
{ {
Clusters = 0, Clusters = 0,
Features, Features,
}; None,
};
static void init(Session& session); static void init(Session& session);
static pointer get(Session& session); static pointer get(Session& session);
// Getters // Getters
std::size_t getScanVersion() const { return _scanVersion; } std::size_t getScanVersion() const { return _scanVersion; }
std::filesystem::path getMediaDirectory() const { return _mediaDirectory; } std::filesystem::path getMediaDirectory() const { return _mediaDirectory; }
Wt::WTime getUpdateStartTime() const { return _startTime; } Wt::WTime getUpdateStartTime() const { return _startTime; }
UpdatePeriod getUpdatePeriod() const { return _updatePeriod; } UpdatePeriod getUpdatePeriod() const { return _updatePeriod; }
std::vector<ObjectPtr<ClusterType>> getClusterTypes() const; std::vector<ObjectPtr<ClusterType>> getClusterTypes() const;
std::vector<std::filesystem::path> getAudioFileExtensions() const; std::vector<std::filesystem::path> getAudioFileExtensions() const;
RecommendationEngineType getRecommendationEngineType() const { return _recommendationEngineType; } SimilarityEngineType getSimilarityEngineType() const { return _similarityEngineType; }
// Setters // Setters
void addAudioFileExtension(const std::filesystem::path& ext); void addAudioFileExtension(const std::filesystem::path& ext);
void setMediaDirectory(const std::filesystem::path& p); void setMediaDirectory(const std::filesystem::path& p);
void setUpdateStartTime(Wt::WTime t) { _startTime = t; } void setUpdateStartTime(Wt::WTime t) { _startTime = t; }
void setUpdatePeriod(UpdatePeriod p) { _updatePeriod = p; } void setUpdatePeriod(UpdatePeriod p) { _updatePeriod = p; }
void setClusterTypes(Session& session, const std::set<std::string>& clusterTypeNames); void setClusterTypes(Session& session, const std::set<std::string>& clusterTypeNames);
void setRecommendationEngineType(RecommendationEngineType type) { _recommendationEngineType = type; } void setSimilarityEngineType(SimilarityEngineType type) { _similarityEngineType = type; }
void incScanVersion(); void incScanVersion();
template<class Action> template<class Action>
void persist(Action& a) void persist(Action& a)
{ {
Wt::Dbo::field(a, _scanVersion, "scan_version"); Wt::Dbo::field(a, _scanVersion, "scan_version");
Wt::Dbo::field(a, _mediaDirectory, "media_directory"); Wt::Dbo::field(a, _mediaDirectory, "media_directory");
Wt::Dbo::field(a, _startTime, "start_time"); Wt::Dbo::field(a, _startTime, "start_time");
Wt::Dbo::field(a, _updatePeriod, "update_period"); Wt::Dbo::field(a, _updatePeriod, "update_period");
Wt::Dbo::field(a, _audioFileExtensions, "audio_file_extensions"); Wt::Dbo::field(a, _audioFileExtensions, "audio_file_extensions");
Wt::Dbo::field(a, _recommendationEngineType,"similarity_engine_type"); Wt::Dbo::field(a, _similarityEngineType, "similarity_engine_type");
Wt::Dbo::hasMany(a, _clusterTypes, Wt::Dbo::ManyToOne, "scan_settings"); Wt::Dbo::hasMany(a, _clusterTypes, Wt::Dbo::ManyToOne, "scan_settings");
} }
private: private:
int _scanVersion {}; int _scanVersion{};
std::string _mediaDirectory; std::string _mediaDirectory;
Wt::WTime _startTime = Wt::WTime {0,0,0}; Wt::WTime _startTime = Wt::WTime{ 0,0,0 };
UpdatePeriod _updatePeriod {UpdatePeriod::Never}; UpdatePeriod _updatePeriod{ UpdatePeriod::Never };
RecommendationEngineType _recommendationEngineType {RecommendationEngineType::Clusters}; SimilarityEngineType _similarityEngineType{ SimilarityEngineType::Clusters };
std::string _audioFileExtensions {".alac .mp3 .ogg .oga .aac .m4a .m4b .flac .wav .wma .aif .aiff .ape .mpc .shn .opus .wv"}; std::string _audioFileExtensions{ ".alac .mp3 .ogg .oga .aac .m4a .m4b .flac .wav .wma .aif .aiff .ape .mpc .shn .opus .wv" };
Wt::Dbo::collection<Wt::Dbo::ptr<ClusterType>> _clusterTypes;
}; Wt::Dbo::collection<Wt::Dbo::ptr<ClusterType>> _clusterTypes;
};
} // namespace Database } // namespace Database
@@ -61,6 +61,7 @@ namespace Database
void checkSharedLocked(); void checkSharedLocked();
void analyze(); void analyze();
void optimize();
void prepareTables(); // need to run only once at startup void prepareTables(); // need to run only once at startup
@@ -30,53 +30,53 @@
namespace Database namespace Database
{ {
class Artist; class Artist;
class Session; class Session;
class User; class User;
class StarredArtist final : public Object<StarredArtist, StarredArtistId> class StarredArtist final : public Object<StarredArtist, StarredArtistId>
{ {
public: public:
StarredArtist() = default; StarredArtist() = default;
// Search utility // Search utility
static std::size_t getCount(Session& session); static std::size_t getCount(Session& session);
static pointer find(Session& session, StarredArtistId id); static pointer find(Session& session, StarredArtistId id);
static pointer find(Session& session, ArtistId artistId, UserId userId, Scrobbler scrobbler); static pointer find(Session& session, ArtistId artistId, UserId userId, FeedbackBackend backend);
// Accessors // Accessors
ObjectPtr<Artist> getArtist() const { return _artist; } ObjectPtr<Artist> getArtist() const { return _artist; }
ObjectPtr<User> getUser() const { return _user; } ObjectPtr<User> getUser() const { return _user; }
Scrobbler getScrobbler() const { return _scrobbler; } FeedbackBackend getFeedbackBackend() const { return _backend; }
const Wt::WDateTime& getDateTime() const { return _dateTime; } const Wt::WDateTime& getDateTime() const { return _dateTime; }
ScrobblingState getScrobblingState() const { return _scrobblingState; } SyncState getSyncState() const { return _syncState; }
// Setters // Setters
void setDateTime(const Wt::WDateTime& dateTime); void setDateTime(const Wt::WDateTime& dateTime);
void setScrobblingState(ScrobblingState state) { _scrobblingState = state; } void setSyncState(SyncState state) { _syncState = state; }
template<class Action> template<class Action>
void persist(Action& a) void persist(Action& a)
{ {
Wt::Dbo::field(a, _scrobbler, "scrobbler"); Wt::Dbo::field(a, _backend, "backend");
Wt::Dbo::field(a, _scrobblingState, "scrobbling_state"); Wt::Dbo::field(a, _syncState, "sync_state");
Wt::Dbo::field(a, _dateTime, "date_time"); Wt::Dbo::field(a, _dateTime, "date_time");
Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade); Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade); Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
} }
private: private:
friend class Session; friend class Session;
StarredArtist(ObjectPtr<Artist> artist, ObjectPtr<User> user, Scrobbler scrobbler); StarredArtist(ObjectPtr<Artist> artist, ObjectPtr<User> user, FeedbackBackend scrobblingbackend);
static pointer create(Session& session, ObjectPtr<Artist> artist, ObjectPtr<User> user, Scrobbler scrobbler); static pointer create(Session& session, ObjectPtr<Artist> artist, ObjectPtr<User> user, FeedbackBackend scrobblingbackend);
Scrobbler _scrobbler; // for which scrobbler FeedbackBackend _backend; // for which backend
ScrobblingState _scrobblingState {ScrobblingState::PendingAdd}; SyncState _syncState{ SyncState::PendingAdd };
Wt::WDateTime _dateTime; // when it was starred Wt::WDateTime _dateTime; // when it was starred
Wt::Dbo::ptr<Artist> _artist; Wt::Dbo::ptr<Artist> _artist;
Wt::Dbo::ptr<User> _user; Wt::Dbo::ptr<User> _user;
}; };
} // namespace Database } // namespace Database
@@ -30,53 +30,53 @@
namespace Database namespace Database
{ {
class Release; class Release;
class Session; class Session;
class User; class User;
class StarredRelease final : public Object<StarredRelease, StarredReleaseId> class StarredRelease final : public Object<StarredRelease, StarredReleaseId>
{ {
public: public:
StarredRelease() = default; StarredRelease() = default;
// Search utility // Search utility
static std::size_t getCount(Session& session); static std::size_t getCount(Session& session);
static pointer find(Session& session, StarredReleaseId id); static pointer find(Session& session, StarredReleaseId id);
static pointer find(Session& session, ReleaseId releaseId, UserId userId, Scrobbler scrobbler); static pointer find(Session& session, ReleaseId releaseId, UserId userId, FeedbackBackend backend);
// Accessors // Accessors
ObjectPtr<Release> getRelease() const { return _release; } ObjectPtr<Release> getRelease() const { return _release; }
ObjectPtr<User> getUser() const { return _user; } ObjectPtr<User> getUser() const { return _user; }
Scrobbler getScrobbler() const { return _scrobbler; } FeedbackBackend getFeedbackBackend() const { return _backend; }
const Wt::WDateTime& getDateTime() const { return _dateTime; } const Wt::WDateTime& getDateTime() const { return _dateTime; }
ScrobblingState getScrobblingState() const { return _scrobblingState; } SyncState getSyncState() const { return _syncState; }
// Setters // Setters
void setDateTime(const Wt::WDateTime& dateTime); void setDateTime(const Wt::WDateTime& dateTime);
void setScrobblingState(ScrobblingState state) { _scrobblingState = state; } void setSyncState(SyncState state) { _syncState = state; }
template<class Action> template<class Action>
void persist(Action& a) void persist(Action& a)
{ {
Wt::Dbo::field(a, _scrobbler, "scrobbler"); Wt::Dbo::field(a, _backend, "backend");
Wt::Dbo::field(a, _scrobblingState, "scrobbling_state"); Wt::Dbo::field(a, _syncState, "sync_state");
Wt::Dbo::field(a, _dateTime, "date_time"); Wt::Dbo::field(a, _dateTime, "date_time");
Wt::Dbo::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade); Wt::Dbo::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade); Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
} }
private: private:
friend class Session; friend class Session;
StarredRelease(ObjectPtr<Release> release, ObjectPtr<User> user, Scrobbler scrobbler); StarredRelease(ObjectPtr<Release> release, ObjectPtr<User> user, FeedbackBackend backend);
static pointer create(Session& session, ObjectPtr<Release> release, ObjectPtr<User> user, Scrobbler scrobbler); static pointer create(Session& session, ObjectPtr<Release> release, ObjectPtr<User> user, FeedbackBackend backend);
Scrobbler _scrobbler; // for which scrobbler FeedbackBackend _backend; // for which backend
ScrobblingState _scrobblingState {ScrobblingState::PendingAdd}; SyncState _syncState{ SyncState::PendingAdd };
Wt::WDateTime _dateTime; // when it was starred Wt::WDateTime _dateTime; // when it was starred
Wt::Dbo::ptr<Release> _release; Wt::Dbo::ptr<Release> _release;
Wt::Dbo::ptr<User> _user; Wt::Dbo::ptr<User> _user;
}; };
} // namespace Database } // namespace Database
@@ -31,66 +31,66 @@
namespace Database namespace Database
{ {
class Track; class Track;
class Session; class Session;
class User; class User;
class StarredTrack final : public Object<StarredTrack, StarredTrackId> class StarredTrack final : public Object<StarredTrack, StarredTrackId>
{ {
public: public:
StarredTrack() = default; StarredTrack() = default;
struct FindParameters struct FindParameters
{ {
std::optional<Scrobbler> scrobbler; // for this scrobbler std::optional<FeedbackBackend> backend; // for this backend
std::optional<ScrobblingState> scrobblingState; // and these states std::optional<SyncState> syncState; // and these states
UserId user; // and this user UserId user; // and this user
Range range; Range range;
FindParameters& setScrobbler(Scrobbler _scrobbler, ScrobblingState _scrobblingState) { scrobbler = _scrobbler; scrobblingState = _scrobblingState; return *this; } FindParameters& setFeedbackBackend(FeedbackBackend _backend, SyncState _syncState) { backend = _backend; syncState = _syncState; return *this; }
FindParameters& setUser(UserId _user) {user = _user; return *this; } FindParameters& setUser(UserId _user) { user = _user; return *this; }
FindParameters& setRange(Range _range) {range = _range; return *this; } FindParameters& setRange(Range _range) { range = _range; return *this; }
}; };
// Search utility // Search utility
static std::size_t getCount(Session& session); static std::size_t getCount(Session& session);
static pointer find(Session& session, StarredTrackId id); static pointer find(Session& session, StarredTrackId id);
static pointer find(Session& session, TrackId trackId, UserId userId, Scrobbler scrobbler); static pointer find(Session& session, TrackId trackId, UserId userId, FeedbackBackend backend);
static RangeResults<StarredTrackId> find(Session& session, const FindParameters& findParams); static RangeResults<StarredTrackId> find(Session& session, const FindParameters& findParams);
// Accessors // Accessors
ObjectPtr<Track> getTrack() const { return _track; } ObjectPtr<Track> getTrack() const { return _track; }
ObjectPtr<User> getUser() const { return _user; } ObjectPtr<User> getUser() const { return _user; }
Scrobbler getScrobbler() const { return _scrobbler; } FeedbackBackend getBackend() const { return _backend; }
const Wt::WDateTime& getDateTime() const { return _dateTime; } const Wt::WDateTime& getDateTime() const { return _dateTime; }
ScrobblingState getScrobblingState() const { return _scrobblingState; } SyncState getSyncState() const { return _syncState; }
// Setters // Setters
void setDateTime(const Wt::WDateTime& dateTime); void setDateTime(const Wt::WDateTime& dateTime);
void setScrobblingState(ScrobblingState state) { _scrobblingState = state; } void setSyncState(SyncState state) { _syncState = state; }
template<class Action> template<class Action>
void persist(Action& a) void persist(Action& a)
{ {
Wt::Dbo::field(a, _scrobbler, "scrobbler"); Wt::Dbo::field(a, _backend, "backend");
Wt::Dbo::field(a, _scrobblingState, "scrobbling_state"); Wt::Dbo::field(a, _syncState, "sync_state");
Wt::Dbo::field(a, _dateTime, "date_time"); Wt::Dbo::field(a, _dateTime, "date_time");
Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade); Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade); Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
} }
private: private:
friend class Session; friend class Session;
StarredTrack(ObjectPtr<Track> track, ObjectPtr<User> user, Scrobbler scrobbler); StarredTrack(ObjectPtr<Track> track, ObjectPtr<User> user, FeedbackBackend backend);
static pointer create(Session& session, ObjectPtr<Track> track, ObjectPtr<User> user, Scrobbler scrobbler); static pointer create(Session& session, ObjectPtr<Track> track, ObjectPtr<User> user, FeedbackBackend backend);
Scrobbler _scrobbler; // for which scrobbler FeedbackBackend _backend; // for which backend
ScrobblingState _scrobblingState {ScrobblingState::PendingAdd}; SyncState _syncState{ SyncState::PendingAdd };
Wt::WDateTime _dateTime; // when it was starred Wt::WDateTime _dateTime; // when it was starred
Wt::Dbo::ptr<Track> _track; Wt::Dbo::ptr<Track> _track;
Wt::Dbo::ptr<User> _user; Wt::Dbo::ptr<User> _user;
}; };
} // namespace Database } // namespace Database
@@ -21,11 +21,10 @@
#include <chrono> #include <chrono>
#include <filesystem> #include <filesystem>
#include <iostream> #include <ostream>
#include <optional> #include <optional>
#include <string> #include <string>
#include <string_view> #include <string_view>
#include <unordered_set>
#include <utility> #include <utility>
#include <vector> #include <vector>
@@ -61,23 +60,23 @@ class Track final : public Object<Track, TrackId>
public: public:
struct FindParameters struct FindParameters
{ {
std::vector<ClusterId> clusters; // if non empty, tracks that belong to these clusters std::vector<ClusterId> clusters; // if non empty, tracks that belong to these clusters
std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords
std::string name; // if non empty, must match this name std::string name; // if non empty, must match this name
TrackSortMethod sortMethod {TrackSortMethod::None}; TrackSortMethod sortMethod {TrackSortMethod::None};
Range range; Range range;
Wt::WDateTime writtenAfter; Wt::WDateTime writtenAfter;
UserId starringUser; // only tracks starred by this user UserId starringUser; // only tracks starred by this user
std::optional<Scrobbler> scrobbler; // and for this scrobbler std::optional<FeedbackBackend> feedbackBackend; // and for this feedback backend
ArtistId artist; // only tracks that involve this artist ArtistId artist; // only tracks that involve this artist
std::string artistName; // only tracks that involve this artist name std::string artistName; // only tracks that involve this artist name
EnumSet<TrackArtistLinkType> trackArtistLinkTypes; // and for these link types EnumSet<TrackArtistLinkType> trackArtistLinkTypes; // and for these link types
bool nonRelease {}; // only tracks that do not belong to a release bool nonRelease {}; // only tracks that do not belong to a release
ReleaseId release; // matching this release ReleaseId release; // matching this release
std::string releaseName; // matching this release name std::string releaseName; // matching this release name
TrackListId trackList; // matching this trackList TrackListId trackList; // matching this trackList
std::optional<int> trackNumber; // matching this track number std::optional<int> trackNumber; // matching this track number
bool distinct {true}; bool distinct {true};
FindParameters& setClusters(const std::vector<ClusterId>& _clusters) { clusters = _clusters; return *this; } FindParameters& setClusters(const std::vector<ClusterId>& _clusters) { clusters = _clusters; return *this; }
FindParameters& setKeywords(const std::vector<std::string_view>& _keywords) { keywords = _keywords; return *this; } FindParameters& setKeywords(const std::vector<std::string_view>& _keywords) { keywords = _keywords; return *this; }
@@ -85,7 +84,7 @@ class Track final : public Object<Track, TrackId>
FindParameters& setSortMethod(TrackSortMethod _method) { sortMethod = _method; return *this; } FindParameters& setSortMethod(TrackSortMethod _method) { sortMethod = _method; return *this; }
FindParameters& setRange(Range _range) { range = _range; return *this; } FindParameters& setRange(Range _range) { range = _range; return *this; }
FindParameters& setWrittenAfter(const Wt::WDateTime& _after) { writtenAfter = _after; return *this; } FindParameters& setWrittenAfter(const Wt::WDateTime& _after) { writtenAfter = _after; return *this; }
FindParameters& setStarringUser(UserId _user, Scrobbler _scrobbler) { starringUser = _user; scrobbler = _scrobbler; return *this; } FindParameters& setStarringUser(UserId _user, FeedbackBackend _feedbackBackend) { starringUser = _user; feedbackBackend = _feedbackBackend; return *this; }
FindParameters& setArtist(ArtistId _artist, EnumSet<TrackArtistLinkType> _trackArtistLinkTypes = {}) { artist = _artist; trackArtistLinkTypes = _trackArtistLinkTypes; return *this; } FindParameters& setArtist(ArtistId _artist, EnumSet<TrackArtistLinkType> _trackArtistLinkTypes = {}) { artist = _artist; trackArtistLinkTypes = _trackArtistLinkTypes; return *this; }
FindParameters& setArtistName(std::string_view _artistName, EnumSet<TrackArtistLinkType> _trackArtistLinkTypes = {}) { artistName = _artistName; trackArtistLinkTypes = _trackArtistLinkTypes; return *this; } FindParameters& setArtistName(std::string_view _artistName, EnumSet<TrackArtistLinkType> _trackArtistLinkTypes = {}) { artistName = _artistName; trackArtistLinkTypes = _trackArtistLinkTypes; return *this; }
FindParameters& setNonRelease(bool _nonRelease) { nonRelease = _nonRelease; return *this; } FindParameters& setNonRelease(bool _nonRelease) { nonRelease = _nonRelease; return *this; }
@@ -111,12 +110,13 @@ class Track final : public Object<Track, TrackId>
static bool exists(Session& session, TrackId id); static bool exists(Session& session, TrackId id);
static std::vector<pointer> findByRecordingMBID(Session& session, const UUID& MBID); static std::vector<pointer> findByRecordingMBID(Session& session, const UUID& MBID);
static std::vector<pointer> findByMBID(Session& session, const UUID& MBID); static std::vector<pointer> findByMBID(Session& session, const UUID& MBID);
static RangeResults<TrackId> findSimilarTracks(Session& session, const std::vector<TrackId>& trackIds, Range range); static RangeResults<TrackId> findSimilarTrackIds(Session& session, const std::vector<TrackId>& trackIds, Range range);
static RangeResults<TrackId> find(Session& session, const FindParameters& parameters); static RangeResults<TrackId> findIds(Session& session, const FindParameters& parameters);
static RangeResults<pointer> find(Session& session, const FindParameters& parameters);
static RangeResults<PathResult> findPaths(Session& session, Range range); static RangeResults<PathResult> findPaths(Session& session, Range range);
static RangeResults<TrackId> findTrackMBIDDuplicates(Session& session, Range range); static RangeResults<TrackId> findIdsTrackMBIDDuplicates(Session& session, Range range);
static RangeResults<TrackId> findWithRecordingMBIDAndMissingFeatures(Session& session, Range range); static RangeResults<TrackId> findIdsWithRecordingMBIDAndMissingFeatures(Session& session, Range range);
// Accessors // Accessors
void setScanVersion(std::size_t version) { _scanVersion = version; } void setScanVersion(std::size_t version) { _scanVersion = version; }
@@ -46,7 +46,7 @@ namespace Database
{ {
Range range; Range range;
std::vector<T> results; std::vector<T> results;
bool moreResults; bool moreResults{};
RangeResults getSubRange(Range subRange) RangeResults getSubRange(Range subRange)
{ {
@@ -160,13 +160,19 @@ namespace Database
void visitAllowedAudioBitrates(std::function<void(Bitrate)>); void visitAllowedAudioBitrates(std::function<void(Bitrate)>);
bool isAudioBitrateAllowed(Bitrate bitrate); bool isAudioBitrateAllowed(Bitrate bitrate);
enum class Scrobbler enum class ScrobblingBackend
{ {
Internal = 0, Internal = 0,
ListenBrainz = 1, ListenBrainz = 1,
}; };
enum class ScrobblingState enum class FeedbackBackend
{
Internal = 0,
ListenBrainz = 1,
};
enum class SyncState
{ {
PendingAdd = 0, PendingAdd = 0,
Synchronized = 1, Synchronized = 1,
@@ -47,20 +47,23 @@ namespace Database {
struct FindParameters struct FindParameters
{ {
std::optional<Scrobbler> scrobbler; std::optional<ScrobblingBackend> scrobblingBackend;
Range range; std::optional<FeedbackBackend> feedbackBackend;
Range range;
FindParameters& setScrobbler(Scrobbler _scrobbler) { scrobbler = _scrobbler; return *this; } FindParameters& setFeedbackBackend(FeedbackBackend _feedbackBackend) { feedbackBackend = _feedbackBackend; return *this; }
FindParameters& setScrobblingBackend(ScrobblingBackend _scrobblingBackend) { scrobblingBackend = _scrobblingBackend; return *this; }
FindParameters& setRange(Range _range) { range = _range; return *this; } FindParameters& setRange(Range _range) { range = _range; return *this; }
}; };
static inline const std::size_t MinNameLength{ 3 }; static inline constexpr std::size_t MinNameLength{ 3 };
static inline const std::size_t MaxNameLength{ 15 }; static inline constexpr std::size_t MaxNameLength{ 15 };
static inline const AudioFormat defaultSubsonicTranscodeFormat{ AudioFormat::OGG_OPUS }; static inline constexpr AudioFormat defaultSubsonicTranscodeFormat{ AudioFormat::OGG_OPUS };
static inline const Bitrate defaultSubsonicTranscodeBitrate{ 128000 }; static inline constexpr Bitrate defaultSubsonicTranscodeBitrate{ 128000 };
static inline const UITheme defaultUITheme{ UITheme::Dark }; static inline constexpr UITheme defaultUITheme{ UITheme::Dark };
static inline const SubsonicArtistListMode defaultSubsonicArtistListMode{ SubsonicArtistListMode::AllArtists }; static inline constexpr SubsonicArtistListMode defaultSubsonicArtistListMode{ SubsonicArtistListMode::AllArtists };
static inline const Scrobbler defaultScrobbler{ Scrobbler::Internal }; static inline constexpr ScrobblingBackend defaultScrobblingBackend{ ScrobblingBackend::Internal };
static inline constexpr FeedbackBackend defaultFeedbackBackend{ FeedbackBackend::Internal };
User() = default; User() = default;
@@ -88,7 +91,8 @@ namespace Database {
void setUITheme(UITheme uiTheme) { _uiTheme = uiTheme; } void setUITheme(UITheme uiTheme) { _uiTheme = uiTheme; }
void clearAuthTokens(); void clearAuthTokens();
void setSubsonicArtistListMode(SubsonicArtistListMode mode) { _subsonicArtistListMode = mode; } void setSubsonicArtistListMode(SubsonicArtistListMode mode) { _subsonicArtistListMode = mode; }
void setScrobbler(Scrobbler scrobbler) { _scrobbler = scrobbler; } void setFeedbackBackend(FeedbackBackend feedbackBackend) { _feedbackBackend = feedbackBackend; }
void setScrobblingBackend(ScrobblingBackend scrobblingBackend) { _scrobblingBackend = scrobblingBackend; }
void setListenBrainzToken(const std::optional<UUID>& MBID) { _listenbrainzToken = MBID ? MBID->getAsString() : ""; } void setListenBrainzToken(const std::optional<UUID>& MBID) { _listenbrainzToken = MBID ? MBID->getAsString() : ""; }
// read // read
@@ -102,7 +106,8 @@ namespace Database {
bool isRadioSet() const { return _radio; } bool isRadioSet() const { return _radio; }
UITheme getUITheme() const { return _uiTheme; } UITheme getUITheme() const { return _uiTheme; }
SubsonicArtistListMode getSubsonicArtistListMode() const { return _subsonicArtistListMode; } SubsonicArtistListMode getSubsonicArtistListMode() const { return _subsonicArtistListMode; }
Scrobbler getScrobbler() const { return _scrobbler; } FeedbackBackend getFeedbackBackend() const { return _feedbackBackend; }
ScrobblingBackend getScrobblingBackend() const { return _scrobblingBackend; }
std::optional<UUID> getListenBrainzToken() const { return UUID::fromString(_listenbrainzToken); } std::optional<UUID> getListenBrainzToken() const { return UUID::fromString(_listenbrainzToken); }
template<class Action> template<class Action>
@@ -117,7 +122,8 @@ namespace Database {
Wt::Dbo::field(a, _subsonicDefaultTranscodeBitrate, "subsonic_default_transcode_bitrate"); Wt::Dbo::field(a, _subsonicDefaultTranscodeBitrate, "subsonic_default_transcode_bitrate");
Wt::Dbo::field(a, _subsonicArtistListMode, "subsonic_artist_list_mode"); Wt::Dbo::field(a, _subsonicArtistListMode, "subsonic_artist_list_mode");
Wt::Dbo::field(a, _uiTheme, "ui_theme"); Wt::Dbo::field(a, _uiTheme, "ui_theme");
Wt::Dbo::field(a, _scrobbler, "scrobbler"); Wt::Dbo::field(a, _feedbackBackend, "feedback_backend");
Wt::Dbo::field(a, _scrobblingBackend, "scrobbling_backend");
Wt::Dbo::field(a, _listenbrainzToken, "listenbrainz_token"); Wt::Dbo::field(a, _listenbrainzToken, "listenbrainz_token");
// UI player settings // UI player settings
@@ -138,7 +144,8 @@ namespace Database {
std::string _passwordHash; std::string _passwordHash;
Wt::WDateTime _lastLogin; Wt::WDateTime _lastLogin;
UITheme _uiTheme{ defaultUITheme }; UITheme _uiTheme{ defaultUITheme };
Scrobbler _scrobbler{ defaultScrobbler }; FeedbackBackend _feedbackBackend{ defaultFeedbackBackend };
ScrobblingBackend _scrobblingBackend{ defaultScrobblingBackend };
std::string _listenbrainzToken; // Musicbrainz Identifier std::string _listenbrainzToken; // Musicbrainz Identifier
// Admin defined settings // Admin defined settings
+315 -307
View File
@@ -23,415 +23,423 @@ using namespace Database;
TEST_F(DatabaseFixture, Artist) TEST_F(DatabaseFixture, Artist)
{ {
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
EXPECT_FALSE(Artist::exists(session, 35)); EXPECT_FALSE(Artist::exists(session, 35));
EXPECT_FALSE(Artist::exists(session, 0)); EXPECT_FALSE(Artist::exists(session, 0));
EXPECT_FALSE(Artist::exists(session, 1)); EXPECT_FALSE(Artist::exists(session, 1));
EXPECT_EQ(Artist::getCount(session), 0); EXPECT_EQ(Artist::getCount(session), 0);
} }
ScopedArtist artist {session, "MyArtist"}; ScopedArtist artist{ session, "MyArtist" };
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
EXPECT_TRUE(artist.get()); EXPECT_TRUE(artist.get());
EXPECT_FALSE(!artist.get()); EXPECT_FALSE(!artist.get());
EXPECT_EQ(artist.get()->getId(), artist.getId()); EXPECT_EQ(artist.get()->getId(), artist.getId());
EXPECT_TRUE(Artist::exists(session, artist.getId())); EXPECT_TRUE(Artist::exists(session, artist.getId()));
EXPECT_EQ(Artist::getCount(session), 1); EXPECT_EQ(Artist::getCount(session), 1);
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
auto artists {Artist::find(session, Artist::FindParameters {})}; auto artists{ Artist::findIds(session, Artist::FindParameters {}) };
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId()); EXPECT_EQ(artists.results.front(), artist.getId());
artists = Artist::findAllOrphans(session, Range {}); artists = Artist::findOrphanIds(session, Range{});
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId()); EXPECT_EQ(artists.results.front(), artist.getId());
} }
{
auto transaction{ session.createSharedTransaction() };
auto artists{ Artist::find(session, Artist::FindParameters {}) };
ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results.front()->getId(), artist.getId());
}
} }
TEST_F(DatabaseFixture, Artist_singleTrack) TEST_F(DatabaseFixture, Artist_singleTrack)
{ {
ScopedTrack track {session, "MyTrack"}; ScopedTrack track{ session, "MyTrack" };
ScopedArtist artist {session, "MyArtist"}; ScopedArtist artist{ session, "MyArtist" };
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction{ session.createUniqueTransaction() };
track.get().modify()->setName("MyTrackName"); track.get().modify()->setName("MyTrackName");
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
EXPECT_TRUE(Artist::findAllOrphans(session, Range {}).results.empty()); EXPECT_TRUE(Artist::findOrphanIds(session, Range{}).results.empty());
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
auto artists {track->getArtists({TrackArtistLinkType::Artist})}; auto artists{ track->getArtists({TrackArtistLinkType::Artist}) };
ASSERT_EQ(artists.size(), 1); ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.front()->getId(), artist.getId()); EXPECT_EQ(artists.front()->getId(), artist.getId());
ASSERT_EQ(track->getArtistLinks().size(), 1); ASSERT_EQ(track->getArtistLinks().size(), 1);
auto artistLink {track->getArtistLinks().front()}; auto artistLink{ track->getArtistLinks().front() };
EXPECT_EQ(artistLink->getTrack()->getId(), track.getId()); EXPECT_EQ(artistLink->getTrack()->getId(), track.getId());
EXPECT_EQ(artistLink->getArtist()->getId(), artist.getId()); EXPECT_EQ(artistLink->getArtist()->getId(), artist.getId());
ASSERT_EQ(track->getArtists({TrackArtistLinkType::Artist}).size(), 1); ASSERT_EQ(track->getArtists({ TrackArtistLinkType::Artist }).size(), 1);
EXPECT_TRUE(track->getArtists({TrackArtistLinkType::ReleaseArtist}).empty()); EXPECT_TRUE(track->getArtists({ TrackArtistLinkType::ReleaseArtist }).empty());
EXPECT_EQ(track->getArtists({}).size(), 1); EXPECT_EQ(track->getArtists({}).size(), 1);
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
auto artists {track->getArtistIds({TrackArtistLinkType::Artist})}; auto artists{ track->getArtistIds({TrackArtistLinkType::Artist}) };
ASSERT_EQ(artists.size(), 1); ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.front(), artist.getId()); EXPECT_EQ(artists.front(), artist.getId());
ASSERT_EQ(track->getArtistIds({TrackArtistLinkType::Artist}).size(), 1); ASSERT_EQ(track->getArtistIds({ TrackArtistLinkType::Artist }).size(), 1);
EXPECT_TRUE(track->getArtistIds({TrackArtistLinkType::ReleaseArtist}).empty()); EXPECT_TRUE(track->getArtistIds({ TrackArtistLinkType::ReleaseArtist }).empty());
EXPECT_EQ(track->getArtistIds({}).size(), 1); EXPECT_EQ(track->getArtistIds({}).size(), 1);
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
auto tracks {Track::find(session, Track::FindParameters{}.setName("MyTrackName").setArtistName("MyArtist"))}; auto tracks{ Track::findIds(session, Track::FindParameters{}.setName("MyTrackName").setArtistName("MyArtist")) };
ASSERT_EQ(tracks.results.size(), 1); ASSERT_EQ(tracks.results.size(), 1);
EXPECT_EQ(tracks.results.front(), track.getId()); EXPECT_EQ(tracks.results.front(), track.getId());
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
auto tracks {Track::find(session, Track::FindParameters{}.setName("MyTrackName").setArtistName("MyArtistFoo"))}; auto tracks{ Track::findIds(session, Track::FindParameters{}.setName("MyTrackName").setArtistName("MyArtistFoo")) };
EXPECT_EQ(tracks.results.size(), 0); EXPECT_EQ(tracks.results.size(), 0);
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
auto tracks {Track::find(session, Track::FindParameters{}.setName("MyTrackNameFoo").setArtistName("MyArtist"))}; auto tracks{ Track::findIds(session, Track::FindParameters{}.setName("MyTrackNameFoo").setArtistName("MyArtist")) };
EXPECT_EQ(tracks.results.size(), 0); EXPECT_EQ(tracks.results.size(), 0);
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
auto artists {Artist::find(session, Artist::FindParameters{}.setTrack(track->getId()))}; auto artists{ Artist::findIds(session, Artist::FindParameters{}.setTrack(track->getId())) };
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId()); EXPECT_EQ(artists.results.front(), artist.getId());
} }
} }
TEST_F(DatabaseFixture, Artist_singleTracktMultiRoles) TEST_F(DatabaseFixture, Artist_singleTracktMultiRoles)
{ {
ScopedTrack track {session, "MyTrack"}; ScopedTrack track{ session, "MyTrack" };
ScopedArtist artist {session, "MyArtist"}; ScopedArtist artist{ session, "MyArtist" };
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction{ session.createUniqueTransaction() };
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::ReleaseArtist); TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::ReleaseArtist);
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Writer); TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Writer);
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
EXPECT_TRUE(Artist::findAllOrphans(session, Range {}).results.empty()); EXPECT_TRUE(Artist::findOrphanIds(session, Range{}).results.empty());
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
EXPECT_EQ(Artist::find(session, Artist::FindParameters {}).results.size(), 1); EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}).results.size(), 1);
EXPECT_EQ(Artist::find(session, Artist::FindParameters {}.setLinkType(TrackArtistLinkType::Artist)).results.size(), 1); EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setLinkType(TrackArtistLinkType::Artist)).results.size(), 1);
EXPECT_EQ(Artist::find(session, Artist::FindParameters {}.setLinkType(TrackArtistLinkType::ReleaseArtist)).results.size(), 1); EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setLinkType(TrackArtistLinkType::ReleaseArtist)).results.size(), 1);
EXPECT_EQ(Artist::find(session, Artist::FindParameters {}.setLinkType(TrackArtistLinkType::Writer)).results.size(), 1); EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setLinkType(TrackArtistLinkType::Writer)).results.size(), 1);
EXPECT_EQ(Artist::find(session, Artist::FindParameters {}.setLinkType(TrackArtistLinkType::Composer)).results.size(), 0); EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setLinkType(TrackArtistLinkType::Composer)).results.size(), 0);
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
auto artists {track->getArtists({TrackArtistLinkType::Artist})}; auto artists{ track->getArtists({TrackArtistLinkType::Artist}) };
ASSERT_EQ(artists.size(), 1); ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.front()->getId(), artist.getId()); EXPECT_EQ(artists.front()->getId(), artist.getId());
artists = track->getArtists({TrackArtistLinkType::ReleaseArtist}); artists = track->getArtists({ TrackArtistLinkType::ReleaseArtist });
ASSERT_EQ(artists.size(), 1); ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.front()->getId(), artist.getId()); EXPECT_EQ(artists.front()->getId(), artist.getId());
EXPECT_EQ(track->getArtistLinks().size(), 3); EXPECT_EQ(track->getArtistLinks().size(), 3);
auto tracks {Track::find(session, Track::FindParameters {}.setArtist(artist.getId()))}; auto tracks{ Track::findIds(session, Track::FindParameters {}.setArtist(artist.getId())) };
EXPECT_EQ(tracks.results.size(), 1); EXPECT_EQ(tracks.results.size(), 1);
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::ReleaseArtist})); tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::ReleaseArtist }));
EXPECT_EQ(tracks.results.size(), 1); EXPECT_EQ(tracks.results.size(), 1);
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::Artist})); tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Artist }));
EXPECT_EQ(tracks.results.size(), 1); EXPECT_EQ(tracks.results.size(), 1);
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::Writer})); tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Writer }));
EXPECT_EQ(tracks.results.size(), 1); EXPECT_EQ(tracks.results.size(), 1);
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::Composer})); tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Composer }));
EXPECT_EQ(tracks.results.size(), 0); EXPECT_EQ(tracks.results.size(), 0);
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
EnumSet<TrackArtistLinkType> types{ TrackArtistLink::findUsedTypes(session, artist.getId()) }; EnumSet<TrackArtistLinkType> types{ TrackArtistLink::findUsedTypes(session, artist.getId()) };
EXPECT_TRUE(types.contains(TrackArtistLinkType::ReleaseArtist)); EXPECT_TRUE(types.contains(TrackArtistLinkType::ReleaseArtist));
EXPECT_TRUE(types.contains(TrackArtistLinkType::Artist)); EXPECT_TRUE(types.contains(TrackArtistLinkType::Artist));
EXPECT_TRUE(types.contains(TrackArtistLinkType::Writer)); EXPECT_TRUE(types.contains(TrackArtistLinkType::Writer));
EXPECT_FALSE(types.contains(TrackArtistLinkType::Composer)); EXPECT_FALSE(types.contains(TrackArtistLinkType::Composer));
} }
} }
TEST_F(DatabaseFixture, Artist_singleTrackMultiArtists) TEST_F(DatabaseFixture, Artist_singleTrackMultiArtists)
{ {
ScopedTrack track {session, "track"}; ScopedTrack track{ session, "track" };
ScopedArtist artist1 {session, "artist1"}; ScopedArtist artist1{ session, "artist1" };
ScopedArtist artist2 {session, "artist2"}; ScopedArtist artist2{ session, "artist2" };
ASSERT_NE(artist1.getId(), artist2.getId()); ASSERT_NE(artist1.getId(), artist2.getId());
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction{ session.createUniqueTransaction() };
TrackArtistLink::create(session, track.get(), artist1.get(), TrackArtistLinkType::Artist); TrackArtistLink::create(session, track.get(), artist1.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, track.get(), artist2.get(), TrackArtistLinkType::Artist); TrackArtistLink::create(session, track.get(), artist2.get(), TrackArtistLinkType::Artist);
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
EXPECT_TRUE(Artist::findAllOrphans(session, Range {}).results.empty()); EXPECT_TRUE(Artist::findOrphanIds(session, Range{}).results.empty());
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
auto artists {track->getArtists({TrackArtistLinkType::Artist})}; auto artists{ track->getArtists({TrackArtistLinkType::Artist}) };
ASSERT_EQ(artists.size(), 2); ASSERT_EQ(artists.size(), 2);
EXPECT_TRUE((artists[0]->getId() == artist1.getId() && artists[1]->getId() == artist2.getId()) EXPECT_TRUE((artists[0]->getId() == artist1.getId() && artists[1]->getId() == artist2.getId())
|| (artists[0]->getId() == artist2.getId() && artists[1]->getId() == artist1.getId())); || (artists[0]->getId() == artist2.getId() && artists[1]->getId() == artist1.getId()));
EXPECT_EQ(track->getArtists({}).size(), 2); EXPECT_EQ(track->getArtists({}).size(), 2);
EXPECT_EQ(track->getArtists({TrackArtistLinkType::Artist}).size(), 2); EXPECT_EQ(track->getArtists({ TrackArtistLinkType::Artist }).size(), 2);
EXPECT_TRUE(track->getArtists({TrackArtistLinkType::ReleaseArtist}).empty()); EXPECT_TRUE(track->getArtists({ TrackArtistLinkType::ReleaseArtist }).empty());
EXPECT_EQ(Artist::find(session, Artist::FindParameters {}).results.size(), 2); EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}).results.size(), 2);
EXPECT_EQ(Artist::find(session, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::Random)).results.size(), 2); EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setSortMethod(ArtistSortMethod::Random)).results.size(), 2);
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
auto tracks {Track::find(session, Track::FindParameters {}.setArtist(artist1->getId()))}; auto tracks{ Track::findIds(session, Track::FindParameters {}.setArtist(artist1->getId())) };
ASSERT_EQ(tracks.results.size(), 1); ASSERT_EQ(tracks.results.size(), 1);
EXPECT_EQ(tracks.results.front(), track->getId()); EXPECT_EQ(tracks.results.front(), track->getId());
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist2->getId())); tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist2->getId()));
ASSERT_EQ(tracks.results.size(), 1); ASSERT_EQ(tracks.results.size(), 1);
EXPECT_EQ(tracks.results.front(), track->getId()); EXPECT_EQ(tracks.results.front(), track->getId());
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist1->getId(), {TrackArtistLinkType::ReleaseArtist})); tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist1->getId(), { TrackArtistLinkType::ReleaseArtist }));
EXPECT_EQ(tracks.results.size(), 0); EXPECT_EQ(tracks.results.size(), 0);
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist1->getId(), {TrackArtistLinkType::Artist})); tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist1->getId(), { TrackArtistLinkType::Artist }));
EXPECT_EQ(tracks.results.size(), 1); EXPECT_EQ(tracks.results.size(), 1);
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist2->getId(), {TrackArtistLinkType::ReleaseArtist})); tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist2->getId(), { TrackArtistLinkType::ReleaseArtist }));
EXPECT_EQ(tracks.results.size(), 0); EXPECT_EQ(tracks.results.size(), 0);
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist2->getId(), {TrackArtistLinkType::Artist})); tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist2->getId(), { TrackArtistLinkType::Artist }));
EXPECT_EQ(tracks.results.size(), 1); EXPECT_EQ(tracks.results.size(), 1);
} }
} }
TEST_F(DatabaseFixture, Artist_findByName) TEST_F(DatabaseFixture, Artist_findByName)
{ {
ScopedArtist artist {session, "AAA"}; ScopedArtist artist{ session, "AAA" };
ScopedTrack track {session, "MyTrack"}; // filters does not work on orphans ScopedTrack track{ session, "MyTrack" }; // filters does not work on orphans
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction{ session.createUniqueTransaction() };
artist.get().modify()->setSortName("ZZZ"); artist.get().modify()->setSortName("ZZZ");
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
EXPECT_TRUE(Artist::find(session, Artist::FindParameters {}.setKeywords({"N"})).results.empty()); EXPECT_TRUE(Artist::findIds(session, Artist::FindParameters{}.setKeywords({ "N" })).results.empty());
const auto artistsByAAA {Artist::find(session, Artist::FindParameters {}.setKeywords({"A"}))}; const auto artistsByAAA{ Artist::findIds(session, Artist::FindParameters {}.setKeywords({"A"})) };
ASSERT_EQ(artistsByAAA.results.size(), 1); ASSERT_EQ(artistsByAAA.results.size(), 1);
EXPECT_EQ(artistsByAAA.results.front(), artist.getId()); EXPECT_EQ(artistsByAAA.results.front(), artist.getId());
const auto artistsByZZZ {Artist::Artist::find(session, Artist::FindParameters {}.setKeywords({"Z"}))}; const auto artistsByZZZ{ Artist::Artist::findIds(session, Artist::FindParameters {}.setKeywords({"Z"})) };
ASSERT_EQ(artistsByZZZ.results.size(), 1); ASSERT_EQ(artistsByZZZ.results.size(), 1);
EXPECT_EQ(artistsByZZZ.results.front(), artist.getId()); EXPECT_EQ(artistsByZZZ.results.front(), artist.getId());
EXPECT_TRUE(Artist::find(session, "NNN").empty()); EXPECT_TRUE(Artist::find(session, "NNN").empty());
EXPECT_EQ(Artist::find(session, "AAA").size(), 1); EXPECT_EQ(Artist::find(session, "AAA").size(), 1);
} }
} }
TEST_F(DatabaseFixture, Artist_findByNameEscaped) TEST_F(DatabaseFixture, Artist_findByNameEscaped)
{ {
ScopedArtist artist1 {session, "MyArtist%"}; ScopedArtist artist1{ session, R"(MyArtist%)" };
ScopedArtist artist2 {session, "%MyArtist"}; ScopedArtist artist2{ session, R"(%MyArtist)" };
ScopedArtist artist3 {session, "%_MyArtist"}; ScopedArtist artist3{ session, R"(%_MyArtist)" };
ScopedArtist artist4 {session, "MyArtist%foo"}; ScopedArtist artist4{ session, R"(MyArtist%foo)" };
ScopedArtist artist5 {session, "foo%MyArtist"}; ScopedArtist artist5{ session, R"(foo%MyArtist)" };
ScopedArtist artist6 {session, "%AMyArtist"}; ScopedArtist artist6{ session, R"(%AMyArtist)" };
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
{ {
const auto artists {Artist::find(session, "MyArtist%")}; const auto artists{ Artist::find(session, R"(MyArtist%)") };
ASSERT_TRUE(artists.size() == 1); ASSERT_TRUE(artists.size() == 1);
EXPECT_EQ(artists.front()->getId(), artist1.getId()); EXPECT_EQ(artists.front()->getId(), artist1.getId());
EXPECT_TRUE(Artist::find(session, "MyArtistFoo").empty()); EXPECT_TRUE(Artist::find(session, R"(MyArtistFoo)").empty());
} }
{ {
const auto artists {Artist::find(session, "%MyArtist")}; const auto artists{ Artist::find(session, R"(%MyArtist)") };
ASSERT_TRUE(artists.size() == 1); ASSERT_TRUE(artists.size() == 1);
EXPECT_EQ(artists.front()->getId(), artist2.getId()); EXPECT_EQ(artists.front()->getId(), artist2.getId());
EXPECT_TRUE(Artist::find(session, "FooMyArtist").empty()); EXPECT_TRUE(Artist::find(session, R"(FooMyArtist)").empty());
} }
{ {
const auto artists {Artist::find(session, "%_MyArtist")}; const auto artists{ Artist::find(session, R"(%_MyArtist)") };
ASSERT_TRUE(artists.size() == 1); ASSERT_TRUE(artists.size() == 1);
ASSERT_EQ(artists.front()->getId(), artist3.getId()); ASSERT_EQ(artists.front()->getId(), artist3.getId());
EXPECT_TRUE(Artist::find(session, "%CMyArtist").empty()); EXPECT_TRUE(Artist::find(session, R"(%CMyArtist)").empty());
} }
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
{ {
const auto artists {Artist::find(session, Artist::FindParameters {}.setKeywords({"MyArtist"}))}; const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setKeywords({"MyArtist"})) };
EXPECT_EQ(artists.results.size(), 6); EXPECT_EQ(artists.results.size(), 6);
} }
{ {
const auto artists {Artist::find(session, Artist::FindParameters {}.setKeywords({"MyArtist%"}).setSortMethod(ArtistSortMethod::ByName))}; const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setKeywords({"MyArtist%"}).setSortMethod(ArtistSortMethod::ByName)) };
ASSERT_EQ(artists.results.size(), 2); ASSERT_EQ(artists.results.size(), 2);
EXPECT_EQ(artists.results[0], artist1.getId()); EXPECT_EQ(artists.results[0], artist1.getId());
EXPECT_EQ(artists.results[1], artist4.getId()); EXPECT_EQ(artists.results[1], artist4.getId());
} }
{ {
const auto artists {Artist::find(session, Artist::FindParameters {}.setKeywords({"%MyArtist"}).setSortMethod(ArtistSortMethod::ByName))}; const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setKeywords({"%MyArtist"}).setSortMethod(ArtistSortMethod::ByName)) };
ASSERT_EQ(artists.results.size(), 2); ASSERT_EQ(artists.results.size(), 2);
EXPECT_EQ(artists.results[0], artist2.getId()); EXPECT_EQ(artists.results[0], artist2.getId());
EXPECT_EQ(artists.results[1], artist5.getId()); EXPECT_EQ(artists.results[1], artist5.getId());
} }
{ {
const auto artists {Artist::find(session, Artist::FindParameters {}.setKeywords({"_MyArtist"}).setSortMethod(ArtistSortMethod::ByName))}; const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setKeywords({"_MyArtist"}).setSortMethod(ArtistSortMethod::ByName)) };
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results[0], artist3.getId()); EXPECT_EQ(artists.results[0], artist3.getId());
} }
} }
} }
TEST_F(DatabaseFixture, Artist_sortMethod) TEST_F(DatabaseFixture, Artist_sortMethod)
{ {
ScopedArtist artistA {session, "artistA"}; ScopedArtist artistA{ session, "artistA" };
ScopedArtist artistB {session, "artistB"}; ScopedArtist artistB{ session, "artistB" };
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction{ session.createUniqueTransaction() };
artistA.get().modify()->setSortName("sortNameB"); artistA.get().modify()->setSortName("sortNameB");
artistB.get().modify()->setSortName("sortNameA"); artistB.get().modify()->setSortName("sortNameA");
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
auto allArtistsByName {Artist::find(session, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::ByName))}; auto allArtistsByName{ Artist::findIds(session, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::ByName)) };
auto allArtistsBySortName {Artist::find(session, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::BySortName))}; auto allArtistsBySortName{ Artist::findIds(session, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::BySortName)) };
ASSERT_EQ(allArtistsByName.results.size(), 2); ASSERT_EQ(allArtistsByName.results.size(), 2);
EXPECT_EQ(allArtistsByName.results.front(), artistA.getId()); EXPECT_EQ(allArtistsByName.results.front(), artistA.getId());
EXPECT_EQ(allArtistsByName.results.back(), artistB.getId()); EXPECT_EQ(allArtistsByName.results.back(), artistB.getId());
ASSERT_EQ(allArtistsBySortName.results.size(), 2); ASSERT_EQ(allArtistsBySortName.results.size(), 2);
EXPECT_EQ(allArtistsBySortName.results.front(), artistB.getId()); EXPECT_EQ(allArtistsBySortName.results.front(), artistB.getId());
EXPECT_EQ(allArtistsBySortName.results.back(), artistA.getId()); EXPECT_EQ(allArtistsBySortName.results.back(), artistA.getId());
} }
} }
TEST_F(DatabaseFixture, Artist_nonReleaseTracks) TEST_F(DatabaseFixture, Artist_nonReleaseTracks)
{ {
ScopedArtist artist {session, "artist"}; ScopedArtist artist{ session, "artist" };
ScopedTrack track1 {session, "MyTrack1"}; ScopedTrack track1{ session, "MyTrack1" };
ScopedTrack track2 {session, "MyTrack2"}; ScopedTrack track2{ session, "MyTrack2" };
ScopedRelease release{session, "MyRelease"}; ScopedRelease release{ session, "MyRelease" };
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
auto tracks {Track::find(session, Track::FindParameters {}.setNonRelease(true).setArtist(artist->getId()))}; auto tracks{ Track::findIds(session, Track::FindParameters {}.setNonRelease(true).setArtist(artist->getId())) };
EXPECT_EQ(tracks.results.size(), 0); EXPECT_EQ(tracks.results.size(), 0);
} }
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction{ session.createUniqueTransaction() };
TrackArtistLink::create(session, track1.get(), artist.get(), TrackArtistLinkType::Artist); TrackArtistLink::create(session, track1.get(), artist.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, track2.get(), artist.get(), TrackArtistLinkType::Artist); TrackArtistLink::create(session, track2.get(), artist.get(), TrackArtistLinkType::Artist);
track1.get().modify()->setRelease(release.get()); track1.get().modify()->setRelease(release.get());
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
const auto tracks {Track::find(session, Track::FindParameters {}.setArtist(artist.getId()).setNonRelease(true))}; const auto tracks{ Track::findIds(session, Track::FindParameters {}.setArtist(artist.getId()).setNonRelease(true)) };
ASSERT_EQ(tracks.results.size(), 1); ASSERT_EQ(tracks.results.size(), 1);
EXPECT_EQ(tracks.results.front(), track2.getId()); EXPECT_EQ(tracks.results.front(), track2.getId());
} }
} }
TEST_F(DatabaseFixture, Artist_findByRelease) TEST_F(DatabaseFixture, Artist_findByRelease)
{ {
ScopedArtist artist {session, "artist"}; ScopedArtist artist{ session, "artist" };
ScopedTrack track {session, "MyTrack"}; ScopedTrack track{ session, "MyTrack" };
ScopedRelease release{session, "MyRelease"}; ScopedRelease release{ session, "MyRelease" };
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
const auto artists {Artist::find(session, Artist::FindParameters {}.setRelease(release.getId()))}; const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setRelease(release.getId())) };
EXPECT_EQ(artists.results.size(), 0); EXPECT_EQ(artists.results.size(), 0);
} }
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction{ session.createUniqueTransaction() };
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
const auto artists {Artist::find(session, Artist::FindParameters {}.setRelease(release.getId()))}; const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setRelease(release.getId())) };
EXPECT_EQ(artists.results.size(), 0); EXPECT_EQ(artists.results.size(), 0);
} }
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction{ session.createUniqueTransaction() };
track.get().modify()->setRelease(release.get()); track.get().modify()->setRelease(release.get());
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
const auto artists {Artist::find(session, Artist::FindParameters {}.setRelease(release.getId()))}; const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setRelease(release.getId())) };
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId()); EXPECT_EQ(artists.results.front(), artist.getId());
} }
} }
+82 -37
View File
@@ -133,11 +133,11 @@ TEST_F(DatabaseFixture, Cluster_singleTrack)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto tracks{ Track::find(session, Track::FindParameters {}.setClusters({cluster1.getId()})) }; auto tracks{ Track::findIds(session, Track::FindParameters {}.setClusters({cluster1.getId()})) };
ASSERT_EQ(tracks.results.size(), 1); ASSERT_EQ(tracks.results.size(), 1);
EXPECT_EQ(tracks.results.front(), track.getId()); EXPECT_EQ(tracks.results.front(), track.getId());
tracks = Track::find(session, Track::FindParameters{}.setClusters({ cluster2.getId() })); tracks = Track::findIds(session, Track::FindParameters{}.setClusters({ cluster2.getId() }));
EXPECT_TRUE(tracks.results.empty()); EXPECT_TRUE(tracks.results.empty());
} }
@@ -154,6 +154,51 @@ TEST_F(DatabaseFixture, Cluster_singleTrack)
} }
} }
TEST_F(DatabaseFixture, Cluster_singleTrackWithSeveralClusters)
{
ScopedTrack track{ session, "MyTrack" };
ScopedClusterType clusterType{ session, "MyClusterType" };
ScopedCluster cluster1{ session, clusterType.lockAndGet(), "MyCluster1" };
ScopedCluster cluster2{ session, clusterType.lockAndGet(), "MyCluster2" };
const std::vector<ClusterId> clusterIds{ cluster1.getId(), cluster2.getId() };
{
auto transaction{ session.createSharedTransaction() };
const auto tracks{ Track::findIds(session, Track::FindParameters{}.setClusters(clusterIds)) };
EXPECT_TRUE(tracks.results.empty());
}
{
auto transaction{ session.createUniqueTransaction() };
cluster1.get().modify()->addTrack(track.get());
}
{
auto transaction{ session.createSharedTransaction() };
const auto tracks{ Track::findIds(session, Track::FindParameters{}.setClusters(clusterIds)) };
EXPECT_TRUE(tracks.results.empty());
}
{
auto transaction{ session.createUniqueTransaction() };
cluster2.get().modify()->addTrack(track.get());
}
{
auto transaction{ session.createSharedTransaction() };
const auto tracks{ Track::findIds(session, Track::FindParameters{}.setClusters(clusterIds)) };
ASSERT_FALSE(tracks.results.empty());
EXPECT_EQ(tracks.results.front(), track.getId());
}
}
TEST_F(DatabaseFixture, Cluster_multiTracks) TEST_F(DatabaseFixture, Cluster_multiTracks)
{ {
std::list<ScopedTrack> tracks; std::list<ScopedTrack> tracks;
@@ -329,7 +374,7 @@ TEST_F(DatabaseFixture, Cluster_singleTrackSingleReleaseSingleCluster)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto releases{ Release::find(session, Release::FindParameters {}.setClusters({cluster.getId()})) }; const auto releases{ Release::findIds(session, Release::FindParameters {}.setClusters({cluster.getId()})) };
ASSERT_EQ(releases.results.size(), 1); ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results.front(), release.getId()); EXPECT_EQ(releases.results.front(), release.getId());
} }
@@ -337,7 +382,7 @@ TEST_F(DatabaseFixture, Cluster_singleTrackSingleReleaseSingleCluster)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto releases{ Release::find(session, Release::FindParameters {}.setClusters({unusedCluster.getId()})) }; const auto releases{ Release::findIds(session, Release::FindParameters {}.setClusters({unusedCluster.getId()})) };
EXPECT_EQ(releases.results.size(), 0); EXPECT_EQ(releases.results.size(), 0);
} }
@@ -370,8 +415,8 @@ TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiClusters)
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
EXPECT_TRUE(ClusterType::findOrphans(session, Range{}).results.empty()); EXPECT_TRUE(ClusterType::findOrphans(session, Range{}).results.empty());
EXPECT_EQ(Cluster::findOrphans(session, Range{}).results.size(), 2); EXPECT_EQ(Cluster::findOrphans(session, Range{}).results.size(), 2);
EXPECT_TRUE(Release::findOrphans(session, Range{}).results.empty()); EXPECT_TRUE(Release::findOrphanIds(session, Range{}).results.empty());
EXPECT_TRUE(Artist::findAllOrphans(session, Range{}).results.empty()); EXPECT_TRUE(Artist::findOrphanIds(session, Range{}).results.empty());
} }
{ {
@@ -383,12 +428,12 @@ TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiClusters)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto artists{ Artist::find(session, Artist::FindParameters {}.setClusters({cluster1.getId()})) }; auto artists{ Artist::findIds(session, Artist::FindParameters {}.setClusters({cluster1.getId()})) };
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId()); EXPECT_EQ(artists.results.front(), artist.getId());
EXPECT_TRUE(Artist::find(session, Artist::FindParameters{}.setClusters({ cluster2.getId() })).results.empty()); EXPECT_TRUE(Artist::findIds(session, Artist::FindParameters{}.setClusters({ cluster2.getId() })).results.empty());
EXPECT_TRUE(Artist::find(session, Artist::FindParameters{}.setClusters({ cluster3.getId() })).results.empty()); EXPECT_TRUE(Artist::findIds(session, Artist::FindParameters{}.setClusters({ cluster3.getId() })).results.empty());
cluster2.get().modify()->addTrack(track.get()); cluster2.get().modify()->addTrack(track.get());
} }
@@ -396,19 +441,19 @@ TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiClusters)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto artists{ Artist::find(session, Artist::FindParameters {}.setClusters({cluster1.getId()})) }; auto artists{ Artist::findIds(session, Artist::FindParameters {}.setClusters({cluster1.getId()})) };
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId()); EXPECT_EQ(artists.results.front(), artist.getId());
artists = Artist::find(session, Artist::FindParameters{}.setClusters({ cluster2.getId() })); artists = Artist::findIds(session, Artist::FindParameters{}.setClusters({ cluster2.getId() }));
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId()); EXPECT_EQ(artists.results.front(), artist.getId());
artists = Artist::find(session, Artist::FindParameters{}.setClusters({ cluster1.getId() })); artists = Artist::findIds(session, Artist::FindParameters{}.setClusters({ cluster1.getId() }));
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId()); EXPECT_EQ(artists.results.front(), artist.getId());
EXPECT_TRUE(Artist::find(session, Artist::FindParameters{}.setClusters({ cluster3.getId() })).results.empty()); EXPECT_TRUE(Artist::findIds(session, Artist::FindParameters{}.setClusters({ cluster3.getId() })).results.empty());
} }
} }
@@ -430,14 +475,14 @@ TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiRolesMultiClusters)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
EXPECT_TRUE(Cluster::findOrphans(session, Range{}).results.empty()); EXPECT_TRUE(Cluster::findOrphans(session, Range{}).results.empty());
EXPECT_TRUE(Release::findOrphans(session, Range{}).results.empty()); EXPECT_TRUE(Release::findOrphanIds(session, Range{}).results.empty());
EXPECT_TRUE(Artist::findAllOrphans(session, Range{}).results.empty()); EXPECT_TRUE(Artist::findOrphanIds(session, Range{}).results.empty());
} }
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto artists{ Artist::find(session, Artist::FindParameters {}.setClusters({cluster.getId()})) }; auto artists{ Artist::findIds(session, Artist::FindParameters {}.setClusters({cluster.getId()})) };
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId()); EXPECT_EQ(artists.results.front(), artist.getId());
} }
@@ -470,7 +515,7 @@ TEST_F(DatabaseFixture, MultiTracksSingleArtistMultiClusters)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
EXPECT_TRUE(Cluster::findOrphans(session, Range{}).results.empty()); EXPECT_TRUE(Cluster::findOrphans(session, Range{}).results.empty());
EXPECT_TRUE(Artist::findAllOrphans(session, Range{}).results.empty()); EXPECT_TRUE(Artist::findOrphanIds(session, Range{}).results.empty());
} }
{ {
@@ -479,7 +524,7 @@ TEST_F(DatabaseFixture, MultiTracksSingleArtistMultiClusters)
std::vector<ClusterId> clusterIds; std::vector<ClusterId> clusterIds;
std::transform(std::cbegin(clusters), std::cend(clusters), std::back_inserter(clusterIds), [](const ScopedCluster& cluster) { return cluster.getId(); }); std::transform(std::cbegin(clusters), std::cend(clusters), std::back_inserter(clusterIds), [](const ScopedCluster& cluster) { return cluster.getId(); });
auto artists{ Artist::find(session, Artist::FindParameters {}.setClusters(clusterIds)) }; auto artists{ Artist::findIds(session, Artist::FindParameters {}.setClusters(clusterIds)) };
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId()); EXPECT_EQ(artists.results.front(), artist.getId());
} }
@@ -505,7 +550,7 @@ TEST_F(DatabaseFixture, MultipleTracksSingleClusterSimilarity)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
const auto similarTracks{ Track::findSimilarTracks(session, {tracks.front().getId()}, Range {}) }; const auto similarTracks{ Track::findSimilarTrackIds(session, {tracks.front().getId()}, Range {}) };
EXPECT_EQ(similarTracks.results.size(), tracks.size() - 1); EXPECT_EQ(similarTracks.results.size(), tracks.size() - 1);
for (const TrackId similarTrackId : similarTracks.results) for (const TrackId similarTrackId : similarTracks.results)
{ {
@@ -546,14 +591,14 @@ TEST_F(DatabaseFixture, MultipleTracksMultipleClustersSimilarity)
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
{ {
auto similarTracks{ Track::findSimilarTracks(session, {tracks.back().getId()}, Range {0, 4}) }; auto similarTracks{ Track::findSimilarTrackIds(session, {tracks.back().getId()}, Range {0, 4}) };
EXPECT_EQ(similarTracks.results.size(), 4); EXPECT_EQ(similarTracks.results.size(), 4);
for (const TrackId similarTrackId : similarTracks.results) for (const TrackId similarTrackId : similarTracks.results)
EXPECT_TRUE(std::find_if(std::next(std::cbegin(tracks), 5), std::next(std::cend(tracks), -1), [&](const auto& track) { return similarTrackId == track.getId(); }) != std::cend(tracks)); EXPECT_TRUE(std::find_if(std::next(std::cbegin(tracks), 5), std::next(std::cend(tracks), -1), [&](const auto& track) { return similarTrackId == track.getId(); }) != std::cend(tracks));
} }
{ {
auto similarTracks{ Track::findSimilarTracks(session, {tracks.front().getId()}, Range {}) }; auto similarTracks{ Track::findSimilarTrackIds(session, {tracks.front().getId()}, Range {}) };
EXPECT_EQ(similarTracks.results.size(), tracks.size() - 1); EXPECT_EQ(similarTracks.results.size(), tracks.size() - 1);
for (const TrackId similarTrackId : similarTracks.results) for (const TrackId similarTrackId : similarTracks.results)
EXPECT_TRUE(std::find_if(std::next(std::cbegin(tracks), 1), std::cend(tracks), [&](const auto& track) { return similarTrackId == track.getId(); }) != std::cend(tracks)); EXPECT_TRUE(std::find_if(std::next(std::cbegin(tracks), 1), std::cend(tracks), [&](const auto& track) { return similarTrackId == track.getId(); }) != std::cend(tracks));
@@ -582,22 +627,22 @@ TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtistSingleCluster)
EXPECT_TRUE(Cluster::findOrphans(session, Range{}).results.empty()); EXPECT_TRUE(Cluster::findOrphans(session, Range{}).results.empty());
EXPECT_TRUE(ClusterType::findOrphans(session, Range{}).results.empty()); EXPECT_TRUE(ClusterType::findOrphans(session, Range{}).results.empty());
EXPECT_TRUE(Artist::findAllOrphans(session, Range{}).results.empty()); EXPECT_TRUE(Artist::findOrphanIds(session, Range{}).results.empty());
EXPECT_TRUE(Release::findOrphans(session, Range{}).results.empty()); EXPECT_TRUE(Release::findOrphanIds(session, Range{}).results.empty());
} }
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto artists{ Artist::find(session, Artist::FindParameters {}.setClusters({cluster.getId()})) }; auto artists{ Artist::findIds(session, Artist::FindParameters {}.setClusters({cluster.getId()})) };
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId()); EXPECT_EQ(artists.results.front(), artist.getId());
auto releases{ Release::find(session, Release::FindParameters {}.setArtist(artist.getId())) }; auto releases{ Release::findIds(session, Release::FindParameters {}.setArtist(artist.getId())) };
ASSERT_EQ(releases.results.size(), 1); ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results.front(), release.getId()); EXPECT_EQ(releases.results.front(), release.getId());
releases = Release::find(session, Release::FindParameters{}.setArtist(artist.getId()).setClusters({ cluster.getId() })); releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId()).setClusters({ cluster.getId() }));
ASSERT_EQ(releases.results.size(), 1); ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results.front(), release.getId()); EXPECT_EQ(releases.results.front(), release.getId());
} }
@@ -624,11 +669,11 @@ TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtistMultiClusters)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto releases{ Release::find(session, Release::FindParameters {}.setArtist(artist.getId())) }; auto releases{ Release::findIds(session, Release::FindParameters {}.setArtist(artist.getId())) };
ASSERT_EQ(releases.results.size(), 1); ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results.front(), release.getId()); EXPECT_EQ(releases.results.front(), release.getId());
releases = Release::find(session, Release::FindParameters{}.setArtist(artist.getId()).setClusters({ cluster1.getId(), cluster2.getId() })); releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId()).setClusters({ cluster1.getId(), cluster2.getId() }));
ASSERT_EQ(releases.results.size(), 1); ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results.front(), release.getId()); EXPECT_EQ(releases.results.front(), release.getId());
} }
@@ -982,9 +1027,9 @@ TEST_F(DatabaseFixture, MultipleTracksMultipleArtistsMultiClusters)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
EXPECT_TRUE(artist1->findSimilarArtists().results.empty()); EXPECT_TRUE(artist1->findSimilarArtistIds().results.empty());
EXPECT_TRUE(artist2->findSimilarArtists().results.empty()); EXPECT_TRUE(artist2->findSimilarArtistIds().results.empty());
EXPECT_TRUE(artist3->findSimilarArtists().results.empty()); EXPECT_TRUE(artist3->findSimilarArtistIds().results.empty());
} }
std::list<ScopedTrack> tracks; std::list<ScopedTrack> tracks;
@@ -1016,35 +1061,35 @@ TEST_F(DatabaseFixture, MultipleTracksMultipleArtistsMultiClusters)
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
{ {
auto artists{ artist1->findSimilarArtists() }; auto artists{ artist1->findSimilarArtistIds() };
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results.front(), artist2.getId()); EXPECT_EQ(artists.results.front(), artist2.getId());
} }
{ {
auto artists{ artist1->findSimilarArtists({TrackArtistLinkType::Artist}) }; auto artists{ artist1->findSimilarArtistIds({TrackArtistLinkType::Artist}) };
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results.front(), artist2.getId()); EXPECT_EQ(artists.results.front(), artist2.getId());
} }
{ {
auto artists{ artist1->findSimilarArtists({TrackArtistLinkType::ReleaseArtist}) }; auto artists{ artist1->findSimilarArtistIds({TrackArtistLinkType::ReleaseArtist}) };
EXPECT_EQ(artists.results.empty(), 1); EXPECT_EQ(artists.results.empty(), 1);
} }
{ {
auto artists{ artist1->findSimilarArtists({TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist}) }; auto artists{ artist1->findSimilarArtistIds({TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist}) };
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results.front(), artist2.getId()); EXPECT_EQ(artists.results.front(), artist2.getId());
} }
{ {
auto artists{ artist1->findSimilarArtists({TrackArtistLinkType::Composer}) }; auto artists{ artist1->findSimilarArtistIds({TrackArtistLinkType::Composer}) };
EXPECT_TRUE(artists.results.empty()); EXPECT_TRUE(artists.results.empty());
} }
{ {
auto artists{ artist2->findSimilarArtists() }; auto artists{ artist2->findSimilarArtistIds() };
ASSERT_EQ(artists.results.size(), 2); ASSERT_EQ(artists.results.size(), 2);
EXPECT_EQ(artists.results[0], artist1.getId()); EXPECT_EQ(artists.results[0], artist1.getId());
EXPECT_EQ(artists.results[1], artist3.getId()); EXPECT_EQ(artists.results[1], artist3.getId());
@@ -25,85 +25,85 @@ using namespace Database;
TEST_F(DatabaseFixture, MultiTracksSingleArtistSingleRelease) TEST_F(DatabaseFixture, MultiTracksSingleArtistSingleRelease)
{ {
constexpr std::size_t nbTracks {10}; constexpr std::size_t nbTracks{ 10 };
std::list<ScopedTrack> tracks; std::list<ScopedTrack> tracks;
ScopedArtist artist {session, "MyArtst"}; ScopedArtist artist{ session, "MyArtst" };
ScopedRelease release {session, "MyRelease"}; ScopedRelease release{ session, "MyRelease" };
for (std::size_t i {}; i < nbTracks; ++i) for (std::size_t i{}; i < nbTracks; ++i)
{ {
tracks.emplace_back(session, "MyTrack" + std::to_string(i)); tracks.emplace_back(session, "MyTrack" + std::to_string(i));
auto transaction {session.createUniqueTransaction()}; auto transaction{ session.createUniqueTransaction() };
TrackArtistLink::create(session, tracks.back().get(), artist.get(), TrackArtistLinkType::Artist); TrackArtistLink::create(session, tracks.back().get(), artist.get(), TrackArtistLinkType::Artist);
tracks.back().get().modify()->setRelease(release.get()); tracks.back().get().modify()->setRelease(release.get());
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
EXPECT_TRUE(Release::findOrphans(session, Range {}).results.empty()); EXPECT_TRUE(Release::findOrphanIds(session, Range{}).results.empty());
EXPECT_TRUE(Artist::findAllOrphans(session, Range {}).results.empty()); EXPECT_TRUE(Artist::findOrphanIds(session, Range{}).results.empty());
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
auto releases {Release::find(session, Release::FindParameters {}.setArtist(artist.getId()))}; auto releases{ Release::findIds(session, Release::FindParameters {}.setArtist(artist.getId())) };
ASSERT_EQ(releases.results.size(), 1); ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results.front(), release.getId()); EXPECT_EQ(releases.results.front(), release.getId());
const auto releaseTracks {Track::find(session, Track::FindParameters {}.setRelease(release.getId()))}; const auto releaseTracks{ Track::find(session, Track::FindParameters {}.setRelease(release.getId())) };
EXPECT_EQ(releaseTracks.results.size(), nbTracks); EXPECT_EQ(releaseTracks.results.size(), nbTracks);
} }
} }
TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtist) TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtist)
{ {
ScopedTrack track {session, "MyTrack"}; ScopedTrack track{ session, "MyTrack" };
ScopedRelease release {session, "MyRelease"}; ScopedRelease release{ session, "MyRelease" };
ScopedArtist artist {session, "MyArtist"}; ScopedArtist artist{ session, "MyArtist" };
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction{ session.createUniqueTransaction() };
auto trackArtistLink {TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist)}; auto trackArtistLink{ TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist) };
track.get().modify()->setRelease(release.get()); track.get().modify()->setRelease(release.get());
} }
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction{ session.createUniqueTransaction() };
auto releases {Release::find(session, Release::FindParameters {}.setArtist(artist.getId()))}; auto releases{ Release::findIds(session, Release::FindParameters {}.setArtist(artist.getId())) };
ASSERT_EQ(releases.results.size(), 1); ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results.front(), release.getId()); EXPECT_EQ(releases.results.front(), release.getId());
auto artists {release->getArtists()}; auto artists{ release->getArtists() };
ASSERT_EQ(artists.size(), 1); ASSERT_EQ(artists.size(), 1);
ASSERT_EQ(artists.front()->getId(), artist.getId()); ASSERT_EQ(artists.front()->getId(), artist.getId());
} }
} }
TEST_F(DatabaseFixture, SingleUser) TEST_F(DatabaseFixture, SingleUser)
{ {
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
EXPECT_TRUE(User::find(session, User::FindParameters {}).results.empty()); EXPECT_TRUE(User::find(session, User::FindParameters{}).results.empty());
EXPECT_EQ(User::getCount(session), 0); EXPECT_EQ(User::getCount(session), 0);
} }
ScopedUser user {session, "MyUser"}; ScopedUser user{ session, "MyUser" };
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
EXPECT_EQ(User::find(session, User::FindParameters {}).results.size(), 1); EXPECT_EQ(User::find(session, User::FindParameters{}).results.size(), 1);
EXPECT_EQ(User::getCount(session), 1); EXPECT_EQ(User::getCount(session), 1);
} }
} }
int main(int argc, char **argv) int main(int argc, char** argv)
{ {
::testing::InitGoogleTest(&argc, argv); ::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS(); return RUN_ALL_TESTS();
} }
+196 -131
View File
@@ -35,7 +35,7 @@ TEST_F(DatabaseFixture, Listen_getAll)
EXPECT_EQ(Listen::getCount(session), 0); EXPECT_EQ(Listen::getCount(session), 0);
} }
ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, Wt::WDateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12, 0, 1}} }; ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, Wt::WDateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12, 0, 1}} };
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
@@ -45,7 +45,7 @@ TEST_F(DatabaseFixture, Listen_getAll)
{ {
auto transaction{ session.createUniqueTransaction() }; auto transaction{ session.createUniqueTransaction() };
ScopedListen listen2{ session, user.get(), track.get(), Scrobbler::Internal, Wt::WDateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{13, 0, 1}} }; ScopedListen listen2{ session, user.get(), track.get(), ScrobblingBackend::Internal, Wt::WDateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{13, 0, 1}} };
EXPECT_EQ(Listen::getCount(session), 2); EXPECT_EQ(Listen::getCount(session), 2);
} }
@@ -61,12 +61,12 @@ TEST_F(DatabaseFixture, Listen_get)
{ {
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session, "MyTrack" };
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, Wt::WDateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12, 0, 1}} }; ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, Wt::WDateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12, 0, 1}} };
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto listens{ Listen::find(session, Listen::FindParameters{}.setUser(user->getId()).setScrobbler(Scrobbler::ListenBrainz)) }; auto listens{ Listen::find(session, Listen::FindParameters{}.setUser(user->getId()).setScrobblingBackend(ScrobblingBackend::ListenBrainz)) };
EXPECT_EQ(listens.results.size(), 0); EXPECT_EQ(listens.results.size(), 0);
} }
@@ -74,18 +74,18 @@ TEST_F(DatabaseFixture, Listen_get)
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
{ {
auto listens{ Listen::find(session, Listen::FindParameters{}.setUser(user->getId()).setScrobbler(Scrobbler::Internal)) }; auto listens{ Listen::find(session, Listen::FindParameters{}.setUser(user->getId()).setScrobblingBackend(ScrobblingBackend::Internal)) };
EXPECT_EQ(listens.moreResults, false); EXPECT_EQ(listens.moreResults, false);
ASSERT_EQ(listens.results.size(), 1); ASSERT_EQ(listens.results.size(), 1);
EXPECT_EQ(listens.results.front(), listen->getId()); EXPECT_EQ(listens.results.front(), listen->getId());
} }
{ {
auto listens{ Listen::find(session, Listen::FindParameters{}.setUser(user->getId()).setScrobbler(Scrobbler::Internal).setScrobblingState(ScrobblingState::PendingAdd)) }; auto listens{ Listen::find(session, Listen::FindParameters{}.setUser(user->getId()).setScrobblingBackend(ScrobblingBackend::Internal).setSyncState(SyncState::PendingAdd)) };
EXPECT_EQ(listens.results.size(), 1); EXPECT_EQ(listens.results.size(), 1);
} }
{ {
auto listens{ Listen::find(session, Listen::FindParameters{}.setUser(user->getId()).setScrobbler(Scrobbler::Internal).setScrobblingState(ScrobblingState::Synchronized)) }; auto listens{ Listen::find(session, Listen::FindParameters{}.setUser(user->getId()).setScrobblingBackend(ScrobblingBackend::Internal).setSyncState(SyncState::Synchronized)) };
EXPECT_EQ(listens.results.size(), 0); EXPECT_EQ(listens.results.size(), 0);
} }
} }
@@ -95,14 +95,14 @@ TEST_F(DatabaseFixture, Listen_get_multi)
{ {
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session, "MyTrack" };
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
ScopedListen listen3{ session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, Wt::WDateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12, 0, 3}} }; ScopedListen listen3{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, Wt::WDateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12, 0, 3}} };
ScopedListen listen1{ session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, Wt::WDateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12, 0, 1}} }; ScopedListen listen1{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, Wt::WDateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12, 0, 1}} };
ScopedListen listen2{ session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, Wt::WDateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12, 0, 2}} }; ScopedListen listen2{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, Wt::WDateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12, 0, 2}} };
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto listens{ Listen::find(session, Listen::FindParameters{}.setUser(user->getId()).setScrobbler(Scrobbler::Internal)) }; auto listens{ Listen::find(session, Listen::FindParameters{}.setUser(user->getId()).setScrobblingBackend(ScrobblingBackend::Internal)) };
ASSERT_EQ(listens.results.size(), 3); ASSERT_EQ(listens.results.size(), 3);
EXPECT_EQ(listens.results[0], listen1.getId()); EXPECT_EQ(listens.results[0], listen1.getId());
EXPECT_EQ(listens.results[1], listen2.getId()); EXPECT_EQ(listens.results[1], listen2.getId());
@@ -118,26 +118,26 @@ TEST_F(DatabaseFixture, Listen_get_byDateTime)
const Wt::WDateTime dateTime2{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 2} }; const Wt::WDateTime dateTime2{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 2} };
ASSERT_GT(dateTime2, dateTime1); ASSERT_GT(dateTime2, dateTime1);
ScopedListen listen1{ session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime1 }; ScopedListen listen1{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime1 };
ScopedListen listen2{ session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime2 }; ScopedListen listen2{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime2 };
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
{ {
Listen::pointer listen{ Listen::find(session, user.getId(), track.getId(), Scrobbler::Internal, dateTime1) }; Listen::pointer listen{ Listen::find(session, user.getId(), track.getId(), ScrobblingBackend::Internal, dateTime1) };
ASSERT_TRUE(listen); ASSERT_TRUE(listen);
EXPECT_EQ(listen->getId(), listen1.getId()); EXPECT_EQ(listen->getId(), listen1.getId());
} }
{ {
Listen::pointer listen{ Listen::find(session, user.getId(), track.getId(), Scrobbler::Internal, dateTime2) }; Listen::pointer listen{ Listen::find(session, user.getId(), track.getId(), ScrobblingBackend::Internal, dateTime2) };
ASSERT_TRUE(listen); ASSERT_TRUE(listen);
EXPECT_EQ(listen->getId(), listen2.getId()); EXPECT_EQ(listen->getId(), listen2.getId());
} }
{ {
Listen::pointer listen{ Listen::find(session, user.getId(), track.getId(), Scrobbler::Internal, dateTime2.addSecs(56)) }; Listen::pointer listen{ Listen::find(session, user.getId(), track.getId(), ScrobblingBackend::Internal, dateTime2.addSecs(56)) };
EXPECT_FALSE(listen); EXPECT_FALSE(listen);
} }
} }
@@ -148,19 +148,19 @@ TEST_F(DatabaseFixture, Listen_getTopArtists)
ScopedTrack track1{ session, "MyTrack" }; ScopedTrack track1{ session, "MyTrack" };
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
const Wt::WDateTime dateTime1{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} }; const Wt::WDateTime dateTime1{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} };
ScopedListen listen1{ session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime1 }; ScopedListen listen1{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime1 };
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto artists{ Listen::getTopArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt) }; auto artists{ Listen::getTopArtists(session, user->getId(), ScrobblingBackend::Internal, {}, std::nullopt) };
EXPECT_EQ(artists.results.size(), 0); EXPECT_EQ(artists.results.size(), 0);
EXPECT_EQ(artists.moreResults, false); EXPECT_EQ(artists.moreResults, false);
} }
ScopedTrack track2{ session, "MyTrack2" }; ScopedTrack track2{ session, "MyTrack2" };
ScopedArtist artist1{ session, "MyArtist1" }; ScopedArtist artist1{ session, "MyArtist1" };
ScopedListen listen2{ session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime1.addSecs(1) }; ScopedListen listen2{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime1.addSecs(1) };
{ {
auto transaction{ session.createUniqueTransaction() }; auto transaction{ session.createUniqueTransaction() };
@@ -171,20 +171,20 @@ TEST_F(DatabaseFixture, Listen_getTopArtists)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto artists{ Listen::getTopArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt) }; auto artists{ Listen::getTopArtists(session, user->getId(), ScrobblingBackend::Internal, {}, std::nullopt) };
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results[0], artist1->getId()); EXPECT_EQ(artists.results[0], artist1->getId());
} }
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto artists{ Listen::getTopArtists(session, user->getId(), Scrobbler::ListenBrainz, {}, std::nullopt) }; auto artists{ Listen::getTopArtists(session, user->getId(), ScrobblingBackend::ListenBrainz, {}, std::nullopt) };
ASSERT_EQ(artists.results.size(), 0); ASSERT_EQ(artists.results.size(), 0);
} }
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto artists{ Listen::getTopArtists(session, user->getId(), Scrobbler::Internal, {}, TrackArtistLinkType::Producer) }; auto artists{ Listen::getTopArtists(session, user->getId(), ScrobblingBackend::Internal, {}, TrackArtistLinkType::Producer) };
EXPECT_EQ(artists.results.size(), 0); EXPECT_EQ(artists.results.size(), 0);
} }
{ {
@@ -194,7 +194,7 @@ TEST_F(DatabaseFixture, Listen_getTopArtists)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto artists{ Listen::getTopArtists(session, user->getId(), Scrobbler::Internal, {cluster->getId()}, std::nullopt) }; auto artists{ Listen::getTopArtists(session, user->getId(), ScrobblingBackend::Internal, {cluster->getId()}, std::nullopt) };
EXPECT_EQ(artists.results.size(), 0); EXPECT_EQ(artists.results.size(), 0);
} }
} }
@@ -218,24 +218,24 @@ TEST_F(DatabaseFixture, Listen_getTopArtists_multi)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto artists{ Listen::getTopArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt) }; auto artists{ Listen::getTopArtists(session, user->getId(), ScrobblingBackend::Internal, {}, std::nullopt) };
EXPECT_EQ(artists.results.size(), 0); EXPECT_EQ(artists.results.size(), 0);
} }
ScopedListen listen1{ session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime.addSecs(1) }; ScopedListen listen1{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime.addSecs(1) };
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto artists{ Listen::getTopArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt) }; auto artists{ Listen::getTopArtists(session, user->getId(), ScrobblingBackend::Internal, {}, std::nullopt) };
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results[0], artist1->getId()); EXPECT_EQ(artists.results[0], artist1->getId());
} }
ScopedListen listen2{ session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime.addSecs(2) }; ScopedListen listen2{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime.addSecs(2) };
ScopedListen listen3{ session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime.addSecs(3) }; ScopedListen listen3{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime.addSecs(3) };
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto artists{ Listen::getTopArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt) }; auto artists{ Listen::getTopArtists(session, user->getId(), ScrobblingBackend::Internal, {}, std::nullopt) };
ASSERT_EQ(artists.results.size(), 2); ASSERT_EQ(artists.results.size(), 2);
EXPECT_EQ(artists.results[0], artist2->getId()); EXPECT_EQ(artists.results[0], artist2->getId());
EXPECT_EQ(artists.results[1], artist1->getId()); EXPECT_EQ(artists.results[1], artist1->getId());
@@ -243,7 +243,7 @@ TEST_F(DatabaseFixture, Listen_getTopArtists_multi)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto artists{ Listen::getTopArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt, Range {0, 1}) }; auto artists{ Listen::getTopArtists(session, user->getId(), ScrobblingBackend::Internal, {}, std::nullopt, Range {0, 1}) };
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.moreResults, true); EXPECT_EQ(artists.moreResults, true);
EXPECT_EQ(artists.results[0], artist2->getId()); EXPECT_EQ(artists.results[0], artist2->getId());
@@ -256,7 +256,7 @@ TEST_F(DatabaseFixture, Listen_getTopArtists_cluster)
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
ScopedArtist artist{ session, "MyArtist" }; ScopedArtist artist{ session, "MyArtist" };
const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} }; const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} };
ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime }; ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime };
ScopedClusterType clusterType{ session, "MyType" }; ScopedClusterType clusterType{ session, "MyType" };
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" }; ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" };
@@ -268,7 +268,7 @@ TEST_F(DatabaseFixture, Listen_getTopArtists_cluster)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto artists{ Listen::getTopArtists(session, user->getId(), Scrobbler::Internal, {cluster.getId()}, std::nullopt) }; auto artists{ Listen::getTopArtists(session, user->getId(), ScrobblingBackend::Internal, {cluster.getId()}, std::nullopt) };
ASSERT_EQ(artists.results.size(), 0); ASSERT_EQ(artists.results.size(), 0);
} }
{ {
@@ -278,7 +278,7 @@ TEST_F(DatabaseFixture, Listen_getTopArtists_cluster)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto artists{ Listen::getTopArtists(session, user->getId(), Scrobbler::Internal, {cluster.getId()}, std::nullopt) }; auto artists{ Listen::getTopArtists(session, user->getId(), ScrobblingBackend::Internal, {cluster.getId()}, std::nullopt) };
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results[0], artist.getId()); EXPECT_EQ(artists.results[0], artist.getId());
} }
@@ -291,24 +291,24 @@ TEST_F(DatabaseFixture, Listen_getTopReleases)
const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} }; const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} };
ScopedRelease release{ session, "MyRelease" }; ScopedRelease release{ session, "MyRelease" };
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createUniqueTransaction() };
track.get().modify()->setRelease(release.get()); track.get().modify()->setRelease(release.get());
} }
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto releases{ Listen::getTopReleases(session, user->getId(), Scrobbler::Internal, {}) }; auto releases{ Listen::getTopReleases(session, user->getId(), ScrobblingBackend::Internal, {}) };
EXPECT_EQ(releases.moreResults, false); EXPECT_EQ(releases.moreResults, false);
EXPECT_EQ(releases.results.size(), 0); EXPECT_EQ(releases.results.size(), 0);
} }
ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime }; ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime };
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto releases{ Listen::getTopReleases(session, user->getId(), Scrobbler::Internal, {}) }; auto releases{ Listen::getTopReleases(session, user->getId(), ScrobblingBackend::Internal, {}) };
EXPECT_EQ(releases.moreResults, false); EXPECT_EQ(releases.moreResults, false);
ASSERT_EQ(releases.results.size(), 1); ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results[0], release.getId()); EXPECT_EQ(releases.results[0], release.getId());
@@ -316,7 +316,7 @@ TEST_F(DatabaseFixture, Listen_getTopReleases)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto releases{ Listen::getTopReleases(session, user->getId(), Scrobbler::ListenBrainz, {}) }; auto releases{ Listen::getTopReleases(session, user->getId(), ScrobblingBackend::ListenBrainz, {}) };
EXPECT_EQ(releases.moreResults, false); EXPECT_EQ(releases.moreResults, false);
EXPECT_EQ(releases.results.size(), 0); EXPECT_EQ(releases.results.size(), 0);
} }
@@ -328,12 +328,12 @@ TEST_F(DatabaseFixture, Listen_getTopReleases_multi)
ScopedTrack track2{ session, "MyTrack" }; ScopedTrack track2{ session, "MyTrack" };
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} }; const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} };
ScopedListen listen1{ session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime }; ScopedListen listen1{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime };
ScopedRelease release1{ session, "MyRelease1" }; ScopedRelease release1{ session, "MyRelease1" };
ScopedRelease release2{ session, "MyRelease2" }; ScopedRelease release2{ session, "MyRelease2" };
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createUniqueTransaction() };
track1.get().modify()->setRelease(release1.get()); track1.get().modify()->setRelease(release1.get());
track2.get().modify()->setRelease(release2.get()); track2.get().modify()->setRelease(release2.get());
} }
@@ -341,28 +341,28 @@ TEST_F(DatabaseFixture, Listen_getTopReleases_multi)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto releases{ Listen::getTopReleases(session, user->getId(), Scrobbler::Internal, {}) }; auto releases{ Listen::getTopReleases(session, user->getId(), ScrobblingBackend::Internal, {}) };
EXPECT_EQ(releases.moreResults, false); EXPECT_EQ(releases.moreResults, false);
ASSERT_EQ(releases.results.size(), 1); ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results[0], release1.getId()); EXPECT_EQ(releases.results[0], release1.getId());
} }
ScopedListen listen2{ session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime }; ScopedListen listen2{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime };
ScopedListen listen3{ session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime }; ScopedListen listen3{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime };
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto releases{ Listen::getTopReleases(session, user->getId(), Scrobbler::Internal, {}) }; auto releases{ Listen::getTopReleases(session, user->getId(), ScrobblingBackend::Internal, {}) };
EXPECT_EQ(releases.moreResults, false); EXPECT_EQ(releases.moreResults, false);
ASSERT_EQ(releases.results.size(), 2); ASSERT_EQ(releases.results.size(), 2);
EXPECT_EQ(releases.results[0], release2.getId()); EXPECT_EQ(releases.results[0], release2.getId());
EXPECT_EQ(releases.results[1], release1.getId()); EXPECT_EQ(releases.results[1], release1.getId());
} }
ScopedListen listen4{ session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime }; ScopedListen listen4{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime };
ScopedListen listen5{ session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime }; ScopedListen listen5{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime };
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto releases{ Listen::getTopReleases(session, user->getId(), Scrobbler::Internal, {}) }; auto releases{ Listen::getTopReleases(session, user->getId(), ScrobblingBackend::Internal, {}) };
EXPECT_EQ(releases.moreResults, false); EXPECT_EQ(releases.moreResults, false);
ASSERT_EQ(releases.results.size(), 2); ASSERT_EQ(releases.results.size(), 2);
EXPECT_EQ(releases.results[0], release1.getId()); EXPECT_EQ(releases.results[0], release1.getId());
@@ -375,20 +375,20 @@ TEST_F(DatabaseFixture, Listen_getTopReleases_cluster)
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session, "MyTrack" };
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} }; const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} };
ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime }; ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime };
ScopedClusterType clusterType{ session, "MyType" }; ScopedClusterType clusterType{ session, "MyType" };
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" }; ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" };
ScopedRelease release{ session, "MyRelease" }; ScopedRelease release{ session, "MyRelease" };
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createUniqueTransaction() };
track.get().modify()->setRelease(release.get()); track.get().modify()->setRelease(release.get());
} }
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto releases{ Listen::getTopReleases(session, user->getId(), Scrobbler::Internal, {cluster.getId()}) }; auto releases{ Listen::getTopReleases(session, user->getId(), ScrobblingBackend::Internal, {cluster.getId()}) };
EXPECT_EQ(releases.results.size(), 0); EXPECT_EQ(releases.results.size(), 0);
} }
{ {
@@ -399,7 +399,7 @@ TEST_F(DatabaseFixture, Listen_getTopReleases_cluster)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto releases{ Listen::getTopReleases(session, user->getId(), Scrobbler::Internal, {cluster.getId()}) }; auto releases{ Listen::getTopReleases(session, user->getId(), ScrobblingBackend::Internal, {cluster.getId()}) };
EXPECT_EQ(releases.results.size(), 1); EXPECT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results[0], release.getId()); EXPECT_EQ(releases.results[0], release.getId());
} }
@@ -414,17 +414,17 @@ TEST_F(DatabaseFixture, Listen_getTopTracks)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto tracks{ Listen::getTopTracks(session, user->getId(), Scrobbler::Internal, {}) }; auto tracks{ Listen::getTopTracks(session, user->getId(), ScrobblingBackend::Internal, {}) };
EXPECT_EQ(tracks.moreResults, false); EXPECT_EQ(tracks.moreResults, false);
ASSERT_EQ(tracks.results.size(), 0); ASSERT_EQ(tracks.results.size(), 0);
} }
ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime }; ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime };
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto tracks{ Listen::getTopTracks(session, user->getId(), Scrobbler::Internal, {}) }; auto tracks{ Listen::getTopTracks(session, user->getId(), ScrobblingBackend::Internal, {}) };
EXPECT_EQ(tracks.moreResults, false); EXPECT_EQ(tracks.moreResults, false);
ASSERT_EQ(tracks.results.size(), 1); ASSERT_EQ(tracks.results.size(), 1);
EXPECT_EQ(tracks.results[0], track.getId()); EXPECT_EQ(tracks.results[0], track.getId());
@@ -432,7 +432,7 @@ TEST_F(DatabaseFixture, Listen_getTopTracks)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto tracks{ Listen::getTopTracks(session, user->getId(), Scrobbler::ListenBrainz, {}) }; auto tracks{ Listen::getTopTracks(session, user->getId(), ScrobblingBackend::ListenBrainz, {}) };
EXPECT_EQ(tracks.moreResults, false); EXPECT_EQ(tracks.moreResults, false);
EXPECT_EQ(tracks.results.size(), 0); EXPECT_EQ(tracks.results.size(), 0);
} }
@@ -444,33 +444,33 @@ TEST_F(DatabaseFixture, Listen_getTopTrack_multi)
ScopedTrack track2{ session, "MyTrack2" }; ScopedTrack track2{ session, "MyTrack2" };
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} }; const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} };
ScopedListen listen1{ session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime }; ScopedListen listen1{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime };
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto tracks{ Listen::getTopTracks(session, user->getId(), Scrobbler::Internal, {}) }; auto tracks{ Listen::getTopTracks(session, user->getId(), ScrobblingBackend::Internal, {}) };
EXPECT_EQ(tracks.moreResults, false); EXPECT_EQ(tracks.moreResults, false);
ASSERT_EQ(tracks.results.size(), 1); ASSERT_EQ(tracks.results.size(), 1);
EXPECT_EQ(tracks.results[0], track1.getId()); EXPECT_EQ(tracks.results[0], track1.getId());
} }
ScopedListen listen2{ session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime }; ScopedListen listen2{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime };
ScopedListen listen3{ session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime }; ScopedListen listen3{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime };
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto tracks{ Listen::getTopTracks(session, user->getId(), Scrobbler::Internal, {}) }; auto tracks{ Listen::getTopTracks(session, user->getId(), ScrobblingBackend::Internal, {}) };
EXPECT_EQ(tracks.moreResults, false); EXPECT_EQ(tracks.moreResults, false);
ASSERT_EQ(tracks.results.size(), 2); ASSERT_EQ(tracks.results.size(), 2);
EXPECT_EQ(tracks.results[0], track2.getId()); EXPECT_EQ(tracks.results[0], track2.getId());
EXPECT_EQ(tracks.results[1], track1.getId()); EXPECT_EQ(tracks.results[1], track1.getId());
} }
ScopedListen listen4{ session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime }; ScopedListen listen4{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime };
ScopedListen listen5{ session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime }; ScopedListen listen5{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime };
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto tracks{ Listen::getTopTracks(session, user->getId(), Scrobbler::Internal, {}) }; auto tracks{ Listen::getTopTracks(session, user->getId(), ScrobblingBackend::Internal, {}) };
EXPECT_EQ(tracks.moreResults, false); EXPECT_EQ(tracks.moreResults, false);
ASSERT_EQ(tracks.results.size(), 2); ASSERT_EQ(tracks.results.size(), 2);
EXPECT_EQ(tracks.results[0], track1.getId()); EXPECT_EQ(tracks.results[0], track1.getId());
@@ -483,14 +483,14 @@ TEST_F(DatabaseFixture, Listen_getTopTracks_cluster)
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session, "MyTrack" };
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} }; const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} };
ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime }; ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime };
ScopedClusterType clusterType{ session, "MyType" }; ScopedClusterType clusterType{ session, "MyType" };
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" }; ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" };
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto tracks{ Listen::getTopTracks(session, user->getId(), Scrobbler::Internal, {cluster.getId()}) }; auto tracks{ Listen::getTopTracks(session, user->getId(), ScrobblingBackend::Internal, {cluster.getId()}) };
EXPECT_EQ(tracks.results.size(), 0); EXPECT_EQ(tracks.results.size(), 0);
} }
{ {
@@ -501,7 +501,7 @@ TEST_F(DatabaseFixture, Listen_getTopTracks_cluster)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto tracks{ Listen::getTopTracks(session, user->getId(), Scrobbler::Internal, {cluster.getId()}) }; auto tracks{ Listen::getTopTracks(session, user->getId(), ScrobblingBackend::Internal, {cluster.getId()}) };
EXPECT_EQ(tracks.results.size(), 1); EXPECT_EQ(tracks.results.size(), 1);
EXPECT_EQ(tracks.results[0], track.getId()); EXPECT_EQ(tracks.results[0], track.getId());
} }
@@ -521,31 +521,31 @@ TEST_F(DatabaseFixture, Listen_getRecentArtists)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto artists{ Listen::getRecentArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt) }; auto artists{ Listen::getRecentArtists(session, user->getId(), ScrobblingBackend::Internal, {}, std::nullopt) };
EXPECT_EQ(artists.results.size(), 0); EXPECT_EQ(artists.results.size(), 0);
EXPECT_EQ(artists.moreResults, false); EXPECT_EQ(artists.moreResults, false);
} }
const Wt::WDateTime dateTime{ Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1} }; const Wt::WDateTime dateTime{ Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1} };
ScopedListen listen1{ session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime }; ScopedListen listen1{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime };
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto artists{ Listen::getRecentArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt) }; auto artists{ Listen::getRecentArtists(session, user->getId(), ScrobblingBackend::Internal, {}, std::nullopt) };
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results[0], artist->getId()); EXPECT_EQ(artists.results[0], artist->getId());
} }
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto artists{ Listen::getRecentArtists(session, user->getId(), Scrobbler::ListenBrainz, {}, std::nullopt) }; auto artists{ Listen::getRecentArtists(session, user->getId(), ScrobblingBackend::ListenBrainz, {}, std::nullopt) };
ASSERT_EQ(artists.results.size(), 0); ASSERT_EQ(artists.results.size(), 0);
} }
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto artists{ Listen::getRecentArtists(session, user->getId(), Scrobbler::Internal, {}, TrackArtistLinkType::Producer) }; auto artists{ Listen::getRecentArtists(session, user->getId(), ScrobblingBackend::Internal, {}, TrackArtistLinkType::Producer) };
EXPECT_EQ(artists.results.size(), 0); EXPECT_EQ(artists.results.size(), 0);
} }
{ {
@@ -555,7 +555,7 @@ TEST_F(DatabaseFixture, Listen_getRecentArtists)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto artists{ Listen::getRecentArtists(session, user->getId(), Scrobbler::Internal, {cluster->getId()}, std::nullopt) }; auto artists{ Listen::getRecentArtists(session, user->getId(), ScrobblingBackend::Internal, {cluster->getId()}, std::nullopt) };
EXPECT_EQ(artists.results.size(), 0); EXPECT_EQ(artists.results.size(), 0);
} }
} }
@@ -579,32 +579,32 @@ TEST_F(DatabaseFixture, Listen_getRecentArtists_multi)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto artists{ Listen::getRecentArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt) }; auto artists{ Listen::getRecentArtists(session, user->getId(), ScrobblingBackend::Internal, {}, std::nullopt) };
EXPECT_EQ(artists.results.size(), 0); EXPECT_EQ(artists.results.size(), 0);
} }
ScopedListen listen1{ session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime.addSecs(1) }; ScopedListen listen1{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime.addSecs(1) };
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto artists{ Listen::getRecentArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt) }; auto artists{ Listen::getRecentArtists(session, user->getId(), ScrobblingBackend::Internal, {}, std::nullopt) };
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results[0], artist1->getId()); EXPECT_EQ(artists.results[0], artist1->getId());
} }
ScopedListen listen2{ session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime.addSecs(2) }; ScopedListen listen2{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime.addSecs(2) };
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto artists{ Listen::getRecentArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt) }; auto artists{ Listen::getRecentArtists(session, user->getId(), ScrobblingBackend::Internal, {}, std::nullopt) };
ASSERT_EQ(artists.results.size(), 2); ASSERT_EQ(artists.results.size(), 2);
EXPECT_EQ(artists.results[0], artist2->getId()); EXPECT_EQ(artists.results[0], artist2->getId());
EXPECT_EQ(artists.results[1], artist1->getId()); EXPECT_EQ(artists.results[1], artist1->getId());
} }
ScopedListen listen3{ session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime.addSecs(-1) }; ScopedListen listen3{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime.addSecs(-1) };
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto artists{ Listen::getRecentArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt, Range {0, 1}) }; auto artists{ Listen::getRecentArtists(session, user->getId(), ScrobblingBackend::Internal, {}, std::nullopt, Range {0, 1}) };
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.moreResults, true); EXPECT_EQ(artists.moreResults, true);
EXPECT_EQ(artists.results[0], artist2->getId()); EXPECT_EQ(artists.results[0], artist2->getId());
@@ -617,7 +617,7 @@ TEST_F(DatabaseFixture, Listen_getRecentArtists_cluster)
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
ScopedArtist artist{ session, "MyArtist" }; ScopedArtist artist{ session, "MyArtist" };
const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} }; const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} };
ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime }; ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime };
ScopedClusterType clusterType{ session, "MyType" }; ScopedClusterType clusterType{ session, "MyType" };
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" }; ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" };
@@ -629,7 +629,7 @@ TEST_F(DatabaseFixture, Listen_getRecentArtists_cluster)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto artists{ Listen::getRecentArtists(session, user->getId(), Scrobbler::Internal, {cluster.getId()}, std::nullopt) }; auto artists{ Listen::getRecentArtists(session, user->getId(), ScrobblingBackend::Internal, {cluster.getId()}, std::nullopt) };
ASSERT_EQ(artists.results.size(), 0); ASSERT_EQ(artists.results.size(), 0);
} }
{ {
@@ -639,7 +639,7 @@ TEST_F(DatabaseFixture, Listen_getRecentArtists_cluster)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto artists{ Listen::getRecentArtists(session, user->getId(), Scrobbler::Internal, {cluster.getId()}, std::nullopt) }; auto artists{ Listen::getRecentArtists(session, user->getId(), ScrobblingBackend::Internal, {cluster.getId()}, std::nullopt) };
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results[0], artist.getId()); EXPECT_EQ(artists.results[0], artist.getId());
} }
@@ -652,25 +652,25 @@ TEST_F(DatabaseFixture, Listen_getRecentReleases)
ScopedRelease release{ session, "MyRelease" }; ScopedRelease release{ session, "MyRelease" };
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createUniqueTransaction() };
track.get().modify()->setRelease(release.get()); track.get().modify()->setRelease(release.get());
} }
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto releases{ Listen::getRecentReleases(session, user->getId(), Scrobbler::Internal, {}) }; auto releases{ Listen::getRecentReleases(session, user->getId(), ScrobblingBackend::Internal, {}) };
EXPECT_EQ(releases.moreResults, false); EXPECT_EQ(releases.moreResults, false);
ASSERT_EQ(releases.results.size(), 0); ASSERT_EQ(releases.results.size(), 0);
} }
const Wt::WDateTime dateTime{ Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1} }; const Wt::WDateTime dateTime{ Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1} };
ScopedListen listen1{ session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime }; ScopedListen listen1{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime };
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto releases{ Listen::getRecentReleases(session, user->getId(), Scrobbler::Internal, {}) }; auto releases{ Listen::getRecentReleases(session, user->getId(), ScrobblingBackend::Internal, {}) };
EXPECT_EQ(releases.moreResults, false); EXPECT_EQ(releases.moreResults, false);
ASSERT_EQ(releases.results.size(), 1); ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results[0], release.getId()); EXPECT_EQ(releases.results[0], release.getId());
@@ -678,7 +678,7 @@ TEST_F(DatabaseFixture, Listen_getRecentReleases)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto releases{ Listen::getRecentReleases(session, user->getId(), Scrobbler::ListenBrainz, {}) }; auto releases{ Listen::getRecentReleases(session, user->getId(), ScrobblingBackend::ListenBrainz, {}) };
EXPECT_EQ(releases.moreResults, false); EXPECT_EQ(releases.moreResults, false);
EXPECT_EQ(releases.results.size(), 0); EXPECT_EQ(releases.results.size(), 0);
} }
@@ -692,46 +692,46 @@ TEST_F(DatabaseFixture, Listen_getMostRecentRelease)
ScopedRelease release{ session, "MyRelease" }; ScopedRelease release{ session, "MyRelease" };
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createUniqueTransaction() };
track.get().modify()->setRelease(release.get()); track.get().modify()->setRelease(release.get());
} }
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
const auto listen{ Listen::getMostRecentListen(session, user->getId(), Scrobbler::Internal, release.getId()) }; const auto listen{ Listen::getMostRecentListen(session, user->getId(), ScrobblingBackend::Internal, release.getId()) };
EXPECT_FALSE(listen); EXPECT_FALSE(listen);
} }
const Wt::WDateTime dateTime1{ Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1} }; const Wt::WDateTime dateTime1{ Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1} };
ScopedListen listen1{ session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime1 }; ScopedListen listen1{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime1 };
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
const auto listen{ Listen::getMostRecentListen(session, user->getId(), Scrobbler::Internal, release.getId()) }; const auto listen{ Listen::getMostRecentListen(session, user->getId(), ScrobblingBackend::Internal, release.getId()) };
EXPECT_TRUE(listen); EXPECT_TRUE(listen);
EXPECT_EQ(listen->getDateTime(), dateTime1); EXPECT_EQ(listen->getDateTime(), dateTime1);
} }
const Wt::WDateTime dateTime2{ Wt::WDate {1999, 1, 2}, Wt::WTime {12,0, 1} }; const Wt::WDateTime dateTime2{ Wt::WDate {1999, 1, 2}, Wt::WTime {12,0, 1} };
ScopedListen listen2{ session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime2 }; ScopedListen listen2{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime2 };
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
const auto listen{ Listen::getMostRecentListen(session, user->getId(), Scrobbler::Internal, release.getId()) }; const auto listen{ Listen::getMostRecentListen(session, user->getId(), ScrobblingBackend::Internal, release.getId()) };
EXPECT_TRUE(listen); EXPECT_TRUE(listen);
EXPECT_EQ(listen->getDateTime(), dateTime1); EXPECT_EQ(listen->getDateTime(), dateTime1);
} }
const Wt::WDateTime dateTime3{ Wt::WDate {2001, 1, 2}, Wt::WTime {12,0, 1} }; const Wt::WDateTime dateTime3{ Wt::WDate {2001, 1, 2}, Wt::WTime {12,0, 1} };
ScopedListen listen3{ session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime3 }; ScopedListen listen3{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime3 };
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
const auto listen{ Listen::getMostRecentListen(session, user->getId(), Scrobbler::Internal, release.getId()) }; const auto listen{ Listen::getMostRecentListen(session, user->getId(), ScrobblingBackend::Internal, release.getId()) };
EXPECT_TRUE(listen); EXPECT_TRUE(listen);
EXPECT_EQ(listen->getDateTime(), dateTime3); EXPECT_EQ(listen->getDateTime(), dateTime3);
} }
@@ -746,50 +746,50 @@ TEST_F(DatabaseFixture, Listen_getRecentReleases_multi)
ScopedRelease release2{ session, "MyRelease2" }; ScopedRelease release2{ session, "MyRelease2" };
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createUniqueTransaction() };
track1.get().modify()->setRelease(release1.get()); track1.get().modify()->setRelease(release1.get());
track2.get().modify()->setRelease(release2.get()); track2.get().modify()->setRelease(release2.get());
} }
const Wt::WDateTime dateTime{ Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1} }; const Wt::WDateTime dateTime{ Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1} };
ScopedListen listen1{ session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime }; ScopedListen listen1{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime };
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto releases{ Listen::getRecentReleases(session, user->getId(), Scrobbler::Internal, {}) }; auto releases{ Listen::getRecentReleases(session, user->getId(), ScrobblingBackend::Internal, {}) };
EXPECT_EQ(releases.moreResults, false); EXPECT_EQ(releases.moreResults, false);
ASSERT_EQ(releases.results.size(), 1); ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results[0], release2.getId()); EXPECT_EQ(releases.results[0], release2.getId());
} }
ScopedListen listen2{ session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime.addSecs(1) }; ScopedListen listen2{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime.addSecs(1) };
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto releases{ Listen::getRecentReleases(session, user->getId(), Scrobbler::Internal, {}) }; auto releases{ Listen::getRecentReleases(session, user->getId(), ScrobblingBackend::Internal, {}) };
EXPECT_EQ(releases.moreResults, false); EXPECT_EQ(releases.moreResults, false);
ASSERT_EQ(releases.results.size(), 2); ASSERT_EQ(releases.results.size(), 2);
EXPECT_EQ(releases.results[0], release1.getId()); EXPECT_EQ(releases.results[0], release1.getId());
EXPECT_EQ(releases.results[1], release2.getId()); EXPECT_EQ(releases.results[1], release2.getId());
} }
ScopedListen listen3{ session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime.addSecs(2) }; ScopedListen listen3{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime.addSecs(2) };
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto releases{ Listen::getRecentReleases(session, user->getId(), Scrobbler::Internal, {}) }; auto releases{ Listen::getRecentReleases(session, user->getId(), ScrobblingBackend::Internal, {}) };
EXPECT_EQ(releases.moreResults, false); EXPECT_EQ(releases.moreResults, false);
ASSERT_EQ(releases.results.size(), 2); ASSERT_EQ(releases.results.size(), 2);
EXPECT_EQ(releases.results[0], release2.getId()); EXPECT_EQ(releases.results[0], release2.getId());
EXPECT_EQ(releases.results[1], release1.getId()); EXPECT_EQ(releases.results[1], release1.getId());
} }
ScopedListen listen4{ session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime.addSecs(-1) }; ScopedListen listen4{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime.addSecs(-1) };
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto releases{ Listen::getRecentReleases(session, user->getId(), Scrobbler::Internal, {}) }; auto releases{ Listen::getRecentReleases(session, user->getId(), ScrobblingBackend::Internal, {}) };
EXPECT_EQ(releases.moreResults, false); EXPECT_EQ(releases.moreResults, false);
ASSERT_EQ(releases.results.size(), 2); ASSERT_EQ(releases.results.size(), 2);
EXPECT_EQ(releases.results[0], release2.getId()); EXPECT_EQ(releases.results[0], release2.getId());
@@ -806,23 +806,23 @@ TEST_F(DatabaseFixture, Listen_getRecentReleases_cluster)
ScopedRelease release{ session, "MyRelease" }; ScopedRelease release{ session, "MyRelease" };
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createUniqueTransaction() };
track.get().modify()->setRelease(release.get()); track.get().modify()->setRelease(release.get());
} }
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto releases{ Listen::getRecentReleases(session, user->getId(), Scrobbler::Internal, {cluster.getId()}) }; auto releases{ Listen::getRecentReleases(session, user->getId(), ScrobblingBackend::Internal, {cluster.getId()}) };
EXPECT_EQ(releases.results.size(), 0); EXPECT_EQ(releases.results.size(), 0);
} }
const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} }; const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} };
ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime }; ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime };
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto releases{ Listen::getRecentReleases(session, user->getId(), Scrobbler::Internal, {cluster.getId()}) }; auto releases{ Listen::getRecentReleases(session, user->getId(), ScrobblingBackend::Internal, {cluster.getId()}) };
EXPECT_EQ(releases.results.size(), 0); EXPECT_EQ(releases.results.size(), 0);
} }
@@ -833,7 +833,7 @@ TEST_F(DatabaseFixture, Listen_getRecentReleases_cluster)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto releases{ Listen::getRecentReleases(session, user->getId(), Scrobbler::Internal, {cluster.getId()}) }; auto releases{ Listen::getRecentReleases(session, user->getId(), ScrobblingBackend::Internal, {cluster.getId()}) };
EXPECT_EQ(releases.results.size(), 1); EXPECT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results[0], release.getId()); EXPECT_EQ(releases.results[0], release.getId());
} }
@@ -847,18 +847,18 @@ TEST_F(DatabaseFixture, Listen_getRecentTracks)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto tracks{ Listen::getRecentTracks(session, user->getId(), Scrobbler::Internal, {}) }; auto tracks{ Listen::getRecentTracks(session, user->getId(), ScrobblingBackend::Internal, {}) };
EXPECT_EQ(tracks.moreResults, false); EXPECT_EQ(tracks.moreResults, false);
ASSERT_EQ(tracks.results.size(), 0); ASSERT_EQ(tracks.results.size(), 0);
} }
const Wt::WDateTime dateTime{ Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1} }; const Wt::WDateTime dateTime{ Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1} };
ScopedListen listen1{ session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime }; ScopedListen listen1{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime };
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto tracks{ Listen::getRecentTracks(session, user->getId(), Scrobbler::Internal, {}) }; auto tracks{ Listen::getRecentTracks(session, user->getId(), ScrobblingBackend::Internal, {}) };
EXPECT_EQ(tracks.moreResults, false); EXPECT_EQ(tracks.moreResults, false);
ASSERT_EQ(tracks.results.size(), 1); ASSERT_EQ(tracks.results.size(), 1);
EXPECT_EQ(tracks.results[0], track.getId()); EXPECT_EQ(tracks.results[0], track.getId());
@@ -867,12 +867,77 @@ TEST_F(DatabaseFixture, Listen_getRecentTracks)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto tracks{ Listen::getRecentTracks(session, user->getId(), Scrobbler::ListenBrainz, {}) }; auto tracks{ Listen::getRecentTracks(session, user->getId(), ScrobblingBackend::ListenBrainz, {}) };
EXPECT_EQ(tracks.moreResults, false); EXPECT_EQ(tracks.moreResults, false);
EXPECT_EQ(tracks.results.size(), 0); EXPECT_EQ(tracks.results.size(), 0);
} }
} }
TEST_F(DatabaseFixture, Listen_getCount_track)
{
ScopedTrack track{ session, "MyTrack" };
ScopedUser user{ session, "MyUser" };
{
auto transaction{ session.createSharedTransaction() };
const std::size_t count{ Listen::getCount(session, user->getId(), ScrobblingBackend::Internal, track.getId()) };
EXPECT_EQ(count, 0);
}
const Wt::WDateTime dateTime1{ Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1} };
ScopedListen listen1{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime1 };
{
auto transaction{ session.createSharedTransaction() };
const std::size_t count{ Listen::getCount(session, user->getId(), ScrobblingBackend::Internal, track.getId()) };
EXPECT_EQ(count, 1);
}
}
TEST_F(DatabaseFixture, Listen_getCount_release)
{
ScopedTrack track1{ session, "MyTrack" };
ScopedTrack track2{ session, "MyTrack" };
ScopedUser user{ session, "MyUser" };
ScopedRelease release{ session, "MyRelease" };
auto getReleaseListenCount{ [&]
{
auto transaction{ session.createSharedTransaction() };
return Listen::getCount(session, user->getId(), ScrobblingBackend::Internal, release.getId());
} };
EXPECT_EQ(getReleaseListenCount(), 0);
const Wt::WDateTime dateTime1{ Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1} };
ScopedListen listen1{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime1 };
EXPECT_EQ(getReleaseListenCount(), 0);
{
auto transaction{ session.createUniqueTransaction() };
track1.get().modify()->setRelease(release.get());
}
EXPECT_EQ(getReleaseListenCount(), 1);
{
auto transaction{ session.createUniqueTransaction() };
track2.get().modify()->setRelease(release.get());
}
EXPECT_EQ(getReleaseListenCount(), 0);
ScopedListen listen2{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime1 };
EXPECT_EQ(getReleaseListenCount(), 1);
ScopedListen listen3{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime1 };
EXPECT_EQ(getReleaseListenCount(), 1);
ScopedListen listen4{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime1 };
EXPECT_EQ(getReleaseListenCount(), 2);
}
TEST_F(DatabaseFixture, Listen_getMostRecentTrack) TEST_F(DatabaseFixture, Listen_getMostRecentTrack)
{ {
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session, "MyTrack" };
@@ -881,39 +946,39 @@ TEST_F(DatabaseFixture, Listen_getMostRecentTrack)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto listen{ Listen::getMostRecentListen(session, user->getId(), Scrobbler::Internal, track.getId()) }; auto listen{ Listen::getMostRecentListen(session, user->getId(), ScrobblingBackend::Internal, track.getId()) };
EXPECT_FALSE(listen); EXPECT_FALSE(listen);
} }
const Wt::WDateTime dateTime1{ Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1} }; const Wt::WDateTime dateTime1{ Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1} };
ScopedListen listen1{ session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime1 }; ScopedListen listen1{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime1 };
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto listen{ Listen::getMostRecentListen(session, user->getId(), Scrobbler::Internal, track.getId()) }; auto listen{ Listen::getMostRecentListen(session, user->getId(), ScrobblingBackend::Internal, track.getId()) };
EXPECT_TRUE(listen); EXPECT_TRUE(listen);
EXPECT_EQ(listen->getDateTime(), dateTime1); EXPECT_EQ(listen->getDateTime(), dateTime1);
} }
const Wt::WDateTime dateTime2{ Wt::WDate {1999, 1, 2}, Wt::WTime {12,0, 1} }; const Wt::WDateTime dateTime2{ Wt::WDate {1999, 1, 2}, Wt::WTime {12,0, 1} };
ScopedListen listen2{ session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime2 }; ScopedListen listen2{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime2 };
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto listen{ Listen::getMostRecentListen(session, user->getId(), Scrobbler::Internal, track.getId()) }; auto listen{ Listen::getMostRecentListen(session, user->getId(), ScrobblingBackend::Internal, track.getId()) };
EXPECT_TRUE(listen); EXPECT_TRUE(listen);
EXPECT_EQ(listen->getDateTime(), dateTime1); EXPECT_EQ(listen->getDateTime(), dateTime1);
} }
const Wt::WDateTime dateTime3{ Wt::WDate {2001, 1, 2}, Wt::WTime {12,0, 1} }; const Wt::WDateTime dateTime3{ Wt::WDate {2001, 1, 2}, Wt::WTime {12,0, 1} };
ScopedListen listen3{ session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime3 }; ScopedListen listen3{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime3 };
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto listen{ Listen::getMostRecentListen(session, user->getId(), Scrobbler::Internal, track.getId()) }; auto listen{ Listen::getMostRecentListen(session, user->getId(), ScrobblingBackend::Internal, track.getId()) };
EXPECT_TRUE(listen); EXPECT_TRUE(listen);
EXPECT_EQ(listen->getDateTime(), dateTime3); EXPECT_EQ(listen->getDateTime(), dateTime3);
} }
@@ -926,44 +991,44 @@ TEST_F(DatabaseFixture, Listen_getRecentTracks_multi)
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
const Wt::WDateTime dateTime{ Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1} }; const Wt::WDateTime dateTime{ Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1} };
ScopedListen listen1{ session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime }; ScopedListen listen1{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime };
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto tracks{ Listen::getRecentTracks(session, user->getId(), Scrobbler::Internal, {}) }; auto tracks{ Listen::getRecentTracks(session, user->getId(), ScrobblingBackend::Internal, {}) };
EXPECT_EQ(tracks.moreResults, false); EXPECT_EQ(tracks.moreResults, false);
ASSERT_EQ(tracks.results.size(), 1); ASSERT_EQ(tracks.results.size(), 1);
EXPECT_EQ(tracks.results[0], track2.getId()); EXPECT_EQ(tracks.results[0], track2.getId());
} }
ScopedListen listen2{ session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime.addSecs(1) }; ScopedListen listen2{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime.addSecs(1) };
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto tracks{ Listen::getRecentTracks(session, user->getId(), Scrobbler::Internal, {}) }; auto tracks{ Listen::getRecentTracks(session, user->getId(), ScrobblingBackend::Internal, {}) };
EXPECT_EQ(tracks.moreResults, false); EXPECT_EQ(tracks.moreResults, false);
ASSERT_EQ(tracks.results.size(), 2); ASSERT_EQ(tracks.results.size(), 2);
EXPECT_EQ(tracks.results[0], track1.getId()); EXPECT_EQ(tracks.results[0], track1.getId());
EXPECT_EQ(tracks.results[1], track2.getId()); EXPECT_EQ(tracks.results[1], track2.getId());
} }
ScopedListen listen3{ session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime.addSecs(2) }; ScopedListen listen3{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime.addSecs(2) };
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto tracks{ Listen::getRecentTracks(session, user->getId(), Scrobbler::Internal, {}) }; auto tracks{ Listen::getRecentTracks(session, user->getId(), ScrobblingBackend::Internal, {}) };
EXPECT_EQ(tracks.moreResults, false); EXPECT_EQ(tracks.moreResults, false);
ASSERT_EQ(tracks.results.size(), 2); ASSERT_EQ(tracks.results.size(), 2);
EXPECT_EQ(tracks.results[0], track2.getId()); EXPECT_EQ(tracks.results[0], track2.getId());
EXPECT_EQ(tracks.results[1], track1.getId()); EXPECT_EQ(tracks.results[1], track1.getId());
} }
ScopedListen listen4{ session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime.addSecs(-1) }; ScopedListen listen4{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime.addSecs(-1) };
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto tracks{ Listen::getRecentTracks(session, user->getId(), Scrobbler::Internal, {}) }; auto tracks{ Listen::getRecentTracks(session, user->getId(), ScrobblingBackend::Internal, {}) };
EXPECT_EQ(tracks.moreResults, false); EXPECT_EQ(tracks.moreResults, false);
ASSERT_EQ(tracks.results.size(), 2); ASSERT_EQ(tracks.results.size(), 2);
EXPECT_EQ(tracks.results[0], track2.getId()); EXPECT_EQ(tracks.results[0], track2.getId());
@@ -976,14 +1041,14 @@ TEST_F(DatabaseFixture, Listen_getRecentTracks_cluster)
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session, "MyTrack" };
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
const Wt::WDateTime dateTime{ Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1} }; const Wt::WDateTime dateTime{ Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1} };
ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime }; ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime };
ScopedClusterType clusterType{ session, "MyType" }; ScopedClusterType clusterType{ session, "MyType" };
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" }; ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" };
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto tracks{ Listen::getRecentTracks(session, user->getId(), Scrobbler::Internal, {cluster.getId()}) }; auto tracks{ Listen::getRecentTracks(session, user->getId(), ScrobblingBackend::Internal, {cluster.getId()}) };
EXPECT_EQ(tracks.results.size(), 0); EXPECT_EQ(tracks.results.size(), 0);
} }
{ {
@@ -994,7 +1059,7 @@ TEST_F(DatabaseFixture, Listen_getRecentTracks_cluster)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto tracks{ Listen::getRecentTracks(session, user->getId(), Scrobbler::Internal, {cluster.getId()}) }; auto tracks{ Listen::getRecentTracks(session, user->getId(), ScrobblingBackend::Internal, {cluster.getId()}) };
EXPECT_EQ(tracks.results.size(), 1); EXPECT_EQ(tracks.results.size(), 1);
EXPECT_EQ(tracks.results[0], track.getId()); EXPECT_EQ(tracks.results[0], track.getId());
} }
File diff suppressed because it is too large Load Diff
@@ -26,102 +26,100 @@ using ScopedStarredArtist = ScopedEntity<Database::StarredArtist>;
TEST_F(DatabaseFixture, StarredArtist) TEST_F(DatabaseFixture, StarredArtist)
{ {
ScopedArtist artist {session, "MyArtist"}; ScopedArtist artist{ session, "MyArtist" };
ScopedUser user {session, "MyUser"}; ScopedUser user{ session, "MyUser" };
ScopedUser user2 {session, "MyUser2"}; ScopedUser user2{ session, "MyUser2" };
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
auto starredArtist {StarredArtist::find(session, artist->getId(), user->getId(), Scrobbler::Internal)}; auto starredArtist{ StarredArtist::find(session, artist->getId(), user->getId(), FeedbackBackend::Internal) };
EXPECT_FALSE(starredArtist); EXPECT_FALSE(starredArtist);
EXPECT_EQ(StarredArtist::getCount(session), 0); EXPECT_EQ(StarredArtist::getCount(session), 0);
auto artists {Artist::find(session, Artist::FindParameters {})}; auto artists{ Artist::findIds(session, Artist::FindParameters {}) };
EXPECT_EQ(artists.results.size(), 1); EXPECT_EQ(artists.results.size(), 1);
} }
ScopedStarredArtist starredArtist {session, artist.lockAndGet(), user.lockAndGet(), Scrobbler::Internal}; ScopedStarredArtist starredArtist{ session, artist.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal };
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
auto gotArtist {StarredArtist::find(session, artist->getId(), user->getId(), Scrobbler::Internal)}; auto gotArtist{ StarredArtist::find(session, artist->getId(), user->getId(), FeedbackBackend::Internal) };
EXPECT_EQ(gotArtist->getId(), starredArtist->getId()); EXPECT_EQ(gotArtist->getId(), starredArtist->getId());
EXPECT_EQ(StarredArtist::getCount(session), 1); EXPECT_EQ(StarredArtist::getCount(session), 1);
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
auto artists {Artist::find(session, Artist::FindParameters {})}; auto artists{ Artist::findIds(session, Artist::FindParameters {}) };
EXPECT_EQ(artists.results.size(), 1); EXPECT_EQ(artists.results.size(), 1);
artists = Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal)); artists = Artist::findIds(session, Artist::FindParameters{}.setStarringUser(user.getId(), FeedbackBackend::Internal));
EXPECT_EQ(artists.results.size(), 1); EXPECT_EQ(artists.results.size(), 1);
artists = Artist::find(session, Artist::FindParameters {}.setStarringUser(user2.getId(), Scrobbler::Internal)); artists = Artist::findIds(session, Artist::FindParameters{}.setStarringUser(user2.getId(), FeedbackBackend::Internal));
EXPECT_EQ(artists.results.size(), 0); EXPECT_EQ(artists.results.size(), 0);
} }
} }
TEST_F(DatabaseFixture, StarredArtist_PendingDestroy) TEST_F(DatabaseFixture, StarredArtist_PendingDestroy)
{ {
ScopedArtist artist {session, "MyArtist"}; ScopedArtist artist{ session, "MyArtist" };
ScopedUser user {session, "MyUser"}; ScopedUser user{ session, "MyUser" };
ScopedStarredArtist starredArtist {session, artist.lockAndGet(), user.lockAndGet(), Scrobbler::Internal}; ScopedStarredArtist starredArtist{ session, artist.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal };
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction{ session.createUniqueTransaction() };
auto artists {Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal))}; auto artists{ Artist::findIds(session, Artist::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal)) };
EXPECT_EQ(artists.results.size(), 1); EXPECT_EQ(artists.results.size(), 1);
starredArtist.get().modify()->setScrobblingState(ScrobblingState::PendingRemove); starredArtist.get().modify()->setSyncState(SyncState::PendingRemove);
artists = Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal)); artists = Artist::findIds(session, Artist::FindParameters{}.setStarringUser(user.getId(), FeedbackBackend::Internal));
EXPECT_EQ(artists.results.size(), 0); EXPECT_EQ(artists.results.size(), 0);
} }
} }
TEST_F(DatabaseFixture, StarredArtist_dateTime) TEST_F(DatabaseFixture, StarredArtist_dateTime)
{ {
ScopedArtist artist1 {session, "MyArtist1"}; ScopedArtist artist1{ session, "MyArtist1" };
ScopedArtist artist2 {session, "MyArtist2"}; ScopedArtist artist2{ session, "MyArtist2" };
ScopedUser user {session, "MyUser"}; ScopedUser user{ session, "MyUser" };
ScopedStarredArtist starredArtist1 {session, artist1.lockAndGet(), user.lockAndGet(), Scrobbler::Internal}; ScopedStarredArtist starredArtist1{ session, artist1.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal };
ScopedStarredArtist starredArtist2 {session, artist2.lockAndGet(), user.lockAndGet(), Scrobbler::Internal}; ScopedStarredArtist starredArtist2{ session, artist2.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal };
const Wt::WDateTime dateTime {Wt::WDate {1950, 1, 2}, Wt::WTime {12, 30, 1}}; const Wt::WDateTime dateTime{ Wt::WDate {1950, 1, 2}, Wt::WTime {12, 30, 1} };
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
auto artists {Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal))}; auto artists{ Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal)) };
EXPECT_EQ(artists.results.size(), 2); EXPECT_EQ(artists.results.size(), 2);
} }
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction{ session.createUniqueTransaction() };
starredArtist1.get().modify()->setDateTime(dateTime); starredArtist1.get().modify()->setDateTime(dateTime);
starredArtist2.get().modify()->setDateTime(dateTime.addSecs(-1)); starredArtist2.get().modify()->setDateTime(dateTime.addSecs(-1));
auto artists {Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal) auto artists{ Artist::findIds(session, Artist::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal).setSortMethod(ArtistSortMethod::StarredDateDesc)) };
.setSortMethod(ArtistSortMethod::StarredDateDesc))}; ASSERT_EQ(artists.results.size(), 2);
ASSERT_EQ(artists.results.size(), 2); EXPECT_EQ(artists.results[0], starredArtist1->getArtist()->getId());
EXPECT_EQ(artists.results[0], starredArtist1->getArtist()->getId()); EXPECT_EQ(artists.results[1], starredArtist2->getArtist()->getId());
EXPECT_EQ(artists.results[1], starredArtist2->getArtist()->getId()); }
} {
{ auto transaction{ session.createUniqueTransaction() };
auto transaction {session.createUniqueTransaction()};
starredArtist1.get().modify()->setDateTime(dateTime); starredArtist1.get().modify()->setDateTime(dateTime);
starredArtist2.get().modify()->setDateTime(dateTime.addSecs(1)); starredArtist2.get().modify()->setDateTime(dateTime.addSecs(1));
auto artists {Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal) auto artists{ Artist::findIds(session, Artist::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal).setSortMethod(ArtistSortMethod::StarredDateDesc)) };
.setSortMethod(ArtistSortMethod::StarredDateDesc))}; ASSERT_EQ(artists.results.size(), 2);
ASSERT_EQ(artists.results.size(), 2); EXPECT_EQ(artists.results[0], starredArtist2->getArtist()->getId());
EXPECT_EQ(artists.results[0], starredArtist2->getArtist()->getId()); EXPECT_EQ(artists.results[1], starredArtist1->getArtist()->getId());
EXPECT_EQ(artists.results[1], starredArtist1->getArtist()->getId()); }
}
} }
@@ -26,102 +26,100 @@ using ScopedStarredRelease = ScopedEntity<Database::StarredRelease>;
TEST_F(DatabaseFixture, StarredRelease) TEST_F(DatabaseFixture, StarredRelease)
{ {
ScopedRelease release {session, "MyRelease"}; ScopedRelease release{ session, "MyRelease" };
ScopedUser user {session, "MyUser"}; ScopedUser user{ session, "MyUser" };
ScopedUser user2 {session, "MyUser2"}; ScopedUser user2{ session, "MyUser2" };
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
auto starredRelease {StarredRelease::find(session, release->getId(), user->getId(), Scrobbler::Internal)}; auto starredRelease{ StarredRelease::find(session, release->getId(), user->getId(), FeedbackBackend::Internal) };
EXPECT_FALSE(starredRelease); EXPECT_FALSE(starredRelease);
EXPECT_EQ(StarredRelease::getCount(session), 0); EXPECT_EQ(StarredRelease::getCount(session), 0);
auto releases {Release::find(session, Release::FindParameters {})}; auto releases{ Release::find(session, Release::FindParameters {}) };
EXPECT_EQ(releases.results.size(), 1); EXPECT_EQ(releases.results.size(), 1);
} }
ScopedStarredRelease starredRelease {session, release.lockAndGet(), user.lockAndGet(), Scrobbler::Internal}; ScopedStarredRelease starredRelease{ session, release.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal };
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
auto gotRelease {StarredRelease::find(session, release->getId(), user->getId(), Scrobbler::Internal)}; auto gotRelease{ StarredRelease::find(session, release->getId(), user->getId(), FeedbackBackend::Internal) };
EXPECT_EQ(gotRelease->getId(), starredRelease->getId()); EXPECT_EQ(gotRelease->getId(), starredRelease->getId());
EXPECT_EQ(StarredRelease::getCount(session), 1); EXPECT_EQ(StarredRelease::getCount(session), 1);
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
auto releases {Release::find(session, Release::FindParameters {})}; auto releases{ Release::find(session, Release::FindParameters {}) };
EXPECT_EQ(releases.results.size(), 1); EXPECT_EQ(releases.results.size(), 1);
releases = Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal)); releases = Release::find(session, Release::FindParameters{}.setStarringUser(user.getId(), FeedbackBackend::Internal));
EXPECT_EQ(releases.results.size(), 1); EXPECT_EQ(releases.results.size(), 1);
releases = Release::find(session, Release::FindParameters {}.setStarringUser(user2.getId(), Scrobbler::Internal)); releases = Release::find(session, Release::FindParameters{}.setStarringUser(user2.getId(), FeedbackBackend::Internal));
EXPECT_EQ(releases.results.size(), 0); EXPECT_EQ(releases.results.size(), 0);
} }
} }
TEST_F(DatabaseFixture, Starredrelease_PendingDestroy) TEST_F(DatabaseFixture, Starredrelease_PendingDestroy)
{ {
ScopedRelease release {session, "MyRelease"}; ScopedRelease release{ session, "MyRelease" };
ScopedUser user {session, "MyUser"}; ScopedUser user{ session, "MyUser" };
ScopedStarredRelease starredRelease {session, release.lockAndGet(), user.lockAndGet(), Scrobbler::Internal}; ScopedStarredRelease starredRelease{ session, release.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal };
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction{ session.createUniqueTransaction() };
auto releases {Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal))}; auto releases{ Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal)) };
EXPECT_EQ(releases.results.size(), 1); EXPECT_EQ(releases.results.size(), 1);
starredRelease.get().modify()->setScrobblingState(ScrobblingState::PendingRemove); starredRelease.get().modify()->setSyncState(SyncState::PendingRemove);
releases = Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal)); releases = Release::find(session, Release::FindParameters{}.setStarringUser(user.getId(), FeedbackBackend::Internal));
EXPECT_EQ(releases.results.size(), 0); EXPECT_EQ(releases.results.size(), 0);
} }
} }
TEST_F(DatabaseFixture, StarredRelease_dateTime) TEST_F(DatabaseFixture, StarredRelease_dateTime)
{ {
ScopedRelease release1 {session, "MyRelease1"}; ScopedRelease release1{ session, "MyRelease1" };
ScopedRelease release2 {session, "MyRelease2"}; ScopedRelease release2{ session, "MyRelease2" };
ScopedUser user {session, "MyUser"}; ScopedUser user{ session, "MyUser" };
ScopedStarredRelease starredRelease1 {session, release1.lockAndGet(), user.lockAndGet(), Scrobbler::Internal}; ScopedStarredRelease starredRelease1{ session, release1.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal };
ScopedStarredRelease starredRelease2 {session, release2.lockAndGet(), user.lockAndGet(), Scrobbler::Internal}; ScopedStarredRelease starredRelease2{ session, release2.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal };
const Wt::WDateTime dateTime {Wt::WDate {1950, 1, 2}, Wt::WTime {12, 30, 1}}; const Wt::WDateTime dateTime{ Wt::WDate {1950, 1, 2}, Wt::WTime {12, 30, 1} };
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
auto releases {Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal))}; auto releases{ Release::findIds(session, Release::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal)) };
EXPECT_EQ(releases.results.size(), 2); EXPECT_EQ(releases.results.size(), 2);
} }
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction{ session.createUniqueTransaction() };
starredRelease1.get().modify()->setDateTime(dateTime); starredRelease1.get().modify()->setDateTime(dateTime);
starredRelease2.get().modify()->setDateTime(dateTime.addSecs(-1)); starredRelease2.get().modify()->setDateTime(dateTime.addSecs(-1));
auto releases {Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal) auto releases{ Release::findIds(session, Release::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal).setSortMethod(ReleaseSortMethod::StarredDateDesc)) };
.setSortMethod(ReleaseSortMethod::StarredDateDesc))}; ASSERT_EQ(releases.results.size(), 2);
ASSERT_EQ(releases.results.size(), 2); EXPECT_EQ(releases.results[0], starredRelease1->getRelease()->getId());
EXPECT_EQ(releases.results[0], starredRelease1->getRelease()->getId()); EXPECT_EQ(releases.results[1], starredRelease2->getRelease()->getId());
EXPECT_EQ(releases.results[1], starredRelease2->getRelease()->getId()); }
} {
{ auto transaction{ session.createUniqueTransaction() };
auto transaction {session.createUniqueTransaction()};
starredRelease1.get().modify()->setDateTime(dateTime); starredRelease1.get().modify()->setDateTime(dateTime);
starredRelease2.get().modify()->setDateTime(dateTime.addSecs(1)); starredRelease2.get().modify()->setDateTime(dateTime.addSecs(1));
auto releases {Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal) auto releases{ Release::findIds(session, Release::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal).setSortMethod(ReleaseSortMethod::StarredDateDesc)) };
.setSortMethod(ReleaseSortMethod::StarredDateDesc))}; ASSERT_EQ(releases.results.size(), 2);
ASSERT_EQ(releases.results.size(), 2); EXPECT_EQ(releases.results[0], starredRelease2->getRelease()->getId());
EXPECT_EQ(releases.results[0], starredRelease2->getRelease()->getId()); EXPECT_EQ(releases.results[1], starredRelease1->getRelease()->getId());
EXPECT_EQ(releases.results[1], starredRelease1->getRelease()->getId()); }
}
} }
@@ -26,102 +26,100 @@ using ScopedStarredTrack = ScopedEntity<Database::StarredTrack>;
TEST_F(DatabaseFixture, StarredTrack) TEST_F(DatabaseFixture, StarredTrack)
{ {
ScopedTrack track {session, "MyTrack"}; ScopedTrack track {session, "MyTrack"};
ScopedUser user {session, "MyUser"}; ScopedUser user {session, "MyUser"};
ScopedUser user2 {session, "MyUser2"}; ScopedUser user2 {session, "MyUser2"};
{ {
auto transaction {session.createSharedTransaction()}; auto transaction {session.createSharedTransaction()};
auto starredTrack {StarredTrack::find(session, track->getId(), user->getId(), Scrobbler::Internal)}; auto starredTrack {StarredTrack::find(session, track->getId(), user->getId(), FeedbackBackend::Internal)};
EXPECT_FALSE(starredTrack); EXPECT_FALSE(starredTrack);
EXPECT_EQ(StarredTrack::getCount(session), 0); EXPECT_EQ(StarredTrack::getCount(session), 0);
auto tracks {Track::find(session, Track::FindParameters {})}; auto tracks {Track::findIds(session, Track::FindParameters {})};
EXPECT_EQ(tracks.results.size(), 1); EXPECT_EQ(tracks.results.size(), 1);
} }
ScopedStarredTrack starredTrack {session, track.lockAndGet(), user.lockAndGet(), Scrobbler::Internal}; ScopedStarredTrack starredTrack {session, track.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal};
{ {
auto transaction {session.createSharedTransaction()}; auto transaction {session.createSharedTransaction()};
auto gotTrack {StarredTrack::find(session, track->getId(), user->getId(), Scrobbler::Internal)}; auto gotTrack {StarredTrack::find(session, track->getId(), user->getId(), FeedbackBackend::Internal)};
EXPECT_EQ(gotTrack->getId(), starredTrack->getId()); EXPECT_EQ(gotTrack->getId(), starredTrack->getId());
EXPECT_EQ(StarredTrack::getCount(session), 1); EXPECT_EQ(StarredTrack::getCount(session), 1);
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction {session.createSharedTransaction()};
auto tracks {Track::find(session, Track::FindParameters {})}; auto tracks {Track::findIds(session, Track::FindParameters {})};
EXPECT_EQ(tracks.results.size(), 1); EXPECT_EQ(tracks.results.size(), 1);
tracks = Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal)); tracks = Track::findIds(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal));
EXPECT_EQ(tracks.results.size(), 1); EXPECT_EQ(tracks.results.size(), 1);
tracks = Track::find(session, Track::FindParameters {}.setStarringUser(user2.getId(), Scrobbler::Internal)); tracks = Track::findIds(session, Track::FindParameters {}.setStarringUser(user2.getId(), FeedbackBackend::Internal));
EXPECT_EQ(tracks.results.size(), 0); EXPECT_EQ(tracks.results.size(), 0);
} }
} }
TEST_F(DatabaseFixture, Starredtrack_PendingDestroy) TEST_F(DatabaseFixture, Starredtrack_PendingDestroy)
{ {
ScopedTrack track {session, "MyTrack"}; ScopedTrack track {session, "MyTrack"};
ScopedUser user {session, "MyUser"}; ScopedUser user {session, "MyUser"};
ScopedStarredTrack starredTrack {session, track.lockAndGet(), user.lockAndGet(), Scrobbler::Internal}; ScopedStarredTrack starredTrack {session, track.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal};
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction {session.createUniqueTransaction()};
auto tracks {Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal))}; auto tracks {Track::findIds(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal))};
EXPECT_EQ(tracks.results.size(), 1); EXPECT_EQ(tracks.results.size(), 1);
starredTrack.get().modify()->setScrobblingState(ScrobblingState::PendingRemove); starredTrack.get().modify()->setSyncState(SyncState::PendingRemove);
tracks = Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal)); tracks = Track::findIds(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal));
EXPECT_EQ(tracks.results.size(), 0); EXPECT_EQ(tracks.results.size(), 0);
} }
} }
TEST_F(DatabaseFixture, StarredTrack_dateTime) TEST_F(DatabaseFixture, StarredTrack_dateTime)
{ {
ScopedTrack track1 {session, "MyTrack1"}; ScopedTrack track1 {session, "MyTrack1"};
ScopedTrack track2 {session, "MyTrack2"}; ScopedTrack track2 {session, "MyTrack2"};
ScopedUser user {session, "MyUser"}; ScopedUser user {session, "MyUser"};
ScopedStarredTrack starredTrack1 {session, track1.lockAndGet(), user.lockAndGet(), Scrobbler::Internal}; ScopedStarredTrack starredTrack1 {session, track1.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal};
ScopedStarredTrack starredTrack2 {session, track2.lockAndGet(), user.lockAndGet(), Scrobbler::Internal}; ScopedStarredTrack starredTrack2 {session, track2.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal};
const Wt::WDateTime dateTime {Wt::WDate {1950, 1, 2}, Wt::WTime {12, 30, 1}}; const Wt::WDateTime dateTime {Wt::WDate {1950, 1, 2}, Wt::WTime {12, 30, 1}};
{ {
auto transaction {session.createSharedTransaction()}; auto transaction {session.createSharedTransaction()};
auto tracks {Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal))}; auto tracks {Track::findIds(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal))};
EXPECT_EQ(tracks.results.size(), 2); EXPECT_EQ(tracks.results.size(), 2);
} }
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction {session.createUniqueTransaction()};
starredTrack1.get().modify()->setDateTime(dateTime); starredTrack1.get().modify()->setDateTime(dateTime);
starredTrack2.get().modify()->setDateTime(dateTime.addSecs(-1)); starredTrack2.get().modify()->setDateTime(dateTime.addSecs(-1));
auto tracks {Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal) auto tracks {Track::findIds(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal).setSortMethod(TrackSortMethod::StarredDateDesc))};
.setSortMethod(TrackSortMethod::StarredDateDesc))}; ASSERT_EQ(tracks.results.size(), 2);
ASSERT_EQ(tracks.results.size(), 2); EXPECT_EQ(tracks.results[0], starredTrack1->getTrack()->getId());
EXPECT_EQ(tracks.results[0], starredTrack1->getTrack()->getId()); EXPECT_EQ(tracks.results[1], starredTrack2->getTrack()->getId());
EXPECT_EQ(tracks.results[1], starredTrack2->getTrack()->getId()); }
} {
{ auto transaction {session.createUniqueTransaction()};
auto transaction {session.createUniqueTransaction()};
starredTrack1.get().modify()->setDateTime(dateTime); starredTrack1.get().modify()->setDateTime(dateTime);
starredTrack2.get().modify()->setDateTime(dateTime.addSecs(1)); starredTrack2.get().modify()->setDateTime(dateTime.addSecs(1));
auto tracks {Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal) auto tracks {Track::findIds(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal).setSortMethod(TrackSortMethod::StarredDateDesc))};
.setSortMethod(TrackSortMethod::StarredDateDesc))}; ASSERT_EQ(tracks.results.size(), 2);
ASSERT_EQ(tracks.results.size(), 2); EXPECT_EQ(tracks.results[0], starredTrack2->getTrack()->getId());
EXPECT_EQ(tracks.results[0], starredTrack2->getTrack()->getId()); EXPECT_EQ(tracks.results[1], starredTrack1->getTrack()->getId());
EXPECT_EQ(tracks.results[1], starredTrack1->getTrack()->getId()); }
}
} }
+101 -100
View File
@@ -25,137 +25,138 @@ using namespace Database;
TEST_F(DatabaseFixture, Track) TEST_F(DatabaseFixture, Track)
{ {
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
EXPECT_EQ(Track::find(session, Track::FindParameters {}).results.size(), 0); EXPECT_EQ(Track::find(session, Track::FindParameters{}).results.size(), 0);
EXPECT_EQ(Track::getCount(session), 0); EXPECT_EQ(Track::findIds(session, Track::FindParameters{}).results.size(), 0);
EXPECT_FALSE(Track::exists(session, 0)); EXPECT_EQ(Track::getCount(session), 0);
} EXPECT_FALSE(Track::exists(session, 0));
}
ScopedTrack track {session, "MyTrackFile"}; ScopedTrack track{ session, "MyTrackFile" };
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
EXPECT_EQ(Track::find(session, Track::FindParameters {}).results.size(), 1); EXPECT_EQ(Track::find(session, Track::FindParameters{}).results.size(), 1);
EXPECT_EQ(Track::getCount(session), 1); EXPECT_EQ(Track::getCount(session), 1);
EXPECT_TRUE(Track::exists(session, track.getId())); EXPECT_TRUE(Track::exists(session, track.getId()));
auto myTrack {Track::find(session, track.getId())}; auto myTrack{ Track::find(session, track.getId()) };
ASSERT_TRUE(myTrack); ASSERT_TRUE(myTrack);
EXPECT_EQ(myTrack->getId(), track.getId()); EXPECT_EQ(myTrack->getId(), track.getId());
} }
} }
TEST_F(DatabaseFixture, MultipleTracks) TEST_F(DatabaseFixture, MultipleTracks)
{ {
ScopedTrack track1 {session, "MyTrackFile1"}; ScopedTrack track1{ session, "MyTrackFile1" };
ScopedTrack track2 {session, "MyTrackFile2"}; ScopedTrack track2{ session, "MyTrackFile2" };
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
EXPECT_TRUE(track1.getId() != track2.getId()); EXPECT_TRUE(track1.getId() != track2.getId());
EXPECT_TRUE(track1.get() != track2.get()); EXPECT_TRUE(track1.get() != track2.get());
EXPECT_FALSE(track1.get() == track2.get()); EXPECT_FALSE(track1.get() == track2.get());
} }
} }
TEST_F(DatabaseFixture, MultipleTracksSearchByFilter) TEST_F(DatabaseFixture, MultipleTracksSearchByFilter)
{ {
ScopedTrack track1 {session, ""}; ScopedTrack track1{ session, "" };
ScopedTrack track2 {session, ""}; ScopedTrack track2{ session, "" };
ScopedTrack track3 {session, ""}; ScopedTrack track3{ session, "" };
ScopedTrack track4 {session, ""}; ScopedTrack track4{ session, "" };
ScopedTrack track5 {session, ""}; ScopedTrack track5{ session, "" };
ScopedTrack track6 {session, ""}; ScopedTrack track6{ session, "" };
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction{ session.createUniqueTransaction() };
track1.get().modify()->setName("MyTrack"); track1.get().modify()->setName("MyTrack");
track2.get().modify()->setName("MyTrack%"); track2.get().modify()->setName("MyTrack%");
track3.get().modify()->setName("MyTrack%Foo"); track3.get().modify()->setName("MyTrack%Foo");
track4.get().modify()->setName("%MyTrack"); track4.get().modify()->setName("%MyTrack");
track5.get().modify()->setName("Foo%MyTrack"); track5.get().modify()->setName("Foo%MyTrack");
track6.get().modify()->setName("M_Track"); track6.get().modify()->setName("M_Track");
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
{ {
const auto tracks {Track::find(session, Track::FindParameters {}.setKeywords({"Track"}))}; const auto tracks{ Track::findIds(session, Track::FindParameters {}.setKeywords({"Track"})) };
EXPECT_EQ(tracks.results.size(), 6); EXPECT_EQ(tracks.results.size(), 6);
} }
{ {
const auto tracks {Track::find(session, Track::FindParameters {}.setKeywords({"MyTrack"}))}; const auto tracks{ Track::findIds(session, Track::FindParameters {}.setKeywords({"MyTrack"})) };
EXPECT_EQ(tracks.results.size(), 5); EXPECT_EQ(tracks.results.size(), 5);
EXPECT_TRUE(std::none_of(std::cbegin(tracks.results), std::cend(tracks.results), [&](const TrackId trackId) { return trackId == track6.getId(); })); EXPECT_TRUE(std::none_of(std::cbegin(tracks.results), std::cend(tracks.results), [&](const TrackId trackId) { return trackId == track6.getId(); }));
} }
{ {
const auto tracks {Track::find(session, Track::FindParameters {}.setKeywords({"MyTrack%"}))}; const auto tracks{ Track::findIds(session, Track::FindParameters {}.setKeywords({"MyTrack%"})) };
ASSERT_EQ(tracks.results.size(), 2); ASSERT_EQ(tracks.results.size(), 2);
EXPECT_EQ(tracks.results[0], track2.getId()); EXPECT_EQ(tracks.results[0], track2.getId());
EXPECT_EQ(tracks.results[1], track3.getId()); EXPECT_EQ(tracks.results[1], track3.getId());
} }
{ {
const auto tracks {Track::find(session, Track::FindParameters {}.setKeywords({"%MyTrack"}))}; const auto tracks{ Track::findIds(session, Track::FindParameters {}.setKeywords({"%MyTrack"})) };
ASSERT_EQ(tracks.results.size(), 2); ASSERT_EQ(tracks.results.size(), 2);
EXPECT_EQ(tracks.results[0], track4.getId()); EXPECT_EQ(tracks.results[0], track4.getId());
EXPECT_EQ(tracks.results[1], track5.getId()); EXPECT_EQ(tracks.results[1], track5.getId());
} }
} }
} }
TEST_F(DatabaseFixture, Track_date) TEST_F(DatabaseFixture, Track_date)
{ {
ScopedTrack track {session, "MyTrack"}; ScopedTrack track{ session, "MyTrack" };
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
EXPECT_EQ(track->getYear(), std::nullopt); EXPECT_EQ(track->getYear(), std::nullopt);
EXPECT_EQ(track->getOriginalYear(), std::nullopt); EXPECT_EQ(track->getOriginalYear(), std::nullopt);
} }
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction{ session.createUniqueTransaction() };
track.get().modify()->setDate(Wt::WDate {1995, 5, 5}); track.get().modify()->setDate(Wt::WDate{ 1995, 5, 5 });
track.get().modify()->setOriginalDate(Wt::WDate {1994, 2, 2}); track.get().modify()->setOriginalDate(Wt::WDate{ 1994, 2, 2 });
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
EXPECT_EQ(track->getYear(), 1995); EXPECT_EQ(track->getYear(), 1995);
EXPECT_EQ(track->getOriginalYear(), 1994); EXPECT_EQ(track->getOriginalYear(), 1994);
} }
} }
TEST_F(DatabaseFixture, Track_writtenAfter) TEST_F(DatabaseFixture, Track_writtenAfter)
{ {
ScopedTrack track {session, "MyTrack"}; ScopedTrack track{ session, "MyTrack" };
const Wt::WDateTime dateTime {Wt::WDate {1950, 1, 1}, Wt::WTime {12, 30, 20}}; const Wt::WDateTime dateTime{ Wt::WDate {1950, 1, 1}, Wt::WTime {12, 30, 20} };
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction{ session.createUniqueTransaction() };
track.get().modify()->setLastWriteTime(dateTime); track.get().modify()->setLastWriteTime(dateTime);
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
const auto tracks {Track::find(session, Track::FindParameters {})}; const auto tracks{ Track::findIds(session, Track::FindParameters {}) };
EXPECT_EQ(tracks.results.size(), 1); EXPECT_EQ(tracks.results.size(), 1);
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
const auto tracks {Track::find(session, Track::FindParameters {}.setWrittenAfter(dateTime.addSecs(-1)))}; const auto tracks{ Track::findIds(session, Track::FindParameters {}.setWrittenAfter(dateTime.addSecs(-1))) };
EXPECT_EQ(tracks.results.size(), 1); EXPECT_EQ(tracks.results.size(), 1);
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
const auto tracks {Track::find(session, Track::FindParameters {}.setWrittenAfter(dateTime.addSecs(+1)))}; const auto tracks{ Track::findIds(session, Track::FindParameters {}.setWrittenAfter(dateTime.addSecs(+1))) };
EXPECT_EQ(tracks.results.size(), 0); EXPECT_EQ(tracks.results.size(), 0);
} }
} }
@@ -49,10 +49,10 @@ TEST_F(DatabaseFixture, SingleTrackListSingleTrack)
{ {
auto transaction {session.createSharedTransaction()}; auto transaction {session.createSharedTransaction()};
auto tracks {Track::find(session, Track::FindParameters {}.setTrackList(trackList1.getId()))}; auto tracks {Track::findIds(session, Track::FindParameters {}.setTrackList(trackList1.getId()))};
EXPECT_EQ(tracks.results.size(), 0); EXPECT_EQ(tracks.results.size(), 0);
tracks = Track::find(session, Track::FindParameters {}.setTrackList(trackList2.getId())); tracks = Track::findIds(session, Track::FindParameters {}.setTrackList(trackList2.getId()));
EXPECT_EQ(tracks.results.size(), 0); EXPECT_EQ(tracks.results.size(), 0);
} }
@@ -65,11 +65,11 @@ TEST_F(DatabaseFixture, SingleTrackListSingleTrack)
{ {
auto transaction {session.createSharedTransaction()}; auto transaction {session.createSharedTransaction()};
auto tracks {Track::find(session, Track::FindParameters {}.setTrackList(trackList1.getId()))}; auto tracks {Track::findIds(session, Track::FindParameters {}.setTrackList(trackList1.getId()))};
ASSERT_EQ(tracks.results.size(), 1); ASSERT_EQ(tracks.results.size(), 1);
EXPECT_EQ(tracks.results.front(), track.getId()); EXPECT_EQ(tracks.results.front(), track.getId());
tracks = Track::find(session, Track::FindParameters {}.setTrackList(trackList2.getId())); tracks = Track::findIds(session, Track::FindParameters {}.setTrackList(trackList2.getId()));
EXPECT_EQ(tracks.results.size(), 0); EXPECT_EQ(tracks.results.size(), 0);
} }
} }
+29
View File
@@ -0,0 +1,29 @@
add_library(lmsfeedback SHARED
impl/internal/InternalBackend.cpp
impl/listenbrainz/FeedbacksParser.cpp
impl/listenbrainz/FeedbacksSynchronizer.cpp
impl/listenbrainz/FeedbackTypes.cpp
impl/listenbrainz/ListenBrainzBackend.cpp
impl/listenbrainz/Utils.cpp
impl/FeedbackService.cpp
)
target_include_directories(lmsfeedback INTERFACE
include
)
target_include_directories(lmsfeedback PRIVATE
include
impl
)
target_link_libraries(lmsfeedback PRIVATE
lmsutils
)
target_link_libraries(lmsfeedback PUBLIC
lmsdatabase
)
install(TARGETS lmsfeedback DESTINATION lib)
@@ -0,0 +1,185 @@
/*
* Copyright (C) 2021 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "FeedbackService.hpp"
#include "FeedbackService.impl.hpp"
#include "services/database/Artist.hpp"
#include "services/database/Db.hpp"
#include "services/database/Release.hpp"
#include "services/database/Session.hpp"
#include "services/database/StarredArtist.hpp"
#include "services/database/StarredRelease.hpp"
#include "services/database/StarredTrack.hpp"
#include "services/database/Track.hpp"
#include "services/database/User.hpp"
#include "utils/Logger.hpp"
#include "internal/InternalBackend.hpp"
#include "listenbrainz/ListenBrainzBackend.hpp"
namespace Feedback
{
std::unique_ptr<IFeedbackService> createFeedbackService(boost::asio::io_context& ioContext, Db& db)
{
return std::make_unique<FeedbackService>(ioContext, db);
}
FeedbackService::FeedbackService(boost::asio::io_context& ioContext, Db& db)
: _db{ db }
{
LMS_LOG(SCROBBLING, INFO) << "Starting service...";
_backends.emplace(Database::FeedbackBackend::Internal, std::make_unique<InternalBackend>(_db));
_backends.emplace(Database::FeedbackBackend::ListenBrainz, std::make_unique<ListenBrainz::ListenBrainzBackend>(ioContext, _db));
LMS_LOG(SCROBBLING, INFO) << "Service started!";
}
FeedbackService::~FeedbackService()
{
LMS_LOG(SCROBBLING, INFO) << "Service stopped!";
}
std::optional<Database::FeedbackBackend> FeedbackService::getUserFeedbackBackend(UserId userId)
{
std::optional<Database::FeedbackBackend> feedbackBackend;
Session& session{ _db.getTLSSession() };
auto transaction{ session.createSharedTransaction() };
if (const User::pointer user{ User::find(session, userId) })
feedbackBackend = user->getFeedbackBackend();
return feedbackBackend;
}
void FeedbackService::star(UserId userId, ArtistId artistId)
{
star<Artist, ArtistId, StarredArtist>(userId, artistId);
}
void FeedbackService::unstar(UserId userId, ArtistId artistId)
{
unstar<Artist, ArtistId, StarredArtist>(userId, artistId);
}
bool FeedbackService::isStarred(UserId userId, ArtistId artistId)
{
return isStarred<Artist, ArtistId, StarredArtist>(userId, artistId);
}
Wt::WDateTime FeedbackService::getStarredDateTime(UserId userId, ArtistId artistId)
{
return getStarredDateTime<Artist, ArtistId, StarredArtist>(userId, artistId);
}
FeedbackService::ArtistContainer FeedbackService::getStarredArtists(UserId userId, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, ArtistSortMethod sortMethod, Range range)
{
auto backend{ getUserFeedbackBackend(userId) };
if (!backend)
return {};
Artist::FindParameters params;
params.setStarringUser(userId, *backend);
params.setClusters(clusterIds);
params.setLinkType(linkType);
params.setSortMethod(sortMethod);
params.setRange(range);
Session& session{ _db.getTLSSession() };
auto transaction{ session.createSharedTransaction() };
return Artist::findIds(session, params);
}
void FeedbackService::star(UserId userId, ReleaseId releaseId)
{
star<Release, ReleaseId, StarredRelease>(userId, releaseId);
}
void FeedbackService::unstar(UserId userId, ReleaseId releaseId)
{
unstar<Release, ReleaseId, StarredRelease>(userId, releaseId);
}
bool FeedbackService::isStarred(UserId userId, ReleaseId releaseId)
{
return isStarred<Release, ReleaseId, StarredRelease>(userId, releaseId);
}
Wt::WDateTime FeedbackService::getStarredDateTime(UserId userId, ReleaseId releaseId)
{
return getStarredDateTime<Release, ReleaseId, StarredRelease>(userId, releaseId);
}
FeedbackService::ReleaseContainer FeedbackService::getStarredReleases(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
{
auto backend{ getUserFeedbackBackend(userId) };
if (!backend)
return {};
Release::FindParameters params;
params.setStarringUser(userId, *backend);
params.setClusters(clusterIds);
params.setSortMethod(ReleaseSortMethod::StarredDateDesc);
params.setRange(range);
Session& session{ _db.getTLSSession() };
auto transaction{ session.createSharedTransaction() };
return Release::findIds(session, params);
}
void FeedbackService::star(UserId userId, TrackId trackId)
{
star<Track, TrackId, StarredTrack>(userId, trackId);
}
void FeedbackService::unstar(UserId userId, TrackId trackId)
{
unstar<Track, TrackId, StarredTrack>(userId, trackId);
}
bool FeedbackService::isStarred(UserId userId, TrackId trackId)
{
return isStarred<Track, TrackId, StarredTrack>(userId, trackId);
}
Wt::WDateTime FeedbackService::getStarredDateTime(UserId userId, TrackId trackId)
{
return getStarredDateTime<Track, TrackId, StarredTrack>(userId, trackId);
}
FeedbackService::TrackContainer FeedbackService::getStarredTracks(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
{
auto backend{ getUserFeedbackBackend(userId) };
if (!backend)
return {};
Track::FindParameters params;
params.setStarringUser(userId, *backend);
params.setClusters(clusterIds);
params.setSortMethod(TrackSortMethod::StarredDateDesc);
params.setRange(range);
Session& session{ _db.getTLSSession() };
auto transaction{ session.createSharedTransaction() };
return Track::findIds(session, params);
}
} // ns Feedback
@@ -0,0 +1,78 @@
/*
* Copyright (C) 2021 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <memory>
#include <unordered_map>
#include "services/feedback/IFeedbackService.hpp"
#include "IFeedbackBackend.hpp"
namespace Database
{
class Db;
}
namespace Feedback
{
class FeedbackService : public IFeedbackService
{
public:
FeedbackService(boost::asio::io_context& ioContext, Database::Db& db);
~FeedbackService();
private:
FeedbackService(const FeedbackService&) = delete;
FeedbackService& operator=(const FeedbackService&) = delete;
void star(Database::UserId userId, Database::ArtistId artistId) override;
void unstar(Database::UserId userId, Database::ArtistId artistId) override;
bool isStarred(Database::UserId userId, Database::ArtistId artistId) override;
Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::ArtistId artistId) override;
ArtistContainer getStarredArtists(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, std::optional<Database::TrackArtistLinkType> linkType, Database::ArtistSortMethod sortMethod, Database::Range range) override;
void star(Database::UserId userId, Database::ReleaseId releaseId) override;
void unstar(Database::UserId userId, Database::ReleaseId releaseId) override;
bool isStarred(Database::UserId userId, Database::ReleaseId releasedId) override;
Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::ReleaseId releasedId) override;
ReleaseContainer getStarredReleases(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) override;
void star(Database::UserId userId, Database::TrackId trackId) override;
void unstar(Database::UserId userId, Database::TrackId trackId) override;
bool isStarred(Database::UserId userId, Database::TrackId trackId) override;
Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::TrackId trackId) override;
TrackContainer getStarredTracks(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) override;
std::optional<Database::FeedbackBackend> getUserFeedbackBackend(Database::UserId userId);
template <typename ObjType, typename ObjIdType, typename StarredObjType>
void star(Database::UserId userId, ObjIdType id);
template <typename ObjType, typename ObjIdType, typename StarredObjType>
void unstar(Database::UserId userId, ObjIdType id);
template <typename ObjType, typename ObjIdType, typename StarredObjType>
bool isStarred(Database::UserId userId, ObjIdType id);
template <typename ObjType, typename ObjIdType, typename StarredObjType>
Wt::WDateTime getStarredDateTime(Database::UserId userId, ObjIdType id);
Database::Db& _db;
std::unordered_map<Database::FeedbackBackend, std::unique_ptr<IFeedbackBackend>> _backends;
};
} // ns Feedback
@@ -0,0 +1,113 @@
/*
* Copyright (C) 2021 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "services/database/Db.hpp"
#include "services/database/Session.hpp"
#include "services/database/User.hpp"
namespace Feedback
{
using namespace Database;
template <typename ObjType, typename ObjIdType, typename StarredObjType>
void FeedbackService::star(UserId userId, ObjIdType objId)
{
const auto backend{ getUserFeedbackBackend(userId) };
if (!backend)
return;
typename StarredObjType::IdType starredObjId;
{
Session& session{ _db.getTLSSession() };
auto transaction{ session.createUniqueTransaction() };
typename StarredObjType::pointer starredObj{ StarredObjType::find(session, objId, userId, *backend) };
if (!starredObj)
{
const typename ObjType::pointer obj{ ObjType::find(session, objId) };
if (!obj)
return;
const User::pointer user{ User::find(session, userId) };
if (!user)
return;
starredObj = session.create<StarredObjType>(obj, user, *backend);
}
starredObj.modify()->setDateTime(Wt::WDateTime::currentDateTime());
starredObjId = starredObj->getId();
}
_backends[*backend]->onStarred(starredObjId);
}
template <typename ObjType, typename ObjIdType, typename StarredObjType>
void FeedbackService::unstar(UserId userId, ObjIdType objId)
{
const auto backend{ getUserFeedbackBackend(userId) };
if (!backend)
return;
typename StarredObjType::IdType starredObjId;
{
Session& session{ _db.getTLSSession() };
auto transaction{ session.createSharedTransaction() };
typename StarredObjType::pointer starredObj{ StarredObjType::find(session, objId, userId, *backend) };
if (!starredObj)
return;
starredObjId = starredObj->getId();
}
_backends[*backend]->onUnstarred(starredObjId);
}
template <typename ObjType, typename ObjIdType, typename StarredObjType>
bool FeedbackService::isStarred(UserId userId, ObjIdType objId)
{
const auto backend{ getUserFeedbackBackend(userId) };
if (!backend)
return false;
Session& session{ _db.getTLSSession() };
auto transaction{ session.createSharedTransaction() };
typename StarredObjType::pointer starredObj{ StarredObjType::find(session, objId, userId, *backend) };
return starredObj && (starredObj->getSyncState() != SyncState::PendingRemove);
}
template <typename ObjType, typename ObjIdType, typename StarredObjType>
Wt::WDateTime FeedbackService::getStarredDateTime(UserId userId, ObjIdType objId)
{
const auto backend{ getUserFeedbackBackend(userId) };
if (!backend)
return {};
Session& session{ _db.getTLSSession() };
auto transaction{ session.createSharedTransaction() };
typename StarredObjType::pointer starredObj{ StarredObjType::find(session, objId, userId, *backend) };
if (starredObj && (starredObj->getSyncState() != SyncState::PendingRemove))
return starredObj->getDateTime();
return {};
}
} // ns Feedback
@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2021 Emeric Poupon * Copyright (C) 2023 Emeric Poupon
* *
* This file is part of LMS. * This file is part of LMS.
* *
@@ -19,35 +19,17 @@
#pragma once #pragma once
#include <chrono>
#include <memory>
#include <optional>
#include "services/database/StarredArtistId.hpp" #include "services/database/StarredArtistId.hpp"
#include "services/database/StarredReleaseId.hpp" #include "services/database/StarredReleaseId.hpp"
#include "services/database/StarredTrackId.hpp" #include "services/database/StarredTrackId.hpp"
#include "services/scrobbling/Listen.hpp"
namespace Database namespace Feedback
{ {
class Session; class IFeedbackBackend
class TrackList;
class User;
}
namespace Scrobbling
{
class IScrobbler
{ {
public: public:
virtual ~IScrobbler() = default; virtual ~IFeedbackBackend() = default;
// Listens
virtual void listenStarted(const Listen& listen) = 0;
virtual void listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration) = 0;
virtual void addTimedListen(const TimedListen& listen) = 0;
// Feedbacks
virtual void onStarred(Database::StarredArtistId) = 0; virtual void onStarred(Database::StarredArtistId) = 0;
virtual void onUnstarred(Database::StarredArtistId) = 0; virtual void onUnstarred(Database::StarredArtistId) = 0;
virtual void onStarred(Database::StarredReleaseId) = 0; virtual void onStarred(Database::StarredReleaseId) = 0;
@@ -56,7 +38,6 @@ namespace Scrobbling
virtual void onUnstarred(Database::StarredTrackId) = 0; virtual void onUnstarred(Database::StarredTrackId) = 0;
}; };
std::unique_ptr<IScrobbler> createScrobbler(std::string_view backendName); std::unique_ptr<IFeedbackBackend> createFeedbackBackend(std::string_view backendName);
} // ns Scrobbling
} // ns Feedback
@@ -0,0 +1,84 @@
/*
* Copyright (C) 2021 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "InternalBackend.hpp"
#include "services/database/Db.hpp"
#include "services/database/Session.hpp"
#include "services/database/StarredArtist.hpp"
#include "services/database/StarredRelease.hpp"
#include "services/database/StarredTrack.hpp"
namespace Feedback
{
namespace details
{
template <typename StarredObjType>
void onStarred(Database::Session& session, typename StarredObjType::IdType id)
{
auto transaction{ session.createUniqueTransaction() };
if (auto starredObj{ StarredObjType::find(session, id) })
starredObj.modify()->setSyncState(Database::SyncState::Synchronized);
}
template <typename StarredObjType>
void onUnstarred(Database::Session& session, typename StarredObjType::IdType id)
{
auto transaction{ session.createUniqueTransaction() };
if (auto starredObj{ StarredObjType::find(session, id) })
starredObj.remove();
}
}
InternalBackend::InternalBackend(Database::Db& db)
: _db{ db }
{}
void InternalBackend::onStarred(Database::StarredArtistId starredArtistId)
{
details::onStarred<Database::StarredArtist>(_db.getTLSSession(), starredArtistId);
}
void InternalBackend::onUnstarred(Database::StarredArtistId starredArtistId)
{
details::onUnstarred<Database::StarredArtist>(_db.getTLSSession(), starredArtistId);
}
void InternalBackend::onStarred(Database::StarredReleaseId starredReleaseId)
{
details::onStarred<Database::StarredRelease>(_db.getTLSSession(), starredReleaseId);
}
void InternalBackend::onUnstarred(Database::StarredReleaseId starredReleaseId)
{
details::onUnstarred<Database::StarredRelease>(_db.getTLSSession(), starredReleaseId);
}
void InternalBackend::onStarred(Database::StarredTrackId starredTrackId)
{
details::onStarred<Database::StarredTrack>(_db.getTLSSession(), starredTrackId);
}
void InternalBackend::onUnstarred(Database::StarredTrackId starredTrackId)
{
details::onUnstarred<Database::StarredTrack>(_db.getTLSSession(), starredTrackId);
}
} // Feedback
@@ -19,26 +19,21 @@
#pragma once #pragma once
#include "IScrobbler.hpp" #include "IFeedbackBackend.hpp"
namespace Database namespace Database
{ {
class Db; class Db;
} }
namespace Scrobbling namespace Feedback
{ {
class InternalScrobbler final : public IScrobbler class InternalBackend final : public IFeedbackBackend
{ {
public: public:
InternalScrobbler(Database::Db& db); InternalBackend(Database::Db& db);
private: private:
// IScrobbler
void listenStarted(const Listen& listen) override;
void listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration) override;
void addTimedListen(const TimedListen& listen) override;
void onStarred(Database::StarredArtistId) override; void onStarred(Database::StarredArtistId) override;
void onUnstarred(Database::StarredArtistId) override; void onUnstarred(Database::StarredArtistId) override;
void onStarred(Database::StarredReleaseId) override; void onStarred(Database::StarredReleaseId) override;
@@ -48,5 +43,5 @@ namespace Scrobbling
Database::Db& _db; Database::Db& _db;
}; };
} // Scrobbling } // Feedback
@@ -0,0 +1,31 @@
/*
* Copyright (C) 2022 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "services/feedback/Exception.hpp"
namespace Feedback::ListenBrainz
{
class Exception : public ::Feedback::Exception
{
public:
using ::Feedback::Exception::Exception;
};
}
@@ -19,7 +19,7 @@
#include "FeedbackTypes.hpp" #include "FeedbackTypes.hpp"
namespace Scrobbling::ListenBrainz namespace Feedback::ListenBrainz
{ {
std::ostream& std::ostream&
operator<<(std::ostream& os, const Feedback& feedback) operator<<(std::ostream& os, const Feedback& feedback)
@@ -27,4 +27,4 @@ namespace Scrobbling::ListenBrainz
os << "created = '" << feedback.created.toString() << "', recording MBID = '" << feedback.recordingMBID.getAsString() << "', score = " << static_cast<int>(feedback.score); os << "created = '" << feedback.created.toString() << "', recording MBID = '" << feedback.recordingMBID.getAsString() << "', score = " << static_cast<int>(feedback.score);
return os; return os;
} }
} // Scrobbling::ListenBrainz } // Feedback::ListenBrainz
@@ -23,7 +23,7 @@
#include <Wt/WDateTime.h> #include <Wt/WDateTime.h>
#include "utils/UUID.hpp" #include "utils/UUID.hpp"
namespace Scrobbling::ListenBrainz namespace Feedback::ListenBrainz
{ {
// See https://listenbrainz.readthedocs.io/en/production/dev/feedback-json/#feedback-json-doc // See https://listenbrainz.readthedocs.io/en/production/dev/feedback-json/#feedback-json-doc
enum class FeedbackType enum class FeedbackType
@@ -42,4 +42,4 @@ namespace Scrobbling::ListenBrainz
std::ostream& operator<<(std::ostream& os, const Feedback& feedback); std::ostream& operator<<(std::ostream& os, const Feedback& feedback);
} // Scrobbling::ListenBrainz } // Feedback::ListenBrainz
@@ -0,0 +1,90 @@
/*
* Copyright (C) 2022 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "FeedbacksParser.hpp"
#include <Wt/Json/Array.h>
#include <Wt/Json/Object.h>
#include <Wt/Json/Value.h>
#include <Wt/Json/Parser.h>
#include "Exception.hpp"
#include "Utils.hpp"
namespace Feedback::ListenBrainz
{
namespace
{
Feedback parseFeedback(const Wt::Json::Object& feedbackObj)
{
const std::optional<UUID> recordingMBID{ UUID::fromString(static_cast<std::string>(feedbackObj.get("recording_mbid"))) };
if (!recordingMBID)
throw Exception{ "MBID not found!" };
return Feedback
{
Wt::WDateTime::fromTime_t(static_cast<int>(feedbackObj.get("created"))),
*recordingMBID,
static_cast<FeedbackType>(static_cast<int>(feedbackObj.get("score")))
};
}
}
FeedbacksParser::Result FeedbacksParser::parse(std::string_view msgBody)
{
Result res;
try
{
Wt::Json::Object root;
Wt::Json::parse(std::string{ msgBody }, root);
const Wt::Json::Array& feedbacks = root.get("feedback");
LOG(DEBUG) << "Got " << feedbacks.size() << " feedbacks";
if (feedbacks.empty())
return res;
res.feedbackCount = feedbacks.size();
for (const Wt::Json::Value& value : feedbacks)
{
try
{
res.feedbacks.push_back(parseFeedback(value));
}
catch (const Exception& e)
{
LOG(DEBUG) << "Cannot parse feedback: " << e.what() << ", skipping";
}
catch (const Wt::WException& e)
{
LOG(DEBUG) << "Cannot parse feedback: " << e.what() << ", skipping";
}
}
}
catch (const Wt::WException& error)
{
LOG(ERROR) << "Cannot parse 'feedback' result: " << error.what();
}
return res;
}
} // Feedback::ListenBrainz
@@ -23,7 +23,7 @@
#include "FeedbackTypes.hpp" #include "FeedbackTypes.hpp"
namespace Scrobbling::ListenBrainz namespace Feedback::ListenBrainz
{ {
class FeedbacksParser class FeedbacksParser
{ {
@@ -37,4 +37,4 @@ namespace Scrobbling::ListenBrainz
static Result parse(std::string_view msgBody); static Result parse(std::string_view msgBody);
}; };
} // Scrobbling::ListenBrainz } // Feedback::ListenBrainz
@@ -0,0 +1,490 @@
/*
* Copyright (C) 2021 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "FeedbacksSynchronizer.hpp"
#include <boost/asio/bind_executor.hpp>
#include <Wt/Json/Array.h>
#include <Wt/Json/Object.h>
#include <Wt/Json/Value.h>
#include <Wt/Json/Serializer.h>
#include "services/database/Db.hpp"
#include "services/database/Session.hpp"
#include "services/database/StarredTrack.hpp"
#include "services/database/Track.hpp"
#include "services/database/User.hpp"
#include "utils/IConfig.hpp"
#include "utils/http/IClient.hpp"
#include "utils/Service.hpp"
#include "Exception.hpp"
#include "FeedbacksParser.hpp"
#include "Utils.hpp"
namespace Feedback::ListenBrainz
{
namespace
{
std::optional<std::size_t> parseTotalFeedbackCount(std::string_view msgBody)
{
try
{
Wt::Json::Object root;
Wt::Json::parse(std::string{ msgBody }, root);
return static_cast<int>(root.get("total_count"));
}
catch (const Wt::WException& e)
{
LOG(ERROR) << "Cannot parse listen count response: " << e.what();
return std::nullopt;
}
}
}
FeedbacksSynchronizer::FeedbacksSynchronizer(boost::asio::io_context& ioContext, Database::Db& db, Http::IClient& client)
: _ioContext{ ioContext }
, _db{ db }
, _client{ client }
, _maxSyncFeedbackCount{ Service<IConfig>::get()->getULong("listenbrainz-max-sync-feedback-count", 1000) }
, _syncFeedbacksPeriod{ Service<IConfig>::get()->getULong("listenbrainz-sync-feedbacks-period-hours", 1) }
{
LOG(INFO) << "Starting Feedbacks synchronizer, maxSyncFeedbackCount = " << _maxSyncFeedbackCount << ", _syncFeedbacksPeriod = " << _syncFeedbacksPeriod.count() << " hours";
scheduleSync(std::chrono::seconds{ 30 });
}
void FeedbacksSynchronizer::enqueFeedback(FeedbackType type, Database::StarredTrackId starredTrackId)
{
try
{
Database::Session& session{ _db.getTLSSession() };
auto transaction{ session.createUniqueTransaction() };
Database::StarredTrack::pointer starredTrack{ Database::StarredTrack::find(session, starredTrackId) };
if (!starredTrack)
return;
std::optional<UUID> recordingMBID{ starredTrack->getTrack()->getRecordingMBID() };
switch (type)
{
case FeedbackType::Love:
if (starredTrack->getSyncState() != Database::SyncState::PendingAdd)
starredTrack.modify()->setSyncState(Database::SyncState::PendingAdd);
break;
case FeedbackType::Erase:
if (!recordingMBID)
{
LOG(DEBUG) << "Track has no recording MBID: erasing star";
starredTrack.remove();
}
else
{
// Send the erase order even if it is not on the remote LB server (it may be
// queued for add, or not)
starredTrack.modify()->setSyncState(Database::SyncState::PendingRemove);
}
break;
default:
throw Exception{ "Unhandled feedback type" };
}
if (!recordingMBID)
{
LOG(DEBUG) << "Track has no recording MBID: skipping";
return;
}
const std::optional<UUID> listenBrainzToken{ starredTrack->getUser()->getListenBrainzToken() };
if (!listenBrainzToken)
return;
Http::ClientPOSTRequestParameters request;
request.relativeUrl = "/1/feedback/recording-feedback";
request.message.addHeader("Authorization", "Token " + std::string{ listenBrainzToken->getAsString() });
Wt::Json::Object root;
root["recording_mbid"] = Wt::Json::Value{ std::string {recordingMBID->getAsString()} };
root["score"] = Wt::Json::Value{ static_cast<int>(type) };
request.message.addBodyText(Wt::Json::serialize(root));
request.message.addHeader("Content-Type", "application/json");
request.onSuccessFunc = [=](std::string_view /*msgBody*/)
{
_strand.dispatch([=]
{
onFeedbackSent(type, starredTrackId);
});
};
_client.sendPOSTRequest(std::move(request));
}
catch (Exception& e)
{
LOG(DEBUG) << "Cannot send feedback: " << e.what();
}
}
void FeedbacksSynchronizer::onFeedbackSent(FeedbackType type, Database::StarredTrackId starredTrackId)
{
assert(_strand.running_in_this_thread());
Database::Session& session{ _db.getTLSSession() };
auto transaction{ session.createUniqueTransaction() };
Database::StarredTrack::pointer starredTrack{ Database::StarredTrack::find(session, starredTrackId) };
if (!starredTrack)
{
LOG(DEBUG) << "Starred track not found. deleted?";
return;
}
UserContext& userContext{ getUserContext(starredTrack->getUser()->getId()) };
switch (type)
{
case FeedbackType::Love:
starredTrack.modify()->setSyncState(Database::SyncState::Synchronized);
LOG(DEBUG) << "State set to synchronized";
if (userContext.feedbackCount)
{
(*userContext.feedbackCount)++;
LOG(DEBUG) << "Feedback count set to " << *userContext.feedbackCount << " for user '" << userContext.listenBrainzUserName << "'";
}
break;
case FeedbackType::Erase:
starredTrack.remove();
LOG(DEBUG) << "Removed starred track";
if (userContext.feedbackCount && *userContext.feedbackCount > 0)
{
(*userContext.feedbackCount)--;
LOG(DEBUG) << "Feedback count set to " << *userContext.feedbackCount << " for user '" << userContext.listenBrainzUserName << "'";
}
break;
default:
throw Exception{ "Unhandled feedback type" };
}
}
void FeedbacksSynchronizer::enquePendingFeedbacks()
{
using namespace Database;
auto processPendingFeedbacks{ [this](SyncState scrobblingState, FeedbackType feedbackType)
{
RangeResults<StarredTrackId> pendingFeedbacks;
{
Database::Session& session {_db.getTLSSession()};
auto transaction {session.createSharedTransaction()};
StarredTrack::FindParameters params;
params.setFeedbackBackend(Database::FeedbackBackend::ListenBrainz, scrobblingState)
.setRange(Database::Range {0, 100}); // don't flood too much?
pendingFeedbacks = StarredTrack::find(session, params);
}
LOG(DEBUG) << "Queing " << pendingFeedbacks.results.size() << " pending '" << (feedbackType == FeedbackType::Love ? "love" : "erase") << "' feedbacks";
for (const StarredTrackId starredTrackId : pendingFeedbacks.results)
enqueFeedback(feedbackType, starredTrackId);
} };
processPendingFeedbacks(SyncState::PendingAdd, FeedbackType::Love);
processPendingFeedbacks(SyncState::PendingRemove, FeedbackType::Erase);
}
FeedbacksSynchronizer::UserContext& FeedbacksSynchronizer::getUserContext(Database::UserId userId)
{
assert(_strand.running_in_this_thread());
auto itContext{ _userContexts.find(userId) };
if (itContext == std::cend(_userContexts))
{
std::tie(itContext, std::ignore) = _userContexts.emplace(userId, userId);
}
return itContext->second;
}
bool FeedbacksSynchronizer::isSyncing() const
{
return std::any_of(std::cbegin(_userContexts), std::cend(_userContexts), [](const auto& contextEntry)
{
return contextEntry.second.syncing;
});
}
void FeedbacksSynchronizer::scheduleSync(std::chrono::seconds fromNow)
{
if (_syncFeedbacksPeriod.count() == 0 || _maxSyncFeedbackCount == 0)
return;
LOG(DEBUG) << "Scheduled sync in " << fromNow.count() << " seconds...";
_syncTimer.expires_after(fromNow);
_syncTimer.async_wait(boost::asio::bind_executor(_strand, [this](const boost::system::error_code& ec)
{
if (ec == boost::asio::error::operation_aborted)
{
LOG(DEBUG) << "getFeedbacks aborted";
return;
}
else if (ec)
{
throw Exception{ "GetFeedbacks timer failure: " + std::string {ec.message()} };
}
startSync();
}));
}
void FeedbacksSynchronizer::startSync()
{
LOG(DEBUG) << "Starting sync!";
assert(!isSyncing());
assert(_strand.running_in_this_thread());
enquePendingFeedbacks();
Database::RangeResults<Database::UserId> userIds;
{
Database::Session& session{ _db.getTLSSession() };
auto transaction{ session.createSharedTransaction() };
userIds = Database::User::find(_db.getTLSSession(), Database::User::FindParameters{}.setFeedbackBackend(Database::FeedbackBackend::ListenBrainz));
}
for (const Database::UserId userId : userIds.results)
startSync(getUserContext(userId));
if (!isSyncing())
scheduleSync(_syncFeedbacksPeriod);
}
void FeedbacksSynchronizer::startSync(UserContext& context)
{
context.syncing = true;
context.listenBrainzUserName = "";
context.fetchedFeedbackCount = 0;
context.matchedFeedbackCount = 0;
context.importedFeedbackCount = 0;
enqueValidateToken(context);
}
void FeedbacksSynchronizer::onSyncEnded(UserContext& context)
{
_strand.dispatch([this, &context]
{
LOG(INFO) << "Feedback sync done for user '" << context.listenBrainzUserName << "', fetched: " << context.fetchedFeedbackCount << ", matched: " << context.matchedFeedbackCount << ", imported: " << context.importedFeedbackCount;
context.syncing = false;
if (!isSyncing())
scheduleSync(_syncFeedbacksPeriod);
});
}
void FeedbacksSynchronizer::enqueValidateToken(UserContext& context)
{
assert(context.listenBrainzUserName.empty());
const std::optional<UUID> listenBrainzToken{ ListenBrainz::Utils::getListenBrainzToken(_db.getTLSSession(), context.userId) };
if (!listenBrainzToken)
{
onSyncEnded(context);
return;
}
Http::ClientGETRequestParameters request;
request.priority = Http::ClientRequestParameters::Priority::Low;
request.relativeUrl = "/1/validate-token";
request.headers = { {"Authorization", "Token " + std::string {listenBrainzToken->getAsString()}} };
request.onSuccessFunc = [this, &context](std::string_view msgBody)
{
context.listenBrainzUserName = ListenBrainz::Utils::parseValidateToken(msgBody);
if (context.listenBrainzUserName.empty())
{
onSyncEnded(context);
return;
}
enqueGetFeedbackCount(context);
};
request.onFailureFunc = [this, &context]
{
onSyncEnded(context);
};
_client.sendGETRequest(std::move(request));
}
void FeedbacksSynchronizer::enqueGetFeedbackCount(UserContext& context)
{
assert(!context.listenBrainzUserName.empty());
Http::ClientGETRequestParameters request;
request.relativeUrl = "/1/feedback/user/" + std::string{ context.listenBrainzUserName } + "/get-feedback?score=1&count=0";
request.priority = Http::ClientRequestParameters::Priority::Low;
request.onSuccessFunc = [this, &context](std::string_view msgBody)
{
std::string msgBodyCopy{ msgBody };
_strand.dispatch([this, msgBodyCopy, &context]
{
LOG(DEBUG) << "Current feedback count = " << (context.feedbackCount ? *context.feedbackCount : 0) << " for user '" << context.listenBrainzUserName << "'";
const auto totalFeedbackCount = parseTotalFeedbackCount(msgBodyCopy);
if (totalFeedbackCount)
LOG(DEBUG) << "Feedback count for listenbrainz user '" << context.listenBrainzUserName << "' = " << *totalFeedbackCount;
bool needSync{ totalFeedbackCount && (!context.feedbackCount || *context.feedbackCount != *totalFeedbackCount) };
context.feedbackCount = totalFeedbackCount;
if (needSync)
enqueGetFeedbacks(context);
else
onSyncEnded(context);
});
};
request.onFailureFunc = [this, &context]
{
onSyncEnded(context);
};
_client.sendGETRequest(std::move(request));
}
void FeedbacksSynchronizer::enqueGetFeedbacks(UserContext& context)
{
assert(!context.listenBrainzUserName.empty());
Http::ClientGETRequestParameters request;
request.relativeUrl = "/1/feedback/user/" + context.listenBrainzUserName + "/get-feedback?offset=" + std::to_string(context.fetchedFeedbackCount);
request.priority = Http::ClientRequestParameters::Priority::Low;
request.onSuccessFunc = [this, &context](std::string_view msgBody)
{
std::string msgBodyCopy{ msgBody };
_strand.dispatch([this, msgBodyCopy, &context]
{
const std::size_t fetchedFeedbackCount{ processGetFeedbacks(msgBodyCopy, context) };
if (fetchedFeedbackCount == 0 // no more thing available on server
|| context.fetchedFeedbackCount >= context.feedbackCount // we may miss something, but we will get it next time
|| context.fetchedFeedbackCount >= _maxSyncFeedbackCount)
{
onSyncEnded(context);
}
else
{
enqueGetFeedbacks(context);
}
});
};
request.onFailureFunc = [=, &context]
{
onSyncEnded(context);
};
_client.sendGETRequest(std::move(request));
}
std::size_t FeedbacksSynchronizer::processGetFeedbacks(std::string_view msgBody, UserContext& context)
{
const FeedbacksParser::Result parseResult{ FeedbacksParser::parse(msgBody) };
LOG(DEBUG) << "Parsed " << parseResult.feedbackCount << " feedbacks, found " << parseResult.feedbacks.size() << " usable entries";
context.fetchedFeedbackCount += parseResult.feedbackCount;
for (const Feedback& feedback : parseResult.feedbacks)
{
tryImportFeedback(feedback, context);
}
return parseResult.feedbackCount;
}
void FeedbacksSynchronizer::tryImportFeedback(const Feedback& feedback, UserContext& context)
{
using namespace Database;
Session& session{ _db.getTLSSession() };
bool needImport{};
TrackId trackId;
{
auto transaction{ session.createSharedTransaction() };
const std::vector<Track::pointer> tracks{ Track::findByRecordingMBID(session, feedback.recordingMBID) };
if (tracks.size() > 1)
{
LOG(DEBUG) << "Too many matches for feedback '" << feedback << "': duplicate recording MBIDs found";
return;
}
else if (tracks.empty())
{
LOG(DEBUG) << "Cannot match feedback '" << feedback << "': no track found for this recording MBID";
return;
}
trackId = tracks.front()->getId();
const StarredTrack::pointer starredTrack{ StarredTrack::find(session, trackId, context.userId, Database::FeedbackBackend::ListenBrainz) };
needImport = !starredTrack;
// don't update starred date time
// no need to update state if it was found as not synchronized
// pending remove => will be removed later
// pending add => will be resent later
}
if (needImport)
{
LOG(DEBUG) << "Importing feedback '" << feedback << "'";
auto transaction{ session.createUniqueTransaction() };
const Track::pointer track{ Track::find(session, trackId) };
if (!track)
return;
const User::pointer user{ User::find(session, context.userId) };
if (!user)
return;
StarredTrack::pointer starredTrack{ session.create<StarredTrack>(track, user, Database::FeedbackBackend::ListenBrainz) };
starredTrack.modify()->setSyncState(SyncState::Synchronized);
starredTrack.modify()->setDateTime(feedback.created);
context.importedFeedbackCount++;
}
else
{
LOG(DEBUG) << "No need to import feedback '" << feedback << "', already imported";
context.matchedFeedbackCount++;
}
}
} // namespace Feedback::ListenBrainz
@@ -0,0 +1,102 @@
/*
* Copyright (C) 2021 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <optional>
#include <unordered_map>
#include <boost/asio/io_context.hpp>
#include <boost/asio/io_context_strand.hpp>
#include <boost/asio/steady_timer.hpp>
#include "services/database/Types.hpp"
#include "services/database/StarredTrackId.hpp"
#include "services/database/UserId.hpp"
#include "FeedbackTypes.hpp"
namespace Database
{
class Db;
}
namespace Http
{
class IClient;
}
namespace Feedback::ListenBrainz
{
class FeedbacksSynchronizer
{
public:
FeedbacksSynchronizer(boost::asio::io_context& ioContext, Database::Db& db, Http::IClient& client);
void enqueFeedback(FeedbackType type, Database::StarredTrackId starredTrackId);
private:
void onFeedbackSent(FeedbackType type, Database::StarredTrackId starredTrackId);
void enquePendingFeedbacks();
struct UserContext
{
UserContext(Database::UserId id) : userId{ id } {}
UserContext(const UserContext&) = delete;
UserContext& operator=(const UserContext&) = delete;
const Database::UserId userId;
bool syncing{};
std::optional<std::size_t> feedbackCount{};
// resetted at each sync
std::string listenBrainzUserName; // need to be resolved first
std::size_t currentOffset{};
std::size_t fetchedFeedbackCount{};
std::size_t matchedFeedbackCount{};
std::size_t importedFeedbackCount{};
};
UserContext& getUserContext(Database::UserId userId);
bool isSyncing() const;
void scheduleSync(std::chrono::seconds fromNow);
void startSync();
void startSync(UserContext& context);
void onSyncEnded(UserContext& context);
void enqueValidateToken(UserContext& context);
void enqueGetFeedbackCount(UserContext& context);
void enqueGetFeedbacks(UserContext& context);
std::size_t processGetFeedbacks(std::string_view body, UserContext& context);
void tryImportFeedback(const Feedback& feedback, UserContext& context);
boost::asio::io_context& _ioContext;
boost::asio::io_context::strand _strand{ _ioContext };
Database::Db& _db;
boost::asio::steady_timer _syncTimer{ _ioContext };
Http::IClient& _client;
std::unordered_map<Database::UserId, UserContext> _userContexts;
const std::size_t _maxSyncFeedbackCount;
const std::chrono::hours _syncFeedbacksPeriod;
};
} // Feedback::ListenBrainz
@@ -0,0 +1,103 @@
/*
* Copyright (C) 2023 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "ListenBrainzBackend.hpp"
#include "services/database/Db.hpp"
#include "services/database/Session.hpp"
#include "services/database/StarredArtist.hpp"
#include "services/database/StarredRelease.hpp"
#include "services/database/Track.hpp"
#include "utils/IConfig.hpp"
#include "utils/http/IClient.hpp"
#include "utils/Logger.hpp"
#include "utils/Service.hpp"
#include "Utils.hpp"
namespace Feedback::ListenBrainz
{
namespace details
{
template <typename StarredObjType>
void onStarred(Database::Session& session, typename StarredObjType::IdType id)
{
auto transaction{ session.createUniqueTransaction() };
if (auto starredObj{ StarredObjType::find(session, id) })
{
// maybe in the future this will be supported by ListenBrainz so set it to PendingAdd for all types
starredObj.modify()->setSyncState(Database::SyncState::PendingAdd);
}
}
template <typename StarredObjType>
void onUnstarred(Database::Session& session, typename StarredObjType::IdType id)
{
auto transaction{ session.createUniqueTransaction() };
if (auto starredObj{ StarredObjType::find(session, id) })
starredObj.remove();
}
}
ListenBrainzBackend::ListenBrainzBackend(boost::asio::io_context& ioContext, Database::Db& db)
: _ioContext{ ioContext }
, _db{ db }
, _baseAPIUrl{ Service<IConfig>::get()->getString("listenbrainz-api-base-url", "https://api.listenbrainz.org") }
, _client{ Http::createClient(_ioContext, _baseAPIUrl) }
, _feedbacksSynchronizer{ _ioContext, db, *_client }
{
LOG(INFO) << "Starting ListenBrainz feedback backend... API endpoint = '" << _baseAPIUrl << "'";
}
ListenBrainzBackend::~ListenBrainzBackend()
{
LOG(INFO) << "Stopped ListenBrainz feedback backend!";
}
void ListenBrainzBackend::onStarred(Database::StarredArtistId starredArtistId)
{
details::onStarred<Database::StarredArtist>(_db.getTLSSession(), starredArtistId);
}
void ListenBrainzBackend::onUnstarred(Database::StarredArtistId starredArtistId)
{
details::onUnstarred<Database::StarredArtist>(_db.getTLSSession(), starredArtistId);
}
void ListenBrainzBackend::onStarred(Database::StarredReleaseId starredReleaseId)
{
details::onStarred<Database::StarredRelease>(_db.getTLSSession(), starredReleaseId);
}
void ListenBrainzBackend::onUnstarred(Database::StarredReleaseId starredReleaseId)
{
details::onUnstarred<Database::StarredRelease>(_db.getTLSSession(), starredReleaseId);
}
void ListenBrainzBackend::onStarred(Database::StarredTrackId starredTrackId)
{
_feedbacksSynchronizer.enqueFeedback(FeedbackType::Love, starredTrackId);
}
void ListenBrainzBackend::onUnstarred(Database::StarredTrackId starredtrackId)
{
_feedbacksSynchronizer.enqueFeedback(FeedbackType::Erase, starredtrackId);
}
} // namespace Scrobbling::ListenBrainz
@@ -0,0 +1,60 @@
/*
* Copyright (C) 2023 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <memory>
#include <string>
#include <boost/asio/io_context.hpp>
#include "IFeedbackBackend.hpp"
#include "FeedbacksSynchronizer.hpp"
namespace Database
{
class Db;
}
namespace Feedback::ListenBrainz
{
class ListenBrainzBackend final : public IFeedbackBackend
{
public:
ListenBrainzBackend(boost::asio::io_context& ioContext, Database::Db& db);
~ListenBrainzBackend() override;
private:
ListenBrainzBackend(const ListenBrainzBackend&) = delete;
ListenBrainzBackend& operator=(const ListenBrainzBackend&) = delete;
void onStarred(Database::StarredArtistId starredArtistId) override;
void onUnstarred(Database::StarredArtistId starredArtistId) override;
void onStarred(Database::StarredReleaseId starredReleaseId) override;
void onUnstarred(Database::StarredReleaseId starredReleaseId) override;
void onStarred(Database::StarredTrackId starredTrackId) override;
void onUnstarred(Database::StarredTrackId starredTrackId) override;
boost::asio::io_context& _ioContext;
Database::Db& _db;
std::string _baseAPIUrl;
std::unique_ptr<Http::IClient> _client;
FeedbacksSynchronizer _feedbacksSynchronizer;
};
} // Feedback::ListenBrainz
@@ -0,0 +1,62 @@
/*
* Copyright (C) 2021 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Utils.hpp"
#include <Wt/Json/Object.h>
#include <Wt/Json/Parser.h>
#include "services/database/Session.hpp"
#include "services/database/User.hpp"
namespace Feedback::ListenBrainz::Utils
{
std::optional<UUID> getListenBrainzToken(Database::Session& session, Database::UserId userId)
{
auto transaction{ session.createSharedTransaction() };
const Database::User::pointer user{ Database::User::find(session, userId) };
if (!user)
return std::nullopt;
return user->getListenBrainzToken();
}
std::string parseValidateToken(std::string_view msgBody)
{
std::string listenBrainzUserName;
Wt::Json::ParseError error;
Wt::Json::Object root;
if (!Wt::Json::parse(std::string{ msgBody }, root, error))
{
LOG(ERROR) << "Cannot parse 'validate-token' result: " << error.what();
return listenBrainzUserName;
}
if (!root.get("valid").orIfNull(false))
{
LOG(INFO) << "Invalid listenbrainz user";
return listenBrainzUserName;
}
listenBrainzUserName = root.get("user_name").orIfNull("");
return listenBrainzUserName;
}
}
@@ -0,0 +1,37 @@
/*
* Copyright (C) 2023 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "services/database/UserId.hpp"
#include "utils/Logger.hpp"
#include "utils/UUID.hpp"
#define LOG(sev) LMS_LOG(FEEDBACK, sev) << "[listenbrainz] "
namespace Database
{
class Session;
}
namespace Feedback::ListenBrainz::Utils
{
std::optional<UUID> getListenBrainzToken(Database::Session& session, Database::UserId userId);
std::string parseValidateToken(std::string_view msgBody);
}
@@ -0,0 +1,31 @@
/*
* Copyright (C) 2019 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "utils/Exception.hpp"
namespace Feedback
{
class Exception : public LmsException
{
public:
using LmsException::LmsException;
};
}
@@ -0,0 +1,73 @@
/*
* Copyright (C) 2023 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <memory>
#include <optional>
#include <boost/asio/io_service.hpp>
#include <Wt/WDateTime.h>
#include "services/database/Types.hpp"
#include "services/database/ArtistId.hpp"
#include "services/database/ClusterId.hpp"
#include "services/database/ReleaseId.hpp"
#include "services/database/TrackId.hpp"
#include "services/database/UserId.hpp"
#include "services/database/Types.hpp"
namespace Database
{
class Db;
}
namespace Feedback
{
class IFeedbackService
{
public:
virtual ~IFeedbackService() = default;
using ArtistContainer = Database::RangeResults<Database::ArtistId>;
using ReleaseContainer = Database::RangeResults<Database::ReleaseId>;
using TrackContainer = Database::RangeResults<Database::TrackId>;
virtual void star(Database::UserId userId, Database::ArtistId artistId) = 0;
virtual void unstar(Database::UserId userId, Database::ArtistId artistId) = 0;
virtual bool isStarred(Database::UserId userId, Database::ArtistId artistId) = 0;
virtual Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::ArtistId artistId) = 0;
virtual ArtistContainer getStarredArtists(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, std::optional<Database::TrackArtistLinkType> linkType, Database::ArtistSortMethod sortMethod, Database::Range range) = 0;
virtual void star(Database::UserId userId, Database::ReleaseId releaseId) = 0;
virtual void unstar(Database::UserId userId, Database::ReleaseId releaseId) = 0;
virtual bool isStarred(Database::UserId userId, Database::ReleaseId artistId) = 0;
virtual Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::ReleaseId artistId) = 0;
virtual ReleaseContainer getStarredReleases(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) = 0;
virtual void star(Database::UserId userId, Database::TrackId trackId) = 0;
virtual void unstar(Database::UserId userId, Database::TrackId trackId) = 0;
virtual bool isStarred(Database::UserId userId, Database::TrackId artistId) = 0;
virtual Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::TrackId artistId) = 0;
virtual TrackContainer getStarredTracks(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) = 0;
};
std::unique_ptr<IFeedbackService> createFeedbackService(boost::asio::io_service& ioService, Database::Db& db);
} // ns Feedback
@@ -30,89 +30,86 @@
namespace Recommendation namespace Recommendation
{ {
using namespace Database; using namespace Database;
std::unique_ptr<IPlaylistGeneratorService> std::unique_ptr<IPlaylistGeneratorService> createPlaylistGeneratorService(Db& db, Recommendation::IRecommendationService& recommendationService)
createPlaylistGeneratorService(Db& db, Recommendation::IRecommendationService& recommendationService) {
{ return std::make_unique<PlaylistGeneratorService>(db, recommendationService);
return std::make_unique<PlaylistGeneratorService>(db, recommendationService); }
}
PlaylistGeneratorService::PlaylistGeneratorService(Db& db, Recommendation::IRecommendationService& recommendationService) PlaylistGeneratorService::PlaylistGeneratorService(Db& db, Recommendation::IRecommendationService& recommendationService)
: _db {db} : _db{ db }
, _recommendationService {recommendationService} , _recommendationService{ recommendationService }
{ {
_constraints.push_back(std::make_unique<PlaylistGeneratorConstraint::ConsecutiveArtists>(_db)); _constraints.push_back(std::make_unique<PlaylistGeneratorConstraint::ConsecutiveArtists>(_db));
_constraints.push_back(std::make_unique<PlaylistGeneratorConstraint::ConsecutiveReleases>(_db)); _constraints.push_back(std::make_unique<PlaylistGeneratorConstraint::ConsecutiveReleases>(_db));
_constraints.push_back(std::make_unique<PlaylistGeneratorConstraint::DuplicateTracks>()); _constraints.push_back(std::make_unique<PlaylistGeneratorConstraint::DuplicateTracks>());
} }
std::vector<TrackId> std::vector<TrackId> PlaylistGeneratorService::extendPlaylist(TrackListId tracklistId, std::size_t maxCount) const
PlaylistGeneratorService::extendPlaylist(TrackListId tracklistId, std::size_t maxCount) const {
{ LMS_LOG(RECOMMENDATION, DEBUG) << "Requested to extend playlist by " << maxCount << " similar tracks";
LMS_LOG(RECOMMENDATION, DEBUG) << "Requested to extend playlist by " << maxCount << " similar tracks";
// supposed to be ordered from most similar to least similar // supposed to be ordered from most similar to least similar
std::vector<TrackId> similarTracks {_recommendationService.findSimilarTracks(tracklistId, maxCount * 2)}; // ask for more tracks than we need as it will be easier to respect constraints std::vector<TrackId> similarTracks{ _recommendationService.findSimilarTracks(tracklistId, maxCount * 2) }; // ask for more tracks than we need as it will be easier to respect constraints
const std::vector<TrackId> startingTracks {getTracksFromTrackList(tracklistId)}; const std::vector<TrackId> startingTracks{ getTracksFromTrackList(tracklistId) };
std::vector<TrackId> finalResult = startingTracks; std::vector<TrackId> finalResult = startingTracks;
finalResult.reserve(startingTracks.size() + maxCount); finalResult.reserve(startingTracks.size() + maxCount);
std::vector<float> scores; std::vector<float> scores;
for (std::size_t i {}; i < maxCount; ++i) for (std::size_t i{}; i < maxCount; ++i)
{ {
if (similarTracks.empty()) if (similarTracks.empty())
break; break;
scores.resize(similarTracks.size(), {}); scores.resize(similarTracks.size(), {});
// select the similar track that has the best score // select the similar track that has the best score
for (std::size_t trackIndex {}; trackIndex < similarTracks.size(); ++trackIndex) for (std::size_t trackIndex{}; trackIndex < similarTracks.size(); ++trackIndex)
{ {
using namespace Database::Debug; using namespace Database::Debug;
finalResult.push_back(similarTracks[trackIndex]); finalResult.push_back(similarTracks[trackIndex]);
scores[trackIndex] = 0; scores[trackIndex] = 0;
for (const auto& constraint : _constraints) for (const auto& constraint : _constraints)
scores[trackIndex] += constraint->computeScore(finalResult, finalResult.size() - 1); scores[trackIndex] += constraint->computeScore(finalResult, finalResult.size() - 1);
finalResult.pop_back(); finalResult.pop_back();
// early exit if we consider we found a track with no constraint violation (since similarTracks sorted from most to least similar) // early exit if we consider we found a track with no constraint violation (since similarTracks sorted from most to least similar)
if (scores[trackIndex] < 0.01) if (scores[trackIndex] < 0.01)
break; break;
} }
// get the best score // get the best score
const std::size_t bestScoreIndex {static_cast<std::size_t>(std::distance(std::cbegin(scores), std::min_element(std::cbegin(scores), std::cend(scores))))}; const std::size_t bestScoreIndex{ static_cast<std::size_t>(std::distance(std::cbegin(scores), std::min_element(std::cbegin(scores), std::cend(scores)))) };
finalResult.push_back(similarTracks[bestScoreIndex]); finalResult.push_back(similarTracks[bestScoreIndex]);
similarTracks.erase(std::begin(similarTracks) + bestScoreIndex); similarTracks.erase(std::begin(similarTracks) + bestScoreIndex);
} }
// for now, just get some more similar tracks // for now, just get some more similar tracks
return std::vector(std::cbegin(finalResult) + startingTracks.size(), std::cend(finalResult)); return std::vector(std::cbegin(finalResult) + startingTracks.size(), std::cend(finalResult));
} }
TrackContainer TrackContainer PlaylistGeneratorService::getTracksFromTrackList(Database::TrackListId tracklistId) const
PlaylistGeneratorService::getTracksFromTrackList(Database::TrackListId tracklistId) const {
{ TrackContainer tracks;
TrackContainer tracks;
Session& dbSession {_db.getTLSSession()}; Session& dbSession{ _db.getTLSSession() };
auto transaction {dbSession.createSharedTransaction()}; auto transaction{ dbSession.createSharedTransaction() };
Track::FindParameters params; Track::FindParameters params;
params.setTrackList(tracklistId); params.setTrackList(tracklistId);
params.setSortMethod(TrackSortMethod::TrackList); params.setSortMethod(TrackSortMethod::TrackList);
params.setDistinct(false); params.setDistinct(false);
for (const TrackId trackId : Track::find(dbSession, params).results) for (const TrackId trackId : Track::findIds(dbSession, params).results)
tracks.push_back(trackId); tracks.push_back(trackId);
return tracks; return tracks;
} }
} }
@@ -90,6 +90,8 @@ namespace Recommendation
if (itEngine == std::cend(_engines)) if (itEngine == std::cend(_engines))
continue; continue;
LMS_LOG(RECOMMENDATION, DEBUG) << "Trying engine '" << engineTypeToString(engineType) << "' to get similar tracks";
const IEngine& engine {*itEngine->second}; const IEngine& engine {*itEngine->second};
res = engine.findSimilarTracks(trackIds, maxCount); res = engine.findSimilarTracks(trackIds, maxCount);
if (!res.empty()) if (!res.empty())
@@ -114,6 +116,8 @@ namespace Recommendation
if (itEngine == std::cend(_engines)) if (itEngine == std::cend(_engines))
continue; continue;
LMS_LOG(RECOMMENDATION, DEBUG) << "Trying engine '" << engineTypeToString(engineType) << "' to get similar releases";
const IEngine& engine {*itEngine->second}; const IEngine& engine {*itEngine->second};
res = engine.getSimilarReleases(releaseId, maxCount); res = engine.getSimilarReleases(releaseId, maxCount);
if (!res.empty()) if (!res.empty())
@@ -140,7 +144,7 @@ namespace Recommendation
if (itEngine == std::cend(_engines)) if (itEngine == std::cend(_engines))
continue; continue;
LMS_LOG(RECOMMENDATION, DEBUG) << "Trying engine '" << engineTypeToString(engineType) << "'"; LMS_LOG(RECOMMENDATION, DEBUG) << "Trying engine '" << engineTypeToString(engineType) << "' to get similar artists";
const IEngine& engine {*itEngine->second}; const IEngine& engine {*itEngine->second};
res = engine.getSimilarArtists(artistId, linkTypes, maxCount); res = engine.getSimilarArtists(artistId, linkTypes, maxCount);
@@ -155,12 +159,12 @@ namespace Recommendation
} }
static static
Database::ScanSettings::RecommendationEngineType Database::ScanSettings::SimilarityEngineType
getRecommendationEngineType(Database::Session& session) getSimilarityEngineType(Database::Session& session)
{ {
auto transaction {session.createSharedTransaction()}; auto transaction {session.createSharedTransaction()};
return Database::ScanSettings::get(session)->getRecommendationEngineType(); return Database::ScanSettings::get(session)->getSimilarityEngineType();
} }
void void
@@ -180,20 +184,24 @@ namespace Recommendation
_engines.clear(); _engines.clear();
} }
switch (getRecommendationEngineType(_db.getTLSSession())) switch (getSimilarityEngineType(_db.getTLSSession()))
{ {
case ScanSettings::RecommendationEngineType::Clusters: case ScanSettings::SimilarityEngineType::Clusters:
_enginePriorities = {EngineType::Clusters}; _enginePriorities = {EngineType::Clusters};
enginesToLoad.try_emplace(EngineType::Clusters, createClustersEngine(_db)); enginesToLoad.try_emplace(EngineType::Clusters, createClustersEngine(_db));
break; break;
case ScanSettings::RecommendationEngineType::Features: case ScanSettings::SimilarityEngineType::Features:
_enginePriorities = {EngineType::Features, EngineType::Clusters}; _enginePriorities = {EngineType::Features, EngineType::Clusters};
// not same order since clusters is faster to load // not same order since clusters is faster to load
enginesToLoad.try_emplace(EngineType::Clusters, createClustersEngine(_db)); enginesToLoad.try_emplace(EngineType::Clusters, createClustersEngine(_db));
enginesToLoad.try_emplace(EngineType::Features, createFeaturesEngine(_db)); enginesToLoad.try_emplace(EngineType::Features, createFeaturesEngine(_db));
break; break;
case ScanSettings::SimilarityEngineType::None:
_enginePriorities.clear();
break;
} }
assert(_pendingEngines.empty()); assert(_pendingEngines.empty());
@@ -44,7 +44,7 @@ namespace Recommendation {
Session& dbSession{ _db.getTLSSession() }; Session& dbSession{ _db.getTLSSession() };
auto transaction{ dbSession.createSharedTransaction() }; auto transaction{ dbSession.createSharedTransaction() };
const auto similarTrackIds{ Track::findSimilarTracks(dbSession, trackIds, Range {0, maxCount}) }; const auto similarTrackIds{ Track::findSimilarTrackIds(dbSession, trackIds, Range {0, maxCount}) };
return std::move(similarTrackIds.results); return std::move(similarTrackIds.results);
} }
@@ -104,7 +104,7 @@ namespace Recommendation {
if (!artist) if (!artist)
return {}; return {};
const auto similarArtistIds{ artist->findSimilarArtists(artistLinkTypes, Range {0, maxCount}) }; const auto similarArtistIds{ artist->findSimilarArtistIds(artistLinkTypes, Range {0, maxCount}) };
return std::move(similarArtistIds.results); return std::move(similarArtistIds.results);
} }
@@ -37,7 +37,7 @@ namespace Scanner
Session& session {_db.getTLSSession()}; Session& session {_db.getTLSSession()};
auto transaction {session.createSharedTransaction()}; auto transaction {session.createSharedTransaction()};
const RangeResults<TrackId> tracks = Track::findTrackMBIDDuplicates(session, Range {}); const RangeResults<TrackId> tracks = Track::findIdsTrackMBIDDuplicates(session, Range {});
for (const TrackId trackId : tracks.results) for (const TrackId trackId : tracks.results)
{ {
const Track::pointer track {Track::find(session, trackId)}; const Track::pointer track {Track::find(session, trackId)};
@@ -137,7 +137,7 @@ namespace Scanner
Session& session {_db.getTLSSession()}; Session& session {_db.getTLSSession()};
auto transaction {session.createUniqueTransaction()}; auto transaction {session.createUniqueTransaction()};
auto artistIds {Artist::findAllOrphans(session, Range {})}; auto artistIds {Artist::findOrphanIds(session, Range {})};
for (const ArtistId artistId : artistIds.results) for (const ArtistId artistId : artistIds.results)
{ {
Artist::pointer artist {Artist::find(session, artistId)}; Artist::pointer artist {Artist::find(session, artistId)};
@@ -153,10 +153,11 @@ namespace Scanner
LMS_LOG(DBUPDATER, DEBUG) << "Checking orphan releases..."; LMS_LOG(DBUPDATER, DEBUG) << "Checking orphan releases...";
// TODO, by batch
Session& session {_db.getTLSSession()}; Session& session {_db.getTLSSession()};
auto transaction {session.createUniqueTransaction()}; auto transaction {session.createUniqueTransaction()};
auto releases {Release::findOrphans(session, Range {})}; auto releases {Release::findOrphanIds(session, Range {})};
for (const ReleaseId releaseId : releases.results) for (const ReleaseId releaseId : releases.results)
{ {
Release::pointer release {Release::find(session, releaseId)}; Release::pointer release {Release::find(session, releaseId)};
@@ -307,6 +307,10 @@ namespace Scanner
context.currentStepStats.processedElems++; context.currentStepStats.processedElems++;
_progressCallback(context.currentStepStats); _progressCallback(context.currentStepStats);
// optimize the database during scan (if we import a very large database, it may be too late to do it once at end)
if ((context.stats.nbChanges() % 5'000) == 0)
_db.getTLSSession().optimize();
} }
return true; return true;
@@ -381,7 +381,7 @@ ScannerService::readSettings()
std::transform(std::cbegin(fileExtensions), std::end(fileExtensions), std::back_inserter(newSettings.supportedExtensions), std::transform(std::cbegin(fileExtensions), std::end(fileExtensions), std::back_inserter(newSettings.supportedExtensions),
[](const std::filesystem::path& extension) { return std::filesystem::path{ StringUtils::stringToLower(extension.string()) }; }); [](const std::filesystem::path& extension) { return std::filesystem::path{ StringUtils::stringToLower(extension.string()) }; });
} }
newSettings.recommendationServiceType = scanSettings->getRecommendationEngineType(); newSettings.similarityServiceType = scanSettings->getSimilarityEngineType();
newSettings.mediaDirectory = scanSettings->getMediaDirectory(); newSettings.mediaDirectory = scanSettings->getMediaDirectory();
const auto clusterTypes = scanSettings->getClusterTypes(); const auto clusterTypes = scanSettings->getClusterTypes();
@@ -34,7 +34,7 @@ namespace Scanner
Wt::WTime startTime; Wt::WTime startTime;
Database::ScanSettings::UpdatePeriod updatePeriod {Database::ScanSettings::UpdatePeriod::Never}; Database::ScanSettings::UpdatePeriod updatePeriod {Database::ScanSettings::UpdatePeriod::Never};
std::vector<std::filesystem::path> supportedExtensions; std::vector<std::filesystem::path> supportedExtensions;
Database::ScanSettings::RecommendationEngineType recommendationServiceType; Database::ScanSettings::SimilarityEngineType similarityServiceType;
std::filesystem::path mediaDirectory; std::filesystem::path mediaDirectory;
bool skipDuplicateMBID {}; bool skipDuplicateMBID {};
std::set<std::string> clusterTypeNames; std::set<std::string> clusterTypeNames;
@@ -45,7 +45,7 @@ namespace Scanner
&& startTime == rhs.startTime && startTime == rhs.startTime
&& updatePeriod == rhs.updatePeriod && updatePeriod == rhs.updatePeriod
&& supportedExtensions == rhs.supportedExtensions && supportedExtensions == rhs.supportedExtensions
&& recommendationServiceType == rhs.recommendationServiceType && similarityServiceType == rhs.similarityServiceType
&& mediaDirectory == rhs.mediaDirectory && mediaDirectory == rhs.mediaDirectory
&& skipDuplicateMBID == rhs.skipDuplicateMBID && skipDuplicateMBID == rhs.skipDuplicateMBID
&& clusterTypeNames == rhs.clusterTypeNames; && clusterTypeNames == rhs.clusterTypeNames;
+2 -5
View File
@@ -1,10 +1,7 @@
add_library(lmsscrobbling SHARED add_library(lmsscrobbling SHARED
impl/internal/InternalScrobbler.cpp impl/internal/InternalBackend.cpp
impl/listenbrainz/FeedbacksParser.cpp impl/listenbrainz/ListenBrainzBackend.cpp
impl/listenbrainz/FeedbacksSynchronizer.cpp
impl/listenbrainz/FeedbackTypes.cpp
impl/listenbrainz/ListenBrainzScrobbler.cpp
impl/listenbrainz/ListenTypes.cpp impl/listenbrainz/ListenTypes.cpp
impl/listenbrainz/ListensParser.cpp impl/listenbrainz/ListensParser.cpp
impl/listenbrainz/ListensSynchronizer.cpp impl/listenbrainz/ListensSynchronizer.cpp
@@ -0,0 +1,48 @@
/*
* Copyright (C) 2021 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <chrono>
#include <memory>
#include <optional>
#include "services/scrobbling/Listen.hpp"
namespace Database
{
class Session;
class TrackList;
class User;
}
namespace Scrobbling
{
class IScrobblingBackend
{
public:
virtual ~IScrobblingBackend() = default;
// Listens
virtual void listenStarted(const Listen& listen) = 0;
virtual void listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration) = 0;
virtual void addTimedListen(const TimedListen& listen) = 0;
};
} // ns Scrobbling
@@ -18,22 +18,18 @@
*/ */
#include "ScrobblingService.hpp" #include "ScrobblingService.hpp"
#include "ScrobblingService.impl.hpp"
#include "services/database/Artist.hpp" #include "services/database/Artist.hpp"
#include "services/database/Db.hpp" #include "services/database/Db.hpp"
#include "services/database/Listen.hpp" #include "services/database/Listen.hpp"
#include "services/database/Release.hpp" #include "services/database/Release.hpp"
#include "services/database/Session.hpp" #include "services/database/Session.hpp"
#include "services/database/StarredArtist.hpp"
#include "services/database/StarredRelease.hpp"
#include "services/database/StarredTrack.hpp"
#include "services/database/Track.hpp" #include "services/database/Track.hpp"
#include "services/database/User.hpp" #include "services/database/User.hpp"
#include "utils/Logger.hpp" #include "utils/Logger.hpp"
#include "internal/InternalScrobbler.hpp" #include "internal/InternalBackend.hpp"
#include "listenbrainz/ListenBrainzScrobbler.hpp" #include "listenbrainz/ListenBrainzBackend.hpp"
namespace Scrobbling namespace Scrobbling
{ {
@@ -48,8 +44,8 @@ namespace Scrobbling
: _db{ db } : _db{ db }
{ {
LMS_LOG(SCROBBLING, INFO) << "Starting service..."; LMS_LOG(SCROBBLING, INFO) << "Starting service...";
_scrobblers.emplace(Scrobbler::Internal, std::make_unique<InternalScrobbler>(_db)); _scrobblingBackends.emplace(ScrobblingBackend::Internal, std::make_unique<InternalBackend>(_db));
_scrobblers.emplace(Scrobbler::ListenBrainz, std::make_unique<ListenBrainz::Scrobbler>(ioContext, _db)); _scrobblingBackends.emplace(ScrobblingBackend::ListenBrainz, std::make_unique<ListenBrainz::ListenBrainzBackend>(ioContext, _db));
LMS_LOG(SCROBBLING, INFO) << "Service started!"; LMS_LOG(SCROBBLING, INFO) << "Service started!";
} }
@@ -60,46 +56,46 @@ namespace Scrobbling
void ScrobblingService::listenStarted(const Listen& listen) void ScrobblingService::listenStarted(const Listen& listen)
{ {
if (std::optional<Scrobbler> scrobbler{ getUserScrobbler(listen.userId) }) if (std::optional<ScrobblingBackend> backend{ getUserBackend(listen.userId) })
_scrobblers[*scrobbler]->listenStarted(listen); _scrobblingBackends[*backend]->listenStarted(listen);
} }
void ScrobblingService::listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration) void ScrobblingService::listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration)
{ {
if (std::optional<Scrobbler> scrobbler{ getUserScrobbler(listen.userId) }) if (std::optional<ScrobblingBackend> backend{ getUserBackend(listen.userId) })
_scrobblers[*scrobbler]->listenFinished(listen, duration); _scrobblingBackends[*backend]->listenFinished(listen, duration);
} }
void ScrobblingService::addTimedListen(const TimedListen& listen) void ScrobblingService::addTimedListen(const TimedListen& listen)
{ {
if (std::optional<Scrobbler> scrobbler{ getUserScrobbler(listen.userId) }) if (std::optional<ScrobblingBackend> backend{ getUserBackend(listen.userId) })
_scrobblers[*scrobbler]->addTimedListen(listen); _scrobblingBackends[*backend]->addTimedListen(listen);
} }
std::optional<Scrobbler> ScrobblingService::getUserScrobbler(UserId userId) std::optional<ScrobblingBackend> ScrobblingService::getUserBackend(UserId userId)
{ {
std::optional<Scrobbler> scrobbler; std::optional<ScrobblingBackend> backend;
Session& session{ _db.getTLSSession() }; Session& session{ _db.getTLSSession() };
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
if (const User::pointer user{ User::find(session, userId) }) if (const User::pointer user{ User::find(session, userId) })
scrobbler = user->getScrobbler(); backend = user->getScrobblingBackend();
return scrobbler; return backend;
} }
ScrobblingService::ArtistContainer ScrobblingService::getRecentArtists(UserId userId, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range) ScrobblingService::ArtistContainer ScrobblingService::getRecentArtists(UserId userId, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range)
{ {
ArtistContainer res; ArtistContainer res;
auto scrobbler{ getUserScrobbler(userId) }; const auto backend{ getUserBackend(userId) };
if (!scrobbler) if (!backend)
return res; return res;
Session& session{ _db.getTLSSession() }; Session& session{ _db.getTLSSession() };
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
res = Database::Listen::getRecentArtists(session, userId, *scrobbler, clusterIds, linkType, range); res = Database::Listen::getRecentArtists(session, userId, *backend, clusterIds, linkType, range);
return res; return res;
} }
@@ -107,14 +103,14 @@ namespace Scrobbling
{ {
ReleaseContainer res; ReleaseContainer res;
auto scrobbler{ getUserScrobbler(userId) }; const auto backend{ getUserBackend(userId) };
if (!scrobbler) if (!backend)
return res; return res;
Session& session{ _db.getTLSSession() }; Session& session{ _db.getTLSSession() };
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
res = Database::Listen::getRecentReleases(session, userId, *scrobbler, clusterIds, range); res = Database::Listen::getRecentReleases(session, userId, *backend, clusterIds, range);
return res; return res;
} }
@@ -122,40 +118,40 @@ namespace Scrobbling
{ {
TrackContainer res; TrackContainer res;
auto scrobbler{ getUserScrobbler(userId) }; const auto backend{ getUserBackend(userId) };
if (!scrobbler) if (!backend)
return res; return res;
Session& session{ _db.getTLSSession() }; Session& session{ _db.getTLSSession() };
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
res = Database::Listen::getRecentTracks(session, userId, *scrobbler, clusterIds, range); res = Database::Listen::getRecentTracks(session, userId, *backend, clusterIds, range);
return res; return res;
} }
Wt::WDateTime ScrobblingService::getLastListenDateTime(Database::UserId userId, Database::ReleaseId releaseId) Wt::WDateTime ScrobblingService::getLastListenDateTime(Database::UserId userId, Database::ReleaseId releaseId)
{ {
auto scrobbler{ getUserScrobbler(userId) }; const auto backend{ getUserBackend(userId) };
if (!scrobbler) if (!backend)
return {}; return {};
Session& session{ _db.getTLSSession() }; Session& session{ _db.getTLSSession() };
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
const Database::Listen::pointer listen{ Database::Listen::getMostRecentListen(session, userId, *scrobbler, releaseId) }; const Database::Listen::pointer listen{ Database::Listen::getMostRecentListen(session, userId, *backend, releaseId) };
return listen ? listen->getDateTime() : Wt::WDateTime{}; return listen ? listen->getDateTime() : Wt::WDateTime{};
} }
Wt::WDateTime ScrobblingService::getLastListenDateTime(Database::UserId userId, Database::TrackId trackId) Wt::WDateTime ScrobblingService::getLastListenDateTime(Database::UserId userId, Database::TrackId trackId)
{ {
auto scrobbler{ getUserScrobbler(userId) }; const auto backend{ getUserBackend(userId) };
if (!scrobbler) if (!backend)
return {}; return {};
Session& session{ _db.getTLSSession() }; Session& session{ _db.getTLSSession() };
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
const Database::Listen::pointer listen{ Database::Listen::getMostRecentListen(session, userId, *scrobbler, trackId) }; const Database::Listen::pointer listen{ Database::Listen::getMostRecentListen(session, userId, *backend, trackId) };
return listen ? listen->getDateTime() : Wt::WDateTime{}; return listen ? listen->getDateTime() : Wt::WDateTime{};
} }
@@ -164,14 +160,14 @@ namespace Scrobbling
{ {
ArtistContainer res; ArtistContainer res;
auto scrobbler{ getUserScrobbler(userId) }; const auto backend{ getUserBackend(userId) };
if (!scrobbler) if (!backend)
return res; return res;
Session& session{ _db.getTLSSession() }; Session& session{ _db.getTLSSession() };
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
res = Database::Listen::getTopArtists(session, userId, *scrobbler, clusterIds, linkType, range); res = Database::Listen::getTopArtists(session, userId, *backend, clusterIds, linkType, range);
return res; return res;
} }
@@ -179,14 +175,14 @@ namespace Scrobbling
{ {
ReleaseContainer res; ReleaseContainer res;
auto scrobbler{ getUserScrobbler(userId) }; const auto backend{ getUserBackend(userId) };
if (!scrobbler) if (!backend)
return res; return res;
Session& session{ _db.getTLSSession() }; Session& session{ _db.getTLSSession() };
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
res = Database::Listen::getTopReleases(session, userId, *scrobbler, clusterIds, range); res = Database::Listen::getTopReleases(session, userId, *backend, clusterIds, range);
return res; return res;
} }
@@ -194,133 +190,15 @@ namespace Scrobbling
{ {
TrackContainer res; TrackContainer res;
auto scrobbler{ getUserScrobbler(userId) }; const auto backend{ getUserBackend(userId) };
if (!scrobbler) if (!backend)
return res; return res;
Session& session{ _db.getTLSSession() }; Session& session{ _db.getTLSSession() };
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
res = Database::Listen::getTopTracks(session, userId, *scrobbler, clusterIds, range); res = Database::Listen::getTopTracks(session, userId, *backend, clusterIds, range);
return res; return res;
} }
void ScrobblingService::star(UserId userId, ArtistId artistId)
{
star<Artist, ArtistId, StarredArtist>(userId, artistId);
}
void ScrobblingService::unstar(UserId userId, ArtistId artistId)
{
unstar<Artist, ArtistId, StarredArtist>(userId, artistId);
}
bool ScrobblingService::isStarred(UserId userId, ArtistId artistId)
{
return isStarred<Artist, ArtistId, StarredArtist>(userId, artistId);
}
Wt::WDateTime ScrobblingService::getStarredDateTime(UserId userId, ArtistId artistId)
{
return getStarredDateTime<Artist, ArtistId, StarredArtist>(userId, artistId);
}
ScrobblingService::ArtistContainer ScrobblingService::getStarredArtists(UserId userId, const std::vector<ClusterId>& clusterIds,
std::optional<TrackArtistLinkType> linkType,
ArtistSortMethod sortMethod,
Range range)
{
auto scrobbler{ getUserScrobbler(userId) };
if (!scrobbler)
return {};
Artist::FindParameters params;
params.setStarringUser(userId, *scrobbler);
params.setClusters(clusterIds);
params.setLinkType(linkType);
params.setSortMethod(sortMethod);
params.setRange(range);
Session& session{ _db.getTLSSession() };
auto transaction{ session.createSharedTransaction() };
return Artist::find(session, params);
}
void ScrobblingService::star(UserId userId, ReleaseId releaseId)
{
star<Release, ReleaseId, StarredRelease>(userId, releaseId);
}
void ScrobblingService::unstar(UserId userId, ReleaseId releaseId)
{
unstar<Release, ReleaseId, StarredRelease>(userId, releaseId);
}
bool ScrobblingService::isStarred(UserId userId, ReleaseId releaseId)
{
return isStarred<Release, ReleaseId, StarredRelease>(userId, releaseId);
}
Wt::WDateTime ScrobblingService::getStarredDateTime(UserId userId, ReleaseId releaseId)
{
return getStarredDateTime<Release, ReleaseId, StarredRelease>(userId, releaseId);
}
ScrobblingService::ReleaseContainer ScrobblingService::getStarredReleases(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
{
auto scrobbler{ getUserScrobbler(userId) };
if (!scrobbler)
return {};
Release::FindParameters params;
params.setStarringUser(userId, *scrobbler);
params.setClusters(clusterIds);
params.setSortMethod(ReleaseSortMethod::StarredDateDesc);
params.setRange(range);
Session& session{ _db.getTLSSession() };
auto transaction{ session.createSharedTransaction() };
return Release::find(session, params);
}
void ScrobblingService::star(UserId userId, TrackId trackId)
{
star<Track, TrackId, StarredTrack>(userId, trackId);
}
void ScrobblingService::unstar(UserId userId, TrackId trackId)
{
unstar<Track, TrackId, StarredTrack>(userId, trackId);
}
bool ScrobblingService::isStarred(UserId userId, TrackId trackId)
{
return isStarred<Track, TrackId, StarredTrack>(userId, trackId);
}
Wt::WDateTime ScrobblingService::getStarredDateTime(UserId userId, TrackId trackId)
{
return getStarredDateTime<Track, TrackId, StarredTrack>(userId, trackId);
}
ScrobblingService::TrackContainer ScrobblingService::getStarredTracks(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
{
auto scrobbler{ getUserScrobbler(userId) };
if (!scrobbler)
return {};
Track::FindParameters params;
params.setStarringUser(userId, *scrobbler);
params.setClusters(clusterIds);
params.setSortMethod(TrackSortMethod::StarredDateDesc);
params.setRange(range);
Session& session{ _db.getTLSSession() };
auto transaction{ session.createSharedTransaction() };
return Track::find(session, params);
}
} // ns Scrobbling } // ns Scrobbling
@@ -24,7 +24,7 @@
#include <unordered_map> #include <unordered_map>
#include "services/scrobbling/IScrobblingService.hpp" #include "services/scrobbling/IScrobblingService.hpp"
#include "IScrobbler.hpp" #include "IScrobblingBackend.hpp"
namespace Scrobbling namespace Scrobbling
{ {
@@ -39,70 +39,21 @@ namespace Scrobbling
void listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration) override; void listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration) override;
void addTimedListen(const TimedListen& listen) override; void addTimedListen(const TimedListen& listen) override;
ArtistContainer getRecentArtists(Database::UserId userId, ArtistContainer getRecentArtists(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, std::optional<Database::TrackArtistLinkType> linkType,Database::Range range) override;
const std::vector<Database::ClusterId>& clusterIds, ReleaseContainer getRecentReleases(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds,Database::Range range) override;
std::optional<Database::TrackArtistLinkType> linkType, TrackContainer getRecentTracks(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) override;
Database::Range range) override;
ReleaseContainer getRecentReleases(Database::UserId userId,
const std::vector<Database::ClusterId>& clusterIds,
Database::Range range) override;
TrackContainer getRecentTracks(Database::UserId userId,
const std::vector<Database::ClusterId>& clusterIds,
Database::Range range) override;
Wt::WDateTime getLastListenDateTime(Database::UserId userId, Database::ReleaseId releaseId) override; Wt::WDateTime getLastListenDateTime(Database::UserId userId, Database::ReleaseId releaseId) override;
Wt::WDateTime getLastListenDateTime(Database::UserId userId, Database::TrackId trackId) override; Wt::WDateTime getLastListenDateTime(Database::UserId userId, Database::TrackId trackId) override;
ArtistContainer getTopArtists(Database::UserId userId, ArtistContainer getTopArtists(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, std::optional<Database::TrackArtistLinkType> linkType, Database::Range range) override;
const std::vector<Database::ClusterId>& clusterIds, ReleaseContainer getTopReleases(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) override;
std::optional<Database::TrackArtistLinkType> linkType, TrackContainer getTopTracks(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) override;
Database::Range range) override;
ReleaseContainer getTopReleases(Database::UserId userId, std::optional<Database::ScrobblingBackend> getUserBackend(Database::UserId userId);
const std::vector<Database::ClusterId>& clusterIds,
Database::Range range) override;
TrackContainer getTopTracks(Database::UserId userId,
const std::vector<Database::ClusterId>& clusterIds,
Database::Range range) override;
void star(Database::UserId userId, Database::ArtistId artistId) override;
void unstar(Database::UserId userId, Database::ArtistId artistId) override;
bool isStarred(Database::UserId userId, Database::ArtistId artistId) override;
Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::ArtistId artistId) override;
ArtistContainer getStarredArtists(Database::UserId userId,
const std::vector<Database::ClusterId>& clusterIds,
std::optional<Database::TrackArtistLinkType> linkType,
Database::ArtistSortMethod sortMethod,
Database::Range range) override;
void star(Database::UserId userId, Database::ReleaseId releaseId) override;
void unstar(Database::UserId userId, Database::ReleaseId releaseId) override;
bool isStarred(Database::UserId userId, Database::ReleaseId releasedId) override;
Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::ReleaseId releasedId) override;
ReleaseContainer getStarredReleases(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) override;
void star(Database::UserId userId, Database::TrackId trackId) override;
void unstar(Database::UserId userId, Database::TrackId trackId) override;
bool isStarred(Database::UserId userId, Database::TrackId trackId) override;
Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::TrackId trackId) override;
TrackContainer getStarredTracks(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) override;
std::optional<Database::Scrobbler> getUserScrobbler(Database::UserId userId);
template <typename ObjType, typename ObjIdType, typename StarredObjType>
void star(Database::UserId userId, ObjIdType id);
template <typename ObjType, typename ObjIdType, typename StarredObjType>
void unstar(Database::UserId userId, ObjIdType id);
template <typename ObjType, typename ObjIdType, typename StarredObjType>
bool isStarred(Database::UserId userId, ObjIdType id);
template <typename ObjType, typename ObjIdType, typename StarredObjType>
Wt::WDateTime getStarredDateTime(Database::UserId userId, ObjIdType id);
Database::Db& _db; Database::Db& _db;
std::unordered_map<Database::Scrobbler, std::unique_ptr<IScrobbler>> _scrobblers; std::unordered_map<Database::ScrobblingBackend, std::unique_ptr<IScrobblingBackend>> _scrobblingBackends;
}; };
} // ns Scrobbling } // ns Scrobbling
@@ -1,114 +0,0 @@
/*
* Copyright (C) 2021 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "services/database/Db.hpp"
#include "services/database/Session.hpp"
#include "services/database/User.hpp"
namespace Scrobbling
{
using namespace Database;
template <typename ObjType, typename ObjIdType, typename StarredObjType>
void ScrobblingService::star(UserId userId, ObjIdType objId)
{
auto scrobbler {getUserScrobbler(userId)};
if (!scrobbler)
return;
typename StarredObjType::IdType starredObjId;
{
Session& session {_db.getTLSSession()};
auto transaction {session.createUniqueTransaction()};
typename StarredObjType::pointer starredObj {StarredObjType::find(session, objId, userId, *scrobbler)};
if (!starredObj)
{
const typename ObjType::pointer obj {ObjType::find(session, objId)};
if (!obj)
return;
const User::pointer user {User::find(session, userId)};
if (!user)
return;
starredObj = session.create<StarredObjType>(obj, user, *scrobbler);
}
starredObj.modify()->setDateTime(Wt::WDateTime::currentDateTime());
starredObjId = starredObj->getId();
}
_scrobblers[*scrobbler]->onStarred(starredObjId);
}
template <typename ObjType, typename ObjIdType, typename StarredObjType>
void ScrobblingService::unstar(UserId userId, ObjIdType objId)
{
auto scrobbler {getUserScrobbler(userId)};
if (!scrobbler)
return;
typename StarredObjType::IdType starredObjId;
{
Session& session {_db.getTLSSession()};
auto transaction {session.createSharedTransaction()};
typename StarredObjType::pointer starredObj {StarredObjType::find(session, objId, userId, *scrobbler)};
if (!starredObj)
return;
starredObjId = starredObj->getId();
}
_scrobblers[*scrobbler]->onUnstarred(starredObjId);
}
template <typename ObjType, typename ObjIdType, typename StarredObjType>
bool ScrobblingService::isStarred(UserId userId, ObjIdType objId)
{
auto scrobbler {getUserScrobbler(userId)};
if (!scrobbler)
return false;
Session& session {_db.getTLSSession()};
auto transaction {session.createSharedTransaction()};
typename StarredObjType::pointer starredObj {StarredObjType::find(session, objId, userId, *scrobbler)};
return starredObj && (starredObj->getScrobblingState() != ScrobblingState::PendingRemove);
}
template <typename ObjType, typename ObjIdType, typename StarredObjType>
Wt::WDateTime ScrobblingService::getStarredDateTime(UserId userId, ObjIdType objId)
{
auto scrobbler {getUserScrobbler(userId)};
if (!scrobbler)
return {};
Session& session {_db.getTLSSession()};
auto transaction {session.createSharedTransaction()};
typename StarredObjType::pointer starredObj {StarredObjType::find(session, objId, userId, *scrobbler)};
if (starredObj && (starredObj->getScrobblingState() != ScrobblingState::PendingRemove))
return starredObj->getDateTime();
return {};
}
} // ns Scrobbling
@@ -0,0 +1,68 @@
/*
* Copyright (C) 2021 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "InternalBackend.hpp"
#include "services/database/Db.hpp"
#include "services/database/Listen.hpp"
#include "services/database/Session.hpp"
#include "services/database/Track.hpp"
#include "services/database/User.hpp"
namespace Scrobbling
{
InternalBackend::InternalBackend(Database::Db& db)
: _db{ db }
{}
void InternalBackend::listenStarted(const Listen&)
{
// nothing to do
}
void InternalBackend::listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration)
{
// only record tracks that have been played for at least of few seconds...
if (duration && *duration < std::chrono::seconds{ 5 })
return;
addTimedListen({ listen, Wt::WDateTime::currentDateTime() });
}
void InternalBackend::addTimedListen(const TimedListen& listen)
{
Database::Session& session{ _db.getTLSSession() };
auto transaction{ session.createUniqueTransaction() };
if (Database::Listen::find(session, listen.userId, listen.trackId, Database::ScrobblingBackend::Internal, listen.listenedAt))
return;
const Database::User::pointer user{ Database::User::find(session, listen.userId) };
if (!user)
return;
const Database::Track::pointer track{ Database::Track::find(session, listen.trackId) };
if (!track)
return;
auto dbListen{ session.create<Database::Listen>(user, track, Database::ScrobblingBackend::Internal, listen.listenedAt) };
dbListen.modify()->setSyncState(Database::SyncState::Synchronized);
}
} // Scrobbling
@@ -0,0 +1,44 @@
/*
* Copyright (C) 2021 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "IScrobblingBackend.hpp"
namespace Database
{
class Db;
}
namespace Scrobbling
{
class InternalBackend final : public IScrobblingBackend
{
public:
InternalBackend(Database::Db& db);
private:
void listenStarted(const Listen& listen) override;
void listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration) override;
void addTimedListen(const TimedListen& listen) override;
Database::Db& _db;
};
} // Scrobbling
@@ -1,122 +0,0 @@
/*
* Copyright (C) 2021 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "InternalScrobbler.hpp"
#include "services/database/Db.hpp"
#include "services/database/Listen.hpp"
#include "services/database/Session.hpp"
#include "services/database/StarredArtist.hpp"
#include "services/database/StarredRelease.hpp"
#include "services/database/StarredTrack.hpp"
#include "services/database/Track.hpp"
#include "services/database/User.hpp"
namespace
{
template <typename StarredObjType>
void onStarred(Database::Session& session, typename StarredObjType::IdType id)
{
auto transaction{ session.createUniqueTransaction() };
if (auto starredObj{ StarredObjType::find(session, id) })
starredObj.modify()->setScrobblingState(Database::ScrobblingState::Synchronized);
}
template <typename StarredObjType>
void onUnstarred(Database::Session& session, typename StarredObjType::IdType id)
{
auto transaction{ session.createUniqueTransaction() };
if (auto starredObj{ StarredObjType::find(session, id) })
starredObj.remove();
}
}
namespace Scrobbling
{
InternalScrobbler::InternalScrobbler(Database::Db& db)
: _db{ db }
{}
void InternalScrobbler::listenStarted(const Listen&)
{
// nothing to do
}
void InternalScrobbler::listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration)
{
// only record tracks that have been played for at least of few seconds...
if (duration && *duration < std::chrono::seconds{ 5 })
return;
addTimedListen({ listen, Wt::WDateTime::currentDateTime() });
}
void InternalScrobbler::addTimedListen(const TimedListen& listen)
{
Database::Session& session{ _db.getTLSSession() };
auto transaction{ session.createUniqueTransaction() };
if (Database::Listen::find(session, listen.userId, listen.trackId, Database::Scrobbler::Internal, listen.listenedAt))
return;
const Database::User::pointer user{ Database::User::find(session, listen.userId) };
if (!user)
return;
const Database::Track::pointer track{ Database::Track::find(session, listen.trackId) };
if (!track)
return;
auto dbListen{ session.create<Database::Listen>(user, track, Database::Scrobbler::Internal, listen.listenedAt) };
dbListen.modify()->setScrobblingState(Database::ScrobblingState::Synchronized);
}
void InternalScrobbler::onStarred(Database::StarredArtistId starredArtistId)
{
::onStarred<Database::StarredArtist>(_db.getTLSSession(), starredArtistId);
}
void InternalScrobbler::onUnstarred(Database::StarredArtistId starredArtistId)
{
::onUnstarred<Database::StarredArtist>(_db.getTLSSession(), starredArtistId);
}
void InternalScrobbler::onStarred(Database::StarredReleaseId starredReleaseId)
{
::onStarred<Database::StarredRelease>(_db.getTLSSession(), starredReleaseId);
}
void InternalScrobbler::onUnstarred(Database::StarredReleaseId starredReleaseId)
{
::onUnstarred<Database::StarredRelease>(_db.getTLSSession(), starredReleaseId);
}
void InternalScrobbler::onStarred(Database::StarredTrackId starredTrackId)
{
::onStarred<Database::StarredTrack>(_db.getTLSSession(), starredTrackId);
}
void InternalScrobbler::onUnstarred(Database::StarredTrackId starredTrackId)
{
::onUnstarred<Database::StarredTrack>(_db.getTLSSession(), starredTrackId);
}
} // Scrobbling
@@ -1,93 +0,0 @@
/*
* Copyright (C) 2022 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "FeedbacksParser.hpp"
#include <Wt/Json/Array.h>
#include <Wt/Json/Object.h>
#include <Wt/Json/Value.h>
#include <Wt/Json/Parser.h>
#include "services/scrobbling/Exception.hpp"
#include "Exception.hpp"
#include "Utils.hpp"
namespace Scrobbling::ListenBrainz
{
namespace
{
Feedback
parseFeedback(const Wt::Json::Object& feedbackObj)
{
const std::optional<UUID> recordingMBID {UUID::fromString(static_cast<std::string>(feedbackObj.get("recording_mbid")))};
if (!recordingMBID)
throw Exception {"MBID not found!"};
return Feedback
{
Wt::WDateTime::fromTime_t(static_cast<int>(feedbackObj.get("created"))),
*recordingMBID,
static_cast<FeedbackType>(static_cast<int>(feedbackObj.get("score")))
};
}
}
FeedbacksParser::Result
FeedbacksParser::parse(std::string_view msgBody)
{
Result res;
try
{
Wt::Json::Object root;
Wt::Json::parse(std::string {msgBody}, root);
const Wt::Json::Array& feedbacks = root.get("feedback");
LOG(DEBUG) << "Got " << feedbacks.size() << " feedbacks";
if (feedbacks.empty())
return res;
res.feedbackCount = feedbacks.size();
for (const Wt::Json::Value& value : feedbacks)
{
try
{
res.feedbacks.push_back(parseFeedback(value));
}
catch (const Exception& e)
{
LOG(DEBUG) << "Cannot parse feedback: " << e.what() << ", skipping";
}
catch (const Wt::WException &e)
{
LOG(DEBUG) << "Cannot parse feedback: " << e.what() << ", skipping";
}
}
}
catch (const Wt::WException& error)
{
LOG(ERROR) << "Cannot parse 'feedback' result: " << error.what();
}
return res;
}
} // Scrobbling::ListenBrainz
@@ -1,505 +0,0 @@
/*
* Copyright (C) 2021 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "ListenBrainzScrobbler.hpp"
#include <tuple>
#include <boost/asio/bind_executor.hpp>
#include <Wt/Json/Array.h>
#include <Wt/Json/Object.h>
#include <Wt/Json/Value.h>
#include <Wt/Json/Serializer.h>
#include "services/database/Db.hpp"
#include "services/database/Session.hpp"
#include "services/database/StarredTrack.hpp"
#include "services/database/Track.hpp"
#include "services/database/User.hpp"
#include "utils/IConfig.hpp"
#include "utils/http/IClient.hpp"
#include "utils/Service.hpp"
#include "Exception.hpp"
#include "FeedbacksParser.hpp"
#include "Utils.hpp"
using namespace Scrobbling::ListenBrainz;
using namespace Database;
namespace
{
std::optional<std::size_t>
parseTotalFeedbackCount(std::string_view msgBody)
{
try
{
Wt::Json::Object root;
Wt::Json::parse(std::string {msgBody}, root);
return static_cast<int>(root.get("total_count"));
}
catch (const Wt::WException& e)
{
LOG(ERROR) << "Cannot parse listen count response: " << e.what();
return std::nullopt;
}
}
}
namespace Scrobbling::ListenBrainz
{
FeedbacksSynchronizer::FeedbacksSynchronizer(boost::asio::io_context& ioContext, Database::Db& db, Http::IClient& client)
: _ioContext {ioContext}
, _db {db}
, _client {client}
, _maxSyncFeedbackCount {Service<IConfig>::get()->getULong("listenbrainz-max-sync-feedback-count", 1000)}
, _syncFeedbacksPeriod {Service<IConfig>::get()->getULong("listenbrainz-sync-feedbacks-period-hours", 1)}
{
LOG(INFO) << "Starting Feedbacks synchronizer, maxSyncFeedbackCount = " << _maxSyncFeedbackCount << ", _syncFeedbacksPeriod = " << _syncFeedbacksPeriod.count() << " hours";
scheduleSync(std::chrono::seconds {30});
}
void
FeedbacksSynchronizer::enqueFeedback(FeedbackType type, Database::StarredTrackId starredTrackId)
{
try
{
Database::Session& session {_db.getTLSSession()};
auto transaction {session.createUniqueTransaction()};
StarredTrack::pointer starredTrack {StarredTrack::find(session, starredTrackId)};
if (!starredTrack)
return;
std::optional<UUID> recordingMBID {starredTrack->getTrack()->getRecordingMBID()};
switch (type)
{
case FeedbackType::Love:
if (starredTrack->getScrobblingState() != ScrobblingState::PendingAdd)
starredTrack.modify()->setScrobblingState(ScrobblingState::PendingAdd);
break;
case FeedbackType::Erase:
if (!recordingMBID)
{
LOG(DEBUG) << "Track has no recording MBID: erasing star";
starredTrack.remove();
}
else
{
// Send the erase order even if it is not on the remote LB server (it may be
// queued for add, or not)
starredTrack.modify()->setScrobblingState(ScrobblingState::PendingRemove);
}
break;
default:
throw Exception {"Unhandled feedback type"};
}
if (!recordingMBID)
{
LOG(DEBUG) << "Track has no recording MBID: skipping";
return;
}
const std::optional<UUID> listenBrainzToken {starredTrack->getUser()->getListenBrainzToken()};
if (!listenBrainzToken)
return;
Http::ClientPOSTRequestParameters request;
request.relativeUrl = "/1/feedback/recording-feedback";
request.message.addHeader("Authorization", "Token " + std::string {listenBrainzToken->getAsString()});
Wt::Json::Object root;
root["recording_mbid"] = Wt::Json::Value {std::string {recordingMBID->getAsString()}};
root["score"] = Wt::Json::Value {static_cast<int>(type)};
request.message.addBodyText(Wt::Json::serialize(root));
request.message.addHeader("Content-Type", "application/json");
request.onSuccessFunc = [=](std::string_view /*msgBody*/)
{
_strand.dispatch([=]
{
onFeedbackSent(type, starredTrackId);
});
};
_client.sendPOSTRequest(std::move(request));
}
catch (Exception& e)
{
LOG(DEBUG) << "Cannot send feedback: " << e.what();
}
}
void
FeedbacksSynchronizer::onFeedbackSent(FeedbackType type, Database::StarredTrackId starredTrackId)
{
assert(_strand.running_in_this_thread());
Session& session {_db.getTLSSession()};
auto transaction {session.createUniqueTransaction()};
StarredTrack::pointer starredTrack {StarredTrack::find(session, starredTrackId)};
if (!starredTrack)
{
LOG(DEBUG) << "Starred track not found. deleted?";
return;
}
UserContext& userContext {getUserContext(starredTrack->getUser()->getId())};
switch (type)
{
case FeedbackType::Love:
starredTrack.modify()->setScrobblingState(ScrobblingState::Synchronized);
LOG(DEBUG) << "State set to synchronized";
if (userContext.feedbackCount)
{
(*userContext.feedbackCount)++;
LOG(DEBUG) << "Feedback count set to " << *userContext.feedbackCount << " for user '" << userContext.listenBrainzUserName <<"'";
}
break;
case FeedbackType::Erase:
starredTrack.remove();
LOG(DEBUG) << "Removed starred track";
if (userContext.feedbackCount && *userContext.feedbackCount > 0)
{
(*userContext.feedbackCount)--;
LOG(DEBUG) << "Feedback count set to " << *userContext.feedbackCount << " for user '" << userContext.listenBrainzUserName <<"'";
}
break;
default:
throw Exception {"Unhandled feedback type"};
}
}
void
FeedbacksSynchronizer::enquePendingFeedbacks()
{
auto processPendingFeedbacks { [this] (ScrobblingState scrobblingState, FeedbackType feedbackType)
{
RangeResults<StarredTrackId> pendingFeedbacks;
{
Database::Session& session {_db.getTLSSession()};
auto transaction {session.createSharedTransaction()};
StarredTrack::FindParameters params;
params.setScrobbler(Database::Scrobbler::ListenBrainz, scrobblingState)
.setRange(Database::Range {0, 100}); // don't flood too much?
pendingFeedbacks = StarredTrack::find(session, params);
}
LOG(DEBUG) << "Queing " << pendingFeedbacks.results.size() << " pending '" << (feedbackType == FeedbackType::Love ? "love" : "erase") << "' feedbacks";
for (const StarredTrackId starredTrackId : pendingFeedbacks.results)
enqueFeedback(feedbackType, starredTrackId);
}};
processPendingFeedbacks(ScrobblingState::PendingAdd, FeedbackType::Love);
processPendingFeedbacks(ScrobblingState::PendingRemove, FeedbackType::Erase);
}
FeedbacksSynchronizer::UserContext&
FeedbacksSynchronizer::getUserContext(Database::UserId userId)
{
assert(_strand.running_in_this_thread());
auto itContext {_userContexts.find(userId)};
if (itContext == std::cend(_userContexts))
{
std::tie(itContext, std::ignore) = _userContexts.emplace(userId, userId);
}
return itContext->second;
}
bool
FeedbacksSynchronizer::isSyncing() const
{
return std::any_of(std::cbegin(_userContexts), std::cend(_userContexts), [](const auto& contextEntry)
{
return contextEntry.second.syncing;
});
}
void
FeedbacksSynchronizer::scheduleSync(std::chrono::seconds fromNow)
{
if (_syncFeedbacksPeriod.count() == 0 || _maxSyncFeedbackCount == 0)
return;
LOG(DEBUG) << "Scheduled sync in " << fromNow.count() << " seconds...";
_syncTimer.expires_after(fromNow);
_syncTimer.async_wait(boost::asio::bind_executor(_strand, [this] (const boost::system::error_code& ec)
{
if (ec == boost::asio::error::operation_aborted)
{
LOG(DEBUG) << "getFeedbacks aborted";
return;
}
else if (ec)
{
throw Exception {"GetFeedbacks timer failure: " + std::string {ec.message()} };
}
startSync();
}));
}
void
FeedbacksSynchronizer::startSync()
{
LOG(DEBUG) << "Starting sync!";
assert(!isSyncing());
assert(_strand.running_in_this_thread());
enquePendingFeedbacks();
Database::RangeResults<Database::UserId> userIds;
{
Database::Session& session {_db.getTLSSession()};
auto transaction {session.createSharedTransaction()};
userIds = Database::User::find(_db.getTLSSession(), Database::User::FindParameters{}.setScrobbler(Database::Scrobbler::ListenBrainz));
}
for (const Database::UserId userId : userIds.results)
startSync(getUserContext(userId));
if (!isSyncing())
scheduleSync(_syncFeedbacksPeriod);
}
void
FeedbacksSynchronizer::startSync(UserContext& context)
{
context.syncing = true;
context.listenBrainzUserName = "";
context.fetchedFeedbackCount = 0;
context.matchedFeedbackCount = 0;
context.importedFeedbackCount = 0;
enqueValidateToken(context);
}
void
FeedbacksSynchronizer::onSyncEnded(UserContext& context)
{
_strand.dispatch([this, &context]
{
LOG(INFO) << "Feedback sync done for user '" << context.listenBrainzUserName << "', fetched: " << context.fetchedFeedbackCount << ", matched: " << context.matchedFeedbackCount << ", imported: " << context.importedFeedbackCount;
context.syncing = false;
if (!isSyncing())
scheduleSync(_syncFeedbacksPeriod);
});
}
void
FeedbacksSynchronizer::enqueValidateToken(UserContext& context)
{
assert(context.listenBrainzUserName.empty());
const std::optional<UUID> listenBrainzToken {Utils::getListenBrainzToken(_db.getTLSSession(), context.userId)};
if (!listenBrainzToken)
{
onSyncEnded(context);
return;
}
Http::ClientGETRequestParameters request;
request.priority = Http::ClientRequestParameters::Priority::Low;
request.relativeUrl = "/1/validate-token";
request.headers = { {"Authorization", "Token " + std::string {listenBrainzToken->getAsString()}} };
request.onSuccessFunc = [this, &context] (std::string_view msgBody)
{
context.listenBrainzUserName = Utils::parseValidateToken(msgBody);
if (context.listenBrainzUserName.empty())
{
onSyncEnded(context);
return;
}
enqueGetFeedbackCount(context);
};
request.onFailureFunc = [this, &context]
{
onSyncEnded(context);
};
_client.sendGETRequest(std::move(request));
}
void
FeedbacksSynchronizer::enqueGetFeedbackCount(UserContext& context)
{
assert(!context.listenBrainzUserName.empty());
Http::ClientGETRequestParameters request;
request.relativeUrl = "/1/feedback/user/" + std::string {context.listenBrainzUserName} + "/get-feedback?score=1&count=0";
request.priority = Http::ClientRequestParameters::Priority::Low;
request.onSuccessFunc = [this, &context] (std::string_view msgBody)
{
std::string msgBodyCopy {msgBody};
_strand.dispatch([this, msgBodyCopy, &context]
{
LOG(DEBUG) << "Current feedback count = " << (context.feedbackCount ? *context.feedbackCount : 0) << " for user '" << context.listenBrainzUserName << "'";
const auto totalFeedbackCount = parseTotalFeedbackCount(msgBodyCopy);
if (totalFeedbackCount)
LOG(DEBUG) << "Feedback count for listenbrainz user '" << context.listenBrainzUserName << "' = " << *totalFeedbackCount;
bool needSync {totalFeedbackCount && (!context.feedbackCount || *context.feedbackCount != *totalFeedbackCount)};
context.feedbackCount = totalFeedbackCount;
if (needSync)
enqueGetFeedbacks(context);
else
onSyncEnded(context);
});
};
request.onFailureFunc = [this, &context]
{
onSyncEnded(context);
};
_client.sendGETRequest(std::move(request));
}
void
FeedbacksSynchronizer::enqueGetFeedbacks(UserContext& context)
{
assert(!context.listenBrainzUserName.empty());
Http::ClientGETRequestParameters request;
request.relativeUrl = "/1/feedback/user/" + context.listenBrainzUserName + "/get-feedback?offset=" + std::to_string(context.fetchedFeedbackCount);
request.priority = Http::ClientRequestParameters::Priority::Low;
request.onSuccessFunc = [this, &context] (std::string_view msgBody)
{
std::string msgBodyCopy {msgBody};
_strand.dispatch([this, msgBodyCopy, &context]
{
const std::size_t fetchedFeedbackCount {processGetFeedbacks(msgBodyCopy, context)};
if (fetchedFeedbackCount == 0 // no more thing available on server
|| context.fetchedFeedbackCount >= context.feedbackCount // we may miss something, but we will get it next time
|| context.fetchedFeedbackCount >= _maxSyncFeedbackCount)
{
onSyncEnded(context);
}
else
{
enqueGetFeedbacks(context);
}
});
};
request.onFailureFunc = [=, &context]
{
onSyncEnded(context);
};
_client.sendGETRequest(std::move(request));
}
std::size_t
FeedbacksSynchronizer::processGetFeedbacks(std::string_view msgBody, UserContext& context)
{
const FeedbacksParser::Result parseResult {FeedbacksParser::parse(msgBody)};
LOG(DEBUG) << "Parsed " << parseResult.feedbackCount << " feedbacks, found " << parseResult.feedbacks.size() << " usable entries";
context.fetchedFeedbackCount += parseResult.feedbackCount;
for (const Feedback& feedback : parseResult.feedbacks)
{
tryImportFeedback(feedback, context);
}
return parseResult.feedbackCount;
}
void
FeedbacksSynchronizer::tryImportFeedback(const Feedback& feedback, UserContext& context)
{
Database::Session& session {_db.getTLSSession()};
bool needImport{};
TrackId trackId;
{
auto transaction {session.createSharedTransaction()};
const std::vector<Track::pointer> tracks {Track::findByRecordingMBID(session, feedback.recordingMBID)};
if (tracks.size() > 1)
{
LOG(DEBUG) << "Too many matches for feedback '" << feedback << "': duplicate recording MBIDs found";
return;
}
else if (tracks.empty())
{
LOG(DEBUG) << "Cannot match feedback '" << feedback << "': no track found for this recording MBID";
return;
}
trackId = tracks.front()->getId();
const StarredTrack::pointer starredTrack {StarredTrack::find(session, trackId, context.userId, Database::Scrobbler::ListenBrainz)};
needImport = !starredTrack;
// don't update starred date time
// no need to update state if it was found as not synchronized
// pending remove => will be removed later
// pending add => will be resent later
}
if (needImport)
{
LOG(DEBUG) << "Importing feedback '" << feedback << "'";
auto transaction {session.createUniqueTransaction()};
const Track::pointer track {Track::find(session, trackId)};
if (!track)
return;
const User::pointer user {User::find(session, context.userId)};
if (!user)
return;
StarredTrack::pointer starredTrack {session.create<StarredTrack>(track, user, Database::Scrobbler::ListenBrainz)};
starredTrack.modify()->setScrobblingState(ScrobblingState::Synchronized);
starredTrack.modify()->setDateTime(feedback.created);
context.importedFeedbackCount++;
}
else
{
LOG(DEBUG) << "No need to import feedback '" << feedback << "', already imported";
context.matchedFeedbackCount++;
}
}
} // namespace Scrobbling::ListenBrainz
@@ -1,106 +0,0 @@
/*
* Copyright (C) 2021 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <optional>
#include <unordered_map>
#include <boost/asio/io_context.hpp>
#include <boost/asio/io_context_strand.hpp>
#include <boost/asio/steady_timer.hpp>
#include "services/database/Types.hpp"
#include "services/database/UserId.hpp"
#include "services/scrobbling/Listen.hpp"
#include "FeedbackTypes.hpp"
namespace Database
{
class Db;
}
namespace Http
{
class IClient;
}
namespace Scrobbling::ListenBrainz
{
class FeedbacksSynchronizer
{
public:
FeedbacksSynchronizer(boost::asio::io_context& ioContext, Database::Db& db, Http::IClient& client);
void enqueFeedback(FeedbackType type, Database::StarredTrackId starredTrackId);
private:
void onFeedbackSent(FeedbackType type, Database::StarredTrackId starredTrackId);
void enquePendingFeedbacks();
struct UserContext
{
UserContext(Database::UserId id) : userId {id} {}
UserContext(const UserContext&) = delete;
UserContext(UserContext&&) = delete;
UserContext& operator=(const UserContext&) = delete;
UserContext& operator=(UserContext&&) = delete;
const Database::UserId userId;
bool syncing {};
std::optional<std::size_t> feedbackCount {};
// resetted at each sync
std::string listenBrainzUserName; // need to be resolved first
std::size_t currentOffset{};
std::size_t fetchedFeedbackCount{};
std::size_t matchedFeedbackCount{};
std::size_t importedFeedbackCount{};
};
UserContext& getUserContext(Database::UserId userId);
bool isSyncing() const;
void scheduleSync(std::chrono::seconds fromNow);
void startSync();
void startSync(UserContext& context);
void onSyncEnded(UserContext& context);
void enqueValidateToken(UserContext& context);
void enqueGetFeedbackCount(UserContext& context);
void enqueGetFeedbacks(UserContext& context);
std::size_t processGetFeedbacks(std::string_view body, UserContext& context);
void tryImportFeedback(const Feedback& feedback, UserContext& context);
boost::asio::io_context& _ioContext;
boost::asio::io_context::strand _strand {_ioContext};
Database::Db& _db;
boost::asio::steady_timer _syncTimer {_ioContext};
Http::IClient& _client;
std::unordered_map<Database::UserId, UserContext> _userContexts;
const std::size_t _maxSyncFeedbackCount;
const std::chrono::hours _syncFeedbacksPeriod;
};
} // Scrobbling::ListenBrainz
@@ -0,0 +1,87 @@
/*
* Copyright (C) 2021 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "ListenBrainzBackend.hpp"
#include "services/database/Db.hpp"
#include "services/database/Session.hpp"
#include "services/database/Track.hpp"
#include "utils/IConfig.hpp"
#include "utils/http/IClient.hpp"
#include "utils/Logger.hpp"
#include "utils/Service.hpp"
#include "Utils.hpp"
namespace Scrobbling::ListenBrainz
{
using namespace Database;
namespace
{
bool canBeScrobbled(Session& session, TrackId trackId, std::chrono::seconds duration)
{
auto transaction{ session.createSharedTransaction() };
const Track::pointer track{ Track::find(session, trackId) };
if (!track)
return false;
const bool res{ duration >= std::chrono::minutes(4) || (duration >= track->getDuration() / 2) };
if (!res)
LOG(DEBUG) << "Track cannot be scrobbled since played duration is too short: " << duration.count() << "s, total duration = " << std::chrono::duration_cast<std::chrono::seconds>(track->getDuration()).count() << "s";
return res;
}
}
ListenBrainzBackend::ListenBrainzBackend(boost::asio::io_context& ioContext, Db& db)
: _ioContext{ ioContext }
, _db{ db }
, _baseAPIUrl{ Service<IConfig>::get()->getString("listenbrainz-api-base-url", "https://api.listenbrainz.org") }
, _client{ Http::createClient(_ioContext, _baseAPIUrl) }
, _listensSynchronizer{ _ioContext, db, *_client }
{
LOG(INFO) << "Starting ListenBrainz backend... API endpoint = '" << _baseAPIUrl << "'";
}
ListenBrainzBackend::~ListenBrainzBackend()
{
LOG(INFO) << "Stopped ListenBrainz backend!";
}
void ListenBrainzBackend::listenStarted(const Listen& listen)
{
_listensSynchronizer.enqueListenNow(listen);
}
void ListenBrainzBackend::listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration)
{
if (duration && !canBeScrobbled(_db.getTLSSession(), listen.trackId, *duration))
return;
const TimedListen timedListen{ listen, Wt::WDateTime::currentDateTime() };
_listensSynchronizer.enqueListen(timedListen);
}
void ListenBrainzBackend::addTimedListen(const TimedListen& timedListen)
{
_listensSynchronizer.enqueListen(timedListen);
}
} // namespace Scrobbling::ListenBrainz
@@ -0,0 +1,60 @@
/*
* Copyright (C) 2021 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <string>
#include <optional>
#include <boost/asio/io_context.hpp>
#include "IScrobblingBackend.hpp"
#include "ListensSynchronizer.hpp"
namespace Database
{
class Db;
}
namespace Scrobbling::ListenBrainz
{
class ListenBrainzBackend final : public IScrobblingBackend
{
public:
ListenBrainzBackend(boost::asio::io_context& ioContext, Database::Db& db);
~ListenBrainzBackend() override;
private:
ListenBrainzBackend(const ListenBrainzBackend&) = delete;
ListenBrainzBackend& operator=(const ListenBrainzBackend&) = delete;
void listenStarted(const Listen& listen) override;
void listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration) override;
void addTimedListen(const TimedListen& listen) override;
// Submit listens
void enqueListen(const Listen& listen, const Wt::WDateTime& timePoint);
boost::asio::io_context& _ioContext;
Database::Db& _db;
std::string _baseAPIUrl;
std::unique_ptr<Http::IClient> _client;
ListensSynchronizer _listensSynchronizer;
};
} // Scrobbling::ListenBrainz
@@ -1,151 +0,0 @@
/*
* Copyright (C) 2021 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "ListenBrainzScrobbler.hpp"
#include "services/database/Db.hpp"
#include "services/database/Session.hpp"
#include "services/database/StarredArtist.hpp"
#include "services/database/StarredRelease.hpp"
#include "services/database/Track.hpp"
#include "utils/IConfig.hpp"
#include "utils/http/IClient.hpp"
#include "utils/Logger.hpp"
#include "utils/Service.hpp"
#include "Utils.hpp"
using namespace Database;
namespace
{
bool
canBeScrobbled(Session& session, TrackId trackId, std::chrono::seconds duration)
{
auto transaction {session.createSharedTransaction()};
const Track::pointer track {Track::find(session, trackId)};
if (!track)
return false;
const bool res {duration >= std::chrono::minutes(4) || (duration >= track->getDuration() / 2)};
if (!res)
LOG(DEBUG) << "Track cannot be scrobbled since played duration is too short: " << duration.count() << "s, total duration = " << std::chrono::duration_cast<std::chrono::seconds>(track->getDuration()).count() << "s";
return res;
}
template <typename StarredObjType>
void onStarred(Database::Session& session, typename StarredObjType::IdType id)
{
auto transaction {session.createUniqueTransaction()};
if (auto starredObj {StarredObjType::find(session, id)})
{
// maybe in the future this will be supported by ListenBrainz so set it to PendingAdd
starredObj.modify()->setScrobblingState(Database::ScrobblingState::PendingAdd);
}
}
template <typename StarredObjType>
void onUnstarred(Database::Session& session, typename StarredObjType::IdType id)
{
auto transaction {session.createUniqueTransaction()};
if (auto starredObj {StarredObjType::find(session, id)})
starredObj.remove();
}
}
namespace Scrobbling::ListenBrainz
{
Scrobbler::Scrobbler(boost::asio::io_context& ioContext, Db& db)
: _ioContext {ioContext}
, _db {db}
, _baseAPIUrl {Service<IConfig>::get()->getString("listenbrainz-api-base-url", "https://api.listenbrainz.org")}
, _client {Http::createClient(_ioContext, _baseAPIUrl)}
, _listensSynchronizer {_ioContext, db, *_client}
, _feedbacksSynchronizer {_ioContext, db, *_client}
{
LOG(INFO) << "Starting ListenBrainz scrobbler... API endpoint = '" << _baseAPIUrl << "'";
}
Scrobbler::~Scrobbler()
{
LOG(INFO) << "Stopped ListenBrainz scrobbler!";
}
void
Scrobbler::listenStarted(const Listen& listen)
{
_listensSynchronizer.enqueListenNow(listen);
}
void
Scrobbler::listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration)
{
if (duration && !canBeScrobbled(_db.getTLSSession(), listen.trackId, *duration))
return;
const TimedListen timedListen {listen, Wt::WDateTime::currentDateTime()};
_listensSynchronizer.enqueListen(timedListen);
}
void
Scrobbler::addTimedListen(const TimedListen& timedListen)
{
_listensSynchronizer.enqueListen(timedListen);
}
void
Scrobbler::onStarred(StarredArtistId starredArtistId)
{
::onStarred<StarredArtist>(_db.getTLSSession(), starredArtistId);
}
void
Scrobbler::onUnstarred(StarredArtistId starredArtistId)
{
::onUnstarred<StarredArtist>(_db.getTLSSession(), starredArtistId);
}
void
Scrobbler::onStarred(StarredReleaseId starredReleaseId)
{
::onStarred<StarredRelease>(_db.getTLSSession(), starredReleaseId);
}
void
Scrobbler::onUnstarred(StarredReleaseId starredReleaseId)
{
::onUnstarred<StarredRelease>(_db.getTLSSession(), starredReleaseId);
}
void
Scrobbler::onStarred(StarredTrackId starredTrackId)
{
_feedbacksSynchronizer.enqueFeedback(FeedbackType::Love, starredTrackId);
}
void
Scrobbler::onUnstarred(StarredTrackId starredtrackId)
{
_feedbacksSynchronizer.enqueFeedback(FeedbackType::Erase, starredtrackId);
}
} // namespace Scrobbling::ListenBrainz
@@ -1,72 +0,0 @@
/*
* Copyright (C) 2021 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <string>
#include <optional>
#include <boost/asio/io_context.hpp>
#include "IScrobbler.hpp"
#include "FeedbacksSynchronizer.hpp"
#include "ListensSynchronizer.hpp"
namespace Database
{
class Db;
}
namespace Scrobbling::ListenBrainz
{
class Scrobbler final : public IScrobbler
{
public:
Scrobbler(boost::asio::io_context& ioContext, Database::Db& db);
~Scrobbler();
Scrobbler(const Scrobbler&) = delete;
Scrobbler(const Scrobbler&&) = delete;
Scrobbler& operator=(const Scrobbler&) = delete;
Scrobbler& operator=(const Scrobbler&&) = delete;
private:
void listenStarted(const Listen& listen) override;
void listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration) override;
void addTimedListen(const TimedListen& listen) override;
// Submit listens
void enqueListen(const Listen& listen, const Wt::WDateTime& timePoint);
// Star
void onStarred(Database::StarredArtistId starredArtistId) override;
void onUnstarred(Database::StarredArtistId starredArtistId) override;
void onStarred(Database::StarredReleaseId starredReleaseId) override;
void onUnstarred(Database::StarredReleaseId starredReleaseId) override;
void onStarred(Database::StarredTrackId starredTrackId) override;
void onUnstarred(Database::StarredTrackId starredTrackId) override;
boost::asio::io_context& _ioContext;
Database::Db& _db;
std::string _baseAPIUrl;
std::unique_ptr<Http::IClient> _client;
ListensSynchronizer _listensSynchronizer;
FeedbacksSynchronizer _feedbacksSynchronizer;
};
} // Scrobbling::ListenBrainz
File diff suppressed because it is too large Load Diff
@@ -52,7 +52,7 @@ namespace Scrobbling::ListenBrainz
private: private:
void enqueListen(const Listen& listen, const Wt::WDateTime& timePoint); void enqueListen(const Listen& listen, const Wt::WDateTime& timePoint);
bool saveListen(const TimedListen& listen, Database::ScrobblingState scrobblinState); bool saveListen(const TimedListen& listen, Database::SyncState scrobblinState);
void enquePendingListens(); void enquePendingListens();
@@ -19,12 +19,11 @@
#pragma once #pragma once
#include "utils/UUID.hpp"
#include "services/database/UserId.hpp" #include "services/database/UserId.hpp"
#include "utils/Logger.hpp" #include "utils/Logger.hpp"
#include "utils/UUID.hpp"
#define LOG(sev) LMS_LOG(SCROBBLING, sev) << "[listenbrainz] - " #define LOG(sev) LMS_LOG(SCROBBLING, sev) << "[listenbrainz] "
namespace Database namespace Database
{ {
@@ -34,6 +33,5 @@ namespace Database
namespace Scrobbling::ListenBrainz::Utils namespace Scrobbling::ListenBrainz::Utils
{ {
std::optional<UUID> getListenBrainzToken(Database::Session& session, Database::UserId userId); std::optional<UUID> getListenBrainzToken(Database::Session& session, Database::UserId userId);
std::string parseValidateToken(std::string_view msgBody); std::string parseValidateToken(std::string_view msgBody);
} }
@@ -55,59 +55,19 @@ namespace Scrobbling
using ArtistContainer = Database::RangeResults<Database::ArtistId>; using ArtistContainer = Database::RangeResults<Database::ArtistId>;
using ReleaseContainer = Database::RangeResults<Database::ReleaseId>; using ReleaseContainer = Database::RangeResults<Database::ReleaseId>;
using TrackContainer = Database::RangeResults<Database::TrackId>; using TrackContainer = Database::RangeResults<Database::TrackId>;
// From most recent to oldest // From most recent to oldest
virtual ArtistContainer getRecentArtists(Database::UserId userId, virtual ArtistContainer getRecentArtists(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, std::optional<Database::TrackArtistLinkType> linkType, Database::Range range) = 0;
const std::vector<Database::ClusterId>& clusterIds, virtual ReleaseContainer getRecentReleases(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) = 0;
std::optional<Database::TrackArtistLinkType> linkType, virtual TrackContainer getRecentTracks(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) = 0;
Database::Range range) = 0;
virtual ReleaseContainer getRecentReleases(Database::UserId userId,
const std::vector<Database::ClusterId>& clusterIds,
Database::Range range) = 0;
virtual TrackContainer getRecentTracks(Database::UserId userId,
const std::vector<Database::ClusterId>& clusterIds,
Database::Range range) = 0;
virtual Wt::WDateTime getLastListenDateTime(Database::UserId userId, Database::ReleaseId releaseId) = 0; virtual Wt::WDateTime getLastListenDateTime(Database::UserId userId, Database::ReleaseId releaseId) = 0;
virtual Wt::WDateTime getLastListenDateTime(Database::UserId userId, Database::TrackId trackId) = 0; virtual Wt::WDateTime getLastListenDateTime(Database::UserId userId, Database::TrackId trackId) = 0;
// Top // Top
virtual ArtistContainer getTopArtists(Database::UserId userId, virtual ArtistContainer getTopArtists(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, std::optional<Database::TrackArtistLinkType> linkType, Database::Range) = 0;
const std::vector<Database::ClusterId>& clusterIds, virtual ReleaseContainer getTopReleases(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) = 0;
std::optional<Database::TrackArtistLinkType> linkType, virtual TrackContainer getTopTracks(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) = 0;
Database::Range) = 0;
virtual ReleaseContainer getTopReleases(Database::UserId userId,
const std::vector<Database::ClusterId>& clusterIds,
Database::Range range) = 0;
virtual TrackContainer getTopTracks(Database::UserId userId,
const std::vector<Database::ClusterId>& clusterIds,
Database::Range range) = 0;
// Star
virtual void star(Database::UserId userId, Database::ArtistId artistId) = 0;
virtual void unstar(Database::UserId userId, Database::ArtistId artistId) = 0;
virtual bool isStarred(Database::UserId userId, Database::ArtistId artistId) = 0;
virtual Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::ArtistId artistId) = 0;
virtual ArtistContainer getStarredArtists(Database::UserId userId,
const std::vector<Database::ClusterId>& clusterIds,
std::optional<Database::TrackArtistLinkType> linkType,
Database::ArtistSortMethod sortMethod,
Database::Range range) = 0;
virtual void star(Database::UserId userId, Database::ReleaseId releaseId) = 0;
virtual void unstar(Database::UserId userId, Database::ReleaseId releaseId) = 0;
virtual bool isStarred(Database::UserId userId, Database::ReleaseId artistId) = 0;
virtual Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::ReleaseId artistId) = 0;
virtual ReleaseContainer getStarredReleases(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) = 0;
virtual void star(Database::UserId userId, Database::TrackId trackId) = 0;
virtual void unstar(Database::UserId userId, Database::TrackId trackId) = 0;
virtual bool isStarred(Database::UserId userId, Database::TrackId artistId) = 0;
virtual Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::TrackId artistId) = 0;
virtual TrackContainer getStarredTracks(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) = 0;
}; };
std::unique_ptr<IScrobblingService> createScrobblingService(boost::asio::io_service& ioService, Database::Db& db); std::unique_ptr<IScrobblingService> createScrobblingService(boost::asio::io_service& ioService, Database::Db& db);
+1
View File
@@ -42,6 +42,7 @@ target_link_libraries(lmssubsonic PRIVATE
lmsauth lmsauth
lmsav lmsav
lmsdatabase lmsdatabase
lmsfeedback
lmsrecommendation lmsrecommendation
lmsscanner lmsscanner
lmsscrobbling lmsscrobbling
@@ -41,6 +41,7 @@ namespace API::Subsonic
Database::UserId userId; Database::UserId userId;
ClientInfo clientInfo; ClientInfo clientInfo;
ProtocolVersion serverProtocolVersion; ProtocolVersion serverProtocolVersion;
bool enableOpenSubsonic{ true };
}; };
} }
+17 -3
View File
@@ -75,6 +75,18 @@ namespace API::Subsonic
return res; return res;
} }
std::unordered_set<std::string> readOpenSubsonicDisabledClients()
{
std::unordered_set<std::string> res;
Service<IConfig>::get()->visitStrings("api-open-subsonic-disabled-clients",
[&](std::string_view client)
{
res.emplace(std::string{ client });
}, { "DSub" });
return res;
}
std::string parameterMapToDebugString(const Wt::Http::ParameterMap& parameterMap) std::string parameterMapToDebugString(const Wt::Http::ParameterMap& parameterMap)
{ {
@@ -253,6 +265,7 @@ namespace API::Subsonic
SubsonicResource::SubsonicResource(Db& db) SubsonicResource::SubsonicResource(Db& db)
: _serverProtocolVersionsByClient{ readConfigProtocolVersions() } : _serverProtocolVersionsByClient{ readConfigProtocolVersions() }
, _openSubsonicDisabledClients{ readOpenSubsonicDisabledClients() }
, _db{ db } , _db{ db }
{ {
} }
@@ -273,13 +286,13 @@ namespace API::Subsonic
const ResponseFormat format{ getParameterAs<std::string>(request.getParameterMap(), "f").value_or("xml") == "json" ? ResponseFormat::json : ResponseFormat::xml }; const ResponseFormat format{ getParameterAs<std::string>(request.getParameterMap(), "f").value_or("xml") == "json" ? ResponseFormat::json : ResponseFormat::xml };
ProtocolVersion protocolVersion{ defaultServerProtocolVersion }; ProtocolVersion protocolVersion{ defaultServerProtocolVersion };
try try
{ {
// We need to parse client a soon as possible to make sure to answer with the right protocol version // We need to parse client a soon as possible to make sure to answer with the right protocol version
protocolVersion = getServerProtocolVersion(getMandatoryParameterAs<std::string>(request.getParameterMap(), "c")); protocolVersion = getServerProtocolVersion(getMandatoryParameterAs<std::string>(request.getParameterMap(), "c"));
RequestContext requestContext{ buildRequestContext(request) }; RequestContext requestContext{ buildRequestContext(request) };
auto itEntryPoint{ requestEntryPoints.find(requestPath) }; auto itEntryPoint{ requestEntryPoints.find(requestPath) };
if (itEntryPoint != requestEntryPoints.end()) if (itEntryPoint != requestEntryPoints.end())
{ {
@@ -364,8 +377,9 @@ namespace API::Subsonic
const Wt::Http::ParameterMap& parameters{ request.getParameterMap() }; const Wt::Http::ParameterMap& parameters{ request.getParameterMap() };
const ClientInfo clientInfo{ getClientInfo(parameters) }; const ClientInfo clientInfo{ getClientInfo(parameters) };
const Database::UserId userId{ authenticateUser(request, clientInfo) }; const Database::UserId userId{ authenticateUser(request, clientInfo) };
bool enableOpenSubsonic{ _openSubsonicDisabledClients.find(clientInfo.name) == std::cend(_openSubsonicDisabledClients) };
return { parameters, _db.getTLSSession(), userId, clientInfo, getServerProtocolVersion(clientInfo.name) }; return { parameters, _db.getTLSSession(), userId, clientInfo, getServerProtocolVersion(clientInfo.name), enableOpenSubsonic };
} }
Database::UserId SubsonicResource::authenticateUser(const Wt::Http::Request& request, const ClientInfo& clientInfo) Database::UserId SubsonicResource::authenticateUser(const Wt::Http::Request& request, const ClientInfo& clientInfo)
@@ -19,6 +19,7 @@
#pragma once #pragma once
#include <string> #include <string>
#include <unordered_set>
#include <unordered_map> #include <unordered_map>
#include <Wt/WResource.h> #include <Wt/WResource.h>
@@ -51,6 +52,7 @@ namespace API::Subsonic
Database::UserId authenticateUser(const Wt::Http::Request& request, const ClientInfo& clientInfo); Database::UserId authenticateUser(const Wt::Http::Request& request, const ClientInfo& clientInfo);
const std::unordered_map<std::string, ProtocolVersion> _serverProtocolVersionsByClient; const std::unordered_map<std::string, ProtocolVersion> _serverProtocolVersionsByClient;
const std::unordered_set<std::string> _openSubsonicDisabledClients;
Database::Db& _db; Database::Db& _db;
}; };
@@ -148,6 +148,7 @@ namespace API::Subsonic
} }
// OpenSubsonic mandatory fields // OpenSubsonic mandatory fields
// No big deal to send them even for legacy clients
responseNode.setAttribute("type", "lms"); responseNode.setAttribute("type", "lms");
responseNode.setAttribute("serverVersion", serverVersion); responseNode.setAttribute("serverVersion", serverVersion);
responseNode.setAttribute("openSubsonic", true); responseNode.setAttribute("openSubsonic", true);
@@ -25,6 +25,7 @@
#include "services/database/Session.hpp" #include "services/database/Session.hpp"
#include "services/database/Track.hpp" #include "services/database/Track.hpp"
#include "services/database/User.hpp" #include "services/database/User.hpp"
#include "services/feedback/IFeedbackService.hpp"
#include "services/scrobbling/IScrobblingService.hpp" #include "services/scrobbling/IScrobblingService.hpp"
#include "responses/Album.hpp" #include "responses/Album.hpp"
#include "responses/Artist.hpp" #include "responses/Artist.hpp"
@@ -36,8 +37,9 @@ namespace API::Subsonic
{ {
using namespace Database; using namespace Database;
namespace { namespace
Response handleGetAlbumListRequestCommon(const RequestContext& context, bool id3) {
Response handleGetAlbumListRequestCommon(RequestContext& context, bool id3)
{ {
// Mandatory params // Mandatory params
const std::string type{ getMandatoryParameterAs<std::string>(context.parameters, "type") }; const std::string type{ getMandatoryParameterAs<std::string>(context.parameters, "type") };
@@ -49,7 +51,8 @@ namespace API::Subsonic
const Range range{ offset, size }; const Range range{ offset, size };
RangeResults<ReleaseId> releases; RangeResults<ReleaseId> releases;
Scrobbling::IScrobblingService& scrobbling{ *Service<Scrobbling::IScrobblingService>::get() }; Scrobbling::IScrobblingService& scrobblingService{ *Service<Scrobbling::IScrobblingService>::get() };
Feedback::IFeedbackService& feedbackService{ *Service<Feedback::IFeedbackService>::get() };
auto transaction{ context.dbSession.createSharedTransaction() }; auto transaction{ context.dbSession.createSharedTransaction() };
@@ -63,11 +66,11 @@ namespace API::Subsonic
params.setSortMethod(ReleaseSortMethod::Name); params.setSortMethod(ReleaseSortMethod::Name);
params.setRange(range); params.setRange(range);
releases = Release::find(context.dbSession, params); releases = Release::findIds(context.dbSession, params);
} }
else if (type == "alphabeticalByArtist") else if (type == "alphabeticalByArtist")
{ {
releases = Release::findOrderedByArtist(context.dbSession, range); releases = Release::findIdsOrderedByArtist(context.dbSession, range);
} }
else if (type == "byGenre") else if (type == "byGenre")
{ {
@@ -83,7 +86,7 @@ namespace API::Subsonic
params.setSortMethod(ReleaseSortMethod::Name); params.setSortMethod(ReleaseSortMethod::Name);
params.setRange(range); params.setRange(range);
releases = Release::find(context.dbSession, params); releases = Release::findIds(context.dbSession, params);
} }
} }
} }
@@ -97,11 +100,11 @@ namespace API::Subsonic
params.setRange(range); params.setRange(range);
params.setDateRange(DateRange::fromYearRange(fromYear, toYear)); params.setDateRange(DateRange::fromYearRange(fromYear, toYear));
releases = Release::find(context.dbSession, params); releases = Release::findIds(context.dbSession, params);
} }
else if (type == "frequent") else if (type == "frequent")
{ {
releases = scrobbling.getTopReleases(context.userId, {}, range); releases = scrobblingService.getTopReleases(context.userId, {}, range);
} }
else if (type == "newest") else if (type == "newest")
{ {
@@ -109,7 +112,7 @@ namespace API::Subsonic
params.setSortMethod(ReleaseSortMethod::LastWritten); params.setSortMethod(ReleaseSortMethod::LastWritten);
params.setRange(range); params.setRange(range);
releases = Release::find(context.dbSession, params); releases = Release::findIds(context.dbSession, params);
} }
else if (type == "random") else if (type == "random")
{ {
@@ -119,15 +122,15 @@ namespace API::Subsonic
params.setSortMethod(ReleaseSortMethod::Random); params.setSortMethod(ReleaseSortMethod::Random);
params.setRange({ 0, size }); params.setRange({ 0, size });
releases = Release::find(context.dbSession, params); releases = Release::findIds(context.dbSession, params);
} }
else if (type == "recent") else if (type == "recent")
{ {
releases = scrobbling.getRecentReleases(context.userId, {}, range); releases = scrobblingService.getRecentReleases(context.userId, {}, range);
} }
else if (type == "starred") else if (type == "starred")
{ {
releases = scrobbling.getStarredReleases(context.userId, {}, range); releases = feedbackService.getStarredReleases(context.userId, {}, range);
} }
else else
throw NotImplementedGenericError{}; throw NotImplementedGenericError{};
@@ -138,7 +141,7 @@ namespace API::Subsonic
for (const ReleaseId releaseId : releases.results) for (const ReleaseId releaseId : releases.results)
{ {
const Release::pointer release{ Release::find(context.dbSession, releaseId) }; const Release::pointer release{ Release::find(context.dbSession, releaseId) };
albumListNode.addArrayChild("album", createAlbumNode(release, context.dbSession, user, id3)); albumListNode.addArrayChild("album", createAlbumNode(context, release, user, id3));
} }
return response; return response;
@@ -155,24 +158,24 @@ namespace API::Subsonic
Response response{ Response::createOkResponse(context.serverProtocolVersion) }; Response response{ Response::createOkResponse(context.serverProtocolVersion) };
Response::Node& starredNode{ response.createNode(id3 ? Response::Node::Key{ "starred2" } : Response::Node::Key{ "starred" }) }; Response::Node& starredNode{ response.createNode(id3 ? Response::Node::Key{ "starred2" } : Response::Node::Key{ "starred" }) };
Scrobbling::IScrobblingService& scrobbling{ *Service<Scrobbling::IScrobblingService>::get() }; Feedback::IFeedbackService& feedbackService{ *Service<Feedback::IFeedbackService>::get() };
for (const ArtistId artistId : scrobbling.getStarredArtists(context.userId, {} /* clusters */, std::nullopt /* linkType */, ArtistSortMethod::BySortName, Range{}).results) for (const ArtistId artistId : feedbackService.getStarredArtists(context.userId, {} /* clusters */, std::nullopt /* linkType */, ArtistSortMethod::BySortName, Range{}).results)
{ {
if (auto artist{ Artist::find(context.dbSession, artistId) }) if (auto artist{ Artist::find(context.dbSession, artistId) })
starredNode.addArrayChild("artist", createArtistNode(artist, context.dbSession, user, id3)); starredNode.addArrayChild("artist", createArtistNode(context, artist, user, id3));
} }
for (const ReleaseId releaseId : scrobbling.getStarredReleases(context.userId, {} /* clusters */, Range{}).results) for (const ReleaseId releaseId : feedbackService.getStarredReleases(context.userId, {} /* clusters */, Range{}).results)
{ {
if (auto release{ Release::find(context.dbSession, releaseId) }) if (auto release{ Release::find(context.dbSession, releaseId) })
starredNode.addArrayChild("album", createAlbumNode(release, context.dbSession, user, id3)); starredNode.addArrayChild("album", createAlbumNode(context, release, user, id3));
} }
for (const TrackId trackId : scrobbling.getStarredTracks(context.userId, {} /* clusters */, Range{}).results) for (const TrackId trackId : feedbackService.getStarredTracks(context.userId, {} /* clusters */, Range{}).results)
{ {
if (auto track{ Track::find(context.dbSession, trackId) }) if (auto track{ Track::find(context.dbSession, trackId) })
starredNode.addArrayChild("song", createSongNode(track, context.dbSession, user)); starredNode.addArrayChild("song", createSongNode(context, track, user));
} }
return response; return response;
@@ -201,16 +204,13 @@ namespace API::Subsonic
if (!user) if (!user)
throw UserNotAuthorizedError{}; throw UserNotAuthorizedError{};
const auto trackIds{ Track::find(context.dbSession, Track::FindParameters {}.setSortMethod(TrackSortMethod::Random).setRange({0, size})) }; const auto tracks{ Track::find(context.dbSession, Track::FindParameters {}.setSortMethod(TrackSortMethod::Random).setRange({0, size})) };
Response response{ Response::createOkResponse(context.serverProtocolVersion) }; Response response{ Response::createOkResponse(context.serverProtocolVersion) };
Response::Node& randomSongsNode{ response.createNode("randomSongs") }; Response::Node& randomSongsNode{ response.createNode("randomSongs") };
for (const TrackId trackId : trackIds.results) for (const Track::pointer& track : tracks.results)
{ randomSongsNode.addArrayChild("song", createSongNode(context, track, user));
const Track::pointer track{ Track::find(context.dbSession, trackId) };
randomSongsNode.addArrayChild("song", createSongNode(track, context.dbSession, user));
}
return response; return response;
} }
@@ -247,12 +247,9 @@ namespace API::Subsonic
params.setClusters({ cluster->getId() }); params.setClusters({ cluster->getId() });
params.setRange({ offset, size }); params.setRange({ offset, size });
auto trackIds{ Track::find(context.dbSession, params) }; const auto tracks{ Track::find(context.dbSession, params) };
for (const TrackId trackId : trackIds.results) for (const Track::pointer& track : tracks.results)
{ songsByGenreNode.addArrayChild("song", createSongNode(context, track, user));
const Track::pointer track{ Track::find(context.dbSession, trackId) };
songsByGenreNode.addArrayChild("song", createSongNode(track, context.dbSession, user));
}
return response; return response;
} }
@@ -49,7 +49,7 @@ namespace API::Subsonic
{ {
const TrackBookmark::pointer bookmark{ TrackBookmark::find(context.dbSession, bookmarkId) }; const TrackBookmark::pointer bookmark{ TrackBookmark::find(context.dbSession, bookmarkId) };
Response::Node bookmarkNode{ createBookmarkNode(bookmark) }; Response::Node bookmarkNode{ createBookmarkNode(bookmark) };
bookmarkNode.addArrayChild("entry", createSongNode(bookmark->getTrack(), context.dbSession, user)); bookmarkNode.addArrayChild("entry", createSongNode(context, bookmark->getTrack(), user));
bookmarksNode.addArrayChild("bookmark", std::move(bookmarkNode)); bookmarksNode.addArrayChild("bookmark", std::move(bookmarkNode));
} }

Some files were not shown because too many files have changed in this diff Show More