Merge branch 'develop' for release v3.43.0
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
|
||||
_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
|
||||
* Recommendation engine
|
||||
@@ -34,12 +34,14 @@ _LMS_ provides several ways to help you find the music you like:
|
||||
* 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
|
||||
_LMS_ relies exclusively on tags to organize your music collection.
|
||||
|
||||
### Filtering
|
||||
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).
|
||||
|
||||
|
||||
@@ -34,17 +34,15 @@
|
||||
</div>
|
||||
</div>
|
||||
<legend>${tr:Lms.Admin.Database.scan-options}</legend>
|
||||
${<if-has-recommendation-engine>}
|
||||
<div class="col-lg-6">
|
||||
<label class="form-label" for="${id:recommendation-engine-type}">
|
||||
${tr:Lms.Admin.Database.recommendation-engine-type}
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="${id:similarity-engine-type}">
|
||||
${tr:Lms.Admin.Database.similarity-engine-type}
|
||||
</label>
|
||||
${recommendation-engine-type class="form-control"}
|
||||
${similarity-engine-type class="form-control"}
|
||||
<div class="invalid-feedback">
|
||||
${recommendation-engine-type-info}
|
||||
${similarity-engine-type-info}
|
||||
</div>
|
||||
</div>
|
||||
${</if-has-recommendation-engine>}
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="${id:clusters}">
|
||||
${tr:Lms.clusters}
|
||||
|
||||
@@ -53,13 +53,13 @@
|
||||
<message id="Lms.Admin.Database.never">Never</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.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-launched">Scan launched!</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.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-start-time">Update start time</message>
|
||||
<message id="Lms.Admin.Database.weekly">Weekly</message>
|
||||
@@ -240,10 +240,14 @@
|
||||
<!--Settings-->
|
||||
<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.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.default-transcode-bitrate">Default transcode bitrate</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.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.password-bad">Bad password</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-no-rg-info">ReplayGain preamp (if no info)</message>
|
||||
<message id="Lms.Settings.scrobbling">Scrobbling</message>
|
||||
<message id="Lms.Settings.scrobbling.scrobbler">Scrobbler</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.services">Services</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.all-artists">All artists</message>
|
||||
|
||||
@@ -53,13 +53,13 @@
|
||||
<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-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-launched">Scan lancé !</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.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-start-time">Heure de départ de la mise à jour</message>
|
||||
<message id="Lms.Admin.Database.weekly">Toutes les semaines</message>
|
||||
@@ -240,10 +240,14 @@
|
||||
<!--Settings-->
|
||||
<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.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.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.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.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>
|
||||
@@ -255,10 +259,7 @@
|
||||
<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.scrobbling">Scrobbling</message>
|
||||
<message id="Lms.Settings.scrobbling.scrobbler">Scrobbler</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.services">Services</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.all-artists">Tous les artistes</message>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<message id="Lms.cancel">Annulla</message>
|
||||
<message id="Lms.clusters">Tags</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.edit">Modifica</message>
|
||||
<message id="Lms.loading">Caricamento...</message>
|
||||
@@ -53,13 +53,13 @@
|
||||
<message id="Lms.Admin.Database.never">Mai</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-launched">Scansione avviata!</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.update-period">Frequenza di aggiornamento</message>
|
||||
<message id="Lms.Admin.Database.update-start-time">Orario di aggiornamento</message>
|
||||
<message id="Lms.Admin.Database.weekly">Settimanale</message>
|
||||
@@ -240,10 +240,14 @@
|
||||
<!--Settings-->
|
||||
<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.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.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-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.all-artists">Tutti gli artisti</message>
|
||||
<message id="Lms.Settings.subsonic-artist-list-mode.release-artists">Artisti album</message>
|
||||
|
||||
@@ -53,13 +53,13 @@
|
||||
<message id="Lms.Admin.Database.never">从不</message>
|
||||
<message id="Lms.Admin.Database.path">媒体根目录</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-launched">扫描已完成!</message>
|
||||
<message id="Lms.Admin.Database.scan-options">扫描选项</message>
|
||||
<message id="Lms.Admin.Database.settings-saved">新设置已保存!</message>
|
||||
|
||||
|
||||
|
||||
<message id="Lms.Admin.Database.update-period">更新周期</message>
|
||||
<message id="Lms.Admin.Database.update-start-time">更新开始时间</message>
|
||||
<message id="Lms.Admin.Database.weekly">每周</message>
|
||||
@@ -240,10 +240,14 @@
|
||||
<!--Settings-->
|
||||
<message id="Lms.Settings.audio">音频</message>
|
||||
<message id="Lms.Settings.audio-settings-are-local">这些音频设置仅保存在您的本地浏览器!</message>
|
||||
|
||||
|
||||
|
||||
<message id="Lms.Settings.change-password">更改密码</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.password-bad">无效密码</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.all-artists">所有歌手</message>
|
||||
<message id="Lms.Settings.subsonic-artist-list-mode.release-artists">专辑歌手</message>
|
||||
|
||||
+22
-22
@@ -3,29 +3,29 @@
|
||||
<!--FORMS message blocks-->
|
||||
|
||||
<message id="Lms.Explore.Search.template">
|
||||
<ul class="nav nav-pills mb-3" id="myTab" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<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>
|
||||
<li class="nav-item" role="presentation">
|
||||
<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>
|
||||
<li class="nav-item" role="presentation">
|
||||
<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>
|
||||
</ul>
|
||||
<div class="tab-content" id="myTabContent">
|
||||
<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 class="mb-3">
|
||||
<ul class="nav nav-pills mb-3">
|
||||
<li class="nav-item">
|
||||
${releases class="nav-link"}
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
${artists class="nav-link"}
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
${tracks class="nav-link"}
|
||||
</li>
|
||||
</ul>
|
||||
</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>
|
||||
|
||||
</messages>
|
||||
|
||||
+17
-8
@@ -28,9 +28,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<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}
|
||||
; </label>
|
||||
</label>
|
||||
<div class="input-group mb-3">
|
||||
${transcode-bitrate class="form-control"}
|
||||
<span class="input-group-text">kbps</span>
|
||||
@@ -105,19 +105,28 @@
|
||||
</div>
|
||||
</div>
|
||||
${</if-has-subsonic-api>}
|
||||
<legend>${tr:Lms.Settings.scrobbling}</legend>
|
||||
<legend>${tr:Lms.Settings.services}</legend>
|
||||
<div class="col-lg-6">
|
||||
<label class="form-label" for="${id:scrobbler}">
|
||||
${tr:Lms.Settings.scrobbling.scrobbler}
|
||||
<label class="form-label" for="${id:scrobbling-backend}">
|
||||
${tr:Lms.Settings.scrobbling}
|
||||
</label>
|
||||
${scrobbler class="form-control"}
|
||||
${scrobbling-backend class="form-control"}
|
||||
<div class="invalid-feedback">
|
||||
${scrobbler-info class="help-block"}
|
||||
${scrobbling-backend-info class="help-block"}
|
||||
</div>
|
||||
</div>
|
||||
<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}">
|
||||
${tr:Lms.Settings.scrobbling.listenbrainz-token}
|
||||
${tr:Lms.Settings.backend.listenbrainz-token}
|
||||
</label>
|
||||
${listenbrainz-token class="form-control"}
|
||||
<div class="invalid-feedback">
|
||||
|
||||
+4
-1
@@ -63,6 +63,9 @@ api-subsonic = true;
|
||||
# Main usage is to make auto detections for the 'p' (password) parameter work
|
||||
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
|
||||
demo = false;
|
||||
|
||||
@@ -85,4 +88,4 @@ playqueue-max-entry-count = 1000;
|
||||
scanner-skip-duplicate-mbid = false;
|
||||
|
||||
# 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)
|
||||
response.setContentLength(*_estimatedContentLength);
|
||||
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)
|
||||
{
|
||||
LMS_LOG(TRANSCODE, DEBUG) << "Writing " << _bytesReadyCount << " bytes back to client";
|
||||
|
||||
response.out().write(reinterpret_cast<const char *>(&_buffer[0]), _bytesReadyCount);
|
||||
_totalServedByteCount += _bytesReadyCount;
|
||||
_bytesReadyCount = 0;
|
||||
@@ -71,6 +73,8 @@ namespace Av
|
||||
continuation->waitForMoreData();
|
||||
_transcoder.asyncRead(_buffer.data(), _buffer.size(), [=](std::size_t nbBytesRead)
|
||||
{
|
||||
LMS_LOG(TRANSCODE, DEBUG) << "Have " << nbBytesRead << " more bytes to send back";
|
||||
|
||||
assert(_bytesReadyCount == 0);
|
||||
_bytesReadyCount = nbBytesRead;
|
||||
continuation->haveMoreData();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
add_subdirectory(auth)
|
||||
add_subdirectory(cover)
|
||||
add_subdirectory(database)
|
||||
add_subdirectory(feedback)
|
||||
add_subdirectory(recommendation)
|
||||
add_subdirectory(scanner)
|
||||
add_subdirectory(scrobbling)
|
||||
|
||||
@@ -34,445 +34,425 @@
|
||||
#include "utils/String.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>
|
||||
getTrackInfo(Database::Session& dbSession, Database::TrackId trackId)
|
||||
{
|
||||
std::optional<TrackInfo> res;
|
||||
namespace
|
||||
{
|
||||
struct TrackInfo
|
||||
{
|
||||
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)};
|
||||
if (!track)
|
||||
return res;
|
||||
auto transaction{ dbSession.createSharedTransaction() };
|
||||
|
||||
res = TrackInfo {};
|
||||
const Database::Track::pointer track{ Database::Track::find(dbSession, trackId) };
|
||||
if (!track)
|
||||
return res;
|
||||
|
||||
res->hasCover = track->hasCover();
|
||||
res->trackPath = track->getPath();
|
||||
res = TrackInfo{};
|
||||
|
||||
if (const Database::Release::pointer& release {track->getRelease()})
|
||||
{
|
||||
res->releaseId = release->getId();
|
||||
if (release->getTotalDisc() > 1)
|
||||
res->isMultiDisc = true;
|
||||
}
|
||||
res->hasCover = track->hasCover();
|
||||
res->trackPath = track->getPath();
|
||||
|
||||
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()
|
||||
{
|
||||
std::vector<std::string> res;
|
||||
return res;
|
||||
}
|
||||
|
||||
Service<IConfig>::get()->visitStrings("cover-preferred-file-names",
|
||||
[&res](std::string_view fileName)
|
||||
{
|
||||
res.emplace_back(fileName);
|
||||
}, {"cover", "front"});
|
||||
std::vector<std::string> constructPreferredFileNames()
|
||||
{
|
||||
std::vector<std::string> res;
|
||||
|
||||
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
|
||||
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));
|
||||
}
|
||||
std::unique_ptr<ICoverService> createCoverService(Database::Db& db, const std::filesystem::path& execPath, const std::filesystem::path& defaultCoverPath)
|
||||
{
|
||||
return std::make_unique<CoverService>(db, execPath, defaultCoverPath);
|
||||
}
|
||||
|
||||
std::unique_ptr<ICoverService>
|
||||
createCoverService(Database::Db& db, const std::filesystem::path& execPath, const std::filesystem::path& defaultCoverPath)
|
||||
{
|
||||
return std::make_unique<CoverService>(db, execPath, defaultCoverPath);
|
||||
}
|
||||
using namespace Image;
|
||||
|
||||
CoverService::CoverService(Database::Db& db,
|
||||
const std::filesystem::path& execPath,
|
||||
const std::filesystem::path& defaultCoverPath)
|
||||
: _db {db}
|
||||
, _defaultCoverPath {defaultCoverPath}
|
||||
, _maxCacheSize {Service<IConfig>::get()->getULong("cover-max-cache-size", 30) * 1000 * 1000}
|
||||
, _maxFileSize {Service<IConfig>::get()->getULong("cover-max-file-size", 10) * 1000 * 1000}
|
||||
, _preferredFileNames {constructPreferredFileNames()}
|
||||
CoverService::CoverService(Database::Db& db,
|
||||
const std::filesystem::path& execPath,
|
||||
const std::filesystem::path& defaultCoverPath)
|
||||
: _db{ db }
|
||||
, _defaultCoverPath{ defaultCoverPath }
|
||||
, _maxCacheSize{ Service<IConfig>::get()->getULong("cover-max-cache-size", 30) * 1000 * 1000 }
|
||||
, _maxFileSize{ Service<IConfig>::get()->getULong("cover-max-file-size", 10) * 1000 * 1000 }
|
||||
, _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) << "Max cache size = " << _maxCacheSize;
|
||||
LMS_LOG(COVER, INFO) << "Max file size = " << _maxFileSize;
|
||||
LMS_LOG(COVER, INFO) << "Preferred file names: " << StringUtils::joinStrings(_preferredFileNames, ",");
|
||||
LMS_LOG(COVER, INFO) << "Default cover path = '" << _defaultCoverPath.string() << "'";
|
||||
LMS_LOG(COVER, INFO) << "Max cache size = " << _maxCacheSize;
|
||||
LMS_LOG(COVER, INFO) << "Max file size = " << _maxFileSize;
|
||||
LMS_LOG(COVER, INFO) << "Preferred file names: " << StringUtils::joinStrings(_preferredFileNames, ",");
|
||||
|
||||
#if LMS_SUPPORT_IMAGE_GM
|
||||
GraphicsMagick::init(execPath);
|
||||
GraphicsMagick::init(execPath);
|
||||
#else
|
||||
(void)execPath;
|
||||
(void)execPath;
|
||||
#endif
|
||||
|
||||
try
|
||||
{
|
||||
getDefault(512);
|
||||
}
|
||||
catch (const Image::ImageException& e)
|
||||
{
|
||||
throw LmsException("Cannot read default cover file '" + _defaultCoverPath.string() + "': " + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<IEncodedImage>
|
||||
CoverService::getFromAvMediaFile(const Av::IAudioFile& input, ImageSize width) const
|
||||
{
|
||||
std::unique_ptr<IEncodedImage> image;
|
||||
|
||||
input.visitAttachedPictures([&](const Av::Picture& picture)
|
||||
{
|
||||
if (image)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
std::unique_ptr<IRawImage> rawImage {decodeImage(picture.data, picture.dataSize)};
|
||||
rawImage->resize(width);
|
||||
image = rawImage->encodeToJPEG(_jpegQuality);
|
||||
}
|
||||
catch (const Image::ImageException& e)
|
||||
{
|
||||
LMS_LOG(COVER, ERROR) << "Cannot read embedded cover: " << e.what();
|
||||
}
|
||||
});
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
std::unique_ptr<IEncodedImage>
|
||||
CoverService::getFromCoverFile(const std::filesystem::path& p, ImageSize width) const
|
||||
{
|
||||
std::unique_ptr<IEncodedImage> image;
|
||||
|
||||
try
|
||||
{
|
||||
std::unique_ptr<IRawImage> rawImage {decodeImage(p)};
|
||||
rawImage->resize(width);
|
||||
image = rawImage->encodeToJPEG(_jpegQuality);
|
||||
}
|
||||
catch (const ImageException& e)
|
||||
{
|
||||
LMS_LOG(COVER, ERROR) << "Cannot read cover in file '" << p.string() << "': " << e.what();
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
std::shared_ptr<IEncodedImage>
|
||||
CoverService::getDefault(ImageSize width)
|
||||
{
|
||||
{
|
||||
std::shared_lock lock {_cacheMutex};
|
||||
|
||||
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))
|
||||
return it->second;
|
||||
|
||||
std::shared_ptr<IEncodedImage> image {getFromCoverFile(_defaultCoverPath, width)};
|
||||
_defaultCoverCache[width] = 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)};
|
||||
|
||||
auto tryLoadImageFromFilename = [&](std::string_view fileName)
|
||||
{
|
||||
std::unique_ptr<IEncodedImage> image;
|
||||
|
||||
auto range {coverPaths.equal_range(std::string {fileName})};
|
||||
for (auto it {range.first}; it != range.second; ++it)
|
||||
{
|
||||
image = getFromCoverFile(it->second, width);
|
||||
if (image)
|
||||
break;
|
||||
}
|
||||
return image;
|
||||
};
|
||||
|
||||
std::unique_ptr<IEncodedImage> image;
|
||||
|
||||
for (std::string_view filename : _preferredFileNames)
|
||||
{
|
||||
image = tryLoadImageFromFilename(filename);
|
||||
if (image)
|
||||
return image;
|
||||
}
|
||||
|
||||
// Just pick one
|
||||
for (const auto& [filename, coverPath] : coverPaths)
|
||||
{
|
||||
image = getFromCoverFile(coverPath, width);
|
||||
if (image)
|
||||
return image;
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
std::unique_ptr<IEncodedImage>
|
||||
CoverService::getFromSameNamedFile(const std::filesystem::path& filePath, ImageSize width) const
|
||||
{
|
||||
std::unique_ptr<IEncodedImage> res;
|
||||
|
||||
std::filesystem::path coverPath {filePath};
|
||||
for (const std::filesystem::path& extension : _fileExtensions)
|
||||
{
|
||||
coverPath.replace_extension(extension);
|
||||
|
||||
if (!checkCoverFile(coverPath))
|
||||
continue;
|
||||
|
||||
res = getFromCoverFile(coverPath, width);
|
||||
if (res)
|
||||
break;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
bool
|
||||
CoverService::checkCoverFile(const std::filesystem::path& filePath) const
|
||||
{
|
||||
std::error_code ec;
|
||||
|
||||
if (!isFileSupported(filePath, _fileExtensions))
|
||||
return false;
|
||||
|
||||
if (!std::filesystem::exists(filePath, ec))
|
||||
return false;
|
||||
|
||||
if (!std::filesystem::is_regular_file(filePath, ec))
|
||||
return false;
|
||||
|
||||
if (std::filesystem::file_size(filePath, ec) > _maxFileSize && !ec)
|
||||
{
|
||||
LMS_LOG(COVER, INFO) << "Cover file '" << filePath.string() << " is too big (" << std::filesystem::file_size(filePath, ec) << "), limit is " << _maxFileSize;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::multimap<std::string, std::filesystem::path>
|
||||
CoverService::getCoverPaths(const std::filesystem::path& directoryPath) const
|
||||
{
|
||||
std::multimap<std::string, std::filesystem::path> res;
|
||||
std::error_code ec;
|
||||
|
||||
std::filesystem::directory_iterator itPath(directoryPath, ec);
|
||||
std::filesystem::directory_iterator itEnd;
|
||||
while (!ec && itPath != itEnd)
|
||||
{
|
||||
const std::filesystem::path& path {*itPath};
|
||||
|
||||
if (checkCoverFile(path))
|
||||
res.emplace(std::filesystem::path{ path }.filename().replace_extension("").string(), path);
|
||||
|
||||
itPath.increment(ec);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::unique_ptr<IEncodedImage>
|
||||
CoverService::getFromTrack(const std::filesystem::path& p, ImageSize width) const
|
||||
{
|
||||
std::unique_ptr<IEncodedImage> image;
|
||||
|
||||
try
|
||||
{
|
||||
image = getFromAvMediaFile(*Av::parseAudioFile(p), width);
|
||||
}
|
||||
catch (Av::Exception& e)
|
||||
{
|
||||
LMS_LOG(COVER, ERROR) << "Cannot get covers from track " << p.string() << ": " << e.what();
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
std::shared_ptr<IEncodedImage>
|
||||
CoverService::getFromTrack(Database::TrackId trackId, ImageSize width)
|
||||
{
|
||||
return getFromTrack(_db.getTLSSession(), trackId, width, true /* allow release fallback*/);
|
||||
}
|
||||
|
||||
std::shared_ptr<IEncodedImage>
|
||||
CoverService::getFromTrack(Database::Session& dbSession, Database::TrackId trackId, ImageSize width, bool allowReleaseFallback)
|
||||
{
|
||||
using namespace Database;
|
||||
|
||||
const CacheEntryDesc cacheEntryDesc {trackId, width};
|
||||
|
||||
std::shared_ptr<IEncodedImage> cover {loadFromCache(cacheEntryDesc)};
|
||||
if (cover)
|
||||
return cover;
|
||||
|
||||
if (const std::optional<TrackInfo> trackInfo {getTrackInfo(dbSession, trackId)})
|
||||
{
|
||||
if (trackInfo->hasCover)
|
||||
cover = getFromTrack(trackInfo->trackPath, width);
|
||||
|
||||
if (!cover)
|
||||
cover = getFromSameNamedFile(trackInfo->trackPath, width);
|
||||
|
||||
if (!cover && trackInfo->releaseId && allowReleaseFallback)
|
||||
cover = getFromRelease(*trackInfo->releaseId, width);
|
||||
|
||||
if (!cover && trackInfo->isMultiDisc)
|
||||
{
|
||||
if (trackInfo->trackPath.parent_path().has_parent_path())
|
||||
cover = getFromDirectory(trackInfo->trackPath.parent_path().parent_path(), width);
|
||||
}
|
||||
}
|
||||
|
||||
if (!cover)
|
||||
cover = getDefault(width);
|
||||
|
||||
if (cover)
|
||||
saveToCache(cacheEntryDesc, cover);
|
||||
|
||||
return cover;
|
||||
}
|
||||
|
||||
std::shared_ptr<IEncodedImage>
|
||||
CoverService::getFromRelease(Database::ReleaseId releaseId, ImageSize width)
|
||||
{
|
||||
using namespace Database;
|
||||
const CacheEntryDesc cacheEntryDesc {releaseId, width};
|
||||
|
||||
std::shared_ptr<IEncodedImage> cover {loadFromCache(cacheEntryDesc)};
|
||||
if (cover)
|
||||
return cover;
|
||||
|
||||
struct ReleaseInfo
|
||||
{
|
||||
TrackId firstTrackId;
|
||||
std::filesystem::path releaseDirectory;
|
||||
};
|
||||
|
||||
Session& session {_db.getTLSSession()};
|
||||
|
||||
auto getReleaseInfo {[&]
|
||||
{
|
||||
std::optional<ReleaseInfo> res;
|
||||
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
const auto tracks {Track::find(session, Track::FindParameters {}.setRelease(releaseId).setRange({0, 1}).setSortMethod(TrackSortMethod::Release))};
|
||||
|
||||
if (!tracks.results.empty())
|
||||
{
|
||||
if (const Track::pointer track {Track::find(session, tracks.results.front())})
|
||||
{
|
||||
res = ReleaseInfo {};
|
||||
res->firstTrackId = track->getId();
|
||||
res->releaseDirectory = track->getPath().parent_path();
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}};
|
||||
|
||||
if (const std::optional<ReleaseInfo> releaseInfo {getReleaseInfo()})
|
||||
{
|
||||
cover = getFromDirectory(releaseInfo->releaseDirectory, width);
|
||||
if (!cover)
|
||||
cover = getFromTrack(session, releaseInfo->firstTrackId, width, false /* no release fallback */);
|
||||
}
|
||||
|
||||
if (!cover)
|
||||
cover = getDefault(width);
|
||||
|
||||
if (cover)
|
||||
saveToCache(cacheEntryDesc, cover);
|
||||
|
||||
return cover;
|
||||
}
|
||||
|
||||
void
|
||||
CoverService::flushCache()
|
||||
{
|
||||
std::unique_lock lock {_cacheMutex};
|
||||
|
||||
LMS_LOG(COVER, DEBUG) << "Cache stats: hits = " << _cacheHits << ", misses = " << _cacheMisses << ", nb entries = " << _cache.size() << ", size = " << _cacheSize;
|
||||
_cacheHits = 0;
|
||||
_cacheMisses = 0;
|
||||
_cacheSize = 0;
|
||||
_cache.clear();
|
||||
}
|
||||
|
||||
void
|
||||
CoverService::setJpegQuality(unsigned quality)
|
||||
{
|
||||
_jpegQuality = Utils::clamp<unsigned>(quality, 1, 100);
|
||||
|
||||
LMS_LOG(COVER, INFO) << "JPEG export quality = " << _jpegQuality;
|
||||
}
|
||||
|
||||
void
|
||||
CoverService::saveToCache(const CacheEntryDesc& entryDesc, std::shared_ptr<IEncodedImage> image)
|
||||
{
|
||||
std::unique_lock lock {_cacheMutex};
|
||||
|
||||
while (_cacheSize + image->getDataSize() > _maxCacheSize && !_cache.empty())
|
||||
{
|
||||
auto itRandom {Random::pickRandom(_cache)};
|
||||
_cacheSize -= itRandom->second->getDataSize();
|
||||
_cache.erase(itRandom);
|
||||
}
|
||||
|
||||
_cacheSize += image->getDataSize();
|
||||
_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;
|
||||
}
|
||||
try
|
||||
{
|
||||
getDefault(512);
|
||||
}
|
||||
catch (const Image::ImageException& e)
|
||||
{
|
||||
throw LmsException("Cannot read default cover file '" + _defaultCoverPath.string() + "': " + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<IEncodedImage> CoverService::getFromAvMediaFile(const Av::IAudioFile& input, ImageSize width) const
|
||||
{
|
||||
std::unique_ptr<IEncodedImage> image;
|
||||
|
||||
input.visitAttachedPictures([&](const Av::Picture& picture)
|
||||
{
|
||||
if (image)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
std::unique_ptr<IRawImage> rawImage{ decodeImage(picture.data, picture.dataSize) };
|
||||
rawImage->resize(width);
|
||||
image = rawImage->encodeToJPEG(_jpegQuality);
|
||||
}
|
||||
catch (const Image::ImageException& e)
|
||||
{
|
||||
LMS_LOG(COVER, ERROR) << "Cannot read embedded cover: " << e.what();
|
||||
}
|
||||
});
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
std::unique_ptr<IEncodedImage> CoverService::getFromCoverFile(const std::filesystem::path& p, ImageSize width) const
|
||||
{
|
||||
std::unique_ptr<IEncodedImage> image;
|
||||
|
||||
try
|
||||
{
|
||||
std::unique_ptr<IRawImage> rawImage{ decodeImage(p) };
|
||||
rawImage->resize(width);
|
||||
image = rawImage->encodeToJPEG(_jpegQuality);
|
||||
}
|
||||
catch (const ImageException& e)
|
||||
{
|
||||
LMS_LOG(COVER, ERROR) << "Cannot read cover in file '" << p.string() << "': " << e.what();
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
std::shared_ptr<IEncodedImage> CoverService::getDefault(ImageSize width)
|
||||
{
|
||||
{
|
||||
std::shared_lock lock{ _cacheMutex };
|
||||
|
||||
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))
|
||||
return it->second;
|
||||
|
||||
std::shared_ptr<IEncodedImage> image{ getFromCoverFile(_defaultCoverPath, width) };
|
||||
_defaultCoverCache[width] = 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) };
|
||||
|
||||
auto tryLoadImageFromFilename = [&](std::string_view fileName)
|
||||
{
|
||||
std::unique_ptr<IEncodedImage> image;
|
||||
|
||||
auto range{ coverPaths.equal_range(std::string {fileName}) };
|
||||
for (auto it{ range.first }; it != range.second; ++it)
|
||||
{
|
||||
image = getFromCoverFile(it->second, width);
|
||||
if (image)
|
||||
break;
|
||||
}
|
||||
return image;
|
||||
};
|
||||
|
||||
std::unique_ptr<IEncodedImage> image;
|
||||
|
||||
for (std::string_view filename : _preferredFileNames)
|
||||
{
|
||||
image = tryLoadImageFromFilename(filename);
|
||||
if (image)
|
||||
return image;
|
||||
}
|
||||
|
||||
// Just pick one
|
||||
for (const auto& [filename, coverPath] : coverPaths)
|
||||
{
|
||||
image = getFromCoverFile(coverPath, width);
|
||||
if (image)
|
||||
return image;
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
std::unique_ptr<IEncodedImage> CoverService::getFromSameNamedFile(const std::filesystem::path& filePath, ImageSize width) const
|
||||
{
|
||||
std::unique_ptr<IEncodedImage> res;
|
||||
|
||||
std::filesystem::path coverPath{ filePath };
|
||||
for (const std::filesystem::path& extension : _fileExtensions)
|
||||
{
|
||||
coverPath.replace_extension(extension);
|
||||
|
||||
if (!checkCoverFile(coverPath))
|
||||
continue;
|
||||
|
||||
res = getFromCoverFile(coverPath, width);
|
||||
if (res)
|
||||
break;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
bool CoverService::checkCoverFile(const std::filesystem::path& filePath) const
|
||||
{
|
||||
std::error_code ec;
|
||||
|
||||
if (!isFileSupported(filePath, _fileExtensions))
|
||||
return false;
|
||||
|
||||
if (!std::filesystem::exists(filePath, ec))
|
||||
return false;
|
||||
|
||||
if (!std::filesystem::is_regular_file(filePath, ec))
|
||||
return false;
|
||||
|
||||
if (std::filesystem::file_size(filePath, ec) > _maxFileSize && !ec)
|
||||
{
|
||||
LMS_LOG(COVER, INFO) << "Cover file '" << filePath.string() << " is too big (" << std::filesystem::file_size(filePath, ec) << "), limit is " << _maxFileSize;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::multimap<std::string, std::filesystem::path> CoverService::getCoverPaths(const std::filesystem::path& directoryPath) const
|
||||
{
|
||||
std::multimap<std::string, std::filesystem::path> res;
|
||||
std::error_code ec;
|
||||
|
||||
std::filesystem::directory_iterator itPath(directoryPath, ec);
|
||||
std::filesystem::directory_iterator itEnd;
|
||||
while (!ec && itPath != itEnd)
|
||||
{
|
||||
const std::filesystem::path& path{ *itPath };
|
||||
|
||||
if (checkCoverFile(path))
|
||||
res.emplace(std::filesystem::path{ path }.filename().replace_extension("").string(), path);
|
||||
|
||||
itPath.increment(ec);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::unique_ptr<IEncodedImage> CoverService::getFromTrack(const std::filesystem::path& p, ImageSize width) const
|
||||
{
|
||||
std::unique_ptr<IEncodedImage> image;
|
||||
|
||||
try
|
||||
{
|
||||
image = getFromAvMediaFile(*Av::parseAudioFile(p), width);
|
||||
}
|
||||
catch (Av::Exception& e)
|
||||
{
|
||||
LMS_LOG(COVER, ERROR) << "Cannot get covers from track " << p.string() << ": " << e.what();
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
std::shared_ptr<IEncodedImage> CoverService::getFromTrack(Database::TrackId trackId, ImageSize width)
|
||||
{
|
||||
return getFromTrack(_db.getTLSSession(), trackId, width, true /* allow release fallback*/);
|
||||
}
|
||||
|
||||
std::shared_ptr<IEncodedImage> CoverService::getFromTrack(Database::Session& dbSession, Database::TrackId trackId, ImageSize width, bool allowReleaseFallback)
|
||||
{
|
||||
using namespace Database;
|
||||
|
||||
const CacheEntryDesc cacheEntryDesc{ trackId, width };
|
||||
|
||||
std::shared_ptr<IEncodedImage> cover{ loadFromCache(cacheEntryDesc) };
|
||||
if (cover)
|
||||
return cover;
|
||||
|
||||
if (const std::optional<TrackInfo> trackInfo{ getTrackInfo(dbSession, trackId) })
|
||||
{
|
||||
if (trackInfo->hasCover)
|
||||
cover = getFromTrack(trackInfo->trackPath, width);
|
||||
|
||||
if (!cover)
|
||||
cover = getFromSameNamedFile(trackInfo->trackPath, width);
|
||||
|
||||
if (!cover && trackInfo->releaseId && allowReleaseFallback)
|
||||
cover = getFromRelease(*trackInfo->releaseId, width);
|
||||
|
||||
if (!cover && trackInfo->isMultiDisc)
|
||||
{
|
||||
if (trackInfo->trackPath.parent_path().has_parent_path())
|
||||
cover = getFromDirectory(trackInfo->trackPath.parent_path().parent_path(), width);
|
||||
}
|
||||
}
|
||||
|
||||
if (!cover)
|
||||
cover = getDefault(width);
|
||||
|
||||
if (cover)
|
||||
saveToCache(cacheEntryDesc, cover);
|
||||
|
||||
return cover;
|
||||
}
|
||||
|
||||
std::shared_ptr<IEncodedImage> CoverService::getFromRelease(Database::ReleaseId releaseId, ImageSize width)
|
||||
{
|
||||
using namespace Database;
|
||||
const CacheEntryDesc cacheEntryDesc{ releaseId, width };
|
||||
|
||||
std::shared_ptr<IEncodedImage> cover{ loadFromCache(cacheEntryDesc) };
|
||||
if (cover)
|
||||
return cover;
|
||||
|
||||
struct ReleaseInfo
|
||||
{
|
||||
TrackId firstTrackId;
|
||||
std::filesystem::path releaseDirectory;
|
||||
};
|
||||
|
||||
Session& session{ _db.getTLSSession() };
|
||||
|
||||
auto getReleaseInfo{ [&]
|
||||
{
|
||||
std::optional<ReleaseInfo> res;
|
||||
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
|
||||
const auto tracks{ Track::find(session, Track::FindParameters {}.setRelease(releaseId).setRange({0, 1}).setSortMethod(TrackSortMethod::Release)) };
|
||||
|
||||
if (!tracks.results.empty())
|
||||
{
|
||||
const Track::pointer& track{ tracks.results.front() };
|
||||
res = ReleaseInfo{};
|
||||
res->firstTrackId = track->getId();
|
||||
res->releaseDirectory = track->getPath().parent_path();
|
||||
}
|
||||
|
||||
return res;
|
||||
} };
|
||||
|
||||
if (const std::optional<ReleaseInfo> releaseInfo{ getReleaseInfo() })
|
||||
{
|
||||
cover = getFromDirectory(releaseInfo->releaseDirectory, width);
|
||||
if (!cover)
|
||||
cover = getFromTrack(session, releaseInfo->firstTrackId, width, false /* no release fallback */);
|
||||
}
|
||||
|
||||
if (!cover)
|
||||
cover = getDefault(width);
|
||||
|
||||
if (cover)
|
||||
saveToCache(cacheEntryDesc, cover);
|
||||
|
||||
return cover;
|
||||
}
|
||||
|
||||
void CoverService::flushCache()
|
||||
{
|
||||
std::unique_lock lock{ _cacheMutex };
|
||||
|
||||
LMS_LOG(COVER, DEBUG) << "Cache stats: hits = " << _cacheHits << ", misses = " << _cacheMisses << ", nb entries = " << _cache.size() << ", size = " << _cacheSize;
|
||||
_cacheHits = 0;
|
||||
_cacheMisses = 0;
|
||||
_cacheSize = 0;
|
||||
_cache.clear();
|
||||
}
|
||||
|
||||
void CoverService::setJpegQuality(unsigned quality)
|
||||
{
|
||||
_jpegQuality = Utils::clamp<unsigned>(quality, 1, 100);
|
||||
|
||||
LMS_LOG(COVER, INFO) << "JPEG export quality = " << _jpegQuality;
|
||||
}
|
||||
|
||||
void CoverService::saveToCache(const CacheEntryDesc& entryDesc, std::shared_ptr<IEncodedImage> image)
|
||||
{
|
||||
std::unique_lock lock{ _cacheMutex };
|
||||
|
||||
while (_cacheSize + image->getDataSize() > _maxCacheSize && !_cache.empty())
|
||||
{
|
||||
auto itRandom{ Random::pickRandom(_cache) };
|
||||
_cacheSize -= itRandom->second->getDataSize();
|
||||
_cache.erase(itRandom);
|
||||
}
|
||||
|
||||
_cacheSize += image->getDataSize();
|
||||
_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
|
||||
|
||||
|
||||
@@ -33,278 +33,298 @@
|
||||
|
||||
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)
|
||||
: _name {std::string(name, 0 , _maxNameLength)},
|
||||
_sortName {_name},
|
||||
_MBID {MBID ? MBID->getAsString() : ""}
|
||||
{
|
||||
}
|
||||
auto query{ session.getDboSession().query<ResultType>("SELECT DISTINCT " + std::string{ itemToSelect } + " FROM artist a") };
|
||||
if (params.sortMethod == ArtistSortMethod::LastWritten
|
||||
|| params.writtenAfter.isValid()
|
||||
|| 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
|
||||
Artist::create(Session& session, const std::string& name, const std::optional<UUID>& MBID)
|
||||
{
|
||||
return session.getDboSession().add(std::unique_ptr<Artist> {new Artist {name, MBID}});
|
||||
}
|
||||
if (params.linkType)
|
||||
query.where("t_a_l.type = ?").bind(*params.linkType);
|
||||
|
||||
std::size_t
|
||||
Artist::getCount(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
if (params.writtenAfter.isValid())
|
||||
query.where("t.file_last_write > ?").bind(params.writtenAfter);
|
||||
|
||||
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>
|
||||
Artist::find(Session& session, const std::string& name)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
for (std::string_view keyword : params.keywords)
|
||||
{
|
||||
clauses.push_back("a.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'");
|
||||
query.bind("%" + Utils::escapeLikeKeyword(keyword) + "%");
|
||||
}
|
||||
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> res = session.getDboSession().find<Artist>()
|
||||
.where("name = ?").bind(std::string {name, 0, _maxNameLength})
|
||||
.orderBy("LENGTH(mbid) DESC"); // put mbid entries first
|
||||
for (std::string_view keyword : params.keywords)
|
||||
{
|
||||
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
|
||||
Artist::find(Session& session, const UUID& mbid)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
return session.getDboSession().find<Artist>().where("mbid = ?").bind(std::string {mbid.getAsString()}).resultValue();
|
||||
}
|
||||
if (params.starringUser.isValid())
|
||||
{
|
||||
assert(params.feedbackBackend);
|
||||
query.join("starred_artist s_a ON s_a.artist_id = a.id")
|
||||
.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
|
||||
Artist::find(Session& session, ArtistId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
return session.getDboSession().find<Artist>().where("id = ?").bind(id).resultValue();
|
||||
}
|
||||
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;
|
||||
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
|
||||
Artist::exists(Session& session, ArtistId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
return session.getDboSession().query<int>("SELECT 1 FROM artist").where("id = ?").bind(id).resultValue() == 1;
|
||||
}
|
||||
WhereClause clusterClause;
|
||||
for (const ClusterId clusterId : params.clusters)
|
||||
{
|
||||
clusterClause.Or(WhereClause("c.id = ?"));
|
||||
query.bind(clusterId);
|
||||
}
|
||||
|
||||
static
|
||||
Wt::Dbo::Query<ArtistId>
|
||||
createQuery(Session& session, const Artist::FindParameters& params)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
oss << " " << clusterClause.get();
|
||||
oss << " GROUP BY t.id,a.id HAVING COUNT(DISTINCT c.id) = " << params.clusters.size() << ")";
|
||||
|
||||
auto query {session.getDboSession().query<ArtistId>("SELECT DISTINCT a.id FROM artist a")};
|
||||
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");
|
||||
}
|
||||
query.where(oss.str());
|
||||
}
|
||||
|
||||
if (params.linkType)
|
||||
query.where("t_a_l.type = ?").bind(*params.linkType);
|
||||
if (params.track.isValid())
|
||||
query.where("t.id = ?").bind(params.track);
|
||||
|
||||
if (params.writtenAfter.isValid())
|
||||
query.where("t.file_last_write > ?").bind(params.writtenAfter);
|
||||
if (params.release.isValid())
|
||||
query.where("t.release_id = ?").bind(params.release);
|
||||
|
||||
if (!params.keywords.empty())
|
||||
{
|
||||
std::vector<std::string> clauses;
|
||||
std::vector<std::string> sortClauses;
|
||||
switch (params.sortMethod)
|
||||
{
|
||||
case ArtistSortMethod::None:
|
||||
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)
|
||||
{
|
||||
clauses.push_back("a.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'");
|
||||
query.bind("%" + Utils::escapeLikeKeyword(keyword) + "%");
|
||||
}
|
||||
return query;
|
||||
}
|
||||
|
||||
for (std::string_view keyword : params.keywords)
|
||||
{
|
||||
sortClauses.push_back("a.sort_name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'");
|
||||
query.bind("%" + Utils::escapeLikeKeyword(keyword) + "%");
|
||||
}
|
||||
template <typename ResultType>
|
||||
Wt::Dbo::Query<ResultType> createQuery(Session& session, const Artist::FindParameters& params)
|
||||
{
|
||||
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())
|
||||
{
|
||||
assert(params.scrobbler);
|
||||
query.join("starred_artist s_a ON s_a.artist_id = a.id")
|
||||
.where("s_a.user_id = ?").bind(params.starringUser)
|
||||
.where("s_a.scrobbler = ?").bind(*params.scrobbler)
|
||||
.where("s_a.scrobbling_state <> ?").bind(ScrobblingState::PendingRemove);
|
||||
}
|
||||
Artist::Artist(const std::string& name, const std::optional<UUID>& MBID)
|
||||
: _name{ std::string(name, 0 , _maxNameLength) },
|
||||
_sortName{ _name },
|
||||
_MBID{ MBID ? MBID->getAsString() : "" }
|
||||
{
|
||||
}
|
||||
|
||||
if (!params.clusters.empty())
|
||||
{
|
||||
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";
|
||||
Artist::pointer Artist::create(Session& session, const std::string& name, const std::optional<UUID>& MBID)
|
||||
{
|
||||
return session.getDboSession().add(std::unique_ptr<Artist> {new Artist{ name, MBID }});
|
||||
}
|
||||
|
||||
WhereClause clusterClause;
|
||||
for (const ClusterId clusterId : params.clusters)
|
||||
{
|
||||
clusterClause.Or(WhereClause("c.id = ?"));
|
||||
query.bind(clusterId);
|
||||
}
|
||||
std::size_t Artist::getCount(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
oss << " " << clusterClause.get();
|
||||
oss << " GROUP BY t.id,a.id HAVING COUNT(DISTINCT c.id) = " << params.clusters.size() << ")";
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM artist");
|
||||
}
|
||||
|
||||
query.where(oss.str());
|
||||
}
|
||||
std::vector<Artist::pointer> Artist::find(Session& session, const std::string& name)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
if (params.track.isValid())
|
||||
query.where("t.id = ?").bind(params.track);
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> res = session.getDboSession().find<Artist>()
|
||||
.where("name = ?").bind(std::string{ name, 0, _maxNameLength })
|
||||
.orderBy("LENGTH(mbid) DESC"); // put mbid entries first
|
||||
|
||||
if (params.release.isValid())
|
||||
query.where("t.release_id = ?").bind(params.release);
|
||||
return std::vector<Artist::pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
switch (params.sortMethod)
|
||||
{
|
||||
case ArtistSortMethod::None:
|
||||
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;
|
||||
}
|
||||
Artist::pointer Artist::find(Session& session, const UUID& mbid)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
return session.getDboSession().find<Artist>().where("mbid = ?").bind(std::string{ mbid.getAsString() }).resultValue();
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
Artist::pointer Artist::find(Session& session, ArtistId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
return session.getDboSession().find<Artist>().where("id = ?").bind(id).resultValue();
|
||||
}
|
||||
|
||||
RangeResults<ArtistId>
|
||||
Artist::findAllOrphans(Session& session, Range range)
|
||||
{
|
||||
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)")};
|
||||
bool Artist::exists(Session& session, ArtistId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
return session.getDboSession().query<int>("SELECT 1 FROM artist").where("id = ?").bind(id).resultValue() == 1;
|
||||
}
|
||||
|
||||
return Utils::execQuery(query, range);
|
||||
}
|
||||
|
||||
RangeResults<ArtistId>
|
||||
Artist::find(Session& session, const FindParameters& params)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
RangeResults<ArtistId> Artist::findOrphanIds(Session& session, Range range)
|
||||
{
|
||||
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, params.range);
|
||||
}
|
||||
return Utils::execQuery(query, range);
|
||||
}
|
||||
|
||||
RangeResults<ArtistId>
|
||||
Artist::findSimilarArtists(EnumSet<TrackArtistLinkType> artistLinkTypes, Range range) const
|
||||
{
|
||||
assert(session());
|
||||
RangeResults<ArtistId> Artist::findIds(Session& session, const FindParameters& params)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
std::ostringstream oss;
|
||||
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 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 <> ?";
|
||||
auto query{ createQuery<ArtistId>(session, params) };
|
||||
return Utils::execQuery(query, params.range);
|
||||
}
|
||||
|
||||
if (!artistLinkTypes.empty())
|
||||
{
|
||||
oss << " AND t_a_l.type IN (";
|
||||
RangeResults<Artist::pointer> Artist::find(Session& session, const FindParameters& params)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
bool first {true};
|
||||
for (TrackArtistLinkType type : artistLinkTypes)
|
||||
{
|
||||
(void) type;
|
||||
if (!first)
|
||||
oss << ", ";
|
||||
oss << "?";
|
||||
first = false;
|
||||
}
|
||||
oss << ")";
|
||||
}
|
||||
auto query{ createQuery<Wt::Dbo::ptr<Artist>>(session, params) };
|
||||
return Utils::execQuery(query, params.range);
|
||||
}
|
||||
|
||||
auto query {session()->query<ArtistId>(oss.str())
|
||||
.bind(getId())
|
||||
.bind(getId())
|
||||
.groupBy("a.id")
|
||||
.orderBy("COUNT(*) DESC, RANDOM()")};
|
||||
RangeResults<ArtistId> Artist::findSimilarArtistIds(EnumSet<TrackArtistLinkType> artistLinkTypes, Range range) const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
for (TrackArtistLinkType type : artistLinkTypes)
|
||||
query.bind(type);
|
||||
std::ostringstream oss;
|
||||
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>>
|
||||
Artist::getClusterGroups(std::vector<ClusterType::pointer> clusterTypes, std::size_t size) const
|
||||
{
|
||||
assert(session());
|
||||
bool first{ true };
|
||||
for (TrackArtistLinkType type : artistLinkTypes)
|
||||
{
|
||||
(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;
|
||||
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";
|
||||
for (TrackArtistLinkType type : artistLinkTypes)
|
||||
query.bind(type);
|
||||
|
||||
where.And(WhereClause("a.id = ?")).bind(getId().toString());
|
||||
{
|
||||
WhereClause clusterClause;
|
||||
for (auto clusterType : clusterTypes)
|
||||
clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterType->getId().toString());
|
||||
return Utils::execQuery(query, range);
|
||||
}
|
||||
|
||||
where.And(clusterClause);
|
||||
}
|
||||
oss << " " << where.get();
|
||||
oss << "GROUP BY c.id ORDER BY COUNT(DISTINCT c.id) DESC";
|
||||
std::vector<std::vector<Cluster::pointer>> Artist::getClusterGroups(std::vector<ClusterType::pointer> clusterTypes, std::size_t size) const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
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())
|
||||
query.bind(bindArg);
|
||||
std::ostringstream oss;
|
||||
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;
|
||||
for (Cluster::pointer cluster : queryRes)
|
||||
{
|
||||
if (clustersByType[cluster->getType()->getId()].size() < size)
|
||||
clustersByType[cluster->getType()->getId()].push_back(cluster);
|
||||
}
|
||||
where.And(clusterClause);
|
||||
}
|
||||
oss << " " << where.get();
|
||||
oss << "GROUP BY c.id ORDER BY COUNT(DISTINCT c.id) DESC";
|
||||
|
||||
std::vector<std::vector<Cluster::pointer>> res;
|
||||
for (const auto& [clusterTypeId, clusters] : clustersByType)
|
||||
res.push_back(clusters);
|
||||
Wt::Dbo::Query<Wt::Dbo::ptr<Cluster>> query = session()->query<Wt::Dbo::ptr<Cluster>>(oss.str());
|
||||
|
||||
return res;
|
||||
}
|
||||
for (const std::string& bindArg : where.getBindArgs())
|
||||
query.bind(bindArg);
|
||||
|
||||
void
|
||||
Artist::setSortName(const std::string& sortName)
|
||||
{
|
||||
_sortName = std::string(sortName, 0 , _maxNameLength);
|
||||
}
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Cluster>> queryRes = query;
|
||||
|
||||
std::map<ClusterTypeId, std::vector<Cluster::pointer>> clustersByType;
|
||||
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
|
||||
|
||||
@@ -40,13 +40,25 @@ namespace Database
|
||||
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:
|
||||
Connection(const Connection&) = delete;
|
||||
Connection& operator=(const Connection&) = delete;
|
||||
|
||||
std::unique_ptr<SqlConnection> clone() const override
|
||||
{
|
||||
return std::make_unique<Connection>(_dbPath);
|
||||
return std::make_unique<Connection>(*this);
|
||||
}
|
||||
|
||||
void prepare()
|
||||
@@ -54,10 +66,17 @@ namespace Database
|
||||
LMS_LOG(DB, DEBUG) << "Setting per-connection settings...";
|
||||
executeSql("pragma journal_mode=WAL");
|
||||
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!";
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
}
|
||||
@@ -68,7 +87,7 @@ namespace Database
|
||||
LMS_LOG(DB, INFO) << "Creating connection pool on file " << 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) };
|
||||
connectionPool->setTimeout(std::chrono::seconds{ 10 });
|
||||
|
||||
@@ -29,14 +29,14 @@ namespace
|
||||
{
|
||||
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")
|
||||
.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("listen l ON l.track_id = t.id")
|
||||
.where("l.user_id = ?").bind(userId)
|
||||
.where("l.scrobbler = ?").bind(scrobbler) };
|
||||
.where("l.backend = ?").bind(backend) };
|
||||
|
||||
if (linkType)
|
||||
query.where("t_a_l.type = ?").bind(*linkType);
|
||||
@@ -66,13 +66,13 @@ namespace
|
||||
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")
|
||||
.join("track t ON t.release_id = r.id")
|
||||
.join("listen l ON l.track_id = t.id")
|
||||
.where("l.user_id = ?").bind(userId)
|
||||
.where("l.scrobbler = ?").bind(scrobbler) };
|
||||
.where("l.backend = ?").bind(backend) };
|
||||
|
||||
if (!clusterIds.empty())
|
||||
{
|
||||
@@ -98,12 +98,12 @@ namespace
|
||||
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")
|
||||
.join("listen l ON l.track_id = t.id")
|
||||
.where("l.user_id = ?").bind(userId)
|
||||
.where("l.scrobbler = ?").bind(scrobbler) };
|
||||
.where("l.backend = ?").bind(backend) };
|
||||
|
||||
if (!clusterIds.empty())
|
||||
{
|
||||
@@ -131,16 +131,17 @@ namespace
|
||||
|
||||
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()) }
|
||||
, _scrobbler{ scrobbler }
|
||||
, _backend{ backend }
|
||||
, _user{ getDboPtr(user) }
|
||||
, _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)
|
||||
@@ -165,30 +166,31 @@ namespace Database
|
||||
if (parameters.user.isValid())
|
||||
query.where("user_id = ?").bind(parameters.user);
|
||||
|
||||
if (parameters.scrobbler)
|
||||
query.where("scrobbler = ?").bind(*parameters.scrobbler);
|
||||
if (parameters.backend)
|
||||
query.where("backend = ?").bind(*parameters.backend);
|
||||
|
||||
if (parameters.scrobblingState)
|
||||
query.where("scrobbling_state = ?").bind(*parameters.scrobblingState);
|
||||
if (parameters.syncState)
|
||||
query.where("sync_state = ?").bind(*parameters.syncState);
|
||||
|
||||
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();
|
||||
|
||||
return session.getDboSession().find<Listen>()
|
||||
.where("user_id = ?").bind(userId)
|
||||
.where("track_id = ?").bind(trackId)
|
||||
.where("scrobbler = ?").bind(scrobbler)
|
||||
.where("backend = ?").bind(backend)
|
||||
.where("date_time = ?").bind(Wt::WDateTime::fromTime_t(dateTime.toTime_t()))
|
||||
.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
|
||||
.orderBy("COUNT(a.id) DESC")
|
||||
@@ -197,72 +199,107 @@ namespace Database
|
||||
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")
|
||||
.groupBy("r.id") };
|
||||
|
||||
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")
|
||||
.groupBy("t.id") };
|
||||
|
||||
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)")
|
||||
.orderBy("l.date_time DESC") };
|
||||
|
||||
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)")
|
||||
.orderBy("l.date_time DESC") };
|
||||
|
||||
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)")
|
||||
.orderBy("l.date_time DESC") };
|
||||
|
||||
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?
|
||||
return session.getDboSession().query<Wt::Dbo::ptr<Listen>>("SELECT l from listen l")
|
||||
.join("track t ON l.track_id = t.id")
|
||||
.where("t.release_id = ?").bind(releaseId)
|
||||
.where("l.user_id = ?").bind(userId)
|
||||
.where("l.scrobbler = ?").bind(scrobbler)
|
||||
.where("l.backend = ?").bind(backend)
|
||||
.orderBy("l.date_time DESC")
|
||||
.limit(1)
|
||||
.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?
|
||||
return session.getDboSession().query<Wt::Dbo::ptr<Listen>>("SELECT l from listen l")
|
||||
.join("track t ON track_id = t.id")
|
||||
.where("t.id = ?").bind(trackId)
|
||||
.where("l.track_id = ?").bind(trackId)
|
||||
.where("l.user_id = ?").bind(userId)
|
||||
.where("l.scrobbler = ?").bind(scrobbler)
|
||||
.where("l.backend = ?").bind(backend)
|
||||
.orderBy("l.date_time DESC")
|
||||
.limit(1)
|
||||
.resultValue();
|
||||
|
||||
@@ -72,414 +72,6 @@ namespace Database::Migration
|
||||
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)
|
||||
{
|
||||
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");
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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
|
||||
{
|
||||
{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},
|
||||
{33, migrateFromV33},
|
||||
{34, migrateFromV34},
|
||||
@@ -652,6 +244,7 @@ CREATE TABLE IF NOT EXISTS "track_backup" (
|
||||
{39, migrateFromV39},
|
||||
{40, migrateFromV40},
|
||||
{41, migrateFromV41},
|
||||
{42, migrateFromV42},
|
||||
};
|
||||
|
||||
{
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace Database
|
||||
class Session;
|
||||
|
||||
using Version = std::size_t;
|
||||
static constexpr Version LMS_DATABASE_VERSION{ 42 };
|
||||
static constexpr Version LMS_DATABASE_VERSION{ 43 };
|
||||
class VersionInfo
|
||||
{
|
||||
public:
|
||||
|
||||
@@ -34,147 +34,170 @@
|
||||
|
||||
namespace Database
|
||||
{
|
||||
|
||||
Wt::Dbo::Query<ReleaseId> createQuery(Session& session, const Release::FindParameters& params)
|
||||
namespace
|
||||
{
|
||||
auto query{ session.getDboSession().query<ReleaseId>("SELECT DISTINCT r.id from release r") };
|
||||
|
||||
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())
|
||||
template <typename ResultType>
|
||||
Wt::Dbo::Query<ResultType> createQuery(Session& session, std::string_view itemToSelect, const Release::FindParameters& params)
|
||||
{
|
||||
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())
|
||||
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.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())
|
||||
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()
|
||||
|| params.clusters.size() == 1)
|
||||
{
|
||||
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());
|
||||
query.join("track t ON t.release_id = r.id");
|
||||
}
|
||||
|
||||
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;
|
||||
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"
|
||||
oss << "r.id IN (SELECT DISTINCT r.id FROM release r"
|
||||
" 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);
|
||||
|
||||
bool first{ true };
|
||||
for (const TrackArtistLinkType linkType : params.excludedTrackArtistLinkTypes)
|
||||
WhereClause clusterClause;
|
||||
for (const ClusterId clusterId : params.clusters)
|
||||
{
|
||||
if (!first)
|
||||
oss << " OR ";
|
||||
oss << "t_a_l.type = ?";
|
||||
query.bind(linkType);
|
||||
|
||||
first = false;
|
||||
clusterClause.Or(WhereClause("t_c.cluster_id = ?"));
|
||||
query.bind(clusterId);
|
||||
}
|
||||
oss << ")))";
|
||||
|
||||
oss << " " << clusterClause.get();
|
||||
oss << " GROUP BY t.id HAVING COUNT(*) = " << params.clusters.size() << ")";
|
||||
|
||||
query.where(oss.str());
|
||||
}
|
||||
}
|
||||
|
||||
if (!params.clusters.empty())
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "r.id IN (SELECT DISTINCT r.id FROM release r"
|
||||
" 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";
|
||||
if (params.primaryType)
|
||||
query.where("primary_type = ?").bind(*params.primaryType);
|
||||
if (!params.secondaryTypes.empty())
|
||||
query.where("secondary_type = ?").bind(params.secondaryTypes);
|
||||
|
||||
WhereClause clusterClause;
|
||||
for (const ClusterId clusterId : params.clusters)
|
||||
switch (params.sortMethod)
|
||||
{
|
||||
clusterClause.Or(WhereClause("c.id = ?"));
|
||||
query.bind(clusterId);
|
||||
case ReleaseSortMethod::None:
|
||||
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();
|
||||
oss << " GROUP BY t.id HAVING COUNT(*) = " << params.clusters.size() << ")";
|
||||
|
||||
query.where(oss.str());
|
||||
return query;
|
||||
}
|
||||
|
||||
if (params.primaryType)
|
||||
query.where("primary_type = ?").bind(*params.primaryType);
|
||||
if (!params.secondaryTypes.empty())
|
||||
query.where("secondary_type = ?").bind(params.secondaryTypes);
|
||||
|
||||
switch (params.sortMethod)
|
||||
template <typename ResultType>
|
||||
Wt::Dbo::Query<ResultType> createQuery(Session& session, const Release::FindParameters& params)
|
||||
{
|
||||
case ReleaseSortMethod::None:
|
||||
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;
|
||||
}
|
||||
std::string_view itemToSelect;
|
||||
|
||||
if constexpr (std::is_same_v<ResultType, ReleaseId>)
|
||||
itemToSelect = "r.id";
|
||||
else if constexpr (std::is_same_v<ResultType, Wt::Dbo::ptr<Release>>)
|
||||
itemToSelect = "r";
|
||||
else
|
||||
static_assert("Unhandled type");
|
||||
|
||||
return query;
|
||||
return createQuery<ResultType>(session, itemToSelect, params);
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
RangeResults<ReleaseId> Release::findOrderedByArtist(Session& session, Range range)
|
||||
RangeResults<ReleaseId> Release::findIdsOrderedByArtist(Session& session, Range range)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
@@ -248,7 +271,7 @@ namespace Database
|
||||
return Utils::execQuery(query, range);
|
||||
}
|
||||
|
||||
RangeResults<ReleaseId> Release::findOrphans(Session& session, Range range)
|
||||
RangeResults<ReleaseId> Release::findOrphanIds(Session& session, Range range)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
@@ -256,12 +279,19 @@ namespace Database
|
||||
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();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -382,12 +412,18 @@ namespace Database
|
||||
{
|
||||
assert(session());
|
||||
|
||||
// Select the similar releases using the 5 most used clusters of the release
|
||||
auto res{ session()->query<Wt::Dbo::ptr<Release>>(
|
||||
"SELECT r FROM release r"
|
||||
" INNER JOIN track t ON t.release_id = r.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 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 <> ?"
|
||||
)
|
||||
.bind(getId())
|
||||
|
||||
@@ -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_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 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_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_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_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_original_date_idx ON track(original_date)");
|
||||
_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_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 listen_scrobbler_idx ON listen(scrobbler)");
|
||||
_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_track_scrobbler_date_time_idx ON listen(user_id,track_id,scrobbler,date_time)");
|
||||
_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 starred_artist_artist_user_scrobbler_idx ON starred_artist(artist_id,user_id,scrobbler)");
|
||||
_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_release_release_user_scrobbler_idx ON starred_release(release_id,user_id,scrobbler)");
|
||||
_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_track_track_user_scrobbler_idx ON starred_track(track_id,user_id,scrobbler)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS listen_backend_idx ON listen(backend)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS listen_user_backend_idx ON listen(user_id,backend)");
|
||||
_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 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_user_backend_idx ON starred_artist(user_id,backend)");
|
||||
_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_user_backend_idx ON starred_release(user_id,backend)");
|
||||
_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_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
|
||||
@@ -187,4 +188,14 @@ namespace Database
|
||||
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
|
||||
|
||||
@@ -29,47 +29,42 @@
|
||||
|
||||
namespace Database
|
||||
{
|
||||
StarredArtist::StarredArtist(ObjectPtr<Artist> artist, ObjectPtr<User> user, Scrobbler scrobbler)
|
||||
: _scrobbler {scrobbler}
|
||||
, _artist {getDboPtr(artist)}
|
||||
, _user {getDboPtr(user)}
|
||||
{
|
||||
}
|
||||
StarredArtist::StarredArtist(ObjectPtr<Artist> artist, ObjectPtr<User> user, FeedbackBackend backend)
|
||||
: _backend{ backend }
|
||||
, _artist{ getDboPtr(artist) }
|
||||
, _user{ getDboPtr(user) }
|
||||
{
|
||||
}
|
||||
|
||||
StarredArtist::pointer
|
||||
StarredArtist::create(Session& session, ObjectPtr<Artist> artist, ObjectPtr<User> user, Scrobbler scrobbler)
|
||||
{
|
||||
return session.getDboSession().add(std::unique_ptr<StarredArtist> {new StarredArtist {artist, user, scrobbler}});
|
||||
}
|
||||
StarredArtist::pointer StarredArtist::create(Session& session, ObjectPtr<Artist> artist, ObjectPtr<User> user, FeedbackBackend backend)
|
||||
{
|
||||
return session.getDboSession().add(std::unique_ptr<StarredArtist> {new StarredArtist{ artist, user, backend }});
|
||||
}
|
||||
|
||||
std::size_t
|
||||
StarredArtist::getCount(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM starred_artist");
|
||||
}
|
||||
std::size_t StarredArtist::getCount(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM starred_artist");
|
||||
}
|
||||
|
||||
StarredArtist::pointer
|
||||
StarredArtist::find(Session& session, StarredArtistId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
return session.getDboSession().find<StarredArtist>().where("id = ?").bind(id).resultValue();
|
||||
}
|
||||
StarredArtist::pointer StarredArtist::find(Session& session, StarredArtistId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
return session.getDboSession().find<StarredArtist>().where("id = ?").bind(id).resultValue();
|
||||
}
|
||||
|
||||
StarredArtist::pointer
|
||||
StarredArtist::find(Session& session, ArtistId artistId, UserId userId, Scrobbler scrobbler)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
return session.getDboSession().find<StarredArtist>()
|
||||
.where("artist_id = ?").bind(artistId)
|
||||
.where("user_id = ?").bind(userId)
|
||||
.where("scrobbler = ?").bind(scrobbler)
|
||||
.resultValue();
|
||||
}
|
||||
StarredArtist::pointer StarredArtist::find(Session& session, ArtistId artistId, UserId userId, FeedbackBackend backend)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
return session.getDboSession().find<StarredArtist>()
|
||||
.where("artist_id = ?").bind(artistId)
|
||||
.where("user_id = ?").bind(userId)
|
||||
.where("backend = ?").bind(backend)
|
||||
.resultValue();
|
||||
}
|
||||
|
||||
void
|
||||
StarredArtist::setDateTime(const Wt::WDateTime& dateTime)
|
||||
{
|
||||
_dateTime = Utils::normalizeDateTime(dateTime);
|
||||
}
|
||||
void StarredArtist::setDateTime(const Wt::WDateTime& dateTime)
|
||||
{
|
||||
_dateTime = Utils::normalizeDateTime(dateTime);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,47 +29,42 @@
|
||||
|
||||
namespace Database
|
||||
{
|
||||
StarredRelease::StarredRelease(ObjectPtr<Release> release, ObjectPtr<User> user, Scrobbler scrobbler)
|
||||
: _scrobbler {scrobbler}
|
||||
, _release {getDboPtr(release)}
|
||||
, _user {getDboPtr(user)}
|
||||
{
|
||||
}
|
||||
StarredRelease::StarredRelease(ObjectPtr<Release> release, ObjectPtr<User> user, FeedbackBackend backend)
|
||||
: _backend{ backend }
|
||||
, _release{ getDboPtr(release) }
|
||||
, _user{ getDboPtr(user) }
|
||||
{
|
||||
}
|
||||
|
||||
StarredRelease::pointer
|
||||
StarredRelease::create(Session& session, ObjectPtr<Release> release, ObjectPtr<User> user, Scrobbler scrobbler)
|
||||
{
|
||||
return session.getDboSession().add(std::unique_ptr<StarredRelease>{new StarredRelease{release, user, scrobbler}});
|
||||
}
|
||||
StarredRelease::pointer StarredRelease::create(Session& session, ObjectPtr<Release> release, ObjectPtr<User> user, FeedbackBackend backend)
|
||||
{
|
||||
return session.getDboSession().add(std::unique_ptr<StarredRelease>{new StarredRelease{ release, user, backend }});
|
||||
}
|
||||
|
||||
std::size_t
|
||||
StarredRelease::getCount(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM starred_release");
|
||||
}
|
||||
std::size_t StarredRelease::getCount(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM starred_release");
|
||||
}
|
||||
|
||||
StarredRelease::pointer
|
||||
StarredRelease::find(Session& session, StarredReleaseId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
return session.getDboSession().find<StarredRelease>().where("id = ?").bind(id).resultValue();
|
||||
}
|
||||
StarredRelease::pointer StarredRelease::find(Session& session, StarredReleaseId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
return session.getDboSession().find<StarredRelease>().where("id = ?").bind(id).resultValue();
|
||||
}
|
||||
|
||||
StarredRelease::pointer
|
||||
StarredRelease::find(Session& session, ReleaseId releaseId, UserId userId, Scrobbler scrobbler)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
return session.getDboSession().find<StarredRelease>()
|
||||
.where("release_id = ?").bind(releaseId)
|
||||
.where("user_id = ?").bind(userId)
|
||||
.where("scrobbler = ?").bind(scrobbler)
|
||||
.resultValue();
|
||||
}
|
||||
StarredRelease::pointer StarredRelease::find(Session& session, ReleaseId releaseId, UserId userId, FeedbackBackend backend)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
return session.getDboSession().find<StarredRelease>()
|
||||
.where("release_id = ?").bind(releaseId)
|
||||
.where("user_id = ?").bind(userId)
|
||||
.where("backend = ?").bind(backend)
|
||||
.resultValue();
|
||||
}
|
||||
|
||||
void
|
||||
StarredRelease::setDateTime(const Wt::WDateTime& dateTime)
|
||||
{
|
||||
_dateTime = Utils::normalizeDateTime(dateTime);
|
||||
}
|
||||
void StarredRelease::setDateTime(const Wt::WDateTime& dateTime)
|
||||
{
|
||||
_dateTime = Utils::normalizeDateTime(dateTime);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,64 +29,58 @@
|
||||
|
||||
namespace Database
|
||||
{
|
||||
StarredTrack::StarredTrack(ObjectPtr<Track> track, ObjectPtr<User> user, Scrobbler scrobbler)
|
||||
: _scrobbler {scrobbler}
|
||||
, _track {getDboPtr(track)}
|
||||
, _user {getDboPtr(user)}
|
||||
{
|
||||
}
|
||||
StarredTrack::StarredTrack(ObjectPtr<Track> track, ObjectPtr<User> user, FeedbackBackend backend)
|
||||
: _backend{ backend }
|
||||
, _track{ getDboPtr(track) }
|
||||
, _user{ getDboPtr(user) }
|
||||
{
|
||||
}
|
||||
|
||||
StarredTrack::pointer
|
||||
StarredTrack::create(Session& session, ObjectPtr<Track> track, ObjectPtr<User> user, Scrobbler scrobbler)
|
||||
{
|
||||
return session.getDboSession().add(std::unique_ptr<StarredTrack> {new StarredTrack{track, user, scrobbler}});
|
||||
}
|
||||
StarredTrack::pointer StarredTrack::create(Session& session, ObjectPtr<Track> track, ObjectPtr<User> user, FeedbackBackend backend)
|
||||
{
|
||||
return session.getDboSession().add(std::unique_ptr<StarredTrack> {new StarredTrack{ track, user, backend }});
|
||||
}
|
||||
|
||||
std::size_t
|
||||
StarredTrack::getCount(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM starred_track");
|
||||
}
|
||||
std::size_t StarredTrack::getCount(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM starred_track");
|
||||
}
|
||||
|
||||
StarredTrack::pointer
|
||||
StarredTrack::find(Session& session, StarredTrackId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
return session.getDboSession().find<StarredTrack>().where("id = ?").bind(id).resultValue();
|
||||
}
|
||||
StarredTrack::pointer StarredTrack::find(Session& session, StarredTrackId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
return session.getDboSession().find<StarredTrack>().where("id = ?").bind(id).resultValue();
|
||||
}
|
||||
|
||||
StarredTrack::pointer
|
||||
StarredTrack::find(Session& session, TrackId trackId, UserId userId, Scrobbler scrobbler)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
return session.getDboSession().find<StarredTrack>()
|
||||
.where("track_id = ?").bind(trackId)
|
||||
.where("user_id = ?").bind(userId)
|
||||
.where("scrobbler = ?").bind(scrobbler)
|
||||
.resultValue();
|
||||
}
|
||||
StarredTrack::pointer StarredTrack::find(Session& session, TrackId trackId, UserId userId, FeedbackBackend backend)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
return session.getDboSession().find<StarredTrack>()
|
||||
.where("track_id = ?").bind(trackId)
|
||||
.where("user_id = ?").bind(userId)
|
||||
.where("backend = ?").bind(backend)
|
||||
.resultValue();
|
||||
}
|
||||
|
||||
RangeResults<StarredTrackId>
|
||||
StarredTrack::find(Session& session, const FindParameters& params)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
RangeResults<StarredTrackId> StarredTrack::find(Session& session, const FindParameters& params)
|
||||
{
|
||||
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)
|
||||
query.where("s_t.scrobbler = ?").bind(*params.scrobbler);
|
||||
if (params.scrobblingState)
|
||||
query.where("s_t.scrobbling_state = ?").bind(*params.scrobblingState);
|
||||
if (params.user.isValid())
|
||||
query.where("s_t.user_id = ?").bind(params.user);
|
||||
if (params.backend)
|
||||
query.where("s_t.backend = ?").bind(*params.backend);
|
||||
if (params.syncState)
|
||||
query.where("s_t.sync_state = ?").bind(*params.syncState);
|
||||
if (params.user.isValid())
|
||||
query.where("s_t.user_id = ?").bind(params.user);
|
||||
|
||||
return Utils::execQuery(query, params.range);
|
||||
}
|
||||
return Utils::execQuery(query, params.range);
|
||||
}
|
||||
|
||||
void
|
||||
StarredTrack::setDateTime(const Wt::WDateTime& dateTime)
|
||||
{
|
||||
_dateTime = Utils::normalizeDateTime(dateTime);
|
||||
}
|
||||
void StarredTrack::setDateTime(const Wt::WDateTime& 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"
|
||||
" 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 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 = ?))"
|
||||
)
|
||||
.bind(getId())
|
||||
|
||||
@@ -53,8 +53,10 @@ namespace Database {
|
||||
|
||||
auto query{ session.getDboSession().query<UserId>("SELECT id FROM user") };
|
||||
|
||||
if (params.scrobbler)
|
||||
query.where("scrobbler = ?").bind(*params.scrobbler);
|
||||
if (params.scrobblingBackend)
|
||||
query.where("scrobbling_backend = ?").bind(*params.scrobblingBackend);
|
||||
if (params.feedbackBackend)
|
||||
query.where("feedback_backend = ?").bind(*params.feedbackBackend);
|
||||
|
||||
return Utils::execQuery(query, params.range);
|
||||
}
|
||||
|
||||
@@ -40,96 +40,97 @@
|
||||
namespace Database
|
||||
{
|
||||
|
||||
class Cluster;
|
||||
class ClusterType;
|
||||
class Release;
|
||||
class Session;
|
||||
class StarredArtist;
|
||||
class Track;
|
||||
class TrackArtistLink;
|
||||
class User;
|
||||
class Cluster;
|
||||
class ClusterType;
|
||||
class Release;
|
||||
class Session;
|
||||
class StarredArtist;
|
||||
class Track;
|
||||
class TrackArtistLink;
|
||||
class User;
|
||||
|
||||
class Artist final : public Object<Artist, ArtistId>
|
||||
{
|
||||
public:
|
||||
struct FindParameters
|
||||
{
|
||||
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::optional<TrackArtistLinkType> linkType; // if set, only artists that have produced at least one track with this link type
|
||||
ArtistSortMethod sortMethod {ArtistSortMethod::None};
|
||||
Range range;
|
||||
Wt::WDateTime writtenAfter;
|
||||
UserId starringUser; // only artists starred by this user
|
||||
std::optional<Scrobbler> scrobbler; // and for this scrobbler
|
||||
TrackId track; // artists involved in this track
|
||||
ReleaseId release; // artists involved in this release
|
||||
class Artist final : public Object<Artist, ArtistId>
|
||||
{
|
||||
public:
|
||||
struct FindParameters
|
||||
{
|
||||
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::optional<TrackArtistLinkType> linkType; // if set, only artists that have produced at least one track with this link type
|
||||
ArtistSortMethod sortMethod{ ArtistSortMethod::None };
|
||||
Range range;
|
||||
Wt::WDateTime writtenAfter;
|
||||
UserId starringUser; // only artists starred by this user
|
||||
std::optional<FeedbackBackend> feedbackBackend; // and for this feedback backend
|
||||
TrackId track; // artists involved in this track
|
||||
ReleaseId release; // artists involved in this release
|
||||
|
||||
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& setLinkType(std::optional<TrackArtistLinkType> _linkType) { linkType = _linkType; return *this; }
|
||||
FindParameters& setSortMethod(ArtistSortMethod _sortMethod) {sortMethod = _sortMethod; return *this; }
|
||||
FindParameters& setRange(Range _range) {range = _range; 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& setTrack(TrackId _track) { track = _track; return *this; }
|
||||
FindParameters& setRelease(ReleaseId _release) { release = _release; 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& setLinkType(std::optional<TrackArtistLinkType> _linkType) { linkType = _linkType; return *this; }
|
||||
FindParameters& setSortMethod(ArtistSortMethod _sortMethod) { sortMethod = _sortMethod; return *this; }
|
||||
FindParameters& setRange(Range _range) { range = _range; return *this; }
|
||||
FindParameters& setWrittenAfter(const Wt::WDateTime& _after) { writtenAfter = _after; return *this; }
|
||||
FindParameters& setStarringUser(UserId _user, FeedbackBackend _feedbackBackend) { starringUser = _user; feedbackBackend = _feedbackBackend; return *this; }
|
||||
FindParameters& setTrack(TrackId _track) { track = _track; return *this; }
|
||||
FindParameters& setRelease(ReleaseId _release) { release = _release; return *this; }
|
||||
};
|
||||
|
||||
Artist() = default;
|
||||
Artist() = default;
|
||||
|
||||
// Accessors
|
||||
static std::size_t getCount(Session& session);
|
||||
static pointer find(Session& session, const UUID& MBID);
|
||||
static pointer find(Session& session, ArtistId id);
|
||||
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<ArtistId> findAllOrphans(Session& session, Range range); // No track related
|
||||
static bool exists(Session& session, ArtistId id);
|
||||
// Accessors
|
||||
static std::size_t getCount(Session& session);
|
||||
static pointer find(Session& session, const UUID& MBID);
|
||||
static pointer find(Session& session, ArtistId id);
|
||||
static std::vector<pointer> find(Session& session, const std::string& name); // exact match on name field
|
||||
static RangeResults<pointer> find(Session& session, const FindParameters& parameters);
|
||||
static RangeResults<ArtistId> findIds(Session& session, const FindParameters& parameters);
|
||||
static RangeResults<ArtistId> findOrphanIds(Session& session, Range range); // No track related
|
||||
static bool exists(Session& session, ArtistId id);
|
||||
|
||||
// Accessors
|
||||
const std::string& getName() const { return _name; }
|
||||
const std::string& getSortName() const { return _sortName; }
|
||||
std::optional<UUID> getMBID() const { return UUID::fromString(_MBID); }
|
||||
// Accessors
|
||||
const std::string& getName() const { return _name; }
|
||||
const std::string& getSortName() const { return _sortName; }
|
||||
std::optional<UUID> getMBID() const { return UUID::fromString(_MBID); }
|
||||
|
||||
// No artistLinkTypes means get them all
|
||||
RangeResults<ArtistId> findSimilarArtists(EnumSet<TrackArtistLinkType> artistLinkTypes = {}, Range range = {}) const;
|
||||
// No artistLinkTypes means get them all
|
||||
RangeResults<ArtistId> findSimilarArtistIds(EnumSet<TrackArtistLinkType> artistLinkTypes = {}, Range range = {}) const;
|
||||
|
||||
// Get the cluster of the tracks made by this artist
|
||||
// Each clusters are grouped by cluster type, sorted by the number of occurence
|
||||
// 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;
|
||||
// Get the cluster of the tracks made by this artist
|
||||
// Each clusters are grouped by cluster type, sorted by the number of occurence
|
||||
// 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;
|
||||
|
||||
void setName(std::string_view name) { _name = name; }
|
||||
void setMBID(const std::optional<UUID>& mbid) { _MBID = mbid ? mbid->getAsString() : ""; }
|
||||
void setSortName(const std::string& sortName);
|
||||
void setName(std::string_view name) { _name = name; }
|
||||
void setMBID(const std::optional<UUID>& mbid) { _MBID = mbid ? mbid->getAsString() : ""; }
|
||||
void setSortName(const std::string& sortName);
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _name, "name");
|
||||
Wt::Dbo::field(a, _sortName, "sort_name");
|
||||
Wt::Dbo::field(a, _MBID, "mbid");
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _name, "name");
|
||||
Wt::Dbo::field(a, _sortName, "sort_name");
|
||||
Wt::Dbo::field(a, _MBID, "mbid");
|
||||
|
||||
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, _trackArtistLinks, Wt::Dbo::ManyToOne, "artist");
|
||||
Wt::Dbo::hasMany(a, _starredArtists, Wt::Dbo::ManyToMany, "user_starred_artists", "", Wt::Dbo::OnDeleteCascade);
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr std::size_t _maxNameLength {128};
|
||||
private:
|
||||
static constexpr std::size_t _maxNameLength{ 128 };
|
||||
|
||||
friend class Session;
|
||||
// Create
|
||||
Artist(const std::string& name, const std::optional<UUID>& MBID = {});
|
||||
static pointer create(Session& session, const std::string& name, const std::optional<UUID>& UUID = {});
|
||||
friend class Session;
|
||||
// Create
|
||||
Artist(const std::string& name, const std::optional<UUID>& MBID = {});
|
||||
static pointer create(Session& session, const std::string& name, const std::optional<UUID>& UUID = {});
|
||||
|
||||
std::string _name;
|
||||
std::string _sortName;
|
||||
std::string _MBID; // Musicbrainz Identifier
|
||||
std::string _name;
|
||||
std::string _sortName;
|
||||
std::string _MBID; // Musicbrainz Identifier
|
||||
|
||||
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<TrackArtistLink>> _trackArtistLinks; // Tracks involving this artist
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<StarredArtist>> _starredArtists; // starred entries for this artist
|
||||
};
|
||||
|
||||
} // namespace Database
|
||||
|
||||
|
||||
@@ -46,47 +46,50 @@ namespace Database
|
||||
struct FindParameters
|
||||
{
|
||||
UserId user;
|
||||
std::optional<Scrobbler> scrobbler;
|
||||
std::optional<ScrobblingState> scrobblingState;
|
||||
std::optional<ScrobblingBackend> backend;
|
||||
std::optional<SyncState> syncState;
|
||||
Range range;
|
||||
|
||||
FindParameters& setUser(UserId _user) { user = _user; return *this; }
|
||||
FindParameters& setScrobbler(Scrobbler _scrobbler) { scrobbler = _scrobbler; return *this; }
|
||||
FindParameters& setScrobblingState(ScrobblingState _scrobblingState) { scrobblingState = _scrobblingState; return *this; }
|
||||
FindParameters& setScrobblingBackend(ScrobblingBackend _backend) { backend = _backend; return *this; }
|
||||
FindParameters& setSyncState(SyncState _syncState) { syncState = _syncState; return *this; }
|
||||
FindParameters& setRange(Range _range) { range = _range; return *this; }
|
||||
};
|
||||
|
||||
// Accessors
|
||||
static std::size_t getCount(Session& session);
|
||||
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);
|
||||
|
||||
// Stats
|
||||
static RangeResults<ArtistId> getTopArtists(Session& session, UserId userId, Scrobbler scrobbler, 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<TrackId> getTopTracks(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, 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, ScrobblingBackend backend, 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<ReleaseId> getRecentReleases(Session& session, UserId userId, Scrobbler scrobbler, 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<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, ScrobblingBackend backend, 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 pointer getMostRecentListen(Session& session, UserId userId, Scrobbler scrobbler, TrackId releaseId);
|
||||
static std::size_t getCount(Session& session, UserId userId, ScrobblingBackend backend, TrackId trackId);
|
||||
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<Track> getTrack() const { return _track; }
|
||||
const Wt::WDateTime& getDateTime() const { return _dateTime; }
|
||||
|
||||
void setScrobblingState(ScrobblingState state) { _scrobblingState = state; }
|
||||
void setSyncState(SyncState state) { _syncState = state; }
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _dateTime, "date_time");
|
||||
Wt::Dbo::field(a, _scrobbler, "scrobbler");
|
||||
Wt::Dbo::field(a, _scrobblingState, "scrobbling_state");
|
||||
Wt::Dbo::field(a, _backend, "backend"); // TODO rename
|
||||
Wt::Dbo::field(a, _syncState, "sync_state"); // TODO rename
|
||||
|
||||
Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
|
||||
@@ -94,12 +97,12 @@ namespace Database
|
||||
|
||||
private:
|
||||
friend class Session;
|
||||
Listen(ObjectPtr<User> user, ObjectPtr<Track> track, Scrobbler scrobbler, const Wt::WDateTime& dateTime);
|
||||
static pointer create(Session& session, 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, ScrobblingBackend backend, const Wt::WDateTime& dateTime);
|
||||
|
||||
Wt::WDateTime _dateTime;
|
||||
Scrobbler _scrobbler;
|
||||
ScrobblingState _scrobblingState{ ScrobblingState::PendingAdd };
|
||||
Wt::WDateTime _dateTime;
|
||||
ScrobblingBackend _backend;
|
||||
SyncState _syncState{ SyncState::PendingAdd };
|
||||
|
||||
Wt::Dbo::ptr<User> _user;
|
||||
Wt::Dbo::ptr<Track> _track;
|
||||
|
||||
@@ -57,7 +57,7 @@ namespace Database
|
||||
Wt::WDateTime writtenAfter;
|
||||
std::optional<DateRange> dateRange;
|
||||
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
|
||||
EnumSet<TrackArtistLinkType> trackArtistLinkTypes; // and 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& setWrittenAfter(const Wt::WDateTime& _after) { writtenAfter = _after; 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 = {})
|
||||
{
|
||||
artist = _artist;
|
||||
@@ -88,9 +88,10 @@ namespace Database
|
||||
static pointer find(Session& session, const UUID& MBID);
|
||||
static std::vector<pointer> find(Session& session, const std::string& name);
|
||||
static pointer find(Session& session, ReleaseId id);
|
||||
static RangeResults<ReleaseId> find(Session& session, const FindParameters& parameters);
|
||||
static RangeResults<ReleaseId> findOrphans(Session& session, Range range); // no track related
|
||||
static RangeResults<ReleaseId> findOrderedByArtist(Session& session, Range range);
|
||||
static RangeResults<pointer> find(Session& session, const FindParameters& parameters);
|
||||
static RangeResults<ReleaseId> findIds(Session& session, const FindParameters& parameters);
|
||||
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
|
||||
// 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;
|
||||
|
||||
// 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<std::size_t> getTotalDisc() const { return _totalDisc; }
|
||||
std::size_t getDiscCount() const; // may not be total disc (if incomplete for example)
|
||||
|
||||
@@ -32,72 +32,74 @@ LMS_DECLARE_IDTYPE(ScanSettingsId)
|
||||
|
||||
namespace Database {
|
||||
|
||||
class ClusterType;
|
||||
class Session;
|
||||
class ClusterType;
|
||||
class Session;
|
||||
|
||||
class ScanSettings final : public Object<ScanSettings, ScanSettingsId>
|
||||
{
|
||||
public:
|
||||
// Do not modify values (just add)
|
||||
enum class UpdatePeriod
|
||||
{
|
||||
Never = 0,
|
||||
Daily,
|
||||
Weekly,
|
||||
Monthly,
|
||||
Hourly,
|
||||
};
|
||||
class ScanSettings final : public Object<ScanSettings, ScanSettingsId>
|
||||
{
|
||||
public:
|
||||
// Do not modify values (just add)
|
||||
enum class UpdatePeriod
|
||||
{
|
||||
Never = 0,
|
||||
Daily,
|
||||
Weekly,
|
||||
Monthly,
|
||||
Hourly,
|
||||
};
|
||||
|
||||
// Do not modify values (just add)
|
||||
enum class RecommendationEngineType
|
||||
{
|
||||
Clusters = 0,
|
||||
Features,
|
||||
};
|
||||
// Do not modify values (just add)
|
||||
enum class SimilarityEngineType
|
||||
{
|
||||
Clusters = 0,
|
||||
Features,
|
||||
None,
|
||||
};
|
||||
|
||||
static void init(Session& session);
|
||||
static void init(Session& session);
|
||||
|
||||
static pointer get(Session& session);
|
||||
static pointer get(Session& session);
|
||||
|
||||
// Getters
|
||||
std::size_t getScanVersion() const { return _scanVersion; }
|
||||
std::filesystem::path getMediaDirectory() const { return _mediaDirectory; }
|
||||
Wt::WTime getUpdateStartTime() const { return _startTime; }
|
||||
UpdatePeriod getUpdatePeriod() const { return _updatePeriod; }
|
||||
std::vector<ObjectPtr<ClusterType>> getClusterTypes() const;
|
||||
std::vector<std::filesystem::path> getAudioFileExtensions() const;
|
||||
RecommendationEngineType getRecommendationEngineType() const { return _recommendationEngineType; }
|
||||
// Getters
|
||||
std::size_t getScanVersion() const { return _scanVersion; }
|
||||
std::filesystem::path getMediaDirectory() const { return _mediaDirectory; }
|
||||
Wt::WTime getUpdateStartTime() const { return _startTime; }
|
||||
UpdatePeriod getUpdatePeriod() const { return _updatePeriod; }
|
||||
std::vector<ObjectPtr<ClusterType>> getClusterTypes() const;
|
||||
std::vector<std::filesystem::path> getAudioFileExtensions() const;
|
||||
SimilarityEngineType getSimilarityEngineType() const { return _similarityEngineType; }
|
||||
|
||||
// Setters
|
||||
void addAudioFileExtension(const std::filesystem::path& ext);
|
||||
void setMediaDirectory(const std::filesystem::path& p);
|
||||
void setUpdateStartTime(Wt::WTime t) { _startTime = t; }
|
||||
void setUpdatePeriod(UpdatePeriod p) { _updatePeriod = p; }
|
||||
void setClusterTypes(Session& session, const std::set<std::string>& clusterTypeNames);
|
||||
void setRecommendationEngineType(RecommendationEngineType type) { _recommendationEngineType = type; }
|
||||
void incScanVersion();
|
||||
// Setters
|
||||
void addAudioFileExtension(const std::filesystem::path& ext);
|
||||
void setMediaDirectory(const std::filesystem::path& p);
|
||||
void setUpdateStartTime(Wt::WTime t) { _startTime = t; }
|
||||
void setUpdatePeriod(UpdatePeriod p) { _updatePeriod = p; }
|
||||
void setClusterTypes(Session& session, const std::set<std::string>& clusterTypeNames);
|
||||
void setSimilarityEngineType(SimilarityEngineType type) { _similarityEngineType = type; }
|
||||
void incScanVersion();
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _scanVersion, "scan_version");
|
||||
Wt::Dbo::field(a, _mediaDirectory, "media_directory");
|
||||
Wt::Dbo::field(a, _startTime, "start_time");
|
||||
Wt::Dbo::field(a, _updatePeriod, "update_period");
|
||||
Wt::Dbo::field(a, _audioFileExtensions, "audio_file_extensions");
|
||||
Wt::Dbo::field(a, _recommendationEngineType,"similarity_engine_type");
|
||||
Wt::Dbo::hasMany(a, _clusterTypes, Wt::Dbo::ManyToOne, "scan_settings");
|
||||
}
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _scanVersion, "scan_version");
|
||||
Wt::Dbo::field(a, _mediaDirectory, "media_directory");
|
||||
Wt::Dbo::field(a, _startTime, "start_time");
|
||||
Wt::Dbo::field(a, _updatePeriod, "update_period");
|
||||
Wt::Dbo::field(a, _audioFileExtensions, "audio_file_extensions");
|
||||
Wt::Dbo::field(a, _similarityEngineType, "similarity_engine_type");
|
||||
Wt::Dbo::hasMany(a, _clusterTypes, Wt::Dbo::ManyToOne, "scan_settings");
|
||||
}
|
||||
|
||||
private:
|
||||
int _scanVersion {};
|
||||
std::string _mediaDirectory;
|
||||
Wt::WTime _startTime = Wt::WTime {0,0,0};
|
||||
UpdatePeriod _updatePeriod {UpdatePeriod::Never};
|
||||
RecommendationEngineType _recommendationEngineType {RecommendationEngineType::Clusters};
|
||||
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;
|
||||
};
|
||||
private:
|
||||
int _scanVersion{};
|
||||
std::string _mediaDirectory;
|
||||
Wt::WTime _startTime = Wt::WTime{ 0,0,0 };
|
||||
UpdatePeriod _updatePeriod{ UpdatePeriod::Never };
|
||||
SimilarityEngineType _similarityEngineType{ SimilarityEngineType::Clusters };
|
||||
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;
|
||||
};
|
||||
|
||||
|
||||
} // namespace Database
|
||||
|
||||
@@ -61,6 +61,7 @@ namespace Database
|
||||
void checkSharedLocked();
|
||||
|
||||
void analyze();
|
||||
void optimize();
|
||||
|
||||
void prepareTables(); // need to run only once at startup
|
||||
|
||||
|
||||
@@ -30,53 +30,53 @@
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Artist;
|
||||
class Session;
|
||||
class User;
|
||||
class Artist;
|
||||
class Session;
|
||||
class User;
|
||||
|
||||
class StarredArtist final : public Object<StarredArtist, StarredArtistId>
|
||||
{
|
||||
public:
|
||||
StarredArtist() = default;
|
||||
class StarredArtist final : public Object<StarredArtist, StarredArtistId>
|
||||
{
|
||||
public:
|
||||
StarredArtist() = default;
|
||||
|
||||
// Search utility
|
||||
static std::size_t getCount(Session& session);
|
||||
static pointer find(Session& session, StarredArtistId id);
|
||||
static pointer find(Session& session, ArtistId artistId, UserId userId, Scrobbler scrobbler);
|
||||
// Search utility
|
||||
static std::size_t getCount(Session& session);
|
||||
static pointer find(Session& session, StarredArtistId id);
|
||||
static pointer find(Session& session, ArtistId artistId, UserId userId, FeedbackBackend backend);
|
||||
|
||||
// Accessors
|
||||
ObjectPtr<Artist> getArtist() const { return _artist; }
|
||||
ObjectPtr<User> getUser() const { return _user; }
|
||||
Scrobbler getScrobbler() const { return _scrobbler; }
|
||||
const Wt::WDateTime& getDateTime() const { return _dateTime; }
|
||||
ScrobblingState getScrobblingState() const { return _scrobblingState; }
|
||||
// Accessors
|
||||
ObjectPtr<Artist> getArtist() const { return _artist; }
|
||||
ObjectPtr<User> getUser() const { return _user; }
|
||||
FeedbackBackend getFeedbackBackend() const { return _backend; }
|
||||
const Wt::WDateTime& getDateTime() const { return _dateTime; }
|
||||
SyncState getSyncState() const { return _syncState; }
|
||||
|
||||
// Setters
|
||||
void setDateTime(const Wt::WDateTime& dateTime);
|
||||
void setScrobblingState(ScrobblingState state) { _scrobblingState = state; }
|
||||
// Setters
|
||||
void setDateTime(const Wt::WDateTime& dateTime);
|
||||
void setSyncState(SyncState state) { _syncState = state; }
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _scrobbler, "scrobbler");
|
||||
Wt::Dbo::field(a, _scrobblingState, "scrobbling_state");
|
||||
Wt::Dbo::field(a, _dateTime, "date_time");
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _backend, "backend");
|
||||
Wt::Dbo::field(a, _syncState, "sync_state");
|
||||
Wt::Dbo::field(a, _dateTime, "date_time");
|
||||
|
||||
Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
|
||||
}
|
||||
Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
|
||||
}
|
||||
|
||||
private:
|
||||
friend class Session;
|
||||
StarredArtist(ObjectPtr<Artist> artist, ObjectPtr<User> user, Scrobbler scrobbler);
|
||||
static pointer create(Session& session, ObjectPtr<Artist> artist, ObjectPtr<User> user, Scrobbler scrobbler);
|
||||
private:
|
||||
friend class Session;
|
||||
StarredArtist(ObjectPtr<Artist> artist, ObjectPtr<User> user, FeedbackBackend scrobblingbackend);
|
||||
static pointer create(Session& session, ObjectPtr<Artist> artist, ObjectPtr<User> user, FeedbackBackend scrobblingbackend);
|
||||
|
||||
Scrobbler _scrobbler; // for which scrobbler
|
||||
ScrobblingState _scrobblingState {ScrobblingState::PendingAdd};
|
||||
Wt::WDateTime _dateTime; // when it was starred
|
||||
FeedbackBackend _backend; // for which backend
|
||||
SyncState _syncState{ SyncState::PendingAdd };
|
||||
Wt::WDateTime _dateTime; // when it was starred
|
||||
|
||||
Wt::Dbo::ptr<Artist> _artist;
|
||||
Wt::Dbo::ptr<User> _user;
|
||||
};
|
||||
Wt::Dbo::ptr<Artist> _artist;
|
||||
Wt::Dbo::ptr<User> _user;
|
||||
};
|
||||
} // namespace Database
|
||||
|
||||
|
||||
@@ -30,53 +30,53 @@
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Release;
|
||||
class Session;
|
||||
class User;
|
||||
class Release;
|
||||
class Session;
|
||||
class User;
|
||||
|
||||
class StarredRelease final : public Object<StarredRelease, StarredReleaseId>
|
||||
{
|
||||
public:
|
||||
StarredRelease() = default;
|
||||
class StarredRelease final : public Object<StarredRelease, StarredReleaseId>
|
||||
{
|
||||
public:
|
||||
StarredRelease() = default;
|
||||
|
||||
// Search utility
|
||||
static std::size_t getCount(Session& session);
|
||||
static pointer find(Session& session, StarredReleaseId id);
|
||||
static pointer find(Session& session, ReleaseId releaseId, UserId userId, Scrobbler scrobbler);
|
||||
// Search utility
|
||||
static std::size_t getCount(Session& session);
|
||||
static pointer find(Session& session, StarredReleaseId id);
|
||||
static pointer find(Session& session, ReleaseId releaseId, UserId userId, FeedbackBackend backend);
|
||||
|
||||
// Accessors
|
||||
ObjectPtr<Release> getRelease() const { return _release; }
|
||||
ObjectPtr<User> getUser() const { return _user; }
|
||||
Scrobbler getScrobbler() const { return _scrobbler; }
|
||||
const Wt::WDateTime& getDateTime() const { return _dateTime; }
|
||||
ScrobblingState getScrobblingState() const { return _scrobblingState; }
|
||||
// Accessors
|
||||
ObjectPtr<Release> getRelease() const { return _release; }
|
||||
ObjectPtr<User> getUser() const { return _user; }
|
||||
FeedbackBackend getFeedbackBackend() const { return _backend; }
|
||||
const Wt::WDateTime& getDateTime() const { return _dateTime; }
|
||||
SyncState getSyncState() const { return _syncState; }
|
||||
|
||||
// Setters
|
||||
void setDateTime(const Wt::WDateTime& dateTime);
|
||||
void setScrobblingState(ScrobblingState state) { _scrobblingState = state; }
|
||||
// Setters
|
||||
void setDateTime(const Wt::WDateTime& dateTime);
|
||||
void setSyncState(SyncState state) { _syncState = state; }
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _scrobbler, "scrobbler");
|
||||
Wt::Dbo::field(a, _scrobblingState, "scrobbling_state");
|
||||
Wt::Dbo::field(a, _dateTime, "date_time");
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _backend, "backend");
|
||||
Wt::Dbo::field(a, _syncState, "sync_state");
|
||||
Wt::Dbo::field(a, _dateTime, "date_time");
|
||||
|
||||
Wt::Dbo::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
|
||||
}
|
||||
Wt::Dbo::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
|
||||
}
|
||||
|
||||
private:
|
||||
friend class Session;
|
||||
StarredRelease(ObjectPtr<Release> release, ObjectPtr<User> user, Scrobbler scrobbler);
|
||||
static pointer create(Session& session, ObjectPtr<Release> release, ObjectPtr<User> user, Scrobbler scrobbler);
|
||||
private:
|
||||
friend class Session;
|
||||
StarredRelease(ObjectPtr<Release> release, ObjectPtr<User> user, FeedbackBackend backend);
|
||||
static pointer create(Session& session, ObjectPtr<Release> release, ObjectPtr<User> user, FeedbackBackend backend);
|
||||
|
||||
Scrobbler _scrobbler; // for which scrobbler
|
||||
ScrobblingState _scrobblingState {ScrobblingState::PendingAdd};
|
||||
Wt::WDateTime _dateTime; // when it was starred
|
||||
FeedbackBackend _backend; // for which backend
|
||||
SyncState _syncState{ SyncState::PendingAdd };
|
||||
Wt::WDateTime _dateTime; // when it was starred
|
||||
|
||||
Wt::Dbo::ptr<Release> _release;
|
||||
Wt::Dbo::ptr<User> _user;
|
||||
};
|
||||
Wt::Dbo::ptr<Release> _release;
|
||||
Wt::Dbo::ptr<User> _user;
|
||||
};
|
||||
} // namespace Database
|
||||
|
||||
|
||||
@@ -31,66 +31,66 @@
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Track;
|
||||
class Session;
|
||||
class User;
|
||||
class Track;
|
||||
class Session;
|
||||
class User;
|
||||
|
||||
class StarredTrack final : public Object<StarredTrack, StarredTrackId>
|
||||
{
|
||||
public:
|
||||
StarredTrack() = default;
|
||||
class StarredTrack final : public Object<StarredTrack, StarredTrackId>
|
||||
{
|
||||
public:
|
||||
StarredTrack() = default;
|
||||
|
||||
struct FindParameters
|
||||
{
|
||||
std::optional<Scrobbler> scrobbler; // for this scrobbler
|
||||
std::optional<ScrobblingState> scrobblingState; // and these states
|
||||
UserId user; // and this user
|
||||
Range range;
|
||||
struct FindParameters
|
||||
{
|
||||
std::optional<FeedbackBackend> backend; // for this backend
|
||||
std::optional<SyncState> syncState; // and these states
|
||||
UserId user; // and this user
|
||||
Range range;
|
||||
|
||||
FindParameters& setScrobbler(Scrobbler _scrobbler, ScrobblingState _scrobblingState) { scrobbler = _scrobbler; scrobblingState = _scrobblingState; return *this; }
|
||||
FindParameters& setUser(UserId _user) {user = _user; return *this; }
|
||||
FindParameters& setRange(Range _range) {range = _range; return *this; }
|
||||
};
|
||||
FindParameters& setFeedbackBackend(FeedbackBackend _backend, SyncState _syncState) { backend = _backend; syncState = _syncState; return *this; }
|
||||
FindParameters& setUser(UserId _user) { user = _user; return *this; }
|
||||
FindParameters& setRange(Range _range) { range = _range; return *this; }
|
||||
};
|
||||
|
||||
// Search utility
|
||||
static std::size_t getCount(Session& session);
|
||||
static pointer find(Session& session, StarredTrackId id);
|
||||
static pointer find(Session& session, TrackId trackId, UserId userId, Scrobbler scrobbler);
|
||||
static RangeResults<StarredTrackId> find(Session& session, const FindParameters& findParams);
|
||||
// Search utility
|
||||
static std::size_t getCount(Session& session);
|
||||
static pointer find(Session& session, StarredTrackId id);
|
||||
static pointer find(Session& session, TrackId trackId, UserId userId, FeedbackBackend backend);
|
||||
static RangeResults<StarredTrackId> find(Session& session, const FindParameters& findParams);
|
||||
|
||||
// Accessors
|
||||
ObjectPtr<Track> getTrack() const { return _track; }
|
||||
ObjectPtr<User> getUser() const { return _user; }
|
||||
Scrobbler getScrobbler() const { return _scrobbler; }
|
||||
const Wt::WDateTime& getDateTime() const { return _dateTime; }
|
||||
ScrobblingState getScrobblingState() const { return _scrobblingState; }
|
||||
// Accessors
|
||||
ObjectPtr<Track> getTrack() const { return _track; }
|
||||
ObjectPtr<User> getUser() const { return _user; }
|
||||
FeedbackBackend getBackend() const { return _backend; }
|
||||
const Wt::WDateTime& getDateTime() const { return _dateTime; }
|
||||
SyncState getSyncState() const { return _syncState; }
|
||||
|
||||
// Setters
|
||||
void setDateTime(const Wt::WDateTime& dateTime);
|
||||
void setScrobblingState(ScrobblingState state) { _scrobblingState = state; }
|
||||
// Setters
|
||||
void setDateTime(const Wt::WDateTime& dateTime);
|
||||
void setSyncState(SyncState state) { _syncState = state; }
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _scrobbler, "scrobbler");
|
||||
Wt::Dbo::field(a, _scrobblingState, "scrobbling_state");
|
||||
Wt::Dbo::field(a, _dateTime, "date_time");
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _backend, "backend");
|
||||
Wt::Dbo::field(a, _syncState, "sync_state");
|
||||
Wt::Dbo::field(a, _dateTime, "date_time");
|
||||
|
||||
Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
|
||||
}
|
||||
Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
|
||||
}
|
||||
|
||||
private:
|
||||
friend class Session;
|
||||
StarredTrack(ObjectPtr<Track> track, ObjectPtr<User> user, Scrobbler scrobbler);
|
||||
static pointer create(Session& session, ObjectPtr<Track> track, ObjectPtr<User> user, Scrobbler scrobbler);
|
||||
private:
|
||||
friend class Session;
|
||||
StarredTrack(ObjectPtr<Track> track, ObjectPtr<User> user, FeedbackBackend backend);
|
||||
static pointer create(Session& session, ObjectPtr<Track> track, ObjectPtr<User> user, FeedbackBackend backend);
|
||||
|
||||
Scrobbler _scrobbler; // for which scrobbler
|
||||
ScrobblingState _scrobblingState {ScrobblingState::PendingAdd};
|
||||
Wt::WDateTime _dateTime; // when it was starred
|
||||
FeedbackBackend _backend; // for which backend
|
||||
SyncState _syncState{ SyncState::PendingAdd };
|
||||
Wt::WDateTime _dateTime; // when it was starred
|
||||
|
||||
Wt::Dbo::ptr<Track> _track;
|
||||
Wt::Dbo::ptr<User> _user;
|
||||
};
|
||||
Wt::Dbo::ptr<Track> _track;
|
||||
Wt::Dbo::ptr<User> _user;
|
||||
};
|
||||
} // namespace Database
|
||||
|
||||
|
||||
@@ -21,11 +21,10 @@
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <ostream>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
@@ -61,23 +60,23 @@ class Track final : public Object<Track, TrackId>
|
||||
public:
|
||||
struct FindParameters
|
||||
{
|
||||
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::string name; // if non empty, must match this name
|
||||
TrackSortMethod sortMethod {TrackSortMethod::None};
|
||||
Range range;
|
||||
Wt::WDateTime writtenAfter;
|
||||
UserId starringUser; // only tracks starred by this user
|
||||
std::optional<Scrobbler> scrobbler; // and for this scrobbler
|
||||
ArtistId artist; // only tracks that involve this artist
|
||||
std::string artistName; // only tracks that involve this artist name
|
||||
EnumSet<TrackArtistLinkType> trackArtistLinkTypes; // and for these link types
|
||||
bool nonRelease {}; // only tracks that do not belong to a release
|
||||
ReleaseId release; // matching this release
|
||||
std::string releaseName; // matching this release name
|
||||
TrackListId trackList; // matching this trackList
|
||||
std::optional<int> trackNumber; // matching this track number
|
||||
bool distinct {true};
|
||||
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::string name; // if non empty, must match this name
|
||||
TrackSortMethod sortMethod {TrackSortMethod::None};
|
||||
Range range;
|
||||
Wt::WDateTime writtenAfter;
|
||||
UserId starringUser; // only tracks starred by this user
|
||||
std::optional<FeedbackBackend> feedbackBackend; // and for this feedback backend
|
||||
ArtistId artist; // only tracks that involve this artist
|
||||
std::string artistName; // only tracks that involve this artist name
|
||||
EnumSet<TrackArtistLinkType> trackArtistLinkTypes; // and for these link types
|
||||
bool nonRelease {}; // only tracks that do not belong to a release
|
||||
ReleaseId release; // matching this release
|
||||
std::string releaseName; // matching this release name
|
||||
TrackListId trackList; // matching this trackList
|
||||
std::optional<int> trackNumber; // matching this track number
|
||||
bool distinct {true};
|
||||
|
||||
FindParameters& setClusters(const std::vector<ClusterId>& _clusters) { clusters = _clusters; 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& setRange(Range _range) { range = _range; 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& setArtistName(std::string_view _artistName, EnumSet<TrackArtistLinkType> _trackArtistLinkTypes = {}) { artistName = _artistName; trackArtistLinkTypes = _trackArtistLinkTypes; 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 std::vector<pointer> findByRecordingMBID(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<TrackId> findTrackMBIDDuplicates(Session& session, Range range);
|
||||
static RangeResults<TrackId> findWithRecordingMBIDAndMissingFeatures(Session& session, Range range);
|
||||
static RangeResults<TrackId> findIdsTrackMBIDDuplicates(Session& session, Range range);
|
||||
static RangeResults<TrackId> findIdsWithRecordingMBIDAndMissingFeatures(Session& session, Range range);
|
||||
|
||||
// Accessors
|
||||
void setScanVersion(std::size_t version) { _scanVersion = version; }
|
||||
|
||||
@@ -46,7 +46,7 @@ namespace Database
|
||||
{
|
||||
Range range;
|
||||
std::vector<T> results;
|
||||
bool moreResults;
|
||||
bool moreResults{};
|
||||
|
||||
RangeResults getSubRange(Range subRange)
|
||||
{
|
||||
@@ -160,13 +160,19 @@ namespace Database
|
||||
void visitAllowedAudioBitrates(std::function<void(Bitrate)>);
|
||||
bool isAudioBitrateAllowed(Bitrate bitrate);
|
||||
|
||||
enum class Scrobbler
|
||||
enum class ScrobblingBackend
|
||||
{
|
||||
Internal = 0,
|
||||
ListenBrainz = 1,
|
||||
};
|
||||
|
||||
enum class ScrobblingState
|
||||
enum class FeedbackBackend
|
||||
{
|
||||
Internal = 0,
|
||||
ListenBrainz = 1,
|
||||
};
|
||||
|
||||
enum class SyncState
|
||||
{
|
||||
PendingAdd = 0,
|
||||
Synchronized = 1,
|
||||
|
||||
@@ -47,20 +47,23 @@ namespace Database {
|
||||
|
||||
struct FindParameters
|
||||
{
|
||||
std::optional<Scrobbler> scrobbler;
|
||||
Range range;
|
||||
std::optional<ScrobblingBackend> scrobblingBackend;
|
||||
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; }
|
||||
};
|
||||
|
||||
static inline const std::size_t MinNameLength{ 3 };
|
||||
static inline const std::size_t MaxNameLength{ 15 };
|
||||
static inline const AudioFormat defaultSubsonicTranscodeFormat{ AudioFormat::OGG_OPUS };
|
||||
static inline const Bitrate defaultSubsonicTranscodeBitrate{ 128000 };
|
||||
static inline const UITheme defaultUITheme{ UITheme::Dark };
|
||||
static inline const SubsonicArtistListMode defaultSubsonicArtistListMode{ SubsonicArtistListMode::AllArtists };
|
||||
static inline const Scrobbler defaultScrobbler{ Scrobbler::Internal };
|
||||
static inline constexpr std::size_t MinNameLength{ 3 };
|
||||
static inline constexpr std::size_t MaxNameLength{ 15 };
|
||||
static inline constexpr AudioFormat defaultSubsonicTranscodeFormat{ AudioFormat::OGG_OPUS };
|
||||
static inline constexpr Bitrate defaultSubsonicTranscodeBitrate{ 128000 };
|
||||
static inline constexpr UITheme defaultUITheme{ UITheme::Dark };
|
||||
static inline constexpr SubsonicArtistListMode defaultSubsonicArtistListMode{ SubsonicArtistListMode::AllArtists };
|
||||
static inline constexpr ScrobblingBackend defaultScrobblingBackend{ ScrobblingBackend::Internal };
|
||||
static inline constexpr FeedbackBackend defaultFeedbackBackend{ FeedbackBackend::Internal };
|
||||
|
||||
User() = default;
|
||||
|
||||
@@ -88,7 +91,8 @@ namespace Database {
|
||||
void setUITheme(UITheme uiTheme) { _uiTheme = uiTheme; }
|
||||
void clearAuthTokens();
|
||||
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() : ""; }
|
||||
|
||||
// read
|
||||
@@ -102,7 +106,8 @@ namespace Database {
|
||||
bool isRadioSet() const { return _radio; }
|
||||
UITheme getUITheme() const { return _uiTheme; }
|
||||
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); }
|
||||
|
||||
template<class Action>
|
||||
@@ -117,7 +122,8 @@ namespace Database {
|
||||
Wt::Dbo::field(a, _subsonicDefaultTranscodeBitrate, "subsonic_default_transcode_bitrate");
|
||||
Wt::Dbo::field(a, _subsonicArtistListMode, "subsonic_artist_list_mode");
|
||||
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");
|
||||
|
||||
// UI player settings
|
||||
@@ -138,7 +144,8 @@ namespace Database {
|
||||
std::string _passwordHash;
|
||||
Wt::WDateTime _lastLogin;
|
||||
UITheme _uiTheme{ defaultUITheme };
|
||||
Scrobbler _scrobbler{ defaultScrobbler };
|
||||
FeedbackBackend _feedbackBackend{ defaultFeedbackBackend };
|
||||
ScrobblingBackend _scrobblingBackend{ defaultScrobblingBackend };
|
||||
std::string _listenbrainzToken; // Musicbrainz Identifier
|
||||
|
||||
// Admin defined settings
|
||||
|
||||
@@ -23,415 +23,423 @@ using namespace Database;
|
||||
|
||||
TEST_F(DatabaseFixture, Artist)
|
||||
{
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
EXPECT_FALSE(Artist::exists(session, 35));
|
||||
EXPECT_FALSE(Artist::exists(session, 0));
|
||||
EXPECT_FALSE(Artist::exists(session, 1));
|
||||
EXPECT_EQ(Artist::getCount(session), 0);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
EXPECT_FALSE(Artist::exists(session, 35));
|
||||
EXPECT_FALSE(Artist::exists(session, 0));
|
||||
EXPECT_FALSE(Artist::exists(session, 1));
|
||||
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_FALSE(!artist.get());
|
||||
EXPECT_EQ(artist.get()->getId(), artist.getId());
|
||||
EXPECT_TRUE(artist.get());
|
||||
EXPECT_FALSE(!artist.get());
|
||||
EXPECT_EQ(artist.get()->getId(), artist.getId());
|
||||
|
||||
EXPECT_TRUE(Artist::exists(session, artist.getId()));
|
||||
EXPECT_EQ(Artist::getCount(session), 1);
|
||||
}
|
||||
EXPECT_TRUE(Artist::exists(session, artist.getId()));
|
||||
EXPECT_EQ(Artist::getCount(session), 1);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
|
||||
auto artists {Artist::find(session, Artist::FindParameters {})};
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
EXPECT_EQ(artists.results.front(), artist.getId());
|
||||
auto artists{ Artist::findIds(session, Artist::FindParameters {}) };
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
EXPECT_EQ(artists.results.front(), artist.getId());
|
||||
|
||||
artists = Artist::findAllOrphans(session, Range {});
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
EXPECT_EQ(artists.results.front(), artist.getId());
|
||||
}
|
||||
artists = Artist::findOrphanIds(session, Range{});
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
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)
|
||||
{
|
||||
ScopedTrack track {session, "MyTrack"};
|
||||
ScopedArtist artist {session, "MyArtist"};
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedArtist artist{ session, "MyArtist" };
|
||||
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
|
||||
track.get().modify()->setName("MyTrackName");
|
||||
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
|
||||
}
|
||||
track.get().modify()->setName("MyTrackName");
|
||||
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
EXPECT_TRUE(Artist::findAllOrphans(session, Range {}).results.empty());
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
EXPECT_TRUE(Artist::findOrphanIds(session, Range{}).results.empty());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
|
||||
auto artists {track->getArtists({TrackArtistLinkType::Artist})};
|
||||
ASSERT_EQ(artists.size(), 1);
|
||||
EXPECT_EQ(artists.front()->getId(), artist.getId());
|
||||
auto artists{ track->getArtists({TrackArtistLinkType::Artist}) };
|
||||
ASSERT_EQ(artists.size(), 1);
|
||||
EXPECT_EQ(artists.front()->getId(), artist.getId());
|
||||
|
||||
ASSERT_EQ(track->getArtistLinks().size(), 1);
|
||||
auto artistLink {track->getArtistLinks().front()};
|
||||
EXPECT_EQ(artistLink->getTrack()->getId(), track.getId());
|
||||
EXPECT_EQ(artistLink->getArtist()->getId(), artist.getId());
|
||||
ASSERT_EQ(track->getArtistLinks().size(), 1);
|
||||
auto artistLink{ track->getArtistLinks().front() };
|
||||
EXPECT_EQ(artistLink->getTrack()->getId(), track.getId());
|
||||
EXPECT_EQ(artistLink->getArtist()->getId(), artist.getId());
|
||||
|
||||
ASSERT_EQ(track->getArtists({TrackArtistLinkType::Artist}).size(), 1);
|
||||
EXPECT_TRUE(track->getArtists({TrackArtistLinkType::ReleaseArtist}).empty());
|
||||
EXPECT_EQ(track->getArtists({}).size(), 1);
|
||||
}
|
||||
ASSERT_EQ(track->getArtists({ TrackArtistLinkType::Artist }).size(), 1);
|
||||
EXPECT_TRUE(track->getArtists({ TrackArtistLinkType::ReleaseArtist }).empty());
|
||||
EXPECT_EQ(track->getArtists({}).size(), 1);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
|
||||
auto artists {track->getArtistIds({TrackArtistLinkType::Artist})};
|
||||
ASSERT_EQ(artists.size(), 1);
|
||||
EXPECT_EQ(artists.front(), artist.getId());
|
||||
auto artists{ track->getArtistIds({TrackArtistLinkType::Artist}) };
|
||||
ASSERT_EQ(artists.size(), 1);
|
||||
EXPECT_EQ(artists.front(), artist.getId());
|
||||
|
||||
ASSERT_EQ(track->getArtistIds({TrackArtistLinkType::Artist}).size(), 1);
|
||||
EXPECT_TRUE(track->getArtistIds({TrackArtistLinkType::ReleaseArtist}).empty());
|
||||
EXPECT_EQ(track->getArtistIds({}).size(), 1);
|
||||
}
|
||||
ASSERT_EQ(track->getArtistIds({ TrackArtistLinkType::Artist }).size(), 1);
|
||||
EXPECT_TRUE(track->getArtistIds({ TrackArtistLinkType::ReleaseArtist }).empty());
|
||||
EXPECT_EQ(track->getArtistIds({}).size(), 1);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
auto tracks {Track::find(session, Track::FindParameters{}.setName("MyTrackName").setArtistName("MyArtist"))};
|
||||
ASSERT_EQ(tracks.results.size(), 1);
|
||||
EXPECT_EQ(tracks.results.front(), track.getId());
|
||||
}
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
auto tracks {Track::find(session, Track::FindParameters{}.setName("MyTrackName").setArtistName("MyArtistFoo"))};
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
}
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
auto tracks {Track::find(session, Track::FindParameters{}.setName("MyTrackNameFoo").setArtistName("MyArtist"))};
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
}
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
auto artists {Artist::find(session, Artist::FindParameters{}.setTrack(track->getId()))};
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
EXPECT_EQ(artists.results.front(), artist.getId());
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto tracks{ Track::findIds(session, Track::FindParameters{}.setName("MyTrackName").setArtistName("MyArtist")) };
|
||||
ASSERT_EQ(tracks.results.size(), 1);
|
||||
EXPECT_EQ(tracks.results.front(), track.getId());
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto tracks{ Track::findIds(session, Track::FindParameters{}.setName("MyTrackName").setArtistName("MyArtistFoo")) };
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto tracks{ Track::findIds(session, Track::FindParameters{}.setName("MyTrackNameFoo").setArtistName("MyArtist")) };
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto artists{ Artist::findIds(session, Artist::FindParameters{}.setTrack(track->getId())) };
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
EXPECT_EQ(artists.results.front(), artist.getId());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Artist_singleTracktMultiRoles)
|
||||
{
|
||||
ScopedTrack track {session, "MyTrack"};
|
||||
ScopedArtist artist {session, "MyArtist"};
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedArtist artist{ session, "MyArtist" };
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
|
||||
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::Writer);
|
||||
}
|
||||
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::Writer);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
EXPECT_TRUE(Artist::findAllOrphans(session, Range {}).results.empty());
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
EXPECT_TRUE(Artist::findOrphanIds(session, Range{}).results.empty());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
EXPECT_EQ(Artist::find(session, Artist::FindParameters {}).results.size(), 1);
|
||||
EXPECT_EQ(Artist::find(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::find(session, Artist::FindParameters {}.setLinkType(TrackArtistLinkType::Writer)).results.size(), 1);
|
||||
EXPECT_EQ(Artist::find(session, Artist::FindParameters {}.setLinkType(TrackArtistLinkType::Composer)).results.size(), 0);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}).results.size(), 1);
|
||||
EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setLinkType(TrackArtistLinkType::Artist)).results.size(), 1);
|
||||
EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setLinkType(TrackArtistLinkType::ReleaseArtist)).results.size(), 1);
|
||||
EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setLinkType(TrackArtistLinkType::Writer)).results.size(), 1);
|
||||
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})};
|
||||
ASSERT_EQ(artists.size(), 1);
|
||||
EXPECT_EQ(artists.front()->getId(), artist.getId());
|
||||
auto artists{ track->getArtists({TrackArtistLinkType::Artist}) };
|
||||
ASSERT_EQ(artists.size(), 1);
|
||||
EXPECT_EQ(artists.front()->getId(), artist.getId());
|
||||
|
||||
artists = track->getArtists({TrackArtistLinkType::ReleaseArtist});
|
||||
ASSERT_EQ(artists.size(), 1);
|
||||
EXPECT_EQ(artists.front()->getId(), artist.getId());
|
||||
artists = track->getArtists({ TrackArtistLinkType::ReleaseArtist });
|
||||
ASSERT_EQ(artists.size(), 1);
|
||||
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()))};
|
||||
EXPECT_EQ(tracks.results.size(), 1);
|
||||
auto tracks{ Track::findIds(session, Track::FindParameters {}.setArtist(artist.getId())) };
|
||||
EXPECT_EQ(tracks.results.size(), 1);
|
||||
|
||||
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::ReleaseArtist}));
|
||||
EXPECT_EQ(tracks.results.size(), 1);
|
||||
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::Artist}));
|
||||
EXPECT_EQ(tracks.results.size(), 1);
|
||||
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::Writer}));
|
||||
EXPECT_EQ(tracks.results.size(), 1);
|
||||
tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::ReleaseArtist }));
|
||||
EXPECT_EQ(tracks.results.size(), 1);
|
||||
tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Artist }));
|
||||
EXPECT_EQ(tracks.results.size(), 1);
|
||||
tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Writer }));
|
||||
EXPECT_EQ(tracks.results.size(), 1);
|
||||
|
||||
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::Composer}));
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
}
|
||||
tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Composer }));
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
EnumSet<TrackArtistLinkType> types{ TrackArtistLink::findUsedTypes(session, artist.getId()) };
|
||||
EXPECT_TRUE(types.contains(TrackArtistLinkType::ReleaseArtist));
|
||||
EXPECT_TRUE(types.contains(TrackArtistLinkType::Artist));
|
||||
EXPECT_TRUE(types.contains(TrackArtistLinkType::Writer));
|
||||
EXPECT_FALSE(types.contains(TrackArtistLinkType::Composer));
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
EnumSet<TrackArtistLinkType> types{ TrackArtistLink::findUsedTypes(session, artist.getId()) };
|
||||
EXPECT_TRUE(types.contains(TrackArtistLinkType::ReleaseArtist));
|
||||
EXPECT_TRUE(types.contains(TrackArtistLinkType::Artist));
|
||||
EXPECT_TRUE(types.contains(TrackArtistLinkType::Writer));
|
||||
EXPECT_FALSE(types.contains(TrackArtistLinkType::Composer));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Artist_singleTrackMultiArtists)
|
||||
{
|
||||
ScopedTrack track {session, "track"};
|
||||
ScopedArtist artist1 {session, "artist1"};
|
||||
ScopedArtist artist2 {session, "artist2"};
|
||||
ASSERT_NE(artist1.getId(), artist2.getId());
|
||||
ScopedTrack track{ session, "track" };
|
||||
ScopedArtist artist1{ session, "artist1" };
|
||||
ScopedArtist artist2{ session, "artist2" };
|
||||
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(), artist2.get(), TrackArtistLinkType::Artist);
|
||||
}
|
||||
TrackArtistLink::create(session, track.get(), artist1.get(), TrackArtistLinkType::Artist);
|
||||
TrackArtistLink::create(session, track.get(), artist2.get(), TrackArtistLinkType::Artist);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
EXPECT_TRUE(Artist::findAllOrphans(session, Range {}).results.empty());
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
EXPECT_TRUE(Artist::findOrphanIds(session, Range{}).results.empty());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
|
||||
auto artists {track->getArtists({TrackArtistLinkType::Artist})};
|
||||
ASSERT_EQ(artists.size(), 2);
|
||||
EXPECT_TRUE((artists[0]->getId() == artist1.getId() && artists[1]->getId() == artist2.getId())
|
||||
|| (artists[0]->getId() == artist2.getId() && artists[1]->getId() == artist1.getId()));
|
||||
auto artists{ track->getArtists({TrackArtistLinkType::Artist}) };
|
||||
ASSERT_EQ(artists.size(), 2);
|
||||
EXPECT_TRUE((artists[0]->getId() == artist1.getId() && artists[1]->getId() == artist2.getId())
|
||||
|| (artists[0]->getId() == artist2.getId() && artists[1]->getId() == artist1.getId()));
|
||||
|
||||
EXPECT_EQ(track->getArtists({}).size(), 2);
|
||||
EXPECT_EQ(track->getArtists({TrackArtistLinkType::Artist}).size(), 2);
|
||||
EXPECT_TRUE(track->getArtists({TrackArtistLinkType::ReleaseArtist}).empty());
|
||||
EXPECT_EQ(Artist::find(session, Artist::FindParameters {}).results.size(), 2);
|
||||
EXPECT_EQ(Artist::find(session, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::Random)).results.size(), 2);
|
||||
}
|
||||
EXPECT_EQ(track->getArtists({}).size(), 2);
|
||||
EXPECT_EQ(track->getArtists({ TrackArtistLinkType::Artist }).size(), 2);
|
||||
EXPECT_TRUE(track->getArtists({ TrackArtistLinkType::ReleaseArtist }).empty());
|
||||
EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}).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()))};
|
||||
ASSERT_EQ(tracks.results.size(), 1);
|
||||
EXPECT_EQ(tracks.results.front(), track->getId());
|
||||
auto tracks{ Track::findIds(session, Track::FindParameters {}.setArtist(artist1->getId())) };
|
||||
ASSERT_EQ(tracks.results.size(), 1);
|
||||
EXPECT_EQ(tracks.results.front(), track->getId());
|
||||
|
||||
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist2->getId()));
|
||||
ASSERT_EQ(tracks.results.size(), 1);
|
||||
EXPECT_EQ(tracks.results.front(), track->getId());
|
||||
tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist2->getId()));
|
||||
ASSERT_EQ(tracks.results.size(), 1);
|
||||
EXPECT_EQ(tracks.results.front(), track->getId());
|
||||
|
||||
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist1->getId(), {TrackArtistLinkType::ReleaseArtist}));
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist1->getId(), { TrackArtistLinkType::ReleaseArtist }));
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
|
||||
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist1->getId(), {TrackArtistLinkType::Artist}));
|
||||
EXPECT_EQ(tracks.results.size(), 1);
|
||||
tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist1->getId(), { TrackArtistLinkType::Artist }));
|
||||
EXPECT_EQ(tracks.results.size(), 1);
|
||||
|
||||
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist2->getId(), {TrackArtistLinkType::ReleaseArtist}));
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist2->getId(), { TrackArtistLinkType::ReleaseArtist }));
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
|
||||
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist2->getId(), {TrackArtistLinkType::Artist}));
|
||||
EXPECT_EQ(tracks.results.size(), 1);
|
||||
}
|
||||
tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist2->getId(), { TrackArtistLinkType::Artist }));
|
||||
EXPECT_EQ(tracks.results.size(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Artist_findByName)
|
||||
{
|
||||
ScopedArtist artist {session, "AAA"};
|
||||
ScopedTrack track {session, "MyTrack"}; // filters does not work on orphans
|
||||
ScopedArtist artist{ session, "AAA" };
|
||||
ScopedTrack track{ session, "MyTrack" }; // filters does not work on orphans
|
||||
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
artist.get().modify()->setSortName("ZZZ");
|
||||
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
artist.get().modify()->setSortName("ZZZ");
|
||||
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"}))};
|
||||
ASSERT_EQ(artistsByAAA.results.size(), 1);
|
||||
EXPECT_EQ(artistsByAAA.results.front(), artist.getId());
|
||||
const auto artistsByAAA{ Artist::findIds(session, Artist::FindParameters {}.setKeywords({"A"})) };
|
||||
ASSERT_EQ(artistsByAAA.results.size(), 1);
|
||||
EXPECT_EQ(artistsByAAA.results.front(), artist.getId());
|
||||
|
||||
const auto artistsByZZZ {Artist::Artist::find(session, Artist::FindParameters {}.setKeywords({"Z"}))};
|
||||
ASSERT_EQ(artistsByZZZ.results.size(), 1);
|
||||
EXPECT_EQ(artistsByZZZ.results.front(), artist.getId());
|
||||
const auto artistsByZZZ{ Artist::Artist::findIds(session, Artist::FindParameters {}.setKeywords({"Z"})) };
|
||||
ASSERT_EQ(artistsByZZZ.results.size(), 1);
|
||||
EXPECT_EQ(artistsByZZZ.results.front(), artist.getId());
|
||||
|
||||
EXPECT_TRUE(Artist::find(session, "NNN").empty());
|
||||
EXPECT_EQ(Artist::find(session, "AAA").size(), 1);
|
||||
}
|
||||
EXPECT_TRUE(Artist::find(session, "NNN").empty());
|
||||
EXPECT_EQ(Artist::find(session, "AAA").size(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Artist_findByNameEscaped)
|
||||
{
|
||||
ScopedArtist artist1 {session, "MyArtist%"};
|
||||
ScopedArtist artist2 {session, "%MyArtist"};
|
||||
ScopedArtist artist3 {session, "%_MyArtist"};
|
||||
ScopedArtist artist1{ session, R"(MyArtist%)" };
|
||||
ScopedArtist artist2{ session, R"(%MyArtist)" };
|
||||
ScopedArtist artist3{ session, R"(%_MyArtist)" };
|
||||
|
||||
ScopedArtist artist4 {session, "MyArtist%foo"};
|
||||
ScopedArtist artist5 {session, "foo%MyArtist"};
|
||||
ScopedArtist artist6 {session, "%AMyArtist"};
|
||||
ScopedArtist artist4{ session, R"(MyArtist%foo)" };
|
||||
ScopedArtist artist5{ session, R"(foo%MyArtist)" };
|
||||
ScopedArtist artist6{ session, R"(%AMyArtist)" };
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
{
|
||||
const auto artists {Artist::find(session, "MyArtist%")};
|
||||
ASSERT_TRUE(artists.size() == 1);
|
||||
EXPECT_EQ(artists.front()->getId(), artist1.getId());
|
||||
EXPECT_TRUE(Artist::find(session, "MyArtistFoo").empty());
|
||||
}
|
||||
{
|
||||
const auto artists {Artist::find(session, "%MyArtist")};
|
||||
ASSERT_TRUE(artists.size() == 1);
|
||||
EXPECT_EQ(artists.front()->getId(), artist2.getId());
|
||||
EXPECT_TRUE(Artist::find(session, "FooMyArtist").empty());
|
||||
}
|
||||
{
|
||||
const auto artists {Artist::find(session, "%_MyArtist")};
|
||||
ASSERT_TRUE(artists.size() == 1);
|
||||
ASSERT_EQ(artists.front()->getId(), artist3.getId());
|
||||
EXPECT_TRUE(Artist::find(session, "%CMyArtist").empty());
|
||||
}
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
{
|
||||
const auto artists{ Artist::find(session, R"(MyArtist%)") };
|
||||
ASSERT_TRUE(artists.size() == 1);
|
||||
EXPECT_EQ(artists.front()->getId(), artist1.getId());
|
||||
EXPECT_TRUE(Artist::find(session, R"(MyArtistFoo)").empty());
|
||||
}
|
||||
{
|
||||
const auto artists{ Artist::find(session, R"(%MyArtist)") };
|
||||
ASSERT_TRUE(artists.size() == 1);
|
||||
EXPECT_EQ(artists.front()->getId(), artist2.getId());
|
||||
EXPECT_TRUE(Artist::find(session, R"(FooMyArtist)").empty());
|
||||
}
|
||||
{
|
||||
const auto artists{ Artist::find(session, R"(%_MyArtist)") };
|
||||
ASSERT_TRUE(artists.size() == 1);
|
||||
ASSERT_EQ(artists.front()->getId(), artist3.getId());
|
||||
EXPECT_TRUE(Artist::find(session, R"(%CMyArtist)").empty());
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
{
|
||||
const auto artists {Artist::find(session, Artist::FindParameters {}.setKeywords({"MyArtist"}))};
|
||||
EXPECT_EQ(artists.results.size(), 6);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
{
|
||||
const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setKeywords({"MyArtist"})) };
|
||||
EXPECT_EQ(artists.results.size(), 6);
|
||||
}
|
||||
|
||||
{
|
||||
const auto artists {Artist::find(session, Artist::FindParameters {}.setKeywords({"MyArtist%"}).setSortMethod(ArtistSortMethod::ByName))};
|
||||
ASSERT_EQ(artists.results.size(), 2);
|
||||
EXPECT_EQ(artists.results[0], artist1.getId());
|
||||
EXPECT_EQ(artists.results[1], artist4.getId());
|
||||
}
|
||||
{
|
||||
const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setKeywords({"MyArtist%"}).setSortMethod(ArtistSortMethod::ByName)) };
|
||||
ASSERT_EQ(artists.results.size(), 2);
|
||||
EXPECT_EQ(artists.results[0], artist1.getId());
|
||||
EXPECT_EQ(artists.results[1], artist4.getId());
|
||||
}
|
||||
|
||||
{
|
||||
const auto artists {Artist::find(session, Artist::FindParameters {}.setKeywords({"%MyArtist"}).setSortMethod(ArtistSortMethod::ByName))};
|
||||
ASSERT_EQ(artists.results.size(), 2);
|
||||
EXPECT_EQ(artists.results[0], artist2.getId());
|
||||
EXPECT_EQ(artists.results[1], artist5.getId());
|
||||
}
|
||||
{
|
||||
const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setKeywords({"%MyArtist"}).setSortMethod(ArtistSortMethod::ByName)) };
|
||||
ASSERT_EQ(artists.results.size(), 2);
|
||||
EXPECT_EQ(artists.results[0], artist2.getId());
|
||||
EXPECT_EQ(artists.results[1], artist5.getId());
|
||||
}
|
||||
|
||||
{
|
||||
const auto artists {Artist::find(session, Artist::FindParameters {}.setKeywords({"_MyArtist"}).setSortMethod(ArtistSortMethod::ByName))};
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
EXPECT_EQ(artists.results[0], artist3.getId());
|
||||
}
|
||||
}
|
||||
{
|
||||
const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setKeywords({"_MyArtist"}).setSortMethod(ArtistSortMethod::ByName)) };
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
EXPECT_EQ(artists.results[0], artist3.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Artist_sortMethod)
|
||||
{
|
||||
ScopedArtist artistA {session, "artistA"};
|
||||
ScopedArtist artistB {session, "artistB"};
|
||||
ScopedArtist artistA{ session, "artistA" };
|
||||
ScopedArtist artistB{ session, "artistB" };
|
||||
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
|
||||
artistA.get().modify()->setSortName("sortNameB");
|
||||
artistB.get().modify()->setSortName("sortNameA");
|
||||
}
|
||||
artistA.get().modify()->setSortName("sortNameB");
|
||||
artistB.get().modify()->setSortName("sortNameA");
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
|
||||
auto allArtistsByName {Artist::find(session, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::ByName))};
|
||||
auto allArtistsBySortName {Artist::find(session, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::BySortName))};
|
||||
auto allArtistsByName{ Artist::findIds(session, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::ByName)) };
|
||||
auto allArtistsBySortName{ Artist::findIds(session, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::BySortName)) };
|
||||
|
||||
ASSERT_EQ(allArtistsByName.results.size(), 2);
|
||||
EXPECT_EQ(allArtistsByName.results.front(), artistA.getId());
|
||||
EXPECT_EQ(allArtistsByName.results.back(), artistB.getId());
|
||||
ASSERT_EQ(allArtistsByName.results.size(), 2);
|
||||
EXPECT_EQ(allArtistsByName.results.front(), artistA.getId());
|
||||
EXPECT_EQ(allArtistsByName.results.back(), artistB.getId());
|
||||
|
||||
ASSERT_EQ(allArtistsBySortName.results.size(), 2);
|
||||
EXPECT_EQ(allArtistsBySortName.results.front(), artistB.getId());
|
||||
EXPECT_EQ(allArtistsBySortName.results.back(), artistA.getId());
|
||||
}
|
||||
ASSERT_EQ(allArtistsBySortName.results.size(), 2);
|
||||
EXPECT_EQ(allArtistsBySortName.results.front(), artistB.getId());
|
||||
EXPECT_EQ(allArtistsBySortName.results.back(), artistA.getId());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Artist_nonReleaseTracks)
|
||||
{
|
||||
ScopedArtist artist {session, "artist"};
|
||||
ScopedTrack track1 {session, "MyTrack1"};
|
||||
ScopedTrack track2 {session, "MyTrack2"};
|
||||
ScopedRelease release{session, "MyRelease"};
|
||||
ScopedArtist artist{ session, "artist" };
|
||||
ScopedTrack track1{ session, "MyTrack1" };
|
||||
ScopedTrack track2{ session, "MyTrack2" };
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
|
||||
auto tracks {Track::find(session, Track::FindParameters {}.setNonRelease(true).setArtist(artist->getId()))};
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
}
|
||||
auto tracks{ Track::findIds(session, Track::FindParameters {}.setNonRelease(true).setArtist(artist->getId())) };
|
||||
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, track2.get(), artist.get(), TrackArtistLinkType::Artist);
|
||||
TrackArtistLink::create(session, track1.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))};
|
||||
ASSERT_EQ(tracks.results.size(), 1);
|
||||
EXPECT_EQ(tracks.results.front(), track2.getId());
|
||||
}
|
||||
const auto tracks{ Track::findIds(session, Track::FindParameters {}.setArtist(artist.getId()).setNonRelease(true)) };
|
||||
ASSERT_EQ(tracks.results.size(), 1);
|
||||
EXPECT_EQ(tracks.results.front(), track2.getId());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Artist_findByRelease)
|
||||
{
|
||||
ScopedArtist artist {session, "artist"};
|
||||
ScopedTrack track {session, "MyTrack"};
|
||||
ScopedRelease release{session, "MyRelease"};
|
||||
ScopedArtist artist{ session, "artist" };
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
const auto artists {Artist::find(session, Artist::FindParameters {}.setRelease(release.getId()))};
|
||||
EXPECT_EQ(artists.results.size(), 0);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setRelease(release.getId())) };
|
||||
EXPECT_EQ(artists.results.size(), 0);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
const auto artists {Artist::find(session, Artist::FindParameters {}.setRelease(release.getId()))};
|
||||
EXPECT_EQ(artists.results.size(), 0);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setRelease(release.getId())) };
|
||||
EXPECT_EQ(artists.results.size(), 0);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
track.get().modify()->setRelease(release.get());
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
track.get().modify()->setRelease(release.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
const auto artists {Artist::find(session, Artist::FindParameters {}.setRelease(release.getId()))};
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
EXPECT_EQ(artists.results.front(), artist.getId());
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setRelease(release.getId())) };
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
EXPECT_EQ(artists.results.front(), artist.getId());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -133,11 +133,11 @@ TEST_F(DatabaseFixture, Cluster_singleTrack)
|
||||
{
|
||||
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);
|
||||
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());
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
std::list<ScopedTrack> tracks;
|
||||
@@ -329,7 +374,7 @@ TEST_F(DatabaseFixture, Cluster_singleTrackSingleReleaseSingleCluster)
|
||||
{
|
||||
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);
|
||||
EXPECT_EQ(releases.results.front(), release.getId());
|
||||
}
|
||||
@@ -337,7 +382,7 @@ TEST_F(DatabaseFixture, Cluster_singleTrackSingleReleaseSingleCluster)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -370,8 +415,8 @@ TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiClusters)
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
EXPECT_TRUE(ClusterType::findOrphans(session, Range{}).results.empty());
|
||||
EXPECT_EQ(Cluster::findOrphans(session, Range{}).results.size(), 2);
|
||||
EXPECT_TRUE(Release::findOrphans(session, Range{}).results.empty());
|
||||
EXPECT_TRUE(Artist::findAllOrphans(session, Range{}).results.empty());
|
||||
EXPECT_TRUE(Release::findOrphanIds(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 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);
|
||||
EXPECT_EQ(artists.results.front(), artist.getId());
|
||||
|
||||
EXPECT_TRUE(Artist::find(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({ cluster2.getId() })).results.empty());
|
||||
EXPECT_TRUE(Artist::findIds(session, Artist::FindParameters{}.setClusters({ cluster3.getId() })).results.empty());
|
||||
|
||||
cluster2.get().modify()->addTrack(track.get());
|
||||
}
|
||||
@@ -396,19 +441,19 @@ TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiClusters)
|
||||
{
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
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() };
|
||||
EXPECT_TRUE(Cluster::findOrphans(session, Range{}).results.empty());
|
||||
EXPECT_TRUE(Release::findOrphans(session, Range{}).results.empty());
|
||||
EXPECT_TRUE(Artist::findAllOrphans(session, Range{}).results.empty());
|
||||
EXPECT_TRUE(Release::findOrphanIds(session, Range{}).results.empty());
|
||||
EXPECT_TRUE(Artist::findOrphanIds(session, Range{}).results.empty());
|
||||
}
|
||||
|
||||
{
|
||||
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);
|
||||
EXPECT_EQ(artists.results.front(), artist.getId());
|
||||
}
|
||||
@@ -470,7 +515,7 @@ TEST_F(DatabaseFixture, MultiTracksSingleArtistMultiClusters)
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
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::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);
|
||||
EXPECT_EQ(artists.results.front(), artist.getId());
|
||||
}
|
||||
@@ -505,7 +550,7 @@ TEST_F(DatabaseFixture, MultipleTracksSingleClusterSimilarity)
|
||||
{
|
||||
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);
|
||||
for (const TrackId similarTrackId : similarTracks.results)
|
||||
{
|
||||
@@ -546,14 +591,14 @@ TEST_F(DatabaseFixture, MultipleTracksMultipleClustersSimilarity)
|
||||
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);
|
||||
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));
|
||||
}
|
||||
|
||||
{
|
||||
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);
|
||||
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));
|
||||
@@ -582,22 +627,22 @@ TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtistSingleCluster)
|
||||
|
||||
EXPECT_TRUE(Cluster::findOrphans(session, Range{}).results.empty());
|
||||
EXPECT_TRUE(ClusterType::findOrphans(session, Range{}).results.empty());
|
||||
EXPECT_TRUE(Artist::findAllOrphans(session, Range{}).results.empty());
|
||||
EXPECT_TRUE(Release::findOrphans(session, Range{}).results.empty());
|
||||
EXPECT_TRUE(Artist::findOrphanIds(session, Range{}).results.empty());
|
||||
EXPECT_TRUE(Release::findOrphanIds(session, Range{}).results.empty());
|
||||
}
|
||||
|
||||
{
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
EXPECT_EQ(releases.results.front(), release.getId());
|
||||
}
|
||||
@@ -624,11 +669,11 @@ TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtistMultiClusters)
|
||||
{
|
||||
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);
|
||||
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);
|
||||
EXPECT_EQ(releases.results.front(), release.getId());
|
||||
}
|
||||
@@ -982,9 +1027,9 @@ TEST_F(DatabaseFixture, MultipleTracksMultipleArtistsMultiClusters)
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
EXPECT_TRUE(artist1->findSimilarArtists().results.empty());
|
||||
EXPECT_TRUE(artist2->findSimilarArtists().results.empty());
|
||||
EXPECT_TRUE(artist3->findSimilarArtists().results.empty());
|
||||
EXPECT_TRUE(artist1->findSimilarArtistIds().results.empty());
|
||||
EXPECT_TRUE(artist2->findSimilarArtistIds().results.empty());
|
||||
EXPECT_TRUE(artist3->findSimilarArtistIds().results.empty());
|
||||
}
|
||||
|
||||
std::list<ScopedTrack> tracks;
|
||||
@@ -1016,35 +1061,35 @@ TEST_F(DatabaseFixture, MultipleTracksMultipleArtistsMultiClusters)
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
|
||||
{
|
||||
auto artists{ artist1->findSimilarArtists() };
|
||||
auto artists{ artist1->findSimilarArtistIds() };
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
{
|
||||
auto artists{ artist1->findSimilarArtists({TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist}) };
|
||||
auto artists{ artist1->findSimilarArtistIds({TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist}) };
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
EXPECT_EQ(artists.results.front(), artist2.getId());
|
||||
}
|
||||
|
||||
{
|
||||
auto artists{ artist1->findSimilarArtists({TrackArtistLinkType::Composer}) };
|
||||
auto artists{ artist1->findSimilarArtistIds({TrackArtistLinkType::Composer}) };
|
||||
EXPECT_TRUE(artists.results.empty());
|
||||
}
|
||||
|
||||
{
|
||||
auto artists{ artist2->findSimilarArtists() };
|
||||
auto artists{ artist2->findSimilarArtistIds() };
|
||||
ASSERT_EQ(artists.results.size(), 2);
|
||||
EXPECT_EQ(artists.results[0], artist1.getId());
|
||||
EXPECT_EQ(artists.results[1], artist3.getId());
|
||||
|
||||
@@ -25,85 +25,85 @@ using namespace Database;
|
||||
|
||||
TEST_F(DatabaseFixture, MultiTracksSingleArtistSingleRelease)
|
||||
{
|
||||
constexpr std::size_t nbTracks {10};
|
||||
std::list<ScopedTrack> tracks;
|
||||
ScopedArtist artist {session, "MyArtst"};
|
||||
ScopedRelease release {session, "MyRelease"};
|
||||
constexpr std::size_t nbTracks{ 10 };
|
||||
std::list<ScopedTrack> tracks;
|
||||
ScopedArtist artist{ session, "MyArtst" };
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
|
||||
for (std::size_t i {}; i < nbTracks; ++i)
|
||||
{
|
||||
tracks.emplace_back(session, "MyTrack" + std::to_string(i));
|
||||
for (std::size_t i{}; i < nbTracks; ++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);
|
||||
tracks.back().get().modify()->setRelease(release.get());
|
||||
}
|
||||
TrackArtistLink::create(session, tracks.back().get(), artist.get(), TrackArtistLinkType::Artist);
|
||||
tracks.back().get().modify()->setRelease(release.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
EXPECT_TRUE(Release::findOrphans(session, Range {}).results.empty());
|
||||
EXPECT_TRUE(Artist::findAllOrphans(session, Range {}).results.empty());
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
EXPECT_TRUE(Release::findOrphanIds(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()))};
|
||||
ASSERT_EQ(releases.results.size(), 1);
|
||||
EXPECT_EQ(releases.results.front(), release.getId());
|
||||
auto releases{ Release::findIds(session, Release::FindParameters {}.setArtist(artist.getId())) };
|
||||
ASSERT_EQ(releases.results.size(), 1);
|
||||
EXPECT_EQ(releases.results.front(), release.getId());
|
||||
|
||||
const auto releaseTracks {Track::find(session, Track::FindParameters {}.setRelease(release.getId()))};
|
||||
EXPECT_EQ(releaseTracks.results.size(), nbTracks);
|
||||
}
|
||||
const auto releaseTracks{ Track::find(session, Track::FindParameters {}.setRelease(release.getId())) };
|
||||
EXPECT_EQ(releaseTracks.results.size(), nbTracks);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtist)
|
||||
{
|
||||
ScopedTrack track {session, "MyTrack"};
|
||||
ScopedRelease release {session, "MyRelease"};
|
||||
ScopedArtist artist {session, "MyArtist"};
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
ScopedArtist artist{ session, "MyArtist" };
|
||||
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
|
||||
auto trackArtistLink {TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist)};
|
||||
track.get().modify()->setRelease(release.get());
|
||||
}
|
||||
auto trackArtistLink{ TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist) };
|
||||
track.get().modify()->setRelease(release.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
|
||||
auto releases {Release::find(session, Release::FindParameters {}.setArtist(artist.getId()))};
|
||||
ASSERT_EQ(releases.results.size(), 1);
|
||||
EXPECT_EQ(releases.results.front(), release.getId());
|
||||
auto releases{ Release::findIds(session, Release::FindParameters {}.setArtist(artist.getId())) };
|
||||
ASSERT_EQ(releases.results.size(), 1);
|
||||
EXPECT_EQ(releases.results.front(), release.getId());
|
||||
|
||||
auto artists {release->getArtists()};
|
||||
ASSERT_EQ(artists.size(), 1);
|
||||
ASSERT_EQ(artists.front()->getId(), artist.getId());
|
||||
}
|
||||
auto artists{ release->getArtists() };
|
||||
ASSERT_EQ(artists.size(), 1);
|
||||
ASSERT_EQ(artists.front()->getId(), artist.getId());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, SingleUser)
|
||||
{
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
EXPECT_TRUE(User::find(session, User::FindParameters {}).results.empty());
|
||||
EXPECT_EQ(User::getCount(session), 0);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
EXPECT_TRUE(User::find(session, User::FindParameters{}).results.empty());
|
||||
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::getCount(session), 1);
|
||||
}
|
||||
EXPECT_EQ(User::find(session, User::FindParameters{}).results.size(), 1);
|
||||
EXPECT_EQ(User::getCount(session), 1);
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ TEST_F(DatabaseFixture, Listen_getAll)
|
||||
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() };
|
||||
@@ -45,7 +45,7 @@ TEST_F(DatabaseFixture, Listen_getAll)
|
||||
|
||||
{
|
||||
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);
|
||||
}
|
||||
@@ -61,12 +61,12 @@ TEST_F(DatabaseFixture, Listen_get)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
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 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);
|
||||
}
|
||||
|
||||
@@ -74,18 +74,18 @@ TEST_F(DatabaseFixture, Listen_get)
|
||||
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);
|
||||
ASSERT_EQ(listens.results.size(), 1);
|
||||
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);
|
||||
}
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -95,14 +95,14 @@ TEST_F(DatabaseFixture, Listen_get_multi)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
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 listen1{ session, user.lockAndGet(), track.lockAndGet(), Scrobbler::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 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(), ScrobblingBackend::Internal, Wt::WDateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12, 0, 1}} };
|
||||
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 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);
|
||||
EXPECT_EQ(listens.results[0], listen1.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} };
|
||||
ASSERT_GT(dateTime2, dateTime1);
|
||||
|
||||
ScopedListen listen1{ session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime1 };
|
||||
ScopedListen listen2{ session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime2 };
|
||||
ScopedListen listen1{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime1 };
|
||||
ScopedListen listen2{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime2 };
|
||||
|
||||
{
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -148,19 +148,19 @@ TEST_F(DatabaseFixture, Listen_getTopArtists)
|
||||
ScopedTrack track1{ session, "MyTrack" };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
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 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.moreResults, false);
|
||||
}
|
||||
|
||||
ScopedTrack track2{ session, "MyTrack2" };
|
||||
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() };
|
||||
@@ -171,20 +171,20 @@ TEST_F(DatabaseFixture, Listen_getTopArtists)
|
||||
{
|
||||
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);
|
||||
EXPECT_EQ(artists.results[0], artist1->getId());
|
||||
}
|
||||
{
|
||||
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);
|
||||
}
|
||||
{
|
||||
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);
|
||||
}
|
||||
{
|
||||
@@ -194,7 +194,7 @@ TEST_F(DatabaseFixture, Listen_getTopArtists)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -218,24 +218,24 @@ TEST_F(DatabaseFixture, Listen_getTopArtists_multi)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
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 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);
|
||||
EXPECT_EQ(artists.results[0], artist1->getId());
|
||||
}
|
||||
ScopedListen listen2{ session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime.addSecs(2) };
|
||||
ScopedListen listen3{ session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime.addSecs(3) };
|
||||
ScopedListen listen2{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime.addSecs(2) };
|
||||
ScopedListen listen3{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime.addSecs(3) };
|
||||
{
|
||||
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);
|
||||
EXPECT_EQ(artists.results[0], artist2->getId());
|
||||
EXPECT_EQ(artists.results[1], artist1->getId());
|
||||
@@ -243,7 +243,7 @@ TEST_F(DatabaseFixture, Listen_getTopArtists_multi)
|
||||
{
|
||||
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);
|
||||
EXPECT_EQ(artists.moreResults, true);
|
||||
EXPECT_EQ(artists.results[0], artist2->getId());
|
||||
@@ -256,7 +256,7 @@ TEST_F(DatabaseFixture, Listen_getTopArtists_cluster)
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
ScopedArtist artist{ session, "MyArtist" };
|
||||
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" };
|
||||
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" };
|
||||
|
||||
@@ -268,7 +268,7 @@ TEST_F(DatabaseFixture, Listen_getTopArtists_cluster)
|
||||
{
|
||||
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);
|
||||
}
|
||||
{
|
||||
@@ -278,7 +278,7 @@ TEST_F(DatabaseFixture, Listen_getTopArtists_cluster)
|
||||
{
|
||||
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);
|
||||
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} };
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
track.get().modify()->setRelease(release.get());
|
||||
}
|
||||
|
||||
{
|
||||
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.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 releases{ Listen::getTopReleases(session, user->getId(), Scrobbler::Internal, {}) };
|
||||
auto releases{ Listen::getTopReleases(session, user->getId(), ScrobblingBackend::Internal, {}) };
|
||||
EXPECT_EQ(releases.moreResults, false);
|
||||
ASSERT_EQ(releases.results.size(), 1);
|
||||
EXPECT_EQ(releases.results[0], release.getId());
|
||||
@@ -316,7 +316,7 @@ TEST_F(DatabaseFixture, Listen_getTopReleases)
|
||||
{
|
||||
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.results.size(), 0);
|
||||
}
|
||||
@@ -328,12 +328,12 @@ TEST_F(DatabaseFixture, Listen_getTopReleases_multi)
|
||||
ScopedTrack track2{ session, "MyTrack" };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
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 release2{ session, "MyRelease2" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
track1.get().modify()->setRelease(release1.get());
|
||||
track2.get().modify()->setRelease(release2.get());
|
||||
}
|
||||
@@ -341,28 +341,28 @@ TEST_F(DatabaseFixture, Listen_getTopReleases_multi)
|
||||
{
|
||||
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);
|
||||
ASSERT_EQ(releases.results.size(), 1);
|
||||
EXPECT_EQ(releases.results[0], release1.getId());
|
||||
}
|
||||
ScopedListen listen2{ session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime };
|
||||
ScopedListen listen3{ 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(), ScrobblingBackend::Internal, dateTime };
|
||||
{
|
||||
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);
|
||||
ASSERT_EQ(releases.results.size(), 2);
|
||||
EXPECT_EQ(releases.results[0], release2.getId());
|
||||
EXPECT_EQ(releases.results[1], release1.getId());
|
||||
}
|
||||
ScopedListen listen4{ session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime };
|
||||
ScopedListen listen5{ 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(), ScrobblingBackend::Internal, dateTime };
|
||||
{
|
||||
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);
|
||||
ASSERT_EQ(releases.results.size(), 2);
|
||||
EXPECT_EQ(releases.results[0], release1.getId());
|
||||
@@ -375,20 +375,20 @@ TEST_F(DatabaseFixture, Listen_getTopReleases_cluster)
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
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" };
|
||||
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" };
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
track.get().modify()->setRelease(release.get());
|
||||
}
|
||||
|
||||
{
|
||||
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);
|
||||
}
|
||||
{
|
||||
@@ -399,7 +399,7 @@ TEST_F(DatabaseFixture, Listen_getTopReleases_cluster)
|
||||
{
|
||||
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[0], release.getId());
|
||||
}
|
||||
@@ -414,17 +414,17 @@ TEST_F(DatabaseFixture, Listen_getTopTracks)
|
||||
{
|
||||
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);
|
||||
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 tracks{ Listen::getTopTracks(session, user->getId(), Scrobbler::Internal, {}) };
|
||||
auto tracks{ Listen::getTopTracks(session, user->getId(), ScrobblingBackend::Internal, {}) };
|
||||
EXPECT_EQ(tracks.moreResults, false);
|
||||
ASSERT_EQ(tracks.results.size(), 1);
|
||||
EXPECT_EQ(tracks.results[0], track.getId());
|
||||
@@ -432,7 +432,7 @@ TEST_F(DatabaseFixture, Listen_getTopTracks)
|
||||
{
|
||||
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.results.size(), 0);
|
||||
}
|
||||
@@ -444,33 +444,33 @@ TEST_F(DatabaseFixture, Listen_getTopTrack_multi)
|
||||
ScopedTrack track2{ session, "MyTrack2" };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
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 tracks{ Listen::getTopTracks(session, user->getId(), Scrobbler::Internal, {}) };
|
||||
auto tracks{ Listen::getTopTracks(session, user->getId(), ScrobblingBackend::Internal, {}) };
|
||||
EXPECT_EQ(tracks.moreResults, false);
|
||||
ASSERT_EQ(tracks.results.size(), 1);
|
||||
EXPECT_EQ(tracks.results[0], track1.getId());
|
||||
}
|
||||
ScopedListen listen2{ session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime };
|
||||
ScopedListen listen3{ 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(), ScrobblingBackend::Internal, dateTime };
|
||||
{
|
||||
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);
|
||||
ASSERT_EQ(tracks.results.size(), 2);
|
||||
EXPECT_EQ(tracks.results[0], track2.getId());
|
||||
EXPECT_EQ(tracks.results[1], track1.getId());
|
||||
}
|
||||
ScopedListen listen4{ session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime };
|
||||
ScopedListen listen5{ 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(), ScrobblingBackend::Internal, dateTime };
|
||||
{
|
||||
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);
|
||||
ASSERT_EQ(tracks.results.size(), 2);
|
||||
EXPECT_EQ(tracks.results[0], track1.getId());
|
||||
@@ -483,14 +483,14 @@ TEST_F(DatabaseFixture, Listen_getTopTracks_cluster)
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
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" };
|
||||
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" };
|
||||
|
||||
{
|
||||
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);
|
||||
}
|
||||
{
|
||||
@@ -501,7 +501,7 @@ TEST_F(DatabaseFixture, Listen_getTopTracks_cluster)
|
||||
{
|
||||
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[0], track.getId());
|
||||
}
|
||||
@@ -521,31 +521,31 @@ TEST_F(DatabaseFixture, Listen_getRecentArtists)
|
||||
{
|
||||
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.moreResults, false);
|
||||
}
|
||||
|
||||
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 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);
|
||||
EXPECT_EQ(artists.results[0], artist->getId());
|
||||
}
|
||||
{
|
||||
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);
|
||||
}
|
||||
{
|
||||
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);
|
||||
}
|
||||
{
|
||||
@@ -555,7 +555,7 @@ TEST_F(DatabaseFixture, Listen_getRecentArtists)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -579,32 +579,32 @@ TEST_F(DatabaseFixture, Listen_getRecentArtists_multi)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
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 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);
|
||||
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 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);
|
||||
EXPECT_EQ(artists.results[0], artist2->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 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);
|
||||
EXPECT_EQ(artists.moreResults, true);
|
||||
EXPECT_EQ(artists.results[0], artist2->getId());
|
||||
@@ -617,7 +617,7 @@ TEST_F(DatabaseFixture, Listen_getRecentArtists_cluster)
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
ScopedArtist artist{ session, "MyArtist" };
|
||||
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" };
|
||||
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" };
|
||||
|
||||
@@ -629,7 +629,7 @@ TEST_F(DatabaseFixture, Listen_getRecentArtists_cluster)
|
||||
{
|
||||
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);
|
||||
}
|
||||
{
|
||||
@@ -639,7 +639,7 @@ TEST_F(DatabaseFixture, Listen_getRecentArtists_cluster)
|
||||
{
|
||||
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);
|
||||
EXPECT_EQ(artists.results[0], artist.getId());
|
||||
}
|
||||
@@ -652,25 +652,25 @@ TEST_F(DatabaseFixture, Listen_getRecentReleases)
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
track.get().modify()->setRelease(release.get());
|
||||
}
|
||||
|
||||
{
|
||||
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);
|
||||
ASSERT_EQ(releases.results.size(), 0);
|
||||
}
|
||||
|
||||
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 releases{ Listen::getRecentReleases(session, user->getId(), Scrobbler::Internal, {}) };
|
||||
auto releases{ Listen::getRecentReleases(session, user->getId(), ScrobblingBackend::Internal, {}) };
|
||||
EXPECT_EQ(releases.moreResults, false);
|
||||
ASSERT_EQ(releases.results.size(), 1);
|
||||
EXPECT_EQ(releases.results[0], release.getId());
|
||||
@@ -678,7 +678,7 @@ TEST_F(DatabaseFixture, Listen_getRecentReleases)
|
||||
{
|
||||
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.results.size(), 0);
|
||||
}
|
||||
@@ -692,46 +692,46 @@ TEST_F(DatabaseFixture, Listen_getMostRecentRelease)
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
track.get().modify()->setRelease(release.get());
|
||||
}
|
||||
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
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() };
|
||||
|
||||
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_EQ(listen->getDateTime(), dateTime1);
|
||||
}
|
||||
|
||||
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() };
|
||||
|
||||
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_EQ(listen->getDateTime(), dateTime1);
|
||||
}
|
||||
|
||||
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() };
|
||||
|
||||
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_EQ(listen->getDateTime(), dateTime3);
|
||||
}
|
||||
@@ -746,50 +746,50 @@ TEST_F(DatabaseFixture, Listen_getRecentReleases_multi)
|
||||
ScopedRelease release2{ session, "MyRelease2" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
track1.get().modify()->setRelease(release1.get());
|
||||
track2.get().modify()->setRelease(release2.get());
|
||||
}
|
||||
|
||||
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 releases{ Listen::getRecentReleases(session, user->getId(), Scrobbler::Internal, {}) };
|
||||
auto releases{ Listen::getRecentReleases(session, user->getId(), ScrobblingBackend::Internal, {}) };
|
||||
EXPECT_EQ(releases.moreResults, false);
|
||||
ASSERT_EQ(releases.results.size(), 1);
|
||||
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 releases{ Listen::getRecentReleases(session, user->getId(), Scrobbler::Internal, {}) };
|
||||
auto releases{ Listen::getRecentReleases(session, user->getId(), ScrobblingBackend::Internal, {}) };
|
||||
EXPECT_EQ(releases.moreResults, false);
|
||||
ASSERT_EQ(releases.results.size(), 2);
|
||||
EXPECT_EQ(releases.results[0], release1.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 releases{ Listen::getRecentReleases(session, user->getId(), Scrobbler::Internal, {}) };
|
||||
auto releases{ Listen::getRecentReleases(session, user->getId(), ScrobblingBackend::Internal, {}) };
|
||||
EXPECT_EQ(releases.moreResults, false);
|
||||
ASSERT_EQ(releases.results.size(), 2);
|
||||
EXPECT_EQ(releases.results[0], release2.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 releases{ Listen::getRecentReleases(session, user->getId(), Scrobbler::Internal, {}) };
|
||||
auto releases{ Listen::getRecentReleases(session, user->getId(), ScrobblingBackend::Internal, {}) };
|
||||
EXPECT_EQ(releases.moreResults, false);
|
||||
ASSERT_EQ(releases.results.size(), 2);
|
||||
EXPECT_EQ(releases.results[0], release2.getId());
|
||||
@@ -806,23 +806,23 @@ TEST_F(DatabaseFixture, Listen_getRecentReleases_cluster)
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
track.get().modify()->setRelease(release.get());
|
||||
}
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
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 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);
|
||||
}
|
||||
|
||||
@@ -833,7 +833,7 @@ TEST_F(DatabaseFixture, Listen_getRecentReleases_cluster)
|
||||
{
|
||||
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[0], release.getId());
|
||||
}
|
||||
@@ -847,18 +847,18 @@ TEST_F(DatabaseFixture, Listen_getRecentTracks)
|
||||
{
|
||||
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);
|
||||
ASSERT_EQ(tracks.results.size(), 0);
|
||||
}
|
||||
|
||||
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 tracks{ Listen::getRecentTracks(session, user->getId(), Scrobbler::Internal, {}) };
|
||||
auto tracks{ Listen::getRecentTracks(session, user->getId(), ScrobblingBackend::Internal, {}) };
|
||||
EXPECT_EQ(tracks.moreResults, false);
|
||||
ASSERT_EQ(tracks.results.size(), 1);
|
||||
EXPECT_EQ(tracks.results[0], track.getId());
|
||||
@@ -867,12 +867,77 @@ TEST_F(DatabaseFixture, Listen_getRecentTracks)
|
||||
{
|
||||
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.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)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
@@ -881,39 +946,39 @@ TEST_F(DatabaseFixture, Listen_getMostRecentTrack)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
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 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_EQ(listen->getDateTime(), dateTime1);
|
||||
}
|
||||
|
||||
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 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_EQ(listen->getDateTime(), dateTime1);
|
||||
}
|
||||
|
||||
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 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_EQ(listen->getDateTime(), dateTime3);
|
||||
}
|
||||
@@ -926,44 +991,44 @@ TEST_F(DatabaseFixture, Listen_getRecentTracks_multi)
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
|
||||
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 tracks{ Listen::getRecentTracks(session, user->getId(), Scrobbler::Internal, {}) };
|
||||
auto tracks{ Listen::getRecentTracks(session, user->getId(), ScrobblingBackend::Internal, {}) };
|
||||
EXPECT_EQ(tracks.moreResults, false);
|
||||
ASSERT_EQ(tracks.results.size(), 1);
|
||||
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 tracks{ Listen::getRecentTracks(session, user->getId(), Scrobbler::Internal, {}) };
|
||||
auto tracks{ Listen::getRecentTracks(session, user->getId(), ScrobblingBackend::Internal, {}) };
|
||||
EXPECT_EQ(tracks.moreResults, false);
|
||||
ASSERT_EQ(tracks.results.size(), 2);
|
||||
EXPECT_EQ(tracks.results[0], track1.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 tracks{ Listen::getRecentTracks(session, user->getId(), Scrobbler::Internal, {}) };
|
||||
auto tracks{ Listen::getRecentTracks(session, user->getId(), ScrobblingBackend::Internal, {}) };
|
||||
EXPECT_EQ(tracks.moreResults, false);
|
||||
ASSERT_EQ(tracks.results.size(), 2);
|
||||
EXPECT_EQ(tracks.results[0], track2.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 tracks{ Listen::getRecentTracks(session, user->getId(), Scrobbler::Internal, {}) };
|
||||
auto tracks{ Listen::getRecentTracks(session, user->getId(), ScrobblingBackend::Internal, {}) };
|
||||
EXPECT_EQ(tracks.moreResults, false);
|
||||
ASSERT_EQ(tracks.results.size(), 2);
|
||||
EXPECT_EQ(tracks.results[0], track2.getId());
|
||||
@@ -976,14 +1041,14 @@ TEST_F(DatabaseFixture, Listen_getRecentTracks_cluster)
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
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" };
|
||||
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" };
|
||||
|
||||
{
|
||||
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);
|
||||
}
|
||||
{
|
||||
@@ -994,7 +1059,7 @@ TEST_F(DatabaseFixture, Listen_getRecentTracks_cluster)
|
||||
{
|
||||
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[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)
|
||||
{
|
||||
ScopedArtist artist {session, "MyArtist"};
|
||||
ScopedUser user {session, "MyUser"};
|
||||
ScopedUser user2 {session, "MyUser2"};
|
||||
ScopedArtist artist{ session, "MyArtist" };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
ScopedUser user2{ session, "MyUser2" };
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
|
||||
auto starredArtist {StarredArtist::find(session, artist->getId(), user->getId(), Scrobbler::Internal)};
|
||||
EXPECT_FALSE(starredArtist);
|
||||
EXPECT_EQ(StarredArtist::getCount(session), 0);
|
||||
auto starredArtist{ StarredArtist::find(session, artist->getId(), user->getId(), FeedbackBackend::Internal) };
|
||||
EXPECT_FALSE(starredArtist);
|
||||
EXPECT_EQ(StarredArtist::getCount(session), 0);
|
||||
|
||||
auto artists {Artist::find(session, Artist::FindParameters {})};
|
||||
EXPECT_EQ(artists.results.size(), 1);
|
||||
}
|
||||
auto artists{ Artist::findIds(session, Artist::FindParameters {}) };
|
||||
EXPECT_EQ(artists.results.size(), 1);
|
||||
}
|
||||
|
||||
ScopedStarredArtist starredArtist {session, artist.lockAndGet(), user.lockAndGet(), Scrobbler::Internal};
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
ScopedStarredArtist starredArtist{ session, artist.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal };
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
|
||||
auto gotArtist {StarredArtist::find(session, artist->getId(), user->getId(), Scrobbler::Internal)};
|
||||
EXPECT_EQ(gotArtist->getId(), starredArtist->getId());
|
||||
EXPECT_EQ(StarredArtist::getCount(session), 1);
|
||||
}
|
||||
auto gotArtist{ StarredArtist::find(session, artist->getId(), user->getId(), FeedbackBackend::Internal) };
|
||||
EXPECT_EQ(gotArtist->getId(), starredArtist->getId());
|
||||
EXPECT_EQ(StarredArtist::getCount(session), 1);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
|
||||
auto artists {Artist::find(session, Artist::FindParameters {})};
|
||||
EXPECT_EQ(artists.results.size(), 1);
|
||||
auto artists{ Artist::findIds(session, Artist::FindParameters {}) };
|
||||
EXPECT_EQ(artists.results.size(), 1);
|
||||
|
||||
artists = Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal));
|
||||
EXPECT_EQ(artists.results.size(), 1);
|
||||
artists = Artist::findIds(session, Artist::FindParameters{}.setStarringUser(user.getId(), FeedbackBackend::Internal));
|
||||
EXPECT_EQ(artists.results.size(), 1);
|
||||
|
||||
artists = Artist::find(session, Artist::FindParameters {}.setStarringUser(user2.getId(), Scrobbler::Internal));
|
||||
EXPECT_EQ(artists.results.size(), 0);
|
||||
}
|
||||
artists = Artist::findIds(session, Artist::FindParameters{}.setStarringUser(user2.getId(), FeedbackBackend::Internal));
|
||||
EXPECT_EQ(artists.results.size(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, StarredArtist_PendingDestroy)
|
||||
{
|
||||
ScopedArtist artist {session, "MyArtist"};
|
||||
ScopedUser user {session, "MyUser"};
|
||||
ScopedStarredArtist starredArtist {session, artist.lockAndGet(), user.lockAndGet(), Scrobbler::Internal};
|
||||
ScopedArtist artist{ session, "MyArtist" };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
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))};
|
||||
EXPECT_EQ(artists.results.size(), 1);
|
||||
auto artists{ Artist::findIds(session, Artist::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal)) };
|
||||
EXPECT_EQ(artists.results.size(), 1);
|
||||
|
||||
starredArtist.get().modify()->setScrobblingState(ScrobblingState::PendingRemove);
|
||||
artists = Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal));
|
||||
EXPECT_EQ(artists.results.size(), 0);
|
||||
}
|
||||
starredArtist.get().modify()->setSyncState(SyncState::PendingRemove);
|
||||
artists = Artist::findIds(session, Artist::FindParameters{}.setStarringUser(user.getId(), FeedbackBackend::Internal));
|
||||
EXPECT_EQ(artists.results.size(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, StarredArtist_dateTime)
|
||||
{
|
||||
ScopedArtist artist1 {session, "MyArtist1"};
|
||||
ScopedArtist artist2 {session, "MyArtist2"};
|
||||
ScopedUser user {session, "MyUser"};
|
||||
ScopedArtist artist1{ session, "MyArtist1" };
|
||||
ScopedArtist artist2{ session, "MyArtist2" };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
|
||||
ScopedStarredArtist starredArtist1 {session, artist1.lockAndGet(), user.lockAndGet(), Scrobbler::Internal};
|
||||
ScopedStarredArtist starredArtist2 {session, artist2.lockAndGet(), user.lockAndGet(), Scrobbler::Internal};
|
||||
ScopedStarredArtist starredArtist1{ session, artist1.lockAndGet(), user.lockAndGet(), FeedbackBackend::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))};
|
||||
EXPECT_EQ(artists.results.size(), 2);
|
||||
}
|
||||
auto artists{ Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal)) };
|
||||
EXPECT_EQ(artists.results.size(), 2);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
|
||||
starredArtist1.get().modify()->setDateTime(dateTime);
|
||||
starredArtist2.get().modify()->setDateTime(dateTime.addSecs(-1));
|
||||
starredArtist1.get().modify()->setDateTime(dateTime);
|
||||
starredArtist2.get().modify()->setDateTime(dateTime.addSecs(-1));
|
||||
|
||||
auto artists {Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal)
|
||||
.setSortMethod(ArtistSortMethod::StarredDateDesc))};
|
||||
ASSERT_EQ(artists.results.size(), 2);
|
||||
EXPECT_EQ(artists.results[0], starredArtist1->getArtist()->getId());
|
||||
EXPECT_EQ(artists.results[1], starredArtist2->getArtist()->getId());
|
||||
}
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
auto artists{ Artist::findIds(session, Artist::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal).setSortMethod(ArtistSortMethod::StarredDateDesc)) };
|
||||
ASSERT_EQ(artists.results.size(), 2);
|
||||
EXPECT_EQ(artists.results[0], starredArtist1->getArtist()->getId());
|
||||
EXPECT_EQ(artists.results[1], starredArtist2->getArtist()->getId());
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
|
||||
starredArtist1.get().modify()->setDateTime(dateTime);
|
||||
starredArtist2.get().modify()->setDateTime(dateTime.addSecs(1));
|
||||
starredArtist1.get().modify()->setDateTime(dateTime);
|
||||
starredArtist2.get().modify()->setDateTime(dateTime.addSecs(1));
|
||||
|
||||
auto artists {Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal)
|
||||
.setSortMethod(ArtistSortMethod::StarredDateDesc))};
|
||||
ASSERT_EQ(artists.results.size(), 2);
|
||||
EXPECT_EQ(artists.results[0], starredArtist2->getArtist()->getId());
|
||||
EXPECT_EQ(artists.results[1], starredArtist1->getArtist()->getId());
|
||||
}
|
||||
auto artists{ Artist::findIds(session, Artist::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal).setSortMethod(ArtistSortMethod::StarredDateDesc)) };
|
||||
ASSERT_EQ(artists.results.size(), 2);
|
||||
EXPECT_EQ(artists.results[0], starredArtist2->getArtist()->getId());
|
||||
EXPECT_EQ(artists.results[1], starredArtist1->getArtist()->getId());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,102 +26,100 @@ using ScopedStarredRelease = ScopedEntity<Database::StarredRelease>;
|
||||
|
||||
TEST_F(DatabaseFixture, StarredRelease)
|
||||
{
|
||||
ScopedRelease release {session, "MyRelease"};
|
||||
ScopedUser user {session, "MyUser"};
|
||||
ScopedUser user2 {session, "MyUser2"};
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
ScopedUser user2{ session, "MyUser2" };
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
|
||||
auto starredRelease {StarredRelease::find(session, release->getId(), user->getId(), Scrobbler::Internal)};
|
||||
EXPECT_FALSE(starredRelease);
|
||||
EXPECT_EQ(StarredRelease::getCount(session), 0);
|
||||
auto starredRelease{ StarredRelease::find(session, release->getId(), user->getId(), FeedbackBackend::Internal) };
|
||||
EXPECT_FALSE(starredRelease);
|
||||
EXPECT_EQ(StarredRelease::getCount(session), 0);
|
||||
|
||||
auto releases {Release::find(session, Release::FindParameters {})};
|
||||
EXPECT_EQ(releases.results.size(), 1);
|
||||
}
|
||||
auto releases{ Release::find(session, Release::FindParameters {}) };
|
||||
EXPECT_EQ(releases.results.size(), 1);
|
||||
}
|
||||
|
||||
ScopedStarredRelease starredRelease {session, release.lockAndGet(), user.lockAndGet(), Scrobbler::Internal};
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
ScopedStarredRelease starredRelease{ session, release.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal };
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
|
||||
auto gotRelease {StarredRelease::find(session, release->getId(), user->getId(), Scrobbler::Internal)};
|
||||
EXPECT_EQ(gotRelease->getId(), starredRelease->getId());
|
||||
EXPECT_EQ(StarredRelease::getCount(session), 1);
|
||||
}
|
||||
auto gotRelease{ StarredRelease::find(session, release->getId(), user->getId(), FeedbackBackend::Internal) };
|
||||
EXPECT_EQ(gotRelease->getId(), starredRelease->getId());
|
||||
EXPECT_EQ(StarredRelease::getCount(session), 1);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
|
||||
auto releases {Release::find(session, Release::FindParameters {})};
|
||||
EXPECT_EQ(releases.results.size(), 1);
|
||||
auto releases{ Release::find(session, Release::FindParameters {}) };
|
||||
EXPECT_EQ(releases.results.size(), 1);
|
||||
|
||||
releases = Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal));
|
||||
EXPECT_EQ(releases.results.size(), 1);
|
||||
releases = Release::find(session, Release::FindParameters{}.setStarringUser(user.getId(), FeedbackBackend::Internal));
|
||||
EXPECT_EQ(releases.results.size(), 1);
|
||||
|
||||
releases = Release::find(session, Release::FindParameters {}.setStarringUser(user2.getId(), Scrobbler::Internal));
|
||||
EXPECT_EQ(releases.results.size(), 0);
|
||||
}
|
||||
releases = Release::find(session, Release::FindParameters{}.setStarringUser(user2.getId(), FeedbackBackend::Internal));
|
||||
EXPECT_EQ(releases.results.size(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Starredrelease_PendingDestroy)
|
||||
{
|
||||
ScopedRelease release {session, "MyRelease"};
|
||||
ScopedUser user {session, "MyUser"};
|
||||
ScopedStarredRelease starredRelease {session, release.lockAndGet(), user.lockAndGet(), Scrobbler::Internal};
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
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))};
|
||||
EXPECT_EQ(releases.results.size(), 1);
|
||||
auto releases{ Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal)) };
|
||||
EXPECT_EQ(releases.results.size(), 1);
|
||||
|
||||
starredRelease.get().modify()->setScrobblingState(ScrobblingState::PendingRemove);
|
||||
releases = Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal));
|
||||
EXPECT_EQ(releases.results.size(), 0);
|
||||
}
|
||||
starredRelease.get().modify()->setSyncState(SyncState::PendingRemove);
|
||||
releases = Release::find(session, Release::FindParameters{}.setStarringUser(user.getId(), FeedbackBackend::Internal));
|
||||
EXPECT_EQ(releases.results.size(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, StarredRelease_dateTime)
|
||||
{
|
||||
ScopedRelease release1 {session, "MyRelease1"};
|
||||
ScopedRelease release2 {session, "MyRelease2"};
|
||||
ScopedUser user {session, "MyUser"};
|
||||
ScopedRelease release1{ session, "MyRelease1" };
|
||||
ScopedRelease release2{ session, "MyRelease2" };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
|
||||
ScopedStarredRelease starredRelease1 {session, release1.lockAndGet(), user.lockAndGet(), Scrobbler::Internal};
|
||||
ScopedStarredRelease starredRelease2 {session, release2.lockAndGet(), user.lockAndGet(), Scrobbler::Internal};
|
||||
ScopedStarredRelease starredRelease1{ session, release1.lockAndGet(), user.lockAndGet(), FeedbackBackend::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))};
|
||||
EXPECT_EQ(releases.results.size(), 2);
|
||||
}
|
||||
auto releases{ Release::findIds(session, Release::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal)) };
|
||||
EXPECT_EQ(releases.results.size(), 2);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
|
||||
starredRelease1.get().modify()->setDateTime(dateTime);
|
||||
starredRelease2.get().modify()->setDateTime(dateTime.addSecs(-1));
|
||||
starredRelease1.get().modify()->setDateTime(dateTime);
|
||||
starredRelease2.get().modify()->setDateTime(dateTime.addSecs(-1));
|
||||
|
||||
auto releases {Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal)
|
||||
.setSortMethod(ReleaseSortMethod::StarredDateDesc))};
|
||||
ASSERT_EQ(releases.results.size(), 2);
|
||||
EXPECT_EQ(releases.results[0], starredRelease1->getRelease()->getId());
|
||||
EXPECT_EQ(releases.results[1], starredRelease2->getRelease()->getId());
|
||||
}
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
auto releases{ Release::findIds(session, Release::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal).setSortMethod(ReleaseSortMethod::StarredDateDesc)) };
|
||||
ASSERT_EQ(releases.results.size(), 2);
|
||||
EXPECT_EQ(releases.results[0], starredRelease1->getRelease()->getId());
|
||||
EXPECT_EQ(releases.results[1], starredRelease2->getRelease()->getId());
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
|
||||
starredRelease1.get().modify()->setDateTime(dateTime);
|
||||
starredRelease2.get().modify()->setDateTime(dateTime.addSecs(1));
|
||||
starredRelease1.get().modify()->setDateTime(dateTime);
|
||||
starredRelease2.get().modify()->setDateTime(dateTime.addSecs(1));
|
||||
|
||||
auto releases {Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal)
|
||||
.setSortMethod(ReleaseSortMethod::StarredDateDesc))};
|
||||
ASSERT_EQ(releases.results.size(), 2);
|
||||
EXPECT_EQ(releases.results[0], starredRelease2->getRelease()->getId());
|
||||
EXPECT_EQ(releases.results[1], starredRelease1->getRelease()->getId());
|
||||
}
|
||||
auto releases{ Release::findIds(session, Release::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal).setSortMethod(ReleaseSortMethod::StarredDateDesc)) };
|
||||
ASSERT_EQ(releases.results.size(), 2);
|
||||
EXPECT_EQ(releases.results[0], starredRelease2->getRelease()->getId());
|
||||
EXPECT_EQ(releases.results[1], starredRelease1->getRelease()->getId());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,102 +26,100 @@ using ScopedStarredTrack = ScopedEntity<Database::StarredTrack>;
|
||||
|
||||
TEST_F(DatabaseFixture, StarredTrack)
|
||||
{
|
||||
ScopedTrack track {session, "MyTrack"};
|
||||
ScopedUser user {session, "MyUser"};
|
||||
ScopedUser user2 {session, "MyUser2"};
|
||||
ScopedTrack track {session, "MyTrack"};
|
||||
ScopedUser user {session, "MyUser"};
|
||||
ScopedUser user2 {session, "MyUser2"};
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
auto starredTrack {StarredTrack::find(session, track->getId(), user->getId(), Scrobbler::Internal)};
|
||||
EXPECT_FALSE(starredTrack);
|
||||
EXPECT_EQ(StarredTrack::getCount(session), 0);
|
||||
auto starredTrack {StarredTrack::find(session, track->getId(), user->getId(), FeedbackBackend::Internal)};
|
||||
EXPECT_FALSE(starredTrack);
|
||||
EXPECT_EQ(StarredTrack::getCount(session), 0);
|
||||
|
||||
auto tracks {Track::find(session, Track::FindParameters {})};
|
||||
EXPECT_EQ(tracks.results.size(), 1);
|
||||
}
|
||||
auto tracks {Track::findIds(session, Track::FindParameters {})};
|
||||
EXPECT_EQ(tracks.results.size(), 1);
|
||||
}
|
||||
|
||||
ScopedStarredTrack starredTrack {session, track.lockAndGet(), user.lockAndGet(), Scrobbler::Internal};
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
ScopedStarredTrack starredTrack {session, track.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal};
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
auto gotTrack {StarredTrack::find(session, track->getId(), user->getId(), Scrobbler::Internal)};
|
||||
EXPECT_EQ(gotTrack->getId(), starredTrack->getId());
|
||||
EXPECT_EQ(StarredTrack::getCount(session), 1);
|
||||
}
|
||||
auto gotTrack {StarredTrack::find(session, track->getId(), user->getId(), FeedbackBackend::Internal)};
|
||||
EXPECT_EQ(gotTrack->getId(), starredTrack->getId());
|
||||
EXPECT_EQ(StarredTrack::getCount(session), 1);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
auto tracks {Track::find(session, Track::FindParameters {})};
|
||||
EXPECT_EQ(tracks.results.size(), 1);
|
||||
auto tracks {Track::findIds(session, Track::FindParameters {})};
|
||||
EXPECT_EQ(tracks.results.size(), 1);
|
||||
|
||||
tracks = Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal));
|
||||
EXPECT_EQ(tracks.results.size(), 1);
|
||||
tracks = Track::findIds(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal));
|
||||
EXPECT_EQ(tracks.results.size(), 1);
|
||||
|
||||
tracks = Track::find(session, Track::FindParameters {}.setStarringUser(user2.getId(), Scrobbler::Internal));
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
}
|
||||
tracks = Track::findIds(session, Track::FindParameters {}.setStarringUser(user2.getId(), FeedbackBackend::Internal));
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Starredtrack_PendingDestroy)
|
||||
{
|
||||
ScopedTrack track {session, "MyTrack"};
|
||||
ScopedUser user {session, "MyUser"};
|
||||
ScopedStarredTrack starredTrack {session, track.lockAndGet(), user.lockAndGet(), Scrobbler::Internal};
|
||||
ScopedTrack track {session, "MyTrack"};
|
||||
ScopedUser user {session, "MyUser"};
|
||||
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))};
|
||||
EXPECT_EQ(tracks.results.size(), 1);
|
||||
auto tracks {Track::findIds(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal))};
|
||||
EXPECT_EQ(tracks.results.size(), 1);
|
||||
|
||||
starredTrack.get().modify()->setScrobblingState(ScrobblingState::PendingRemove);
|
||||
tracks = Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal));
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
}
|
||||
starredTrack.get().modify()->setSyncState(SyncState::PendingRemove);
|
||||
tracks = Track::findIds(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal));
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, StarredTrack_dateTime)
|
||||
{
|
||||
ScopedTrack track1 {session, "MyTrack1"};
|
||||
ScopedTrack track2 {session, "MyTrack2"};
|
||||
ScopedUser user {session, "MyUser"};
|
||||
ScopedTrack track1 {session, "MyTrack1"};
|
||||
ScopedTrack track2 {session, "MyTrack2"};
|
||||
ScopedUser user {session, "MyUser"};
|
||||
|
||||
ScopedStarredTrack starredTrack1 {session, track1.lockAndGet(), user.lockAndGet(), Scrobbler::Internal};
|
||||
ScopedStarredTrack starredTrack2 {session, track2.lockAndGet(), user.lockAndGet(), Scrobbler::Internal};
|
||||
ScopedStarredTrack starredTrack1 {session, track1.lockAndGet(), user.lockAndGet(), FeedbackBackend::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))};
|
||||
EXPECT_EQ(tracks.results.size(), 2);
|
||||
}
|
||||
auto tracks {Track::findIds(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal))};
|
||||
EXPECT_EQ(tracks.results.size(), 2);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
|
||||
starredTrack1.get().modify()->setDateTime(dateTime);
|
||||
starredTrack2.get().modify()->setDateTime(dateTime.addSecs(-1));
|
||||
starredTrack1.get().modify()->setDateTime(dateTime);
|
||||
starredTrack2.get().modify()->setDateTime(dateTime.addSecs(-1));
|
||||
|
||||
auto tracks {Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal)
|
||||
.setSortMethod(TrackSortMethod::StarredDateDesc))};
|
||||
ASSERT_EQ(tracks.results.size(), 2);
|
||||
EXPECT_EQ(tracks.results[0], starredTrack1->getTrack()->getId());
|
||||
EXPECT_EQ(tracks.results[1], starredTrack2->getTrack()->getId());
|
||||
}
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
auto tracks {Track::findIds(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal).setSortMethod(TrackSortMethod::StarredDateDesc))};
|
||||
ASSERT_EQ(tracks.results.size(), 2);
|
||||
EXPECT_EQ(tracks.results[0], starredTrack1->getTrack()->getId());
|
||||
EXPECT_EQ(tracks.results[1], starredTrack2->getTrack()->getId());
|
||||
}
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
|
||||
starredTrack1.get().modify()->setDateTime(dateTime);
|
||||
starredTrack2.get().modify()->setDateTime(dateTime.addSecs(1));
|
||||
starredTrack1.get().modify()->setDateTime(dateTime);
|
||||
starredTrack2.get().modify()->setDateTime(dateTime.addSecs(1));
|
||||
|
||||
auto tracks {Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal)
|
||||
.setSortMethod(TrackSortMethod::StarredDateDesc))};
|
||||
ASSERT_EQ(tracks.results.size(), 2);
|
||||
EXPECT_EQ(tracks.results[0], starredTrack2->getTrack()->getId());
|
||||
EXPECT_EQ(tracks.results[1], starredTrack1->getTrack()->getId());
|
||||
}
|
||||
auto tracks {Track::findIds(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal).setSortMethod(TrackSortMethod::StarredDateDesc))};
|
||||
ASSERT_EQ(tracks.results.size(), 2);
|
||||
EXPECT_EQ(tracks.results[0], starredTrack2->getTrack()->getId());
|
||||
EXPECT_EQ(tracks.results[1], starredTrack1->getTrack()->getId());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,137 +25,138 @@ using namespace Database;
|
||||
|
||||
TEST_F(DatabaseFixture, Track)
|
||||
{
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
EXPECT_EQ(Track::find(session, Track::FindParameters {}).results.size(), 0);
|
||||
EXPECT_EQ(Track::getCount(session), 0);
|
||||
EXPECT_FALSE(Track::exists(session, 0));
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
EXPECT_EQ(Track::find(session, Track::FindParameters{}).results.size(), 0);
|
||||
EXPECT_EQ(Track::findIds(session, Track::FindParameters{}).results.size(), 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::getCount(session), 1);
|
||||
EXPECT_TRUE(Track::exists(session, track.getId()));
|
||||
auto myTrack {Track::find(session, track.getId())};
|
||||
ASSERT_TRUE(myTrack);
|
||||
EXPECT_EQ(myTrack->getId(), track.getId());
|
||||
}
|
||||
EXPECT_EQ(Track::find(session, Track::FindParameters{}).results.size(), 1);
|
||||
EXPECT_EQ(Track::getCount(session), 1);
|
||||
EXPECT_TRUE(Track::exists(session, track.getId()));
|
||||
auto myTrack{ Track::find(session, track.getId()) };
|
||||
ASSERT_TRUE(myTrack);
|
||||
EXPECT_EQ(myTrack->getId(), track.getId());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, MultipleTracks)
|
||||
{
|
||||
ScopedTrack track1 {session, "MyTrackFile1"};
|
||||
ScopedTrack track2 {session, "MyTrackFile2"};
|
||||
ScopedTrack track1{ session, "MyTrackFile1" };
|
||||
ScopedTrack track2{ session, "MyTrackFile2" };
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
|
||||
EXPECT_TRUE(track1.getId() != track2.getId());
|
||||
EXPECT_TRUE(track1.get() != track2.get());
|
||||
EXPECT_FALSE(track1.get() == track2.get());
|
||||
}
|
||||
EXPECT_TRUE(track1.getId() != track2.getId());
|
||||
EXPECT_TRUE(track1.get() != track2.get());
|
||||
EXPECT_FALSE(track1.get() == track2.get());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, MultipleTracksSearchByFilter)
|
||||
{
|
||||
ScopedTrack track1 {session, ""};
|
||||
ScopedTrack track2 {session, ""};
|
||||
ScopedTrack track3 {session, ""};
|
||||
ScopedTrack track4 {session, ""};
|
||||
ScopedTrack track5 {session, ""};
|
||||
ScopedTrack track6 {session, ""};
|
||||
ScopedTrack track1{ session, "" };
|
||||
ScopedTrack track2{ session, "" };
|
||||
ScopedTrack track3{ session, "" };
|
||||
ScopedTrack track4{ session, "" };
|
||||
ScopedTrack track5{ session, "" };
|
||||
ScopedTrack track6{ session, "" };
|
||||
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
track1.get().modify()->setName("MyTrack");
|
||||
track2.get().modify()->setName("MyTrack%");
|
||||
track3.get().modify()->setName("MyTrack%Foo");
|
||||
track4.get().modify()->setName("%MyTrack");
|
||||
track5.get().modify()->setName("Foo%MyTrack");
|
||||
track6.get().modify()->setName("M_Track");
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
track1.get().modify()->setName("MyTrack");
|
||||
track2.get().modify()->setName("MyTrack%");
|
||||
track3.get().modify()->setName("MyTrack%Foo");
|
||||
track4.get().modify()->setName("%MyTrack");
|
||||
track5.get().modify()->setName("Foo%MyTrack");
|
||||
track6.get().modify()->setName("M_Track");
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
|
||||
{
|
||||
const auto tracks {Track::find(session, Track::FindParameters {}.setKeywords({"Track"}))};
|
||||
EXPECT_EQ(tracks.results.size(), 6);
|
||||
}
|
||||
{
|
||||
const auto tracks {Track::find(session, Track::FindParameters {}.setKeywords({"MyTrack"}))};
|
||||
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(); }));
|
||||
}
|
||||
{
|
||||
const auto tracks {Track::find(session, Track::FindParameters {}.setKeywords({"MyTrack%"}))};
|
||||
ASSERT_EQ(tracks.results.size(), 2);
|
||||
EXPECT_EQ(tracks.results[0], track2.getId());
|
||||
EXPECT_EQ(tracks.results[1], track3.getId());
|
||||
}
|
||||
{
|
||||
const auto tracks {Track::find(session, Track::FindParameters {}.setKeywords({"%MyTrack"}))};
|
||||
ASSERT_EQ(tracks.results.size(), 2);
|
||||
EXPECT_EQ(tracks.results[0], track4.getId());
|
||||
EXPECT_EQ(tracks.results[1], track5.getId());
|
||||
}
|
||||
}
|
||||
{
|
||||
const auto tracks{ Track::findIds(session, Track::FindParameters {}.setKeywords({"Track"})) };
|
||||
EXPECT_EQ(tracks.results.size(), 6);
|
||||
}
|
||||
{
|
||||
const auto tracks{ Track::findIds(session, Track::FindParameters {}.setKeywords({"MyTrack"})) };
|
||||
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(); }));
|
||||
}
|
||||
{
|
||||
const auto tracks{ Track::findIds(session, Track::FindParameters {}.setKeywords({"MyTrack%"})) };
|
||||
ASSERT_EQ(tracks.results.size(), 2);
|
||||
EXPECT_EQ(tracks.results[0], track2.getId());
|
||||
EXPECT_EQ(tracks.results[1], track3.getId());
|
||||
}
|
||||
{
|
||||
const auto tracks{ Track::findIds(session, Track::FindParameters {}.setKeywords({"%MyTrack"})) };
|
||||
ASSERT_EQ(tracks.results.size(), 2);
|
||||
EXPECT_EQ(tracks.results[0], track4.getId());
|
||||
EXPECT_EQ(tracks.results[1], track5.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Track_date)
|
||||
{
|
||||
ScopedTrack track {session, "MyTrack"};
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
EXPECT_EQ(track->getYear(), std::nullopt);
|
||||
EXPECT_EQ(track->getOriginalYear(), std::nullopt);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
EXPECT_EQ(track->getYear(), std::nullopt);
|
||||
EXPECT_EQ(track->getOriginalYear(), std::nullopt);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
track.get().modify()->setDate(Wt::WDate {1995, 5, 5});
|
||||
track.get().modify()->setOriginalDate(Wt::WDate {1994, 2, 2});
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
track.get().modify()->setDate(Wt::WDate{ 1995, 5, 5 });
|
||||
track.get().modify()->setOriginalDate(Wt::WDate{ 1994, 2, 2 });
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
EXPECT_EQ(track->getYear(), 1995);
|
||||
EXPECT_EQ(track->getOriginalYear(), 1994);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
EXPECT_EQ(track->getYear(), 1995);
|
||||
EXPECT_EQ(track->getOriginalYear(), 1994);
|
||||
}
|
||||
}
|
||||
|
||||
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()};
|
||||
track.get().modify()->setLastWriteTime(dateTime);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
track.get().modify()->setLastWriteTime(dateTime);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
const auto tracks {Track::find(session, Track::FindParameters {})};
|
||||
EXPECT_EQ(tracks.results.size(), 1);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
const auto tracks{ Track::findIds(session, Track::FindParameters {}) };
|
||||
EXPECT_EQ(tracks.results.size(), 1);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
const auto tracks {Track::find(session, Track::FindParameters {}.setWrittenAfter(dateTime.addSecs(-1)))};
|
||||
EXPECT_EQ(tracks.results.size(), 1);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
const auto tracks{ Track::findIds(session, Track::FindParameters {}.setWrittenAfter(dateTime.addSecs(-1))) };
|
||||
EXPECT_EQ(tracks.results.size(), 1);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
const auto tracks {Track::find(session, Track::FindParameters {}.setWrittenAfter(dateTime.addSecs(+1)))};
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
const auto tracks{ Track::findIds(session, Track::FindParameters {}.setWrittenAfter(dateTime.addSecs(+1))) };
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -49,10 +49,10 @@ TEST_F(DatabaseFixture, SingleTrackListSingleTrack)
|
||||
{
|
||||
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);
|
||||
|
||||
tracks = Track::find(session, Track::FindParameters {}.setTrackList(trackList2.getId()));
|
||||
tracks = Track::findIds(session, Track::FindParameters {}.setTrackList(trackList2.getId()));
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
}
|
||||
|
||||
@@ -65,11 +65,11 @@ TEST_F(DatabaseFixture, SingleTrackListSingleTrack)
|
||||
{
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
+6
-25
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Emeric Poupon
|
||||
* Copyright (C) 2023 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
@@ -19,35 +19,17 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
#include "services/database/StarredArtistId.hpp"
|
||||
#include "services/database/StarredReleaseId.hpp"
|
||||
#include "services/database/StarredTrackId.hpp"
|
||||
#include "services/scrobbling/Listen.hpp"
|
||||
|
||||
namespace Database
|
||||
namespace Feedback
|
||||
{
|
||||
class Session;
|
||||
class TrackList;
|
||||
class User;
|
||||
}
|
||||
|
||||
namespace Scrobbling
|
||||
{
|
||||
class IScrobbler
|
||||
class IFeedbackBackend
|
||||
{
|
||||
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 onUnstarred(Database::StarredArtistId) = 0;
|
||||
virtual void onStarred(Database::StarredReleaseId) = 0;
|
||||
@@ -56,7 +38,6 @@ namespace Scrobbling
|
||||
virtual void onUnstarred(Database::StarredTrackId) = 0;
|
||||
};
|
||||
|
||||
std::unique_ptr<IScrobbler> createScrobbler(std::string_view backendName);
|
||||
|
||||
} // ns Scrobbling
|
||||
std::unique_ptr<IFeedbackBackend> createFeedbackBackend(std::string_view backendName);
|
||||
|
||||
} // 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
|
||||
+5
-10
@@ -19,26 +19,21 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "IScrobbler.hpp"
|
||||
#include "IFeedbackBackend.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Db;
|
||||
}
|
||||
|
||||
namespace Scrobbling
|
||||
namespace Feedback
|
||||
{
|
||||
class InternalScrobbler final : public IScrobbler
|
||||
class InternalBackend final : public IFeedbackBackend
|
||||
{
|
||||
public:
|
||||
InternalScrobbler(Database::Db& db);
|
||||
InternalBackend(Database::Db& db);
|
||||
|
||||
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 onUnstarred(Database::StarredArtistId) override;
|
||||
void onStarred(Database::StarredReleaseId) override;
|
||||
@@ -48,5 +43,5 @@ namespace Scrobbling
|
||||
|
||||
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;
|
||||
};
|
||||
}
|
||||
+2
-2
@@ -19,7 +19,7 @@
|
||||
|
||||
#include "FeedbackTypes.hpp"
|
||||
|
||||
namespace Scrobbling::ListenBrainz
|
||||
namespace Feedback::ListenBrainz
|
||||
{
|
||||
std::ostream&
|
||||
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);
|
||||
return os;
|
||||
}
|
||||
} // Scrobbling::ListenBrainz
|
||||
} // Feedback::ListenBrainz
|
||||
+2
-2
@@ -23,7 +23,7 @@
|
||||
#include <Wt/WDateTime.h>
|
||||
#include "utils/UUID.hpp"
|
||||
|
||||
namespace Scrobbling::ListenBrainz
|
||||
namespace Feedback::ListenBrainz
|
||||
{
|
||||
// See https://listenbrainz.readthedocs.io/en/production/dev/feedback-json/#feedback-json-doc
|
||||
enum class FeedbackType
|
||||
@@ -42,4 +42,4 @@ namespace Scrobbling::ListenBrainz
|
||||
|
||||
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
|
||||
+2
-2
@@ -23,7 +23,7 @@
|
||||
|
||||
#include "FeedbackTypes.hpp"
|
||||
|
||||
namespace Scrobbling::ListenBrainz
|
||||
namespace Feedback::ListenBrainz
|
||||
{
|
||||
class FeedbacksParser
|
||||
{
|
||||
@@ -37,4 +37,4 @@ namespace Scrobbling::ListenBrainz
|
||||
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
|
||||
{
|
||||
using namespace Database;
|
||||
using namespace Database;
|
||||
|
||||
std::unique_ptr<IPlaylistGeneratorService>
|
||||
createPlaylistGeneratorService(Db& db, Recommendation::IRecommendationService& recommendationService)
|
||||
{
|
||||
return std::make_unique<PlaylistGeneratorService>(db, recommendationService);
|
||||
}
|
||||
std::unique_ptr<IPlaylistGeneratorService> createPlaylistGeneratorService(Db& db, Recommendation::IRecommendationService& recommendationService)
|
||||
{
|
||||
return std::make_unique<PlaylistGeneratorService>(db, recommendationService);
|
||||
}
|
||||
|
||||
PlaylistGeneratorService::PlaylistGeneratorService(Db& db, Recommendation::IRecommendationService& recommendationService)
|
||||
: _db {db}
|
||||
, _recommendationService {recommendationService}
|
||||
{
|
||||
_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::DuplicateTracks>());
|
||||
}
|
||||
PlaylistGeneratorService::PlaylistGeneratorService(Db& db, Recommendation::IRecommendationService& recommendationService)
|
||||
: _db{ db }
|
||||
, _recommendationService{ recommendationService }
|
||||
{
|
||||
_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::DuplicateTracks>());
|
||||
}
|
||||
|
||||
std::vector<TrackId>
|
||||
PlaylistGeneratorService::extendPlaylist(TrackListId tracklistId, std::size_t maxCount) const
|
||||
{
|
||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Requested to extend playlist by " << maxCount << " similar tracks";
|
||||
std::vector<TrackId> PlaylistGeneratorService::extendPlaylist(TrackListId tracklistId, std::size_t maxCount) const
|
||||
{
|
||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Requested to extend playlist by " << maxCount << " similar tracks";
|
||||
|
||||
// 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
|
||||
// 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
|
||||
|
||||
const std::vector<TrackId> startingTracks {getTracksFromTrackList(tracklistId)};
|
||||
const std::vector<TrackId> startingTracks{ getTracksFromTrackList(tracklistId) };
|
||||
|
||||
std::vector<TrackId> finalResult = startingTracks;
|
||||
finalResult.reserve(startingTracks.size() + maxCount);
|
||||
std::vector<TrackId> finalResult = startingTracks;
|
||||
finalResult.reserve(startingTracks.size() + maxCount);
|
||||
|
||||
std::vector<float> scores;
|
||||
for (std::size_t i {}; i < maxCount; ++i)
|
||||
{
|
||||
if (similarTracks.empty())
|
||||
break;
|
||||
std::vector<float> scores;
|
||||
for (std::size_t i{}; i < maxCount; ++i)
|
||||
{
|
||||
if (similarTracks.empty())
|
||||
break;
|
||||
|
||||
scores.resize(similarTracks.size(), {});
|
||||
scores.resize(similarTracks.size(), {});
|
||||
|
||||
// select the similar track that has the best score
|
||||
for (std::size_t trackIndex {}; trackIndex < similarTracks.size(); ++trackIndex)
|
||||
{
|
||||
using namespace Database::Debug;
|
||||
// select the similar track that has the best score
|
||||
for (std::size_t trackIndex{}; trackIndex < similarTracks.size(); ++trackIndex)
|
||||
{
|
||||
using namespace Database::Debug;
|
||||
|
||||
finalResult.push_back(similarTracks[trackIndex]);
|
||||
finalResult.push_back(similarTracks[trackIndex]);
|
||||
|
||||
scores[trackIndex] = 0;
|
||||
for (const auto& constraint : _constraints)
|
||||
scores[trackIndex] += constraint->computeScore(finalResult, finalResult.size() - 1);
|
||||
scores[trackIndex] = 0;
|
||||
for (const auto& constraint : _constraints)
|
||||
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)
|
||||
if (scores[trackIndex] < 0.01)
|
||||
break;
|
||||
}
|
||||
// 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)
|
||||
break;
|
||||
}
|
||||
|
||||
// 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))))};
|
||||
// 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)))) };
|
||||
|
||||
finalResult.push_back(similarTracks[bestScoreIndex]);
|
||||
similarTracks.erase(std::begin(similarTracks) + bestScoreIndex);
|
||||
}
|
||||
finalResult.push_back(similarTracks[bestScoreIndex]);
|
||||
similarTracks.erase(std::begin(similarTracks) + bestScoreIndex);
|
||||
}
|
||||
|
||||
// for now, just get some more similar tracks
|
||||
return std::vector(std::cbegin(finalResult) + startingTracks.size(), std::cend(finalResult));
|
||||
}
|
||||
// for now, just get some more similar tracks
|
||||
return std::vector(std::cbegin(finalResult) + startingTracks.size(), std::cend(finalResult));
|
||||
}
|
||||
|
||||
TrackContainer
|
||||
PlaylistGeneratorService::getTracksFromTrackList(Database::TrackListId tracklistId) const
|
||||
{
|
||||
TrackContainer tracks;
|
||||
TrackContainer PlaylistGeneratorService::getTracksFromTrackList(Database::TrackListId tracklistId) const
|
||||
{
|
||||
TrackContainer tracks;
|
||||
|
||||
Session& dbSession {_db.getTLSSession()};
|
||||
auto transaction {dbSession.createSharedTransaction()};
|
||||
Session& dbSession{ _db.getTLSSession() };
|
||||
auto transaction{ dbSession.createSharedTransaction() };
|
||||
|
||||
Track::FindParameters params;
|
||||
params.setTrackList(tracklistId);
|
||||
params.setSortMethod(TrackSortMethod::TrackList);
|
||||
params.setDistinct(false);
|
||||
Track::FindParameters params;
|
||||
params.setTrackList(tracklistId);
|
||||
params.setSortMethod(TrackSortMethod::TrackList);
|
||||
params.setDistinct(false);
|
||||
|
||||
for (const TrackId trackId : Track::find(dbSession, params).results)
|
||||
tracks.push_back(trackId);
|
||||
for (const TrackId trackId : Track::findIds(dbSession, params).results)
|
||||
tracks.push_back(trackId);
|
||||
|
||||
return tracks;
|
||||
}
|
||||
return tracks;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +90,8 @@ namespace Recommendation
|
||||
if (itEngine == std::cend(_engines))
|
||||
continue;
|
||||
|
||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Trying engine '" << engineTypeToString(engineType) << "' to get similar tracks";
|
||||
|
||||
const IEngine& engine {*itEngine->second};
|
||||
res = engine.findSimilarTracks(trackIds, maxCount);
|
||||
if (!res.empty())
|
||||
@@ -114,6 +116,8 @@ namespace Recommendation
|
||||
if (itEngine == std::cend(_engines))
|
||||
continue;
|
||||
|
||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Trying engine '" << engineTypeToString(engineType) << "' to get similar releases";
|
||||
|
||||
const IEngine& engine {*itEngine->second};
|
||||
res = engine.getSimilarReleases(releaseId, maxCount);
|
||||
if (!res.empty())
|
||||
@@ -140,7 +144,7 @@ namespace Recommendation
|
||||
if (itEngine == std::cend(_engines))
|
||||
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};
|
||||
res = engine.getSimilarArtists(artistId, linkTypes, maxCount);
|
||||
@@ -155,12 +159,12 @@ namespace Recommendation
|
||||
}
|
||||
|
||||
static
|
||||
Database::ScanSettings::RecommendationEngineType
|
||||
getRecommendationEngineType(Database::Session& session)
|
||||
Database::ScanSettings::SimilarityEngineType
|
||||
getSimilarityEngineType(Database::Session& session)
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
return Database::ScanSettings::get(session)->getRecommendationEngineType();
|
||||
return Database::ScanSettings::get(session)->getSimilarityEngineType();
|
||||
}
|
||||
|
||||
void
|
||||
@@ -180,20 +184,24 @@ namespace Recommendation
|
||||
_engines.clear();
|
||||
}
|
||||
|
||||
switch (getRecommendationEngineType(_db.getTLSSession()))
|
||||
switch (getSimilarityEngineType(_db.getTLSSession()))
|
||||
{
|
||||
case ScanSettings::RecommendationEngineType::Clusters:
|
||||
case ScanSettings::SimilarityEngineType::Clusters:
|
||||
_enginePriorities = {EngineType::Clusters};
|
||||
enginesToLoad.try_emplace(EngineType::Clusters, createClustersEngine(_db));
|
||||
break;
|
||||
|
||||
case ScanSettings::RecommendationEngineType::Features:
|
||||
case ScanSettings::SimilarityEngineType::Features:
|
||||
_enginePriorities = {EngineType::Features, EngineType::Clusters};
|
||||
|
||||
// not same order since clusters is faster to load
|
||||
enginesToLoad.try_emplace(EngineType::Clusters, createClustersEngine(_db));
|
||||
enginesToLoad.try_emplace(EngineType::Features, createFeaturesEngine(_db));
|
||||
break;
|
||||
|
||||
case ScanSettings::SimilarityEngineType::None:
|
||||
_enginePriorities.clear();
|
||||
break;
|
||||
}
|
||||
|
||||
assert(_pendingEngines.empty());
|
||||
|
||||
@@ -44,7 +44,7 @@ namespace Recommendation {
|
||||
Session& dbSession{ _db.getTLSSession() };
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ namespace Recommendation {
|
||||
if (!artist)
|
||||
return {};
|
||||
|
||||
const auto similarArtistIds{ artist->findSimilarArtists(artistLinkTypes, Range {0, maxCount}) };
|
||||
const auto similarArtistIds{ artist->findSimilarArtistIds(artistLinkTypes, Range {0, maxCount}) };
|
||||
return std::move(similarArtistIds.results);
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace Scanner
|
||||
Session& session {_db.getTLSSession()};
|
||||
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)
|
||||
{
|
||||
const Track::pointer track {Track::find(session, trackId)};
|
||||
|
||||
@@ -137,7 +137,7 @@ namespace Scanner
|
||||
Session& session {_db.getTLSSession()};
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
|
||||
auto artistIds {Artist::findAllOrphans(session, Range {})};
|
||||
auto artistIds {Artist::findOrphanIds(session, Range {})};
|
||||
for (const ArtistId artistId : artistIds.results)
|
||||
{
|
||||
Artist::pointer artist {Artist::find(session, artistId)};
|
||||
@@ -153,10 +153,11 @@ namespace Scanner
|
||||
|
||||
LMS_LOG(DBUPDATER, DEBUG) << "Checking orphan releases...";
|
||||
|
||||
// TODO, by batch
|
||||
Session& session {_db.getTLSSession()};
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
|
||||
auto releases {Release::findOrphans(session, Range {})};
|
||||
auto releases {Release::findOrphanIds(session, Range {})};
|
||||
for (const ReleaseId releaseId : releases.results)
|
||||
{
|
||||
Release::pointer release {Release::find(session, releaseId)};
|
||||
|
||||
@@ -307,6 +307,10 @@ namespace Scanner
|
||||
|
||||
context.currentStepStats.processedElems++;
|
||||
_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;
|
||||
|
||||
@@ -381,7 +381,7 @@ ScannerService::readSettings()
|
||||
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()) }; });
|
||||
}
|
||||
newSettings.recommendationServiceType = scanSettings->getRecommendationEngineType();
|
||||
newSettings.similarityServiceType = scanSettings->getSimilarityEngineType();
|
||||
newSettings.mediaDirectory = scanSettings->getMediaDirectory();
|
||||
|
||||
const auto clusterTypes = scanSettings->getClusterTypes();
|
||||
|
||||
@@ -34,7 +34,7 @@ namespace Scanner
|
||||
Wt::WTime startTime;
|
||||
Database::ScanSettings::UpdatePeriod updatePeriod {Database::ScanSettings::UpdatePeriod::Never};
|
||||
std::vector<std::filesystem::path> supportedExtensions;
|
||||
Database::ScanSettings::RecommendationEngineType recommendationServiceType;
|
||||
Database::ScanSettings::SimilarityEngineType similarityServiceType;
|
||||
std::filesystem::path mediaDirectory;
|
||||
bool skipDuplicateMBID {};
|
||||
std::set<std::string> clusterTypeNames;
|
||||
@@ -45,7 +45,7 @@ namespace Scanner
|
||||
&& startTime == rhs.startTime
|
||||
&& updatePeriod == rhs.updatePeriod
|
||||
&& supportedExtensions == rhs.supportedExtensions
|
||||
&& recommendationServiceType == rhs.recommendationServiceType
|
||||
&& similarityServiceType == rhs.similarityServiceType
|
||||
&& mediaDirectory == rhs.mediaDirectory
|
||||
&& skipDuplicateMBID == rhs.skipDuplicateMBID
|
||||
&& clusterTypeNames == rhs.clusterTypeNames;
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
|
||||
add_library(lmsscrobbling SHARED
|
||||
impl/internal/InternalScrobbler.cpp
|
||||
impl/listenbrainz/FeedbacksParser.cpp
|
||||
impl/listenbrainz/FeedbacksSynchronizer.cpp
|
||||
impl/listenbrainz/FeedbackTypes.cpp
|
||||
impl/listenbrainz/ListenBrainzScrobbler.cpp
|
||||
impl/internal/InternalBackend.cpp
|
||||
impl/listenbrainz/ListenBrainzBackend.cpp
|
||||
impl/listenbrainz/ListenTypes.cpp
|
||||
impl/listenbrainz/ListensParser.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.impl.hpp"
|
||||
|
||||
#include "services/database/Artist.hpp"
|
||||
#include "services/database/Db.hpp"
|
||||
#include "services/database/Listen.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/InternalScrobbler.hpp"
|
||||
#include "listenbrainz/ListenBrainzScrobbler.hpp"
|
||||
#include "internal/InternalBackend.hpp"
|
||||
#include "listenbrainz/ListenBrainzBackend.hpp"
|
||||
|
||||
namespace Scrobbling
|
||||
{
|
||||
@@ -48,8 +44,8 @@ namespace Scrobbling
|
||||
: _db{ db }
|
||||
{
|
||||
LMS_LOG(SCROBBLING, INFO) << "Starting service...";
|
||||
_scrobblers.emplace(Scrobbler::Internal, std::make_unique<InternalScrobbler>(_db));
|
||||
_scrobblers.emplace(Scrobbler::ListenBrainz, std::make_unique<ListenBrainz::Scrobbler>(ioContext, _db));
|
||||
_scrobblingBackends.emplace(ScrobblingBackend::Internal, std::make_unique<InternalBackend>(_db));
|
||||
_scrobblingBackends.emplace(ScrobblingBackend::ListenBrainz, std::make_unique<ListenBrainz::ListenBrainzBackend>(ioContext, _db));
|
||||
LMS_LOG(SCROBBLING, INFO) << "Service started!";
|
||||
}
|
||||
|
||||
@@ -60,46 +56,46 @@ namespace Scrobbling
|
||||
|
||||
void ScrobblingService::listenStarted(const Listen& listen)
|
||||
{
|
||||
if (std::optional<Scrobbler> scrobbler{ getUserScrobbler(listen.userId) })
|
||||
_scrobblers[*scrobbler]->listenStarted(listen);
|
||||
if (std::optional<ScrobblingBackend> backend{ getUserBackend(listen.userId) })
|
||||
_scrobblingBackends[*backend]->listenStarted(listen);
|
||||
}
|
||||
|
||||
void ScrobblingService::listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration)
|
||||
{
|
||||
if (std::optional<Scrobbler> scrobbler{ getUserScrobbler(listen.userId) })
|
||||
_scrobblers[*scrobbler]->listenFinished(listen, duration);
|
||||
if (std::optional<ScrobblingBackend> backend{ getUserBackend(listen.userId) })
|
||||
_scrobblingBackends[*backend]->listenFinished(listen, duration);
|
||||
}
|
||||
|
||||
void ScrobblingService::addTimedListen(const TimedListen& listen)
|
||||
{
|
||||
if (std::optional<Scrobbler> scrobbler{ getUserScrobbler(listen.userId) })
|
||||
_scrobblers[*scrobbler]->addTimedListen(listen);
|
||||
if (std::optional<ScrobblingBackend> backend{ getUserBackend(listen.userId) })
|
||||
_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() };
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
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)
|
||||
{
|
||||
ArtistContainer res;
|
||||
|
||||
auto scrobbler{ getUserScrobbler(userId) };
|
||||
if (!scrobbler)
|
||||
const auto backend{ getUserBackend(userId) };
|
||||
if (!backend)
|
||||
return res;
|
||||
|
||||
Session& session{ _db.getTLSSession() };
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -107,14 +103,14 @@ namespace Scrobbling
|
||||
{
|
||||
ReleaseContainer res;
|
||||
|
||||
auto scrobbler{ getUserScrobbler(userId) };
|
||||
if (!scrobbler)
|
||||
const auto backend{ getUserBackend(userId) };
|
||||
if (!backend)
|
||||
return res;
|
||||
|
||||
Session& session{ _db.getTLSSession() };
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
|
||||
res = Database::Listen::getRecentReleases(session, userId, *scrobbler, clusterIds, range);
|
||||
res = Database::Listen::getRecentReleases(session, userId, *backend, clusterIds, range);
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -122,40 +118,40 @@ namespace Scrobbling
|
||||
{
|
||||
TrackContainer res;
|
||||
|
||||
auto scrobbler{ getUserScrobbler(userId) };
|
||||
if (!scrobbler)
|
||||
const auto backend{ getUserBackend(userId) };
|
||||
if (!backend)
|
||||
return res;
|
||||
|
||||
Session& session{ _db.getTLSSession() };
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
|
||||
res = Database::Listen::getRecentTracks(session, userId, *scrobbler, clusterIds, range);
|
||||
res = Database::Listen::getRecentTracks(session, userId, *backend, clusterIds, range);
|
||||
return res;
|
||||
}
|
||||
|
||||
Wt::WDateTime ScrobblingService::getLastListenDateTime(Database::UserId userId, Database::ReleaseId releaseId)
|
||||
{
|
||||
auto scrobbler{ getUserScrobbler(userId) };
|
||||
if (!scrobbler)
|
||||
const auto backend{ getUserBackend(userId) };
|
||||
if (!backend)
|
||||
return {};
|
||||
|
||||
Session& session{ _db.getTLSSession() };
|
||||
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{};
|
||||
}
|
||||
|
||||
Wt::WDateTime ScrobblingService::getLastListenDateTime(Database::UserId userId, Database::TrackId trackId)
|
||||
{
|
||||
auto scrobbler{ getUserScrobbler(userId) };
|
||||
if (!scrobbler)
|
||||
const auto backend{ getUserBackend(userId) };
|
||||
if (!backend)
|
||||
return {};
|
||||
|
||||
Session& session{ _db.getTLSSession() };
|
||||
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{};
|
||||
}
|
||||
|
||||
@@ -164,14 +160,14 @@ namespace Scrobbling
|
||||
{
|
||||
ArtistContainer res;
|
||||
|
||||
auto scrobbler{ getUserScrobbler(userId) };
|
||||
if (!scrobbler)
|
||||
const auto backend{ getUserBackend(userId) };
|
||||
if (!backend)
|
||||
return res;
|
||||
|
||||
Session& session{ _db.getTLSSession() };
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -179,14 +175,14 @@ namespace Scrobbling
|
||||
{
|
||||
ReleaseContainer res;
|
||||
|
||||
auto scrobbler{ getUserScrobbler(userId) };
|
||||
if (!scrobbler)
|
||||
const auto backend{ getUserBackend(userId) };
|
||||
if (!backend)
|
||||
return res;
|
||||
|
||||
Session& session{ _db.getTLSSession() };
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
|
||||
res = Database::Listen::getTopReleases(session, userId, *scrobbler, clusterIds, range);
|
||||
res = Database::Listen::getTopReleases(session, userId, *backend, clusterIds, range);
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -194,133 +190,15 @@ namespace Scrobbling
|
||||
{
|
||||
TrackContainer res;
|
||||
|
||||
auto scrobbler{ getUserScrobbler(userId) };
|
||||
if (!scrobbler)
|
||||
const auto backend{ getUserBackend(userId) };
|
||||
if (!backend)
|
||||
return res;
|
||||
|
||||
Session& session{ _db.getTLSSession() };
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
|
||||
res = Database::Listen::getTopTracks(session, userId, *scrobbler, clusterIds, range);
|
||||
res = Database::Listen::getTopTracks(session, userId, *backend, clusterIds, range);
|
||||
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
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
#include <unordered_map>
|
||||
|
||||
#include "services/scrobbling/IScrobblingService.hpp"
|
||||
#include "IScrobbler.hpp"
|
||||
#include "IScrobblingBackend.hpp"
|
||||
|
||||
namespace Scrobbling
|
||||
{
|
||||
@@ -39,70 +39,21 @@ namespace Scrobbling
|
||||
void listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration) override;
|
||||
void addTimedListen(const TimedListen& listen) override;
|
||||
|
||||
ArtistContainer getRecentArtists(Database::UserId userId,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
std::optional<Database::TrackArtistLinkType> linkType,
|
||||
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;
|
||||
ArtistContainer getRecentArtists(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, std::optional<Database::TrackArtistLinkType> linkType,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::TrackId trackId) override;
|
||||
|
||||
ArtistContainer getTopArtists(Database::UserId userId,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
std::optional<Database::TrackArtistLinkType> linkType,
|
||||
Database::Range range) override;
|
||||
ArtistContainer getTopArtists(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, std::optional<Database::TrackArtistLinkType> linkType, Database::Range range) override;
|
||||
ReleaseContainer getTopReleases(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;
|
||||
|
||||
ReleaseContainer getTopReleases(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);
|
||||
std::optional<Database::ScrobblingBackend> getUserBackend(Database::UserId userId);
|
||||
|
||||
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
|
||||
|
||||
@@ -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:
|
||||
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();
|
||||
|
||||
|
||||
@@ -19,12 +19,11 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "utils/UUID.hpp"
|
||||
|
||||
#include "services/database/UserId.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
|
||||
{
|
||||
@@ -34,6 +33,5 @@ namespace Database
|
||||
namespace Scrobbling::ListenBrainz::Utils
|
||||
{
|
||||
std::optional<UUID> getListenBrainzToken(Database::Session& session, Database::UserId userId);
|
||||
|
||||
std::string parseValidateToken(std::string_view msgBody);
|
||||
}
|
||||
|
||||
@@ -55,59 +55,19 @@ namespace Scrobbling
|
||||
using ArtistContainer = Database::RangeResults<Database::ArtistId>;
|
||||
using ReleaseContainer = Database::RangeResults<Database::ReleaseId>;
|
||||
using TrackContainer = Database::RangeResults<Database::TrackId>;
|
||||
|
||||
// From most recent to oldest
|
||||
virtual ArtistContainer getRecentArtists(Database::UserId userId,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
std::optional<Database::TrackArtistLinkType> linkType,
|
||||
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 ArtistContainer getRecentArtists(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, std::optional<Database::TrackArtistLinkType> linkType, 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::TrackId trackId) = 0;
|
||||
|
||||
// Top
|
||||
virtual ArtistContainer getTopArtists(Database::UserId userId,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
std::optional<Database::TrackArtistLinkType> linkType,
|
||||
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;
|
||||
virtual ArtistContainer getTopArtists(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, std::optional<Database::TrackArtistLinkType> linkType, 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;
|
||||
};
|
||||
|
||||
std::unique_ptr<IScrobblingService> createScrobblingService(boost::asio::io_service& ioService, Database::Db& db);
|
||||
|
||||
@@ -42,6 +42,7 @@ target_link_libraries(lmssubsonic PRIVATE
|
||||
lmsauth
|
||||
lmsav
|
||||
lmsdatabase
|
||||
lmsfeedback
|
||||
lmsrecommendation
|
||||
lmsscanner
|
||||
lmsscrobbling
|
||||
|
||||
@@ -41,6 +41,7 @@ namespace API::Subsonic
|
||||
Database::UserId userId;
|
||||
ClientInfo clientInfo;
|
||||
ProtocolVersion serverProtocolVersion;
|
||||
bool enableOpenSubsonic{ true };
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -75,6 +75,18 @@ namespace API::Subsonic
|
||||
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)
|
||||
{
|
||||
@@ -253,6 +265,7 @@ namespace API::Subsonic
|
||||
|
||||
SubsonicResource::SubsonicResource(Db& db)
|
||||
: _serverProtocolVersionsByClient{ readConfigProtocolVersions() }
|
||||
, _openSubsonicDisabledClients{ readOpenSubsonicDisabledClients() }
|
||||
, _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 };
|
||||
|
||||
ProtocolVersion protocolVersion{ defaultServerProtocolVersion };
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
// 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"));
|
||||
RequestContext requestContext{ buildRequestContext(request) };
|
||||
|
||||
|
||||
auto itEntryPoint{ requestEntryPoints.find(requestPath) };
|
||||
if (itEntryPoint != requestEntryPoints.end())
|
||||
{
|
||||
@@ -364,8 +377,9 @@ namespace API::Subsonic
|
||||
const Wt::Http::ParameterMap& parameters{ request.getParameterMap() };
|
||||
const ClientInfo clientInfo{ getClientInfo(parameters) };
|
||||
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)
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <Wt/WResource.h>
|
||||
@@ -51,6 +52,7 @@ namespace API::Subsonic
|
||||
Database::UserId authenticateUser(const Wt::Http::Request& request, const ClientInfo& clientInfo);
|
||||
|
||||
const std::unordered_map<std::string, ProtocolVersion> _serverProtocolVersionsByClient;
|
||||
const std::unordered_set<std::string> _openSubsonicDisabledClients;
|
||||
Database::Db& _db;
|
||||
};
|
||||
|
||||
|
||||
@@ -148,6 +148,7 @@ namespace API::Subsonic
|
||||
}
|
||||
|
||||
// OpenSubsonic mandatory fields
|
||||
// No big deal to send them even for legacy clients
|
||||
responseNode.setAttribute("type", "lms");
|
||||
responseNode.setAttribute("serverVersion", serverVersion);
|
||||
responseNode.setAttribute("openSubsonic", true);
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/Track.hpp"
|
||||
#include "services/database/User.hpp"
|
||||
#include "services/feedback/IFeedbackService.hpp"
|
||||
#include "services/scrobbling/IScrobblingService.hpp"
|
||||
#include "responses/Album.hpp"
|
||||
#include "responses/Artist.hpp"
|
||||
@@ -36,8 +37,9 @@ namespace API::Subsonic
|
||||
{
|
||||
using namespace Database;
|
||||
|
||||
namespace {
|
||||
Response handleGetAlbumListRequestCommon(const RequestContext& context, bool id3)
|
||||
namespace
|
||||
{
|
||||
Response handleGetAlbumListRequestCommon(RequestContext& context, bool id3)
|
||||
{
|
||||
// Mandatory params
|
||||
const std::string type{ getMandatoryParameterAs<std::string>(context.parameters, "type") };
|
||||
@@ -49,7 +51,8 @@ namespace API::Subsonic
|
||||
const Range range{ offset, size };
|
||||
|
||||
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() };
|
||||
|
||||
@@ -63,11 +66,11 @@ namespace API::Subsonic
|
||||
params.setSortMethod(ReleaseSortMethod::Name);
|
||||
params.setRange(range);
|
||||
|
||||
releases = Release::find(context.dbSession, params);
|
||||
releases = Release::findIds(context.dbSession, params);
|
||||
}
|
||||
else if (type == "alphabeticalByArtist")
|
||||
{
|
||||
releases = Release::findOrderedByArtist(context.dbSession, range);
|
||||
releases = Release::findIdsOrderedByArtist(context.dbSession, range);
|
||||
}
|
||||
else if (type == "byGenre")
|
||||
{
|
||||
@@ -83,7 +86,7 @@ namespace API::Subsonic
|
||||
params.setSortMethod(ReleaseSortMethod::Name);
|
||||
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.setDateRange(DateRange::fromYearRange(fromYear, toYear));
|
||||
|
||||
releases = Release::find(context.dbSession, params);
|
||||
releases = Release::findIds(context.dbSession, params);
|
||||
}
|
||||
else if (type == "frequent")
|
||||
{
|
||||
releases = scrobbling.getTopReleases(context.userId, {}, range);
|
||||
releases = scrobblingService.getTopReleases(context.userId, {}, range);
|
||||
}
|
||||
else if (type == "newest")
|
||||
{
|
||||
@@ -109,7 +112,7 @@ namespace API::Subsonic
|
||||
params.setSortMethod(ReleaseSortMethod::LastWritten);
|
||||
params.setRange(range);
|
||||
|
||||
releases = Release::find(context.dbSession, params);
|
||||
releases = Release::findIds(context.dbSession, params);
|
||||
}
|
||||
else if (type == "random")
|
||||
{
|
||||
@@ -119,15 +122,15 @@ namespace API::Subsonic
|
||||
params.setSortMethod(ReleaseSortMethod::Random);
|
||||
params.setRange({ 0, size });
|
||||
|
||||
releases = Release::find(context.dbSession, params);
|
||||
releases = Release::findIds(context.dbSession, params);
|
||||
}
|
||||
else if (type == "recent")
|
||||
{
|
||||
releases = scrobbling.getRecentReleases(context.userId, {}, range);
|
||||
releases = scrobblingService.getRecentReleases(context.userId, {}, range);
|
||||
}
|
||||
else if (type == "starred")
|
||||
{
|
||||
releases = scrobbling.getStarredReleases(context.userId, {}, range);
|
||||
releases = feedbackService.getStarredReleases(context.userId, {}, range);
|
||||
}
|
||||
else
|
||||
throw NotImplementedGenericError{};
|
||||
@@ -138,7 +141,7 @@ namespace API::Subsonic
|
||||
for (const ReleaseId releaseId : releases.results)
|
||||
{
|
||||
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;
|
||||
@@ -155,24 +158,24 @@ namespace API::Subsonic
|
||||
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||
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) })
|
||||
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) })
|
||||
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) })
|
||||
starredNode.addArrayChild("song", createSongNode(track, context.dbSession, user));
|
||||
starredNode.addArrayChild("song", createSongNode(context, track, user));
|
||||
}
|
||||
|
||||
return response;
|
||||
@@ -201,16 +204,13 @@ namespace API::Subsonic
|
||||
if (!user)
|
||||
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::Node& randomSongsNode{ response.createNode("randomSongs") };
|
||||
for (const TrackId trackId : trackIds.results)
|
||||
{
|
||||
const Track::pointer track{ Track::find(context.dbSession, trackId) };
|
||||
randomSongsNode.addArrayChild("song", createSongNode(track, context.dbSession, user));
|
||||
}
|
||||
for (const Track::pointer& track : tracks.results)
|
||||
randomSongsNode.addArrayChild("song", createSongNode(context, track, user));
|
||||
|
||||
return response;
|
||||
}
|
||||
@@ -247,12 +247,9 @@ namespace API::Subsonic
|
||||
params.setClusters({ cluster->getId() });
|
||||
params.setRange({ offset, size });
|
||||
|
||||
auto trackIds{ Track::find(context.dbSession, params) };
|
||||
for (const TrackId trackId : trackIds.results)
|
||||
{
|
||||
const Track::pointer track{ Track::find(context.dbSession, trackId) };
|
||||
songsByGenreNode.addArrayChild("song", createSongNode(track, context.dbSession, user));
|
||||
}
|
||||
const auto tracks{ Track::find(context.dbSession, params) };
|
||||
for (const Track::pointer& track : tracks.results)
|
||||
songsByGenreNode.addArrayChild("song", createSongNode(context, track, user));
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ namespace API::Subsonic
|
||||
{
|
||||
const TrackBookmark::pointer bookmark{ TrackBookmark::find(context.dbSession, bookmarkId) };
|
||||
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));
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user