Merge branch 'develop' for release v3.43.0

This commit is contained in:
emeric
2023-11-04 22:09:46 +01:00
137 changed files with 9477 additions and 9165 deletions
+4 -2
View File
@@ -4,7 +4,7 @@
_LMS_ is a self-hosted music streaming software: access your music collection from anywhere using a web interface!
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).
+5 -7
View File
@@ -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}
+8 -7
View File
@@ -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>
+8 -7
View File
@@ -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>
+8 -7
View File
@@ -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>
+7 -6
View File
@@ -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>
+13 -13
View File
@@ -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>
<div class="mb-3">
<ul class="nav nav-pills mb-3">
<li class="nav-item">
${releases class="nav-link"}
</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 class="nav-item">
${artists class="nav-link"}
</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 class="nav-item">
${tracks class="nav-link"}
</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>
${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}
</div>
<div class="tab-pane" id="tracks" role="tabpanel" aria-labelledby="tracks-tab">${tracks}</div>
</div>
</message>
</messages>
+16 -7
View File
@@ -30,7 +30,7 @@
<div class="col-lg-4">
<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
View File
@@ -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
View File
@@ -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)
+25 -45
View File
@@ -34,6 +34,9 @@
#include "utils/String.hpp"
#include "utils/Utils.hpp"
namespace Cover
{
namespace
{
struct TrackInfo
@@ -44,8 +47,7 @@ namespace
std::optional<Database::ReleaseId> releaseId;
};
std::optional<TrackInfo>
getTrackInfo(Database::Session& dbSession, Database::TrackId trackId)
std::optional<TrackInfo> getTrackInfo(Database::Session& dbSession, Database::TrackId trackId)
{
std::optional<TrackInfo> res;
@@ -82,25 +84,20 @@ namespace
return res;
}
}
namespace Cover {
using namespace Image;
static
bool
isFileSupported(const std::filesystem::path& file, const std::vector<std::filesystem::path>& extensions)
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)
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)
@@ -134,8 +131,7 @@ CoverService::CoverService(Database::Db& db,
}
}
std::unique_ptr<IEncodedImage>
CoverService::getFromAvMediaFile(const Av::IAudioFile& input, ImageSize width) const
std::unique_ptr<IEncodedImage> CoverService::getFromAvMediaFile(const Av::IAudioFile& input, ImageSize width) const
{
std::unique_ptr<IEncodedImage> image;
@@ -159,8 +155,7 @@ CoverService::getFromAvMediaFile(const Av::IAudioFile& input, ImageSize width) c
return image;
}
std::unique_ptr<IEncodedImage>
CoverService::getFromCoverFile(const std::filesystem::path& p, ImageSize width) const
std::unique_ptr<IEncodedImage> CoverService::getFromCoverFile(const std::filesystem::path& p, ImageSize width) const
{
std::unique_ptr<IEncodedImage> image;
@@ -178,8 +173,7 @@ CoverService::getFromCoverFile(const std::filesystem::path& p, ImageSize width)
return image;
}
std::shared_ptr<IEncodedImage>
CoverService::getDefault(ImageSize width)
std::shared_ptr<IEncodedImage> CoverService::getDefault(ImageSize width)
{
{
std::shared_lock lock{ _cacheMutex };
@@ -202,8 +196,7 @@ CoverService::getDefault(ImageSize width)
}
}
std::unique_ptr<IEncodedImage>
CoverService::getFromDirectory(const std::filesystem::path& directory, ImageSize width) const
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) };
@@ -241,8 +234,7 @@ CoverService::getFromDirectory(const std::filesystem::path& directory, ImageSize
return image;
}
std::unique_ptr<IEncodedImage>
CoverService::getFromSameNamedFile(const std::filesystem::path& filePath, ImageSize width) const
std::unique_ptr<IEncodedImage> CoverService::getFromSameNamedFile(const std::filesystem::path& filePath, ImageSize width) const
{
std::unique_ptr<IEncodedImage> res;
@@ -262,8 +254,7 @@ CoverService::getFromSameNamedFile(const std::filesystem::path& filePath, ImageS
return res;
}
bool
CoverService::checkCoverFile(const std::filesystem::path& filePath) const
bool CoverService::checkCoverFile(const std::filesystem::path& filePath) const
{
std::error_code ec;
@@ -285,8 +276,7 @@ CoverService::checkCoverFile(const std::filesystem::path& filePath) const
return true;
}
std::multimap<std::string, std::filesystem::path>
CoverService::getCoverPaths(const std::filesystem::path& directoryPath) const
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;
@@ -306,8 +296,7 @@ CoverService::getCoverPaths(const std::filesystem::path& directoryPath) const
return res;
}
std::unique_ptr<IEncodedImage>
CoverService::getFromTrack(const std::filesystem::path& p, ImageSize width) const
std::unique_ptr<IEncodedImage> CoverService::getFromTrack(const std::filesystem::path& p, ImageSize width) const
{
std::unique_ptr<IEncodedImage> image;
@@ -323,14 +312,12 @@ CoverService::getFromTrack(const std::filesystem::path& p, ImageSize width) cons
return image;
}
std::shared_ptr<IEncodedImage>
CoverService::getFromTrack(Database::TrackId trackId, ImageSize width)
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)
std::shared_ptr<IEncodedImage> CoverService::getFromTrack(Database::Session& dbSession, Database::TrackId trackId, ImageSize width, bool allowReleaseFallback)
{
using namespace Database;
@@ -367,8 +354,7 @@ CoverService::getFromTrack(Database::Session& dbSession, Database::TrackId track
return cover;
}
std::shared_ptr<IEncodedImage>
CoverService::getFromRelease(Database::ReleaseId releaseId, ImageSize width)
std::shared_ptr<IEncodedImage> CoverService::getFromRelease(Database::ReleaseId releaseId, ImageSize width)
{
using namespace Database;
const CacheEntryDesc cacheEntryDesc{ releaseId, width };
@@ -395,13 +381,11 @@ CoverService::getFromRelease(Database::ReleaseId releaseId, ImageSize width)
if (!tracks.results.empty())
{
if (const Track::pointer track {Track::find(session, tracks.results.front())})
{
const Track::pointer& track{ tracks.results.front() };
res = ReleaseInfo{};
res->firstTrackId = track->getId();
res->releaseDirectory = track->getPath().parent_path();
}
}
return res;
} };
@@ -422,8 +406,7 @@ CoverService::getFromRelease(Database::ReleaseId releaseId, ImageSize width)
return cover;
}
void
CoverService::flushCache()
void CoverService::flushCache()
{
std::unique_lock lock{ _cacheMutex };
@@ -434,16 +417,14 @@ CoverService::flushCache()
_cache.clear();
}
void
CoverService::setJpegQuality(unsigned quality)
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)
void CoverService::saveToCache(const CacheEntryDesc& entryDesc, std::shared_ptr<IEncodedImage> image)
{
std::unique_lock lock{ _cacheMutex };
@@ -458,8 +439,7 @@ CoverService::saveToCache(const CacheEntryDesc& entryDesc, std::shared_ptr<IEnco
_cache[entryDesc] = image;
}
std::shared_ptr<IEncodedImage>
CoverService::loadFromCache(const CacheEntryDesc& entryDesc)
std::shared_ptr<IEncodedImage> CoverService::loadFromCache(const CacheEntryDesc& entryDesc)
{
std::shared_lock lock{ _cacheMutex };
+95 -75
View File
@@ -33,73 +33,20 @@
namespace Database
{
Artist::Artist(const std::string& name, const std::optional<UUID>& MBID)
: _name {std::string(name, 0 , _maxNameLength)},
_sortName {_name},
_MBID {MBID ? MBID->getAsString() : ""}
namespace
{
}
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}});
}
std::size_t
Artist::getCount(Session& session)
template <typename ResultType>
Wt::Dbo::Query<ResultType> createQuery(Session& session, std::string_view itemToSelect, const Artist::FindParameters& params)
{
session.checkSharedLocked();
return session.getDboSession().query<int>("SELECT COUNT(*) FROM artist");
}
std::vector<Artist::pointer>
Artist::find(Session& session, const std::string& name)
{
session.checkSharedLocked();
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
return std::vector<Artist::pointer>(res.begin(), res.end());
}
Artist::pointer
Artist::find(Session& session, const UUID& mbid)
{
session.checkSharedLocked();
return session.getDboSession().find<Artist>().where("mbid = ?").bind(std::string {mbid.getAsString()}).resultValue();
}
Artist::pointer
Artist::find(Session& session, ArtistId id)
{
session.checkSharedLocked();
return session.getDboSession().find<Artist>().where("id = ?").bind(id).resultValue();
}
bool
Artist::exists(Session& session, ArtistId id)
{
session.checkSharedLocked();
return session.getDboSession().query<int>("SELECT 1 FROM artist").where("id = ?").bind(id).resultValue() == 1;
}
static
Wt::Dbo::Query<ArtistId>
createQuery(Session& session, const Artist::FindParameters& params)
{
session.checkSharedLocked();
auto query {session.getDboSession().query<ArtistId>("SELECT DISTINCT a.id FROM artist a")};
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.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");
@@ -133,14 +80,19 @@ createQuery(Session& session, const Artist::FindParameters& params)
if (params.starringUser.isValid())
{
assert(params.scrobbler);
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.scrobbler = ?").bind(*params.scrobbler)
.where("s_a.scrobbling_state <> ?").bind(ScrobblingState::PendingRemove);
.where("s_a.backend = ?").bind(*params.feedbackBackend)
.where("s_a.sync_state <> ?").bind(SyncState::PendingRemove);
}
if (!params.clusters.empty())
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"
@@ -193,8 +145,72 @@ createQuery(Session& session, const Artist::FindParameters& params)
return query;
}
RangeResults<ArtistId>
Artist::findAllOrphans(Session& session, Range range)
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");
return createQuery<ResultType>(session, itemToSelect, params);
}
}
Artist::Artist(const std::string& name, const std::optional<UUID>& MBID)
: _name{ std::string(name, 0 , _maxNameLength) },
_sortName{ _name },
_MBID{ MBID ? MBID->getAsString() : "" }
{
}
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 }});
}
std::size_t Artist::getCount(Session& session)
{
session.checkSharedLocked();
return session.getDboSession().query<int>("SELECT COUNT(*) FROM artist");
}
std::vector<Artist::pointer> Artist::find(Session& session, const std::string& name)
{
session.checkSharedLocked();
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
return std::vector<Artist::pointer>(res.begin(), res.end());
}
Artist::pointer Artist::find(Session& session, const UUID& mbid)
{
session.checkSharedLocked();
return session.getDboSession().find<Artist>().where("mbid = ?").bind(std::string{ mbid.getAsString() }).resultValue();
}
Artist::pointer Artist::find(Session& session, ArtistId id)
{
session.checkSharedLocked();
return session.getDboSession().find<Artist>().where("id = ?").bind(id).resultValue();
}
bool Artist::exists(Session& session, ArtistId id)
{
session.checkSharedLocked();
return session.getDboSession().query<int>("SELECT 1 FROM artist").where("id = ?").bind(id).resultValue() == 1;
}
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)") };
@@ -202,17 +218,23 @@ Artist::findAllOrphans(Session& session, Range range)
return Utils::execQuery(query, range);
}
RangeResults<ArtistId>
Artist::find(Session& session, const FindParameters& params)
RangeResults<ArtistId> Artist::findIds(Session& session, const FindParameters& params)
{
session.checkSharedLocked();
auto query {createQuery(session, params)};
auto query{ createQuery<ArtistId>(session, params) };
return Utils::execQuery(query, params.range);
}
RangeResults<ArtistId>
Artist::findSimilarArtists(EnumSet<TrackArtistLinkType> artistLinkTypes, Range range) const
RangeResults<Artist::pointer> Artist::find(Session& session, const FindParameters& params)
{
session.checkSharedLocked();
auto query{ createQuery<Wt::Dbo::ptr<Artist>>(session, params) };
return Utils::execQuery(query, params.range);
}
RangeResults<ArtistId> Artist::findSimilarArtistIds(EnumSet<TrackArtistLinkType> artistLinkTypes, Range range) const
{
assert(session());
@@ -223,7 +245,7 @@ Artist::findSimilarArtists(EnumSet<TrackArtistLinkType> artistLinkTypes, Range r
" 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"
" 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"
@@ -259,8 +281,7 @@ Artist::findSimilarArtists(EnumSet<TrackArtistLinkType> artistLinkTypes, Range r
return Utils::execQuery(query, range);
}
std::vector<std::vector<Cluster::pointer>>
Artist::getClusterGroups(std::vector<ClusterType::pointer> clusterTypes, std::size_t size) const
std::vector<std::vector<Cluster::pointer>> Artist::getClusterGroups(std::vector<ClusterType::pointer> clusterTypes, std::size_t size) const
{
assert(session());
@@ -301,8 +322,7 @@ Artist::getClusterGroups(std::vector<ClusterType::pointer> clusterTypes, std::si
return res;
}
void
Artist::setSortName(const std::string& sortName)
void Artist::setSortName(const std::string& sortName)
{
_sortName = std::string(sortName, 0, _maxNameLength);
}
+22 -3
View File
@@ -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;
};
}
+71 -34
View File
@@ -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();
+28 -435
View File
@@ -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:
+51 -15
View File
@@ -34,10 +34,12 @@
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") };
template <typename ResultType>
Wt::Dbo::Query<ResultType> createQuery(Session& session, std::string_view itemToSelect, const Release::FindParameters& params)
{
auto query{ session.getDboSession().query<ResultType>("SELECT DISTINCT " + std::string{ itemToSelect } + " from release r") };
if (params.sortMethod == ReleaseSortMethod::LastWritten
|| params.sortMethod == ReleaseSortMethod::Date
@@ -45,7 +47,8 @@ namespace Database
|| params.sortMethod == ReleaseSortMethod::OriginalDateDesc
|| params.writtenAfter.isValid()
|| params.dateRange
|| params.artist.isValid())
|| params.artist.isValid()
|| params.clusters.size() == 1)
{
query.join("track t ON t.release_id = r.id");
}
@@ -64,11 +67,11 @@ namespace Database
if (params.starringUser.isValid())
{
assert(params.scrobbler);
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.scrobbler = ?").bind(*params.scrobbler)
.where("s_r.scrobbling_state <> ?").bind(ScrobblingState::PendingRemove);
.where("s_r.backend = ?").bind(*params.feedbackBackend)
.where("s_r.sync_state <> ?").bind(SyncState::PendingRemove);
}
if (params.artist.isValid())
@@ -120,18 +123,22 @@ namespace Database
}
}
if (!params.clusters.empty())
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 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";
WhereClause clusterClause;
for (const ClusterId clusterId : params.clusters)
{
clusterClause.Or(WhereClause("c.id = ?"));
clusterClause.Or(WhereClause("t_c.cluster_id = ?"));
query.bind(clusterId);
}
@@ -177,6 +184,22 @@ namespace Database
return query;
}
template <typename ResultType>
Wt::Dbo::Query<ResultType> createQuery(Session& session, const Release::FindParameters& params)
{
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 createQuery<ResultType>(session, itemToSelect, params);
}
}
Release::Release(const std::string& name, const std::optional<UUID>& MBID)
: _name{ std::string(name, 0 , _maxNameLength) },
_MBID{ MBID ? MBID->getAsString() : "" }
@@ -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())
+21 -10
View File
@@ -140,13 +140,13 @@ namespace Database
_session.execute("CREATE INDEX IF NOT EXISTS release_name_idx ON release(name)");
_session.execute("CREATE INDEX IF NOT EXISTS release_name_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,46 +29,41 @@
namespace Database
{
StarredArtist::StarredArtist(ObjectPtr<Artist> artist, ObjectPtr<User> user, Scrobbler scrobbler)
: _scrobbler {scrobbler}
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)
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, scrobbler}});
return session.getDboSession().add(std::unique_ptr<StarredArtist> {new StarredArtist{ artist, user, backend }});
}
std::size_t
StarredArtist::getCount(Session& session)
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)
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)
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("scrobbler = ?").bind(scrobbler)
.where("backend = ?").bind(backend)
.resultValue();
}
void
StarredArtist::setDateTime(const Wt::WDateTime& dateTime)
void StarredArtist::setDateTime(const Wt::WDateTime& dateTime)
{
_dateTime = Utils::normalizeDateTime(dateTime);
}
@@ -29,46 +29,41 @@
namespace Database
{
StarredRelease::StarredRelease(ObjectPtr<Release> release, ObjectPtr<User> user, Scrobbler scrobbler)
: _scrobbler {scrobbler}
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)
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, scrobbler}});
return session.getDboSession().add(std::unique_ptr<StarredRelease>{new StarredRelease{ release, user, backend }});
}
std::size_t
StarredRelease::getCount(Session& session)
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)
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)
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("scrobbler = ?").bind(scrobbler)
.where("backend = ?").bind(backend)
.resultValue();
}
void
StarredRelease::setDateTime(const Wt::WDateTime& dateTime)
void StarredRelease::setDateTime(const Wt::WDateTime& dateTime)
{
_dateTime = Utils::normalizeDateTime(dateTime);
}
@@ -29,63 +29,57 @@
namespace Database
{
StarredTrack::StarredTrack(ObjectPtr<Track> track, ObjectPtr<User> user, Scrobbler scrobbler)
: _scrobbler {scrobbler}
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)
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, scrobbler}});
return session.getDboSession().add(std::unique_ptr<StarredTrack> {new StarredTrack{ track, user, backend }});
}
std::size_t
StarredTrack::getCount(Session& session)
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)
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)
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("scrobbler = ?").bind(scrobbler)
.where("backend = ?").bind(backend)
.resultValue();
}
RangeResults<StarredTrackId>
StarredTrack::find(Session& session, const FindParameters& params)
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") };
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.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);
}
void
StarredTrack::setDateTime(const Wt::WDateTime& dateTime)
void StarredTrack::setDateTime(const Wt::WDateTime& dateTime)
{
_dateTime = Utils::normalizeDateTime(dateTime);
}
+70 -67
View File
@@ -35,15 +35,16 @@
#include "StringViewTraits.hpp"
#include "Utils.hpp"
namespace Database {
static
Wt::Dbo::Query<TrackId>
createQuery(Session& session, const Track::FindParameters& params)
namespace Database
{
namespace
{
template <typename ResultType>
Wt::Dbo::Query<ResultType> createQuery(Session& session, std::string_view itemToSelect, const Track::FindParameters& params)
{
session.checkSharedLocked();
auto query {session.getDboSession().query<TrackId>(params.distinct ? "SELECT DISTINCT t.id FROM track t" : "SELECT t.id FROM track t")};
auto query{ session.getDboSession().query<ResultType>(params.distinct ? "SELECT DISTINCT " + std::string{ itemToSelect } + " FROM track t" : "SELECT t.id FROM track t") };
assert(params.keywords.empty() || params.name.empty());
for (std::string_view keyword : params.keywords)
@@ -57,24 +58,29 @@ createQuery(Session& session, const Track::FindParameters& params)
if (params.starringUser.isValid())
{
assert(params.scrobbler);
assert(params.feedbackBackend);
query.join("starred_track s_t ON s_t.track_id = t.id")
.where("s_t.user_id = ?").bind(params.starringUser)
.where("s_t.scrobbler = ?").bind(*params.scrobbler)
.where("s_t.scrobbling_state <> ?").bind(ScrobblingState::PendingRemove);
.where("s_t.backend = ?").bind(*params.feedbackBackend)
.where("s_t.sync_state <> ?").bind(SyncState::PendingRemove);
}
if (!params.clusters.empty())
if (params.clusters.size() == 1)
{
// optim
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 << "t.id IN (SELECT DISTINCT t.id FROM track t"
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
" INNER JOIN cluster c ON c.id = t_c.cluster_id";
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
WhereClause clusterClause;
for (const ClusterId clusterId : params.clusters)
{
clusterClause.Or(WhereClause("c.id = ?"));
clusterClause.Or(WhereClause("t_c.cluster_id = ?"));
query.bind(clusterId);
}
@@ -164,13 +170,28 @@ createQuery(Session& session, const Track::FindParameters& params)
return query;
}
template <typename ResultType>
Wt::Dbo::Query<ResultType> createQuery(Session& session, const Track::FindParameters& params)
{
std::string_view itemToSelect;
if constexpr (std::is_same_v<ResultType, TrackId>)
itemToSelect = "t.id";
else if constexpr (std::is_same_v<ResultType, Wt::Dbo::ptr<Track>>)
itemToSelect = "t";
else
static_assert("Unhandled type");
return createQuery<ResultType>(session, itemToSelect, params);
}
}
Track::Track(const std::filesystem::path& p)
: _filePath{ p.string() }
{
}
Track::pointer
Track::create(Session& session, const std::filesystem::path& p)
Track::pointer Track::create(Session& session, const std::filesystem::path& p)
{
return session.getDboSession().add(std::unique_ptr<Track> {new Track{ p }});
}
@@ -183,16 +204,14 @@ Track::getCount(Session& session)
return session.getDboSession().query<int>("SELECT COUNT(*) FROM track");
}
Track::pointer
Track::findByPath(Session& session, const std::filesystem::path& p)
Track::pointer Track::findByPath(Session& session, const std::filesystem::path& p)
{
session.checkSharedLocked();
return session.getDboSession().find<Track>().where("file_path = ?").bind(p.string()).resultValue();
}
Track::pointer
Track::find(Session& session, TrackId id)
Track::pointer Track::find(Session& session, TrackId id)
{
session.checkSharedLocked();
@@ -201,16 +220,14 @@ Track::find(Session& session, TrackId id)
.resultValue();
}
bool
Track::exists(Session& session, TrackId id)
bool Track::exists(Session& session, TrackId id)
{
session.checkSharedLocked();
return session.getDboSession().query<int>("SELECT 1 from track").where("id = ?").bind(id).resultValue() == 1;
}
std::vector<Track::pointer>
Track::findByMBID(Session& session, const UUID& mbid)
std::vector<Track::pointer> Track::findByMBID(Session& session, const UUID& mbid)
{
session.checkSharedLocked();
@@ -221,8 +238,7 @@ Track::findByMBID(Session& session, const UUID& mbid)
return std::vector<Track::pointer>(res.begin(), res.end());
}
std::vector<Track::pointer>
Track::findByRecordingMBID(Session& session, const UUID& mbid)
std::vector<Track::pointer> Track::findByRecordingMBID(Session& session, const UUID& mbid)
{
session.checkSharedLocked();
@@ -233,8 +249,7 @@ Track::findByRecordingMBID(Session& session, const UUID& mbid)
return std::vector<Track::pointer>(res.begin(), res.end());
}
RangeResults<Track::PathResult>
Track::findPaths(Session& session, Range range)
RangeResults<Track::PathResult> Track::findPaths(Session& session, Range range)
{
using QueryResultType = std::tuple<TrackId, std::string>;
session.checkSharedLocked();
@@ -258,8 +273,7 @@ Track::findPaths(Session& session, Range range)
return res;
}
RangeResults<TrackId>
Track::findTrackMBIDDuplicates(Session& session, Range range)
RangeResults<TrackId> Track::findIdsTrackMBIDDuplicates(Session& session, Range range)
{
session.checkSharedLocked();
@@ -269,8 +283,7 @@ Track::findTrackMBIDDuplicates(Session& session, Range range)
return Utils::execQuery(query, range);
}
RangeResults<TrackId>
Track::findWithRecordingMBIDAndMissingFeatures(Session& session, Range range)
RangeResults<TrackId> Track::findIdsWithRecordingMBIDAndMissingFeatures(Session& session, Range range)
{
session.checkSharedLocked();
@@ -281,14 +294,12 @@ Track::findWithRecordingMBIDAndMissingFeatures(Session& session, Range range)
return Utils::execQuery(query, range);
}
std::vector<Cluster::pointer>
Track::getClusters() const
std::vector<Cluster::pointer> Track::getClusters() const
{
return std::vector<Cluster::pointer>(_clusters.begin(), _clusters.end());
}
std::vector<ClusterId>
Track::getClusterIds() const
std::vector<ClusterId> Track::getClusterIds() const
{
assert(session());
@@ -300,18 +311,23 @@ Track::getClusterIds() const
return std::vector<ClusterId>(res.begin(), res.end());
}
RangeResults<TrackId>
Track::find(Session& session, const FindParameters& parameters)
RangeResults<TrackId> Track::findIds(Session& session, const FindParameters& parameters)
{
session.checkSharedLocked();
auto query {createQuery(session, parameters)};
auto query{ createQuery<TrackId>(session, parameters) };
return Utils::execQuery(query, parameters.range);
}
RangeResults<TrackId>
Track::findSimilarTracks(Session& session, const std::vector<TrackId>& tracks, Range range)
RangeResults<Track::pointer> Track::find(Session& session, const FindParameters& parameters)
{
session.checkSharedLocked();
auto query{ createQuery<Wt::Dbo::ptr<Track>>(session, parameters) };
return Utils::execQuery(query, parameters.range);
}
RangeResults<TrackId> Track::findSimilarTrackIds(Session& session, const std::vector<TrackId>& tracks, Range range)
{
assert(!tracks.empty());
session.checkSharedLocked();
@@ -327,7 +343,7 @@ Track::findSimilarTracks(Session& session, const std::vector<TrackId>& tracks, R
auto query{ session.getDboSession().query<TrackId>(
"SELECT t.id FROM track t"
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
" AND t_c.cluster_id IN (SELECT c.id FROM cluster c INNER JOIN track_cluster t_c ON t_c.cluster_id = c.id WHERE t_c.track_id IN (" + oss.str() + "))"
" AND t_c.cluster_id IN (SELECT DISTINCT c.id FROM cluster c INNER JOIN track_cluster t_c ON t_c.cluster_id = c.id WHERE t_c.track_id IN (" + oss.str() + "))"
" AND t.id NOT IN (" + oss.str() + ")")
.groupBy("t.id")
.orderBy("COUNT(*) DESC, RANDOM()") };
@@ -341,52 +357,44 @@ Track::findSimilarTracks(Session& session, const std::vector<TrackId>& tracks, R
return Utils::execQuery(query, range);
}
void
Track::clearArtistLinks()
void Track::clearArtistLinks()
{
_trackArtistLinks.clear();
}
void
Track::addArtistLink(const ObjectPtr<TrackArtistLink>& artistLink)
void Track::addArtistLink(const ObjectPtr<TrackArtistLink>& artistLink)
{
_trackArtistLinks.insert(getDboPtr(artistLink));
}
void
Track::setClusters(const std::vector<ObjectPtr<Cluster>>& clusters)
void Track::setClusters(const std::vector<ObjectPtr<Cluster>>& clusters)
{
_clusters.clear();
for (const ObjectPtr<Cluster>& cluster : clusters)
_clusters.insert(getDboPtr(cluster));
}
std::optional<int>
Track::getYear() const
std::optional<int> Track::getYear() const
{
return (_date.isValid() ? std::make_optional<int>(_date.year()) : std::nullopt);
}
std::optional<int>
Track::getOriginalYear() const
std::optional<int> Track::getOriginalYear() const
{
return (_originalDate.isValid() ? std::make_optional<int>(_originalDate.year()) : std::nullopt);
}
std::optional<std::string>
Track::getCopyright() const
std::optional<std::string> Track::getCopyright() const
{
return _copyright != "" ? std::make_optional<std::string>(_copyright) : std::nullopt;
}
std::optional<std::string>
Track::getCopyrightURL() const
std::optional<std::string> Track::getCopyrightURL() const
{
return _copyrightURL != "" ? std::make_optional<std::string>(_copyrightURL) : std::nullopt;
}
std::vector<Artist::pointer>
Track::getArtists(EnumSet<TrackArtistLinkType> linkTypes) const
std::vector<Artist::pointer> Track::getArtists(EnumSet<TrackArtistLinkType> linkTypes) const
{
assert(session());
@@ -421,8 +429,7 @@ Track::getArtists(EnumSet<TrackArtistLinkType> linkTypes) const
return std::vector<Artist::pointer>(std::begin(res), std::end(res));
}
std::vector<ArtistId>
Track::getArtistIds(EnumSet<TrackArtistLinkType> linkTypes) const
std::vector<ArtistId> Track::getArtistIds(EnumSet<TrackArtistLinkType> linkTypes) const
{
assert(self());
assert(session());
@@ -458,14 +465,12 @@ Track::getArtistIds(EnumSet<TrackArtistLinkType> linkTypes) const
return std::vector<ArtistId>(std::cbegin(res), std::cend(res));
}
std::vector<TrackArtistLink::pointer>
Track::getArtistLinks() const
std::vector<TrackArtistLink::pointer> Track::getArtistLinks() const
{
return std::vector<TrackArtistLink::pointer>(_trackArtistLinks.begin(), _trackArtistLinks.end());
}
std::vector<std::vector<Cluster::pointer>>
Track::getClusterGroups(const std::vector<ClusterType::pointer>& clusterTypes, std::size_t size) const
std::vector<std::vector<Cluster::pointer>> Track::getClusterGroups(const std::vector<ClusterType::pointer>& clusterTypes, std::size_t size) const
{
assert(self());
assert(session());
@@ -508,8 +513,7 @@ Track::getClusterGroups(const std::vector<ClusterType::pointer>& clusterTypes, s
namespace Debug
{
std::ostream&
operator<<(std::ostream& os, const TrackInfo& trackInfo)
std::ostream& operator<<(std::ostream& os, const TrackInfo& trackInfo)
{
auto transaction{ trackInfo.session.createSharedTransaction() };
@@ -535,4 +539,3 @@ namespace Debug
}
} // namespace Database
@@ -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())
+4 -2
View File
@@ -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);
}
@@ -61,7 +61,7 @@ class Artist final : public Object<Artist, ArtistId>
Range range;
Wt::WDateTime writtenAfter;
UserId starringUser; // only artists starred by this user
std::optional<Scrobbler> scrobbler; // and for this scrobbler
std::optional<FeedbackBackend> feedbackBackend; // and for this feedback backend
TrackId track; // artists involved in this track
ReleaseId release; // artists involved in this release
@@ -71,7 +71,7 @@ class Artist final : public Object<Artist, ArtistId>
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& 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; }
};
@@ -83,8 +83,9 @@ class Artist final : public Object<Artist, ArtistId>
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 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
@@ -93,7 +94,7 @@ class Artist final : public Object<Artist, ArtistId>
std::optional<UUID> getMBID() const { return UUID::fromString(_MBID); }
// No artistLinkTypes means get them all
RangeResults<ArtistId> findSimilarArtists(EnumSet<TrackArtistLinkType> artistLinkTypes = {}, Range range = {}) const;
RangeResults<ArtistId> findSimilarArtistIds(EnumSet<TrackArtistLinkType> artistLinkTypes = {}, Range range = {}) const;
// Get the cluster of the tracks made by this artist
// Each clusters are grouped by cluster type, sorted by the number of occurence
@@ -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 };
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)
@@ -49,10 +49,11 @@ class ScanSettings final : public Object<ScanSettings, ScanSettingsId>
};
// Do not modify values (just add)
enum class RecommendationEngineType
enum class SimilarityEngineType
{
Clusters = 0,
Features,
None,
};
static void init(Session& session);
@@ -66,7 +67,7 @@ class ScanSettings final : public Object<ScanSettings, ScanSettingsId>
UpdatePeriod getUpdatePeriod() const { return _updatePeriod; }
std::vector<ObjectPtr<ClusterType>> getClusterTypes() const;
std::vector<std::filesystem::path> getAudioFileExtensions() const;
RecommendationEngineType getRecommendationEngineType() const { return _recommendationEngineType; }
SimilarityEngineType getSimilarityEngineType() const { return _similarityEngineType; }
// Setters
void addAudioFileExtension(const std::filesystem::path& ext);
@@ -74,7 +75,7 @@ class ScanSettings final : public Object<ScanSettings, ScanSettingsId>
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 setSimilarityEngineType(SimilarityEngineType type) { _similarityEngineType = type; }
void incScanVersion();
template<class Action>
@@ -85,7 +86,7 @@ class ScanSettings final : public Object<ScanSettings, ScanSettingsId>
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::field(a, _similarityEngineType, "similarity_engine_type");
Wt::Dbo::hasMany(a, _clusterTypes, Wt::Dbo::ManyToOne, "scan_settings");
}
@@ -94,8 +95,9 @@ class ScanSettings final : public Object<ScanSettings, ScanSettingsId>
std::string _mediaDirectory;
Wt::WTime _startTime = Wt::WTime{ 0,0,0 };
UpdatePeriod _updatePeriod{ UpdatePeriod::Never };
RecommendationEngineType _recommendationEngineType {RecommendationEngineType::Clusters};
SimilarityEngineType _similarityEngineType{ SimilarityEngineType::Clusters };
std::string _audioFileExtensions{ ".alac .mp3 .ogg .oga .aac .m4a .m4b .flac .wav .wma .aif .aiff .ape .mpc .shn .opus .wv" };
Wt::Dbo::collection<Wt::Dbo::ptr<ClusterType>> _clusterTypes;
};
@@ -61,6 +61,7 @@ namespace Database
void checkSharedLocked();
void analyze();
void optimize();
void prepareTables(); // need to run only once at startup
@@ -42,24 +42,24 @@ namespace Database
// 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);
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; }
FeedbackBackend getFeedbackBackend() const { return _backend; }
const Wt::WDateTime& getDateTime() const { return _dateTime; }
ScrobblingState getScrobblingState() const { return _scrobblingState; }
SyncState getSyncState() const { return _syncState; }
// Setters
void setDateTime(const Wt::WDateTime& dateTime);
void setScrobblingState(ScrobblingState state) { _scrobblingState = state; }
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, _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);
@@ -68,11 +68,11 @@ namespace Database
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);
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};
FeedbackBackend _backend; // for which backend
SyncState _syncState{ SyncState::PendingAdd };
Wt::WDateTime _dateTime; // when it was starred
Wt::Dbo::ptr<Artist> _artist;
@@ -42,24 +42,24 @@ namespace Database
// 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);
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; }
FeedbackBackend getFeedbackBackend() const { return _backend; }
const Wt::WDateTime& getDateTime() const { return _dateTime; }
ScrobblingState getScrobblingState() const { return _scrobblingState; }
SyncState getSyncState() const { return _syncState; }
// Setters
void setDateTime(const Wt::WDateTime& dateTime);
void setScrobblingState(ScrobblingState state) { _scrobblingState = state; }
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, _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);
@@ -68,11 +68,11 @@ namespace Database
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);
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};
FeedbackBackend _backend; // for which backend
SyncState _syncState{ SyncState::PendingAdd };
Wt::WDateTime _dateTime; // when it was starred
Wt::Dbo::ptr<Release> _release;
@@ -42,12 +42,12 @@ namespace Database
struct FindParameters
{
std::optional<Scrobbler> scrobbler; // for this scrobbler
std::optional<ScrobblingState> scrobblingState; // and these states
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& 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; }
};
@@ -55,25 +55,25 @@ namespace Database
// 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 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; }
FeedbackBackend getBackend() const { return _backend; }
const Wt::WDateTime& getDateTime() const { return _dateTime; }
ScrobblingState getScrobblingState() const { return _scrobblingState; }
SyncState getSyncState() const { return _syncState; }
// Setters
void setDateTime(const Wt::WDateTime& dateTime);
void setScrobblingState(ScrobblingState state) { _scrobblingState = state; }
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, _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);
@@ -82,11 +82,11 @@ namespace Database
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);
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};
FeedbackBackend _backend; // for which backend
SyncState _syncState{ SyncState::PendingAdd };
Wt::WDateTime _dateTime; // when it was starred
Wt::Dbo::ptr<Track> _track;
@@ -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>
@@ -68,7 +67,7 @@ class Track final : public Object<Track, TrackId>
Range range;
Wt::WDateTime writtenAfter;
UserId starringUser; // only tracks starred by this user
std::optional<Scrobbler> scrobbler; // and for this scrobbler
std::optional<FeedbackBackend> feedbackBackend; // and for this feedback backend
ArtistId artist; // only tracks that involve this artist
std::string artistName; // only tracks that involve this artist name
EnumSet<TrackArtistLinkType> trackArtistLinkTypes; // and for these link types
@@ -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;
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
+62 -54
View File
@@ -47,14 +47,23 @@ TEST_F(DatabaseFixture, Artist)
{
auto transaction{ session.createSharedTransaction() };
auto artists {Artist::find(session, Artist::FindParameters {})};
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 {});
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)
@@ -71,7 +80,7 @@ TEST_F(DatabaseFixture, Artist_singleTrack)
{
auto transaction{ session.createSharedTransaction() };
EXPECT_TRUE(Artist::findAllOrphans(session, Range {}).results.empty());
EXPECT_TRUE(Artist::findOrphanIds(session, Range{}).results.empty());
}
{
@@ -105,23 +114,23 @@ TEST_F(DatabaseFixture, Artist_singleTrack)
{
auto transaction{ session.createSharedTransaction() };
auto tracks {Track::find(session, Track::FindParameters{}.setName("MyTrackName").setArtistName("MyArtist"))};
auto tracks{ Track::findIds(session, Track::FindParameters{}.setName("MyTrackName").setArtistName("MyArtist")) };
ASSERT_EQ(tracks.results.size(), 1);
EXPECT_EQ(tracks.results.front(), track.getId());
}
{
auto transaction{ session.createSharedTransaction() };
auto tracks {Track::find(session, Track::FindParameters{}.setName("MyTrackName").setArtistName("MyArtistFoo"))};
auto tracks{ Track::findIds(session, Track::FindParameters{}.setName("MyTrackName").setArtistName("MyArtistFoo")) };
EXPECT_EQ(tracks.results.size(), 0);
}
{
auto transaction{ session.createSharedTransaction() };
auto tracks {Track::find(session, Track::FindParameters{}.setName("MyTrackNameFoo").setArtistName("MyArtist"))};
auto tracks{ Track::findIds(session, Track::FindParameters{}.setName("MyTrackNameFoo").setArtistName("MyArtist")) };
EXPECT_EQ(tracks.results.size(), 0);
}
{
auto transaction{ session.createSharedTransaction() };
auto artists {Artist::find(session, Artist::FindParameters{}.setTrack(track->getId()))};
auto artists{ Artist::findIds(session, Artist::FindParameters{}.setTrack(track->getId())) };
ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId());
}
@@ -141,16 +150,16 @@ TEST_F(DatabaseFixture, Artist_singleTracktMultiRoles)
{
auto transaction{ session.createSharedTransaction() };
EXPECT_TRUE(Artist::findAllOrphans(session, Range {}).results.empty());
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);
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);
}
{
@@ -166,17 +175,17 @@ TEST_F(DatabaseFixture, Artist_singleTracktMultiRoles)
EXPECT_EQ(track->getArtistLinks().size(), 3);
auto tracks {Track::find(session, Track::FindParameters {}.setArtist(artist.getId()))};
auto tracks{ Track::findIds(session, Track::FindParameters {}.setArtist(artist.getId())) };
EXPECT_EQ(tracks.results.size(), 1);
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::ReleaseArtist}));
tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::ReleaseArtist }));
EXPECT_EQ(tracks.results.size(), 1);
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::Artist}));
tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Artist }));
EXPECT_EQ(tracks.results.size(), 1);
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::Writer}));
tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Writer }));
EXPECT_EQ(tracks.results.size(), 1);
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::Composer}));
tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Composer }));
EXPECT_EQ(tracks.results.size(), 0);
}
@@ -206,7 +215,7 @@ TEST_F(DatabaseFixture, Artist_singleTrackMultiArtists)
{
auto transaction{ session.createSharedTransaction() };
EXPECT_TRUE(Artist::findAllOrphans(session, Range {}).results.empty());
EXPECT_TRUE(Artist::findOrphanIds(session, Range{}).results.empty());
}
{
@@ -220,31 +229,31 @@ TEST_F(DatabaseFixture, Artist_singleTrackMultiArtists)
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(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 tracks {Track::find(session, Track::FindParameters {}.setArtist(artist1->getId()))};
auto tracks{ Track::findIds(session, Track::FindParameters {}.setArtist(artist1->getId())) };
ASSERT_EQ(tracks.results.size(), 1);
EXPECT_EQ(tracks.results.front(), track->getId());
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist2->getId()));
tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist2->getId()));
ASSERT_EQ(tracks.results.size(), 1);
EXPECT_EQ(tracks.results.front(), track->getId());
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist1->getId(), {TrackArtistLinkType::ReleaseArtist}));
tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist1->getId(), { TrackArtistLinkType::ReleaseArtist }));
EXPECT_EQ(tracks.results.size(), 0);
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist1->getId(), {TrackArtistLinkType::Artist}));
tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist1->getId(), { TrackArtistLinkType::Artist }));
EXPECT_EQ(tracks.results.size(), 1);
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist2->getId(), {TrackArtistLinkType::ReleaseArtist}));
tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist2->getId(), { TrackArtistLinkType::ReleaseArtist }));
EXPECT_EQ(tracks.results.size(), 0);
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist2->getId(), {TrackArtistLinkType::Artist}));
tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist2->getId(), { TrackArtistLinkType::Artist }));
EXPECT_EQ(tracks.results.size(), 1);
}
}
@@ -263,13 +272,13 @@ TEST_F(DatabaseFixture, Artist_findByName)
{
auto transaction{ session.createSharedTransaction() };
EXPECT_TRUE(Artist::find(session, Artist::FindParameters {}.setKeywords({"N"})).results.empty());
EXPECT_TRUE(Artist::findIds(session, Artist::FindParameters{}.setKeywords({ "N" })).results.empty());
const auto artistsByAAA {Artist::find(session, Artist::FindParameters {}.setKeywords({"A"}))};
const auto artistsByAAA{ Artist::findIds(session, Artist::FindParameters {}.setKeywords({"A"})) };
ASSERT_EQ(artistsByAAA.results.size(), 1);
EXPECT_EQ(artistsByAAA.results.front(), artist.getId());
const auto artistsByZZZ {Artist::Artist::find(session, Artist::FindParameters {}.setKeywords({"Z"}))};
const auto artistsByZZZ{ Artist::Artist::findIds(session, Artist::FindParameters {}.setKeywords({"Z"})) };
ASSERT_EQ(artistsByZZZ.results.size(), 1);
EXPECT_EQ(artistsByZZZ.results.front(), artist.getId());
@@ -280,59 +289,59 @@ TEST_F(DatabaseFixture, Artist_findByName)
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%")};
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, "MyArtistFoo").empty());
EXPECT_TRUE(Artist::find(session, R"(MyArtistFoo)").empty());
}
{
const auto artists {Artist::find(session, "%MyArtist")};
const auto artists{ Artist::find(session, R"(%MyArtist)") };
ASSERT_TRUE(artists.size() == 1);
EXPECT_EQ(artists.front()->getId(), artist2.getId());
EXPECT_TRUE(Artist::find(session, "FooMyArtist").empty());
EXPECT_TRUE(Artist::find(session, R"(FooMyArtist)").empty());
}
{
const auto artists {Artist::find(session, "%_MyArtist")};
const auto artists{ Artist::find(session, R"(%_MyArtist)") };
ASSERT_TRUE(artists.size() == 1);
ASSERT_EQ(artists.front()->getId(), artist3.getId());
EXPECT_TRUE(Artist::find(session, "%CMyArtist").empty());
EXPECT_TRUE(Artist::find(session, R"(%CMyArtist)").empty());
}
}
{
auto transaction{ session.createSharedTransaction() };
{
const auto artists {Artist::find(session, Artist::FindParameters {}.setKeywords({"MyArtist"}))};
const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setKeywords({"MyArtist"})) };
EXPECT_EQ(artists.results.size(), 6);
}
{
const auto artists {Artist::find(session, Artist::FindParameters {}.setKeywords({"MyArtist%"}).setSortMethod(ArtistSortMethod::ByName))};
const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setKeywords({"MyArtist%"}).setSortMethod(ArtistSortMethod::ByName)) };
ASSERT_EQ(artists.results.size(), 2);
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))};
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))};
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());
}
@@ -354,8 +363,8 @@ TEST_F(DatabaseFixture, Artist_sortMethod)
{
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());
@@ -377,7 +386,7 @@ TEST_F(DatabaseFixture, Artist_nonReleaseTracks)
{
auto transaction{ session.createSharedTransaction() };
auto tracks {Track::find(session, Track::FindParameters {}.setNonRelease(true).setArtist(artist->getId()))};
auto tracks{ Track::findIds(session, Track::FindParameters {}.setNonRelease(true).setArtist(artist->getId())) };
EXPECT_EQ(tracks.results.size(), 0);
}
@@ -393,7 +402,7 @@ TEST_F(DatabaseFixture, Artist_nonReleaseTracks)
{
auto transaction{ session.createSharedTransaction() };
const auto tracks {Track::find(session, Track::FindParameters {}.setArtist(artist.getId()).setNonRelease(true))};
const auto tracks{ Track::findIds(session, Track::FindParameters {}.setArtist(artist.getId()).setNonRelease(true)) };
ASSERT_EQ(tracks.results.size(), 1);
EXPECT_EQ(tracks.results.front(), track2.getId());
}
@@ -407,7 +416,7 @@ TEST_F(DatabaseFixture, Artist_findByRelease)
{
auto transaction{ session.createSharedTransaction() };
const auto artists {Artist::find(session, Artist::FindParameters {}.setRelease(release.getId()))};
const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setRelease(release.getId())) };
EXPECT_EQ(artists.results.size(), 0);
}
@@ -418,7 +427,7 @@ TEST_F(DatabaseFixture, Artist_findByRelease)
{
auto transaction{ session.createSharedTransaction() };
const auto artists {Artist::find(session, Artist::FindParameters {}.setRelease(release.getId()))};
const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setRelease(release.getId())) };
EXPECT_EQ(artists.results.size(), 0);
}
@@ -429,9 +438,8 @@ TEST_F(DatabaseFixture, Artist_findByRelease)
{
auto transaction{ session.createSharedTransaction() };
const auto artists {Artist::find(session, Artist::FindParameters {}.setRelease(release.getId()))};
const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setRelease(release.getId())) };
ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId());
}
}
+82 -37
View File
@@ -133,11 +133,11 @@ TEST_F(DatabaseFixture, Cluster_singleTrack)
{
auto transaction{ session.createSharedTransaction() };
auto 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());
@@ -42,14 +42,14 @@ TEST_F(DatabaseFixture, MultiTracksSingleArtistSingleRelease)
{
auto transaction{ session.createSharedTransaction() };
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 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());
@@ -74,7 +74,7 @@ TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtist)
{
auto transaction{ session.createUniqueTransaction() };
auto releases {Release::find(session, Release::FindParameters {}.setArtist(artist.getId()))};
auto releases{ Release::findIds(session, Release::FindParameters {}.setArtist(artist.getId())) };
ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results.front(), release.getId());
+196 -131
View File
@@ -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());
}
+49 -43
View File
@@ -40,17 +40,23 @@ TEST_F(DatabaseFixture, Release)
EXPECT_TRUE(Release::exists(session, release.getId()));
{
auto releases {Release::findOrphans(session, Range {})};
const auto releases{ Release::findOrphanIds(session, Range {}) };
ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results.front(), release.getId());
}
{
auto releases {Release::find(session, Release::FindParameters {})};
const auto releases{ Release::findIds(session, Release::FindParameters {}) };
ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results.front(), release.getId());
EXPECT_EQ(release->getDuration(), std::chrono::seconds{ 0 });
}
{
const auto releases{ Release::find(session, Release::FindParameters {}) };
ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results.front()->getId(), release.getId());
}
}
}
@@ -70,9 +76,9 @@ TEST_F(DatabaseFixture, Release_singleTrack)
{
auto transaction{ session.createSharedTransaction() };
EXPECT_TRUE(Release::findOrphans(session, Range {}).results.empty());
EXPECT_TRUE(Release::findOrphanIds(session, Range{}).results.empty());
const auto tracks {Track::find(session, Track::FindParameters {}.setRelease(release.getId()))};
const auto tracks{ Track::findIds(session, Track::FindParameters {}.setRelease(release.getId())) };
ASSERT_EQ(tracks.results.size(), 1);
EXPECT_EQ(tracks.results.front(), track.getId());
}
@@ -86,18 +92,18 @@ TEST_F(DatabaseFixture, Release_singleTrack)
{
auto transaction{ session.createUniqueTransaction() };
auto tracks {Track::find(session, Track::FindParameters{}.setName("MyTrackName").setReleaseName("MyReleaseName"))};
auto tracks{ Track::findIds(session, Track::FindParameters{}.setName("MyTrackName").setReleaseName("MyReleaseName")) };
ASSERT_EQ(tracks.results.size(), 1);
EXPECT_EQ(tracks.results.front(), track.getId());
}
{
auto transaction{ session.createUniqueTransaction() };
auto tracks {Track::find(session, Track::FindParameters{}.setName("MyTrackName").setReleaseName("MyReleaseFoo"))};
auto tracks{ Track::findIds(session, Track::FindParameters{}.setName("MyTrackName").setReleaseName("MyReleaseFoo")) };
EXPECT_EQ(tracks.results.size(), 0);
}
{
auto transaction{ session.createUniqueTransaction() };
auto tracks {Track::find(session, Track::FindParameters{}.setName("MyTrackFoo").setReleaseName("MyReleaseName"))};
auto tracks{ Track::findIds(session, Track::FindParameters{}.setName("MyTrackFoo").setReleaseName("MyReleaseName")) };
EXPECT_EQ(tracks.results.size(), 0);
}
}
@@ -105,10 +111,10 @@ TEST_F(DatabaseFixture, Release_singleTrack)
{
auto transaction{ session.createUniqueTransaction() };
const auto tracks {Track::find(session, Track::FindParameters {}.setRelease(release.getId()))};
const auto tracks{ Track::findIds(session, Track::FindParameters {}.setRelease(release.getId())) };
EXPECT_TRUE(tracks.results.empty());
auto releases {Release::findOrphans(session, Range {})};
auto releases{ Release::findOrphanIds(session, Range {}) };
ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results.front(), release.getId());
}
@@ -146,34 +152,34 @@ TEST_F(DatabaseFixture, MulitpleReleaseSearchByName)
auto transaction{ session.createSharedTransaction() };
{
const auto releases {Release::find(session, Release::FindParameters {}.setKeywords({"Release"}))};
const auto releases{ Release::findIds(session, Release::FindParameters {}.setKeywords({"Release"})) };
EXPECT_EQ(releases.results.size(), 6);
}
{
const auto releases {Release::find(session, Release::FindParameters {}.setKeywords({"MyRelease"}))};
const auto releases{ Release::findIds(session, Release::FindParameters {}.setKeywords({"MyRelease"})) };
ASSERT_EQ(releases.results.size(), 5);
EXPECT_TRUE(std::none_of(std::cbegin(releases.results), std::cend(releases.results), [&](const ReleaseId releaseId) { return releaseId == release6.getId(); }));
}
{
const auto releases {Release::find(session, Release::FindParameters {}.setKeywords({"MyRelease%"}))};
const auto releases{ Release::findIds(session, Release::FindParameters {}.setKeywords({"MyRelease%"})) };
ASSERT_EQ(releases.results.size(), 2);
EXPECT_EQ(releases.results[0], release2.getId());
EXPECT_EQ(releases.results[1], release4.getId());
}
{
const auto releases {Release::find(session, Release::FindParameters {}.setKeywords({"%MyRelease"}))};
const auto releases{ Release::findIds(session, Release::FindParameters {}.setKeywords({"%MyRelease"})) };
ASSERT_EQ(releases.results.size(), 2);
EXPECT_EQ(releases.results[0], release3.getId());
EXPECT_EQ(releases.results[1], release5.getId());
}
{
const auto releases {Release::find(session, Release::FindParameters {}.setKeywords({"Foo%MyRelease"}))};
const auto releases{ Release::findIds(session, Release::FindParameters {}.setKeywords({"Foo%MyRelease"})) };
ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results[0], release5.getId());
}
{
const auto releases {Release::find(session, Release::FindParameters {}.setKeywords({"MyRelease%Foo"}))};
const auto releases{ Release::findIds(session, Release::FindParameters {}.setKeywords({"MyRelease%Foo"})) };
ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results[0], release4.getId());
}
@@ -279,8 +285,8 @@ TEST_F(DatabaseFixture, MultiTracksSingleReleaseFirstTrack)
{
auto transaction{ session.createSharedTransaction() };
EXPECT_TRUE(Track::find(session, Track::FindParameters {}.setRelease(release1.getId())).results.empty());
EXPECT_TRUE(Track::find(session, Track::FindParameters {}.setRelease(release2.getId())).results.empty());
EXPECT_TRUE(Track::findIds(session, Track::FindParameters{}.setRelease(release1.getId())).results.empty());
EXPECT_TRUE(Track::findIds(session, Track::FindParameters{}.setRelease(release2.getId())).results.empty());
}
{
@@ -304,13 +310,13 @@ TEST_F(DatabaseFixture, MultiTracksSingleReleaseFirstTrack)
auto transaction{ session.createSharedTransaction() };
{
const auto tracks {Track::find(session, Track::FindParameters {}.setRelease(release1.getId()).setSortMethod(TrackSortMethod::Release))};
const auto tracks{ Track::findIds(session, Track::FindParameters {}.setRelease(release1.getId()).setSortMethod(TrackSortMethod::Release)) };
ASSERT_FALSE(tracks.results.empty());
EXPECT_EQ(tracks.results.front(), track1A.getId());
}
{
const auto tracks {Track::find(session, Track::FindParameters {}.setRelease(release2.getId()).setSortMethod(TrackSortMethod::Release))};
const auto tracks{ Track::findIds(session, Track::FindParameters {}.setRelease(release2.getId()).setSortMethod(TrackSortMethod::Release)) };
ASSERT_FALSE(tracks.results.empty());
EXPECT_EQ(tracks.results.front(), track2B.getId());
}
@@ -332,7 +338,7 @@ TEST_F(DatabaseFixture, MultiTracksSingleReleaseDate)
{
auto transaction{ session.createSharedTransaction() };
const auto releases {Release::find(session, Release::FindParameters {}.setDateRange(DateRange::fromYearRange(0, 3000)))};
const auto releases{ Release::findIds(session, Release::FindParameters {}.setDateRange(DateRange::fromYearRange(0, 3000))) };
EXPECT_EQ(releases.results.size(), 0);
}
@@ -357,15 +363,15 @@ TEST_F(DatabaseFixture, MultiTracksSingleReleaseDate)
{
auto transaction{ session.createSharedTransaction() };
auto releases {Release::find(session, Release::FindParameters {}.setDateRange(DateRange::fromYearRange(1950, 2000)))};
auto releases{ Release::findIds(session, Release::FindParameters {}.setDateRange(DateRange::fromYearRange(1950, 2000))) };
ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results.front(), release1.getId());
releases = Release::find(session, Release::FindParameters {}.setDateRange(DateRange::fromYearRange(1994, 1994)));
releases = Release::findIds(session, Release::FindParameters{}.setDateRange(DateRange::fromYearRange(1994, 1994)));
ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results.front(), release1.getId());
releases = Release::find(session, Release::FindParameters {}.setDateRange(DateRange::fromYearRange(1993, 1993)));
releases = Release::findIds(session, Release::FindParameters{}.setDateRange(DateRange::fromYearRange(1993, 1993)));
ASSERT_EQ(releases.results.size(), 0);
}
}
@@ -385,19 +391,19 @@ TEST_F(DatabaseFixture, Release_writtenAfter)
{
auto transaction{ session.createSharedTransaction() };
const auto releases {Release::find(session, Release::FindParameters {})};
const auto releases{ Release::findIds(session, Release::FindParameters {}) };
EXPECT_EQ(releases.results.size(), 1);
}
{
auto transaction{ session.createSharedTransaction() };
const auto releases {Release::find(session, Release::FindParameters {}.setWrittenAfter(dateTime.addSecs(-1)))};
const auto releases{ Release::findIds(session, Release::FindParameters {}.setWrittenAfter(dateTime.addSecs(-1))) };
EXPECT_EQ(releases.results.size(), 1);
}
{
auto transaction{ session.createSharedTransaction() };
const auto releases {Release::find(session, Release::FindParameters {}.setWrittenAfter(dateTime.addSecs(+1)))};
const auto releases{ Release::findIds(session, Release::FindParameters {}.setWrittenAfter(dateTime.addSecs(+1))) };
EXPECT_EQ(releases.results.size(), 0);
}
}
@@ -416,10 +422,10 @@ TEST_F(DatabaseFixture, Release_artist)
{
auto transaction{ session.createSharedTransaction() };
auto releases {Release::find(session, Release::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::Artist}))};
auto releases{ Release::findIds(session, Release::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::Artist})) };
EXPECT_EQ(releases.results.size(), 0);
releases = Release::find(session, Release::FindParameters {}.setArtist(artist2.getId(), {TrackArtistLinkType::Artist}));
releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist2.getId(), { TrackArtistLinkType::Artist }));
EXPECT_EQ(releases.results.size(), 0);
}
@@ -427,38 +433,38 @@ TEST_F(DatabaseFixture, Release_artist)
auto transaction{ session.createUniqueTransaction() };
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
auto releases {Release::find(session, Release::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::Artist}))};
auto releases{ Release::findIds(session, Release::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::Artist})) };
ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results.front(), release.getId());
releases = Release::find(session, Release::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::Artist, TrackArtistLinkType::Mixer}));
releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Artist, TrackArtistLinkType::Mixer }));
EXPECT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results.front(), release.getId());
releases = Release::find(session, Release::FindParameters {}.setArtist(artist2.getId(), {TrackArtistLinkType::Artist}));
releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist2.getId(), { TrackArtistLinkType::Artist }));
EXPECT_EQ(releases.results.size(), 0);
releases = Release::find(session, Release::FindParameters {}.setArtist(artist2.getId()));
releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist2.getId()));
EXPECT_EQ(releases.results.size(), 0);
releases = Release::find(session, Release::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::ReleaseArtist, TrackArtistLinkType::Artist}));
releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::ReleaseArtist, TrackArtistLinkType::Artist }));
ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results.front(), release.getId());
releases = Release::find(session, Release::FindParameters {}.setArtist(artist.getId()));
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(), {TrackArtistLinkType::Composer}));
releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Composer }));
EXPECT_EQ(releases.results.size(), 0);
releases = Release::find(session, Release::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::Composer, TrackArtistLinkType::Mixer}));
releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Composer, TrackArtistLinkType::Mixer }));
EXPECT_EQ(releases.results.size(), 0);
releases = Release::find(session, Release::FindParameters {}.setArtist(artist.getId(), {}, {TrackArtistLinkType::Artist}));
releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId(), {}, { TrackArtistLinkType::Artist }));
EXPECT_EQ(releases.results.size(), 0);
releases = Release::find(session, Release::FindParameters {}.setArtist(artist.getId(), {}, {TrackArtistLinkType::Artist, TrackArtistLinkType::Composer}));
releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId(), {}, { TrackArtistLinkType::Artist, TrackArtistLinkType::Composer }));
EXPECT_EQ(releases.results.size(), 0);
}
}
@@ -560,7 +566,7 @@ TEST_F(DatabaseFixture, ReleaseSortOrder)
{
auto transaction{ session.createSharedTransaction() };
const auto releases {Release::find(session, Release::FindParameters {}.setSortMethod(ReleaseSortMethod::Name) )};
const auto releases{ Release::findIds(session, Release::FindParameters {}.setSortMethod(ReleaseSortMethod::Name)) };
ASSERT_EQ(releases.results.size(), 2);
EXPECT_EQ(releases.results.front(), release1.getId());
EXPECT_EQ(releases.results.back(), release2.getId());
@@ -569,14 +575,14 @@ TEST_F(DatabaseFixture, ReleaseSortOrder)
{
auto transaction{ session.createSharedTransaction() };
const auto releases {Release::find(session, Release::FindParameters {}.setSortMethod(ReleaseSortMethod::Random) )};
const auto releases{ Release::findIds(session, Release::FindParameters {}.setSortMethod(ReleaseSortMethod::Random)) };
ASSERT_EQ(releases.results.size(), 2);
}
{
auto transaction{ session.createSharedTransaction() };
const auto releases {Release::find(session, Release::FindParameters {}.setSortMethod(ReleaseSortMethod::Date) )};
const auto releases{ Release::findIds(session, Release::FindParameters {}.setSortMethod(ReleaseSortMethod::Date)) };
ASSERT_EQ(releases.results.size(), 2);
EXPECT_EQ(releases.results.front(), release2.getId());
EXPECT_EQ(releases.results.back(), release1.getId());
@@ -585,7 +591,7 @@ TEST_F(DatabaseFixture, ReleaseSortOrder)
{
auto transaction{ session.createSharedTransaction() };
const auto releases {Release::find(session, Release::FindParameters {}.setSortMethod(ReleaseSortMethod::OriginalDate) )};
const auto releases{ Release::findIds(session, Release::FindParameters {}.setSortMethod(ReleaseSortMethod::OriginalDate)) };
ASSERT_EQ(releases.results.size(), 2);
EXPECT_EQ(releases.results.front(), release1.getId());
EXPECT_EQ(releases.results.back(), release2.getId());
@@ -593,7 +599,7 @@ TEST_F(DatabaseFixture, ReleaseSortOrder)
{
auto transaction{ session.createSharedTransaction() };
const auto releases {Release::find(session, Release::FindParameters {}.setSortMethod(ReleaseSortMethod::OriginalDateDesc) )};
const auto releases{ Release::findIds(session, Release::FindParameters {}.setSortMethod(ReleaseSortMethod::OriginalDateDesc)) };
ASSERT_EQ(releases.results.size(), 2);
EXPECT_EQ(releases.results.front(), release2.getId());
EXPECT_EQ(releases.results.back(), release1.getId());
@@ -33,19 +33,19 @@ TEST_F(DatabaseFixture, StarredArtist)
{
auto transaction{ session.createSharedTransaction() };
auto starredArtist {StarredArtist::find(session, artist->getId(), user->getId(), Scrobbler::Internal)};
auto starredArtist{ StarredArtist::find(session, artist->getId(), user->getId(), FeedbackBackend::Internal) };
EXPECT_FALSE(starredArtist);
EXPECT_EQ(StarredArtist::getCount(session), 0);
auto artists {Artist::find(session, Artist::FindParameters {})};
auto artists{ Artist::findIds(session, Artist::FindParameters {}) };
EXPECT_EQ(artists.results.size(), 1);
}
ScopedStarredArtist starredArtist {session, artist.lockAndGet(), user.lockAndGet(), Scrobbler::Internal};
ScopedStarredArtist starredArtist{ session, artist.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal };
{
auto transaction{ session.createSharedTransaction() };
auto gotArtist {StarredArtist::find(session, artist->getId(), user->getId(), Scrobbler::Internal)};
auto gotArtist{ StarredArtist::find(session, artist->getId(), user->getId(), FeedbackBackend::Internal) };
EXPECT_EQ(gotArtist->getId(), starredArtist->getId());
EXPECT_EQ(StarredArtist::getCount(session), 1);
}
@@ -53,13 +53,13 @@ TEST_F(DatabaseFixture, StarredArtist)
{
auto transaction{ session.createSharedTransaction() };
auto artists {Artist::find(session, Artist::FindParameters {})};
auto artists{ Artist::findIds(session, Artist::FindParameters {}) };
EXPECT_EQ(artists.results.size(), 1);
artists = Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal));
artists = Artist::findIds(session, Artist::FindParameters{}.setStarringUser(user.getId(), FeedbackBackend::Internal));
EXPECT_EQ(artists.results.size(), 1);
artists = Artist::find(session, Artist::FindParameters {}.setStarringUser(user2.getId(), Scrobbler::Internal));
artists = Artist::findIds(session, Artist::FindParameters{}.setStarringUser(user2.getId(), FeedbackBackend::Internal));
EXPECT_EQ(artists.results.size(), 0);
}
}
@@ -68,16 +68,16 @@ TEST_F(DatabaseFixture, StarredArtist_PendingDestroy)
{
ScopedArtist artist{ session, "MyArtist" };
ScopedUser user{ session, "MyUser" };
ScopedStarredArtist starredArtist {session, artist.lockAndGet(), user.lockAndGet(), Scrobbler::Internal};
ScopedStarredArtist starredArtist{ session, artist.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal };
{
auto transaction{ session.createUniqueTransaction() };
auto artists {Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal))};
auto artists{ Artist::findIds(session, Artist::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal)) };
EXPECT_EQ(artists.results.size(), 1);
starredArtist.get().modify()->setScrobblingState(ScrobblingState::PendingRemove);
artists = Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal));
starredArtist.get().modify()->setSyncState(SyncState::PendingRemove);
artists = Artist::findIds(session, Artist::FindParameters{}.setStarringUser(user.getId(), FeedbackBackend::Internal));
EXPECT_EQ(artists.results.size(), 0);
}
}
@@ -88,15 +88,15 @@ TEST_F(DatabaseFixture, StarredArtist_dateTime)
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} };
{
auto transaction{ session.createSharedTransaction() };
auto artists {Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal))};
auto artists{ Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal)) };
EXPECT_EQ(artists.results.size(), 2);
}
@@ -106,8 +106,7 @@ TEST_F(DatabaseFixture, StarredArtist_dateTime)
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))};
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());
@@ -118,8 +117,7 @@ TEST_F(DatabaseFixture, StarredArtist_dateTime)
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))};
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());
@@ -33,7 +33,7 @@ TEST_F(DatabaseFixture, StarredRelease)
{
auto transaction{ session.createSharedTransaction() };
auto starredRelease {StarredRelease::find(session, release->getId(), user->getId(), Scrobbler::Internal)};
auto starredRelease{ StarredRelease::find(session, release->getId(), user->getId(), FeedbackBackend::Internal) };
EXPECT_FALSE(starredRelease);
EXPECT_EQ(StarredRelease::getCount(session), 0);
@@ -41,11 +41,11 @@ TEST_F(DatabaseFixture, StarredRelease)
EXPECT_EQ(releases.results.size(), 1);
}
ScopedStarredRelease starredRelease {session, release.lockAndGet(), user.lockAndGet(), Scrobbler::Internal};
ScopedStarredRelease starredRelease{ session, release.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal };
{
auto transaction{ session.createSharedTransaction() };
auto gotRelease {StarredRelease::find(session, release->getId(), user->getId(), Scrobbler::Internal)};
auto gotRelease{ StarredRelease::find(session, release->getId(), user->getId(), FeedbackBackend::Internal) };
EXPECT_EQ(gotRelease->getId(), starredRelease->getId());
EXPECT_EQ(StarredRelease::getCount(session), 1);
}
@@ -56,10 +56,10 @@ TEST_F(DatabaseFixture, StarredRelease)
auto releases{ Release::find(session, Release::FindParameters {}) };
EXPECT_EQ(releases.results.size(), 1);
releases = Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal));
releases = Release::find(session, Release::FindParameters{}.setStarringUser(user.getId(), FeedbackBackend::Internal));
EXPECT_EQ(releases.results.size(), 1);
releases = Release::find(session, Release::FindParameters {}.setStarringUser(user2.getId(), Scrobbler::Internal));
releases = Release::find(session, Release::FindParameters{}.setStarringUser(user2.getId(), FeedbackBackend::Internal));
EXPECT_EQ(releases.results.size(), 0);
}
}
@@ -68,16 +68,16 @@ TEST_F(DatabaseFixture, Starredrelease_PendingDestroy)
{
ScopedRelease release{ session, "MyRelease" };
ScopedUser user{ session, "MyUser" };
ScopedStarredRelease starredRelease {session, release.lockAndGet(), user.lockAndGet(), Scrobbler::Internal};
ScopedStarredRelease starredRelease{ session, release.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal };
{
auto transaction{ session.createUniqueTransaction() };
auto releases {Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal))};
auto releases{ Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal)) };
EXPECT_EQ(releases.results.size(), 1);
starredRelease.get().modify()->setScrobblingState(ScrobblingState::PendingRemove);
releases = Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal));
starredRelease.get().modify()->setSyncState(SyncState::PendingRemove);
releases = Release::find(session, Release::FindParameters{}.setStarringUser(user.getId(), FeedbackBackend::Internal));
EXPECT_EQ(releases.results.size(), 0);
}
}
@@ -88,15 +88,15 @@ TEST_F(DatabaseFixture, StarredRelease_dateTime)
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} };
{
auto transaction{ session.createSharedTransaction() };
auto releases {Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal))};
auto releases{ Release::findIds(session, Release::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal)) };
EXPECT_EQ(releases.results.size(), 2);
}
@@ -106,8 +106,7 @@ TEST_F(DatabaseFixture, StarredRelease_dateTime)
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))};
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());
@@ -118,8 +117,7 @@ TEST_F(DatabaseFixture, StarredRelease_dateTime)
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))};
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());
@@ -33,19 +33,19 @@ TEST_F(DatabaseFixture, StarredTrack)
{
auto transaction {session.createSharedTransaction()};
auto starredTrack {StarredTrack::find(session, track->getId(), user->getId(), Scrobbler::Internal)};
auto starredTrack {StarredTrack::find(session, track->getId(), user->getId(), FeedbackBackend::Internal)};
EXPECT_FALSE(starredTrack);
EXPECT_EQ(StarredTrack::getCount(session), 0);
auto tracks {Track::find(session, Track::FindParameters {})};
auto tracks {Track::findIds(session, Track::FindParameters {})};
EXPECT_EQ(tracks.results.size(), 1);
}
ScopedStarredTrack starredTrack {session, track.lockAndGet(), user.lockAndGet(), Scrobbler::Internal};
ScopedStarredTrack starredTrack {session, track.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal};
{
auto transaction {session.createSharedTransaction()};
auto gotTrack {StarredTrack::find(session, track->getId(), user->getId(), Scrobbler::Internal)};
auto gotTrack {StarredTrack::find(session, track->getId(), user->getId(), FeedbackBackend::Internal)};
EXPECT_EQ(gotTrack->getId(), starredTrack->getId());
EXPECT_EQ(StarredTrack::getCount(session), 1);
}
@@ -53,13 +53,13 @@ TEST_F(DatabaseFixture, StarredTrack)
{
auto transaction {session.createSharedTransaction()};
auto tracks {Track::find(session, Track::FindParameters {})};
auto tracks {Track::findIds(session, Track::FindParameters {})};
EXPECT_EQ(tracks.results.size(), 1);
tracks = Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal));
tracks = Track::findIds(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal));
EXPECT_EQ(tracks.results.size(), 1);
tracks = Track::find(session, Track::FindParameters {}.setStarringUser(user2.getId(), Scrobbler::Internal));
tracks = Track::findIds(session, Track::FindParameters {}.setStarringUser(user2.getId(), FeedbackBackend::Internal));
EXPECT_EQ(tracks.results.size(), 0);
}
}
@@ -68,16 +68,16 @@ TEST_F(DatabaseFixture, Starredtrack_PendingDestroy)
{
ScopedTrack track {session, "MyTrack"};
ScopedUser user {session, "MyUser"};
ScopedStarredTrack starredTrack {session, track.lockAndGet(), user.lockAndGet(), Scrobbler::Internal};
ScopedStarredTrack starredTrack {session, track.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal};
{
auto transaction {session.createUniqueTransaction()};
auto tracks {Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal))};
auto tracks {Track::findIds(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal))};
EXPECT_EQ(tracks.results.size(), 1);
starredTrack.get().modify()->setScrobblingState(ScrobblingState::PendingRemove);
tracks = Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal));
starredTrack.get().modify()->setSyncState(SyncState::PendingRemove);
tracks = Track::findIds(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal));
EXPECT_EQ(tracks.results.size(), 0);
}
}
@@ -88,15 +88,15 @@ TEST_F(DatabaseFixture, StarredTrack_dateTime)
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}};
{
auto transaction {session.createSharedTransaction()};
auto tracks {Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal))};
auto tracks {Track::findIds(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal))};
EXPECT_EQ(tracks.results.size(), 2);
}
@@ -106,8 +106,7 @@ TEST_F(DatabaseFixture, StarredTrack_dateTime)
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))};
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());
@@ -118,8 +117,7 @@ TEST_F(DatabaseFixture, StarredTrack_dateTime)
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))};
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());
+8 -7
View File
@@ -28,6 +28,7 @@ TEST_F(DatabaseFixture, Track)
{
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));
}
@@ -83,22 +84,22 @@ TEST_F(DatabaseFixture, MultipleTracksSearchByFilter)
auto transaction{ session.createSharedTransaction() };
{
const auto tracks {Track::find(session, Track::FindParameters {}.setKeywords({"Track"}))};
const auto tracks{ Track::findIds(session, Track::FindParameters {}.setKeywords({"Track"})) };
EXPECT_EQ(tracks.results.size(), 6);
}
{
const auto tracks {Track::find(session, Track::FindParameters {}.setKeywords({"MyTrack"}))};
const auto tracks{ Track::findIds(session, Track::FindParameters {}.setKeywords({"MyTrack"})) };
EXPECT_EQ(tracks.results.size(), 5);
EXPECT_TRUE(std::none_of(std::cbegin(tracks.results), std::cend(tracks.results), [&](const TrackId trackId) { return trackId == track6.getId(); }));
}
{
const auto tracks {Track::find(session, Track::FindParameters {}.setKeywords({"MyTrack%"}))};
const auto tracks{ Track::findIds(session, Track::FindParameters {}.setKeywords({"MyTrack%"})) };
ASSERT_EQ(tracks.results.size(), 2);
EXPECT_EQ(tracks.results[0], track2.getId());
EXPECT_EQ(tracks.results[1], track3.getId());
}
{
const auto tracks {Track::find(session, Track::FindParameters {}.setKeywords({"%MyTrack"}))};
const auto tracks{ Track::findIds(session, Track::FindParameters {}.setKeywords({"%MyTrack"})) };
ASSERT_EQ(tracks.results.size(), 2);
EXPECT_EQ(tracks.results[0], track4.getId());
EXPECT_EQ(tracks.results[1], track5.getId());
@@ -142,19 +143,19 @@ TEST_F(DatabaseFixture, Track_writtenAfter)
{
auto transaction{ session.createSharedTransaction() };
const auto tracks {Track::find(session, Track::FindParameters {})};
const auto tracks{ Track::findIds(session, Track::FindParameters {}) };
EXPECT_EQ(tracks.results.size(), 1);
}
{
auto transaction{ session.createSharedTransaction() };
const auto tracks {Track::find(session, Track::FindParameters {}.setWrittenAfter(dateTime.addSecs(-1)))};
const auto tracks{ Track::findIds(session, Track::FindParameters {}.setWrittenAfter(dateTime.addSecs(-1))) };
EXPECT_EQ(tracks.results.size(), 1);
}
{
auto transaction{ session.createSharedTransaction() };
const auto tracks {Track::find(session, Track::FindParameters {}.setWrittenAfter(dateTime.addSecs(+1)))};
const auto tracks{ Track::findIds(session, Track::FindParameters {}.setWrittenAfter(dateTime.addSecs(+1))) };
EXPECT_EQ(tracks.results.size(), 0);
}
}
@@ -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);
}
}
+29
View File
@@ -0,0 +1,29 @@
add_library(lmsfeedback SHARED
impl/internal/InternalBackend.cpp
impl/listenbrainz/FeedbacksParser.cpp
impl/listenbrainz/FeedbacksSynchronizer.cpp
impl/listenbrainz/FeedbackTypes.cpp
impl/listenbrainz/ListenBrainzBackend.cpp
impl/listenbrainz/Utils.cpp
impl/FeedbackService.cpp
)
target_include_directories(lmsfeedback INTERFACE
include
)
target_include_directories(lmsfeedback PRIVATE
include
impl
)
target_link_libraries(lmsfeedback PRIVATE
lmsutils
)
target_link_libraries(lmsfeedback PUBLIC
lmsdatabase
)
install(TARGETS lmsfeedback DESTINATION lib)
@@ -0,0 +1,185 @@
/*
* Copyright (C) 2021 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "FeedbackService.hpp"
#include "FeedbackService.impl.hpp"
#include "services/database/Artist.hpp"
#include "services/database/Db.hpp"
#include "services/database/Release.hpp"
#include "services/database/Session.hpp"
#include "services/database/StarredArtist.hpp"
#include "services/database/StarredRelease.hpp"
#include "services/database/StarredTrack.hpp"
#include "services/database/Track.hpp"
#include "services/database/User.hpp"
#include "utils/Logger.hpp"
#include "internal/InternalBackend.hpp"
#include "listenbrainz/ListenBrainzBackend.hpp"
namespace Feedback
{
std::unique_ptr<IFeedbackService> createFeedbackService(boost::asio::io_context& ioContext, Db& db)
{
return std::make_unique<FeedbackService>(ioContext, db);
}
FeedbackService::FeedbackService(boost::asio::io_context& ioContext, Db& db)
: _db{ db }
{
LMS_LOG(SCROBBLING, INFO) << "Starting service...";
_backends.emplace(Database::FeedbackBackend::Internal, std::make_unique<InternalBackend>(_db));
_backends.emplace(Database::FeedbackBackend::ListenBrainz, std::make_unique<ListenBrainz::ListenBrainzBackend>(ioContext, _db));
LMS_LOG(SCROBBLING, INFO) << "Service started!";
}
FeedbackService::~FeedbackService()
{
LMS_LOG(SCROBBLING, INFO) << "Service stopped!";
}
std::optional<Database::FeedbackBackend> FeedbackService::getUserFeedbackBackend(UserId userId)
{
std::optional<Database::FeedbackBackend> feedbackBackend;
Session& session{ _db.getTLSSession() };
auto transaction{ session.createSharedTransaction() };
if (const User::pointer user{ User::find(session, userId) })
feedbackBackend = user->getFeedbackBackend();
return feedbackBackend;
}
void FeedbackService::star(UserId userId, ArtistId artistId)
{
star<Artist, ArtistId, StarredArtist>(userId, artistId);
}
void FeedbackService::unstar(UserId userId, ArtistId artistId)
{
unstar<Artist, ArtistId, StarredArtist>(userId, artistId);
}
bool FeedbackService::isStarred(UserId userId, ArtistId artistId)
{
return isStarred<Artist, ArtistId, StarredArtist>(userId, artistId);
}
Wt::WDateTime FeedbackService::getStarredDateTime(UserId userId, ArtistId artistId)
{
return getStarredDateTime<Artist, ArtistId, StarredArtist>(userId, artistId);
}
FeedbackService::ArtistContainer FeedbackService::getStarredArtists(UserId userId, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, ArtistSortMethod sortMethod, Range range)
{
auto backend{ getUserFeedbackBackend(userId) };
if (!backend)
return {};
Artist::FindParameters params;
params.setStarringUser(userId, *backend);
params.setClusters(clusterIds);
params.setLinkType(linkType);
params.setSortMethod(sortMethod);
params.setRange(range);
Session& session{ _db.getTLSSession() };
auto transaction{ session.createSharedTransaction() };
return Artist::findIds(session, params);
}
void FeedbackService::star(UserId userId, ReleaseId releaseId)
{
star<Release, ReleaseId, StarredRelease>(userId, releaseId);
}
void FeedbackService::unstar(UserId userId, ReleaseId releaseId)
{
unstar<Release, ReleaseId, StarredRelease>(userId, releaseId);
}
bool FeedbackService::isStarred(UserId userId, ReleaseId releaseId)
{
return isStarred<Release, ReleaseId, StarredRelease>(userId, releaseId);
}
Wt::WDateTime FeedbackService::getStarredDateTime(UserId userId, ReleaseId releaseId)
{
return getStarredDateTime<Release, ReleaseId, StarredRelease>(userId, releaseId);
}
FeedbackService::ReleaseContainer FeedbackService::getStarredReleases(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
{
auto backend{ getUserFeedbackBackend(userId) };
if (!backend)
return {};
Release::FindParameters params;
params.setStarringUser(userId, *backend);
params.setClusters(clusterIds);
params.setSortMethod(ReleaseSortMethod::StarredDateDesc);
params.setRange(range);
Session& session{ _db.getTLSSession() };
auto transaction{ session.createSharedTransaction() };
return Release::findIds(session, params);
}
void FeedbackService::star(UserId userId, TrackId trackId)
{
star<Track, TrackId, StarredTrack>(userId, trackId);
}
void FeedbackService::unstar(UserId userId, TrackId trackId)
{
unstar<Track, TrackId, StarredTrack>(userId, trackId);
}
bool FeedbackService::isStarred(UserId userId, TrackId trackId)
{
return isStarred<Track, TrackId, StarredTrack>(userId, trackId);
}
Wt::WDateTime FeedbackService::getStarredDateTime(UserId userId, TrackId trackId)
{
return getStarredDateTime<Track, TrackId, StarredTrack>(userId, trackId);
}
FeedbackService::TrackContainer FeedbackService::getStarredTracks(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
{
auto backend{ getUserFeedbackBackend(userId) };
if (!backend)
return {};
Track::FindParameters params;
params.setStarringUser(userId, *backend);
params.setClusters(clusterIds);
params.setSortMethod(TrackSortMethod::StarredDateDesc);
params.setRange(range);
Session& session{ _db.getTLSSession() };
auto transaction{ session.createSharedTransaction() };
return Track::findIds(session, params);
}
} // ns Feedback
@@ -0,0 +1,78 @@
/*
* Copyright (C) 2021 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <memory>
#include <unordered_map>
#include "services/feedback/IFeedbackService.hpp"
#include "IFeedbackBackend.hpp"
namespace Database
{
class Db;
}
namespace Feedback
{
class FeedbackService : public IFeedbackService
{
public:
FeedbackService(boost::asio::io_context& ioContext, Database::Db& db);
~FeedbackService();
private:
FeedbackService(const FeedbackService&) = delete;
FeedbackService& operator=(const FeedbackService&) = delete;
void star(Database::UserId userId, Database::ArtistId artistId) override;
void unstar(Database::UserId userId, Database::ArtistId artistId) override;
bool isStarred(Database::UserId userId, Database::ArtistId artistId) override;
Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::ArtistId artistId) override;
ArtistContainer getStarredArtists(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, std::optional<Database::TrackArtistLinkType> linkType, Database::ArtistSortMethod sortMethod, Database::Range range) override;
void star(Database::UserId userId, Database::ReleaseId releaseId) override;
void unstar(Database::UserId userId, Database::ReleaseId releaseId) override;
bool isStarred(Database::UserId userId, Database::ReleaseId releasedId) override;
Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::ReleaseId releasedId) override;
ReleaseContainer getStarredReleases(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) override;
void star(Database::UserId userId, Database::TrackId trackId) override;
void unstar(Database::UserId userId, Database::TrackId trackId) override;
bool isStarred(Database::UserId userId, Database::TrackId trackId) override;
Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::TrackId trackId) override;
TrackContainer getStarredTracks(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) override;
std::optional<Database::FeedbackBackend> getUserFeedbackBackend(Database::UserId userId);
template <typename ObjType, typename ObjIdType, typename StarredObjType>
void star(Database::UserId userId, ObjIdType id);
template <typename ObjType, typename ObjIdType, typename StarredObjType>
void unstar(Database::UserId userId, ObjIdType id);
template <typename ObjType, typename ObjIdType, typename StarredObjType>
bool isStarred(Database::UserId userId, ObjIdType id);
template <typename ObjType, typename ObjIdType, typename StarredObjType>
Wt::WDateTime getStarredDateTime(Database::UserId userId, ObjIdType id);
Database::Db& _db;
std::unordered_map<Database::FeedbackBackend, std::unique_ptr<IFeedbackBackend>> _backends;
};
} // ns Feedback
@@ -0,0 +1,113 @@
/*
* Copyright (C) 2021 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "services/database/Db.hpp"
#include "services/database/Session.hpp"
#include "services/database/User.hpp"
namespace Feedback
{
using namespace Database;
template <typename ObjType, typename ObjIdType, typename StarredObjType>
void FeedbackService::star(UserId userId, ObjIdType objId)
{
const auto backend{ getUserFeedbackBackend(userId) };
if (!backend)
return;
typename StarredObjType::IdType starredObjId;
{
Session& session{ _db.getTLSSession() };
auto transaction{ session.createUniqueTransaction() };
typename StarredObjType::pointer starredObj{ StarredObjType::find(session, objId, userId, *backend) };
if (!starredObj)
{
const typename ObjType::pointer obj{ ObjType::find(session, objId) };
if (!obj)
return;
const User::pointer user{ User::find(session, userId) };
if (!user)
return;
starredObj = session.create<StarredObjType>(obj, user, *backend);
}
starredObj.modify()->setDateTime(Wt::WDateTime::currentDateTime());
starredObjId = starredObj->getId();
}
_backends[*backend]->onStarred(starredObjId);
}
template <typename ObjType, typename ObjIdType, typename StarredObjType>
void FeedbackService::unstar(UserId userId, ObjIdType objId)
{
const auto backend{ getUserFeedbackBackend(userId) };
if (!backend)
return;
typename StarredObjType::IdType starredObjId;
{
Session& session{ _db.getTLSSession() };
auto transaction{ session.createSharedTransaction() };
typename StarredObjType::pointer starredObj{ StarredObjType::find(session, objId, userId, *backend) };
if (!starredObj)
return;
starredObjId = starredObj->getId();
}
_backends[*backend]->onUnstarred(starredObjId);
}
template <typename ObjType, typename ObjIdType, typename StarredObjType>
bool FeedbackService::isStarred(UserId userId, ObjIdType objId)
{
const auto backend{ getUserFeedbackBackend(userId) };
if (!backend)
return false;
Session& session{ _db.getTLSSession() };
auto transaction{ session.createSharedTransaction() };
typename StarredObjType::pointer starredObj{ StarredObjType::find(session, objId, userId, *backend) };
return starredObj && (starredObj->getSyncState() != SyncState::PendingRemove);
}
template <typename ObjType, typename ObjIdType, typename StarredObjType>
Wt::WDateTime FeedbackService::getStarredDateTime(UserId userId, ObjIdType objId)
{
const auto backend{ getUserFeedbackBackend(userId) };
if (!backend)
return {};
Session& session{ _db.getTLSSession() };
auto transaction{ session.createSharedTransaction() };
typename StarredObjType::pointer starredObj{ StarredObjType::find(session, objId, userId, *backend) };
if (starredObj && (starredObj->getSyncState() != SyncState::PendingRemove))
return starredObj->getDateTime();
return {};
}
} // ns Feedback
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Emeric Poupon
* Copyright (C) 2023 Emeric Poupon
*
* This file is part of LMS.
*
@@ -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
@@ -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;
};
}
@@ -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
@@ -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
@@ -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
@@ -32,8 +32,7 @@ namespace Recommendation
{
using namespace Database;
std::unique_ptr<IPlaylistGeneratorService>
createPlaylistGeneratorService(Db& db, Recommendation::IRecommendationService& recommendationService)
std::unique_ptr<IPlaylistGeneratorService> createPlaylistGeneratorService(Db& db, Recommendation::IRecommendationService& recommendationService)
{
return std::make_unique<PlaylistGeneratorService>(db, recommendationService);
}
@@ -47,8 +46,7 @@ namespace Recommendation
_constraints.push_back(std::make_unique<PlaylistGeneratorConstraint::DuplicateTracks>());
}
std::vector<TrackId>
PlaylistGeneratorService::extendPlaylist(TrackListId tracklistId, std::size_t maxCount) const
std::vector<TrackId> PlaylistGeneratorService::extendPlaylist(TrackListId tracklistId, std::size_t maxCount) const
{
LMS_LOG(RECOMMENDATION, DEBUG) << "Requested to extend playlist by " << maxCount << " similar tracks";
@@ -97,8 +95,7 @@ namespace Recommendation
return std::vector(std::cbegin(finalResult) + startingTracks.size(), std::cend(finalResult));
}
TrackContainer
PlaylistGeneratorService::getTracksFromTrackList(Database::TrackListId tracklistId) const
TrackContainer PlaylistGeneratorService::getTracksFromTrackList(Database::TrackListId tracklistId) const
{
TrackContainer tracks;
@@ -110,7 +107,7 @@ namespace Recommendation
params.setSortMethod(TrackSortMethod::TrackList);
params.setDistinct(false);
for (const TrackId trackId : Track::find(dbSession, params).results)
for (const TrackId trackId : Track::findIds(dbSession, params).results)
tracks.push_back(trackId);
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;
+2 -5
View File
@@ -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
@@ -17,9 +17,8 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "ListenBrainzScrobbler.hpp"
#include "ListensSynchronizer.hpp"
#include <tuple>
#include <boost/asio/bind_executor.hpp>
#include <Wt/Json/Array.h>
#include <Wt/Json/Object.h>
@@ -45,8 +44,7 @@ namespace
{
using namespace Scrobbling::ListenBrainz;
std::optional<Wt::Json::Object>
listenToJsonPayload(Database::Session& session, const Scrobbling::Listen& listen, const Wt::WDateTime& timePoint)
std::optional<Wt::Json::Object> listenToJsonPayload(Database::Session& session, const Scrobbling::Listen& listen, const Wt::WDateTime& timePoint)
{
auto transaction{ session.createSharedTransaction() };
@@ -108,8 +106,7 @@ namespace
return payload;
}
std::string
listenToJsonString(Database::Session& session, const Scrobbling::Listen& listen, const Wt::WDateTime& timePoint, std::string_view listenType)
std::string listenToJsonString(Database::Session& session, const Scrobbling::Listen& listen, const Wt::WDateTime& timePoint, std::string_view listenType)
{
std::string res;
@@ -125,8 +122,7 @@ namespace
return res;
}
std::optional<std::size_t>
parseListenCount(std::string_view msgBody)
std::optional<std::size_t> parseListenCount(std::string_view msgBody)
{
try
{
@@ -143,8 +139,7 @@ namespace
}
}
Database::TrackId
tryGetMatchingTrack(Database::Session& session, const Listen& listen)
Database::TrackId tryGetMatchingTrack(Database::Session& session, const Listen& listen)
{
using namespace Database;
@@ -193,7 +188,7 @@ namespace
if (listen.trackNumber)
params.setTrackNumber(*listen.trackNumber);
const auto tracks {Track::find(session, params)};
const auto tracks{ Track::findIds(session, params) };
// conservative behavior: in case of multiple matches: reject
if (tracks.results.size() == 1)
{
@@ -225,21 +220,18 @@ namespace Scrobbling::ListenBrainz
scheduleSync(std::chrono::seconds{ 30 });
}
void
ListensSynchronizer::enqueListen(const TimedListen& listen)
void ListensSynchronizer::enqueListen(const TimedListen& listen)
{
assert(listen.listenedAt.isValid());
enqueListen(listen, listen.listenedAt);
}
void
ListensSynchronizer::enqueListenNow(const Scrobbling::Listen& listen)
void ListensSynchronizer::enqueListenNow(const Scrobbling::Listen& listen)
{
enqueListen(listen, {});
}
void
ListensSynchronizer::enqueListen(const Scrobbling::Listen& listen, const Wt::WDateTime& timePoint)
void ListensSynchronizer::enqueListen(const Scrobbling::Listen& listen, const Wt::WDateTime& timePoint)
{
Http::ClientPOSTRequestParameters request;
request.relativeUrl = "/1/submit-listens";
@@ -248,14 +240,14 @@ namespace Scrobbling::ListenBrainz
{
const TimedListen timedListen{ listen, timePoint };
// We want the listen to be sent again later in case of failure, so we just save it as pending send
saveListen(timedListen, Database::ScrobblingState::PendingAdd);
saveListen(timedListen, Database::SyncState::PendingAdd);
request.priority = Http::ClientRequestParameters::Priority::Normal;
request.onSuccessFunc = [=](std::string_view)
{
_strand.dispatch([=]
{
if (saveListen(timedListen, Database::ScrobblingState::Synchronized))
if (saveListen(timedListen, Database::SyncState::Synchronized))
{
UserContext& context{ getUserContext(listen.userId) };
if (context.listenCount)
@@ -292,15 +284,14 @@ namespace Scrobbling::ListenBrainz
_client.sendPOSTRequest(std::move(request));
}
bool
ListensSynchronizer::saveListen(const TimedListen& listen, Database::ScrobblingState scrobblingState)
bool ListensSynchronizer::saveListen(const TimedListen& listen, Database::SyncState scrobblingState)
{
using namespace Database;
Session& session{ _db.getTLSSession() };
auto transaction{ session.createUniqueTransaction() }; // TODO: unique only if needed
Database::Listen::pointer dbListen {Database::Listen::find(session, listen.userId, listen.trackId, Database::Scrobbler::ListenBrainz, listen.listenedAt)};
Database::Listen::pointer dbListen{ Database::Listen::find(session, listen.userId, listen.trackId, Database::ScrobblingBackend::ListenBrainz, listen.listenedAt) };
if (!dbListen)
{
const User::pointer user{ User::find(session, listen.userId) };
@@ -311,23 +302,22 @@ namespace Scrobbling::ListenBrainz
if (!track)
return false;
dbListen = session.create<Database::Listen>(user, track, Database::Scrobbler::ListenBrainz, listen.listenedAt);
dbListen.modify()->setScrobblingState(scrobblingState);
dbListen = session.create<Database::Listen>(user, track, Database::ScrobblingBackend::ListenBrainz, listen.listenedAt);
dbListen.modify()->setSyncState(scrobblingState);
LOG(DEBUG) << "LISTEN CREATED for user " << user->getLoginName() << ", track '" << track->getName() << "' AT " << listen.listenedAt.toString();
return true;
}
if (dbListen->getScrobblingState() == scrobblingState)
if (dbListen->getSyncState() == scrobblingState)
return false;
dbListen.modify()->setScrobblingState(scrobblingState);
dbListen.modify()->setSyncState(scrobblingState);
return true;
}
void
ListensSynchronizer::enquePendingListens()
void ListensSynchronizer::enquePendingListens()
{
std::vector<TimedListen> pendingListens;
@@ -337,8 +327,8 @@ namespace Scrobbling::ListenBrainz
auto transaction{ session.createUniqueTransaction() };
Database::Listen::FindParameters params;
params.setScrobbler(Database::Scrobbler::ListenBrainz)
.setScrobblingState(Database::ScrobblingState::PendingAdd)
params.setScrobblingBackend(Database::ScrobblingBackend::ListenBrainz)
.setSyncState(Database::SyncState::PendingAdd)
.setRange(Database::Range{ 0, 100 }); // don't flood too much?
const Database::RangeResults results{ Database::Listen::find(session, params) };
@@ -363,8 +353,7 @@ namespace Scrobbling::ListenBrainz
enqueListen(pendingListen);
}
ListensSynchronizer::UserContext&
ListensSynchronizer::getUserContext(Database::UserId userId)
ListensSynchronizer::UserContext& ListensSynchronizer::getUserContext(Database::UserId userId)
{
assert(_strand.running_in_this_thread());
@@ -377,8 +366,7 @@ namespace Scrobbling::ListenBrainz
return itContext->second;
}
bool
ListensSynchronizer::isSyncing() const
bool ListensSynchronizer::isSyncing() const
{
return std::any_of(std::cbegin(_userContexts), std::cend(_userContexts), [](const auto& contextEntry)
{
@@ -386,8 +374,7 @@ namespace Scrobbling::ListenBrainz
});
}
void
ListensSynchronizer::scheduleSync(std::chrono::seconds fromNow)
void ListensSynchronizer::scheduleSync(std::chrono::seconds fromNow)
{
if (_syncListensPeriod.count() == 0 || _maxSyncListenCount == 0)
return;
@@ -410,8 +397,7 @@ namespace Scrobbling::ListenBrainz
}));
}
void
ListensSynchronizer::startSync()
void ListensSynchronizer::startSync()
{
LOG(DEBUG) << "Starting sync!";
@@ -423,7 +409,7 @@ namespace Scrobbling::ListenBrainz
{
Database::Session& session{ _db.getTLSSession() };
auto transaction{ session.createSharedTransaction() };
userIds = Database::User::find(_db.getTLSSession(), Database::User::FindParameters{}.setScrobbler(Database::Scrobbler::ListenBrainz));
userIds = Database::User::find(_db.getTLSSession(), Database::User::FindParameters{}.setScrobblingBackend(Database::ScrobblingBackend::ListenBrainz));
}
for (const Database::UserId userId : userIds.results)
@@ -433,8 +419,7 @@ namespace Scrobbling::ListenBrainz
scheduleSync(_syncListensPeriod);
}
void
ListensSynchronizer::startSync(UserContext& context)
void ListensSynchronizer::startSync(UserContext& context)
{
context.syncing = true;
context.listenBrainzUserName = "";
@@ -446,8 +431,7 @@ namespace Scrobbling::ListenBrainz
enqueValidateToken(context);
}
void
ListensSynchronizer::onSyncEnded(UserContext& context)
void ListensSynchronizer::onSyncEnded(UserContext& context)
{
_strand.dispatch([this, &context]
{
@@ -459,8 +443,7 @@ namespace Scrobbling::ListenBrainz
});
}
void
ListensSynchronizer::enqueValidateToken(UserContext& context)
void ListensSynchronizer::enqueValidateToken(UserContext& context)
{
assert(context.listenBrainzUserName.empty());
@@ -493,8 +476,7 @@ namespace Scrobbling::ListenBrainz
_client.sendGETRequest(std::move(request));
}
void
ListensSynchronizer::enqueGetListenCount(UserContext& context)
void ListensSynchronizer::enqueGetListenCount(UserContext& context)
{
assert(!context.listenBrainzUserName.empty());
@@ -530,8 +512,7 @@ namespace Scrobbling::ListenBrainz
_client.sendGETRequest(std::move(request));
}
void
ListensSynchronizer::enqueGetListens(UserContext& context)
void ListensSynchronizer::enqueGetListens(UserContext& context)
{
assert(!context.listenBrainzUserName.empty());
@@ -557,8 +538,7 @@ namespace Scrobbling::ListenBrainz
_client.sendGETRequest(std::move(request));
}
void
ListensSynchronizer::processGetListensResponse(std::string_view msgBody, UserContext& context)
void ListensSynchronizer::processGetListensResponse(std::string_view msgBody, UserContext& context)
{
Database::Session& session{ _db.getTLSSession() };
@@ -583,7 +563,7 @@ namespace Scrobbling::ListenBrainz
context.matchedListenCount++;
const Scrobbling::TimedListen listen{ {context.userId, trackId}, parsedListen.listenedAt };
if (saveListen(listen, Database::ScrobblingState::Synchronized))
if (saveListen(listen, Database::SyncState::Synchronized))
context.importedListenCount++;
}
}
@@ -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);
+1
View File
@@ -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 };
};
}
+15 -1
View File
@@ -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 }
{
}
@@ -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