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>
+22 -22
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>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="artists-tab" data-bs-toggle="tab" data-bs-target="#artists" type="button" role="tab" aria-controls="artists" aria-selected="false">${tr:Lms.Explore.artists}</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="tracks-tab" data-bs-toggle="tab" data-bs-target="#tracks" type="button" role="tab" aria-controls="tracks" aria-selected="false">${tr:Lms.Explore.tracks}</button>
</li>
</ul>
<div class="tab-content" id="myTabContent">
<div class="tab-pane show active" id="releases" role="tabpanel" aria-labelledby="releases-tab">${releases}</div>
<div class="tab-pane" id="artists" role="tabpanel" aria-labelledby="artists-tab">
<div class="row mb-3">
<div class="col-lg-3">
${link-type class="form-select"}
</div>
</div>
${artists}
</div>
<div class="tab-pane" id="tracks" role="tabpanel" aria-labelledby="tracks-tab">${tracks}</div>
<div class="mb-3">
<ul class="nav nav-pills mb-3">
<li class="nav-item">
${releases class="nav-link"}
</li>
<li class="nav-item">
${artists class="nav-link"}
</li>
<li class="nav-item">
${tracks class="nav-link"}
</li>
</ul>
</div>
${results}
</message>
<message id="Lms.Explore.Search.template.artists">
<div class="row mb-3">
<div class="col-lg-3">
${link-type class="form-select"}
</div>
</div>
${artists}
</message>
</messages>
+17 -8
View File
@@ -28,9 +28,9 @@
</div>
</div>
<div class="col-lg-4">
<label class="form-label" for="${id:transcode-bitrate}">
<label class="form-label" for="${id:transcode-bitrate}">
${tr:Lms.Settings.transcode-bitrate}
; </label>
</label>
<div class="input-group mb-3">
${transcode-bitrate class="form-control"}
<span class="input-group-text">kbps</span>
@@ -105,19 +105,28 @@
</div>
</div>
${</if-has-subsonic-api>}
<legend>${tr:Lms.Settings.scrobbling}</legend>
<legend>${tr:Lms.Settings.services}</legend>
<div class="col-lg-6">
<label class="form-label" for="${id:scrobbler}">
${tr:Lms.Settings.scrobbling.scrobbler}
<label class="form-label" for="${id:scrobbling-backend}">
${tr:Lms.Settings.scrobbling}
</label>
${scrobbler class="form-control"}
${scrobbling-backend class="form-control"}
<div class="invalid-feedback">
${scrobbler-info class="help-block"}
${scrobbling-backend-info class="help-block"}
</div>
</div>
<div class="col-lg-6">
<label class="form-label" for="${id:feedback-backend}">
${tr:Lms.Settings.feedback}
</label>
${feedback-backend class="form-control"}
<div class="invalid-feedback">
${feedback-backend-info class="help-block"}
</div>
</div>
<div class="col-lg-12">
<label class="form-label" for="${id:listenbrainz-token}">
${tr:Lms.Settings.scrobbling.listenbrainz-token}
${tr:Lms.Settings.backend.listenbrainz-token}
</label>
${listenbrainz-token class="form-control"}
<div class="invalid-feedback">
+4 -1
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)
+396 -416
View File
@@ -34,445 +34,425 @@
#include "utils/String.hpp"
#include "utils/Utils.hpp"
namespace
namespace Cover
{
struct TrackInfo
{
bool hasCover {};
bool isMultiDisc {};
std::filesystem::path trackPath;
std::optional<Database::ReleaseId> releaseId;
};
std::optional<TrackInfo>
getTrackInfo(Database::Session& dbSession, Database::TrackId trackId)
{
std::optional<TrackInfo> res;
namespace
{
struct TrackInfo
{
bool hasCover{};
bool isMultiDisc{};
std::filesystem::path trackPath;
std::optional<Database::ReleaseId> releaseId;
};
auto transaction {dbSession.createSharedTransaction()};
std::optional<TrackInfo> getTrackInfo(Database::Session& dbSession, Database::TrackId trackId)
{
std::optional<TrackInfo> res;
const Database::Track::pointer track {Database::Track::find(dbSession, trackId)};
if (!track)
return res;
auto transaction{ dbSession.createSharedTransaction() };
res = TrackInfo {};
const Database::Track::pointer track{ Database::Track::find(dbSession, trackId) };
if (!track)
return res;
res->hasCover = track->hasCover();
res->trackPath = track->getPath();
res = TrackInfo{};
if (const Database::Release::pointer& release {track->getRelease()})
{
res->releaseId = release->getId();
if (release->getTotalDisc() > 1)
res->isMultiDisc = true;
}
res->hasCover = track->hasCover();
res->trackPath = track->getPath();
return res;
}
if (const Database::Release::pointer & release{ track->getRelease() })
{
res->releaseId = release->getId();
if (release->getTotalDisc() > 1)
res->isMultiDisc = true;
}
std::vector<std::string> constructPreferredFileNames()
{
std::vector<std::string> res;
return res;
}
Service<IConfig>::get()->visitStrings("cover-preferred-file-names",
[&res](std::string_view fileName)
{
res.emplace_back(fileName);
}, {"cover", "front"});
std::vector<std::string> constructPreferredFileNames()
{
std::vector<std::string> res;
return res;
}
}
Service<IConfig>::get()->visitStrings("cover-preferred-file-names",
[&res](std::string_view fileName)
{
res.emplace_back(fileName);
}, { "cover", "front" });
namespace Cover {
return res;
}
using namespace Image;
bool isFileSupported(const std::filesystem::path& file, const std::vector<std::filesystem::path>& extensions)
{
return (std::find(std::cbegin(extensions), std::cend(extensions), file.extension()) != std::cend(extensions));
}
}
static
bool
isFileSupported(const std::filesystem::path& file, const std::vector<std::filesystem::path>& extensions)
{
return (std::find(std::cbegin(extensions), std::cend(extensions), file.extension()) != std::cend(extensions));
}
std::unique_ptr<ICoverService> createCoverService(Database::Db& db, const std::filesystem::path& execPath, const std::filesystem::path& defaultCoverPath)
{
return std::make_unique<CoverService>(db, execPath, defaultCoverPath);
}
std::unique_ptr<ICoverService>
createCoverService(Database::Db& db, const std::filesystem::path& execPath, const std::filesystem::path& defaultCoverPath)
{
return std::make_unique<CoverService>(db, execPath, defaultCoverPath);
}
using namespace Image;
CoverService::CoverService(Database::Db& db,
const std::filesystem::path& execPath,
const std::filesystem::path& defaultCoverPath)
: _db {db}
, _defaultCoverPath {defaultCoverPath}
, _maxCacheSize {Service<IConfig>::get()->getULong("cover-max-cache-size", 30) * 1000 * 1000}
, _maxFileSize {Service<IConfig>::get()->getULong("cover-max-file-size", 10) * 1000 * 1000}
, _preferredFileNames {constructPreferredFileNames()}
CoverService::CoverService(Database::Db& db,
const std::filesystem::path& execPath,
const std::filesystem::path& defaultCoverPath)
: _db{ db }
, _defaultCoverPath{ defaultCoverPath }
, _maxCacheSize{ Service<IConfig>::get()->getULong("cover-max-cache-size", 30) * 1000 * 1000 }
, _maxFileSize{ Service<IConfig>::get()->getULong("cover-max-file-size", 10) * 1000 * 1000 }
, _preferredFileNames{ constructPreferredFileNames() }
{
setJpegQuality(Service<IConfig>::get()->getULong("cover-jpeg-quality", 75));
{
setJpegQuality(Service<IConfig>::get()->getULong("cover-jpeg-quality", 75));
LMS_LOG(COVER, INFO) << "Default cover path = '" << _defaultCoverPath.string() << "'";
LMS_LOG(COVER, INFO) << "Max cache size = " << _maxCacheSize;
LMS_LOG(COVER, INFO) << "Max file size = " << _maxFileSize;
LMS_LOG(COVER, INFO) << "Preferred file names: " << StringUtils::joinStrings(_preferredFileNames, ",");
LMS_LOG(COVER, INFO) << "Default cover path = '" << _defaultCoverPath.string() << "'";
LMS_LOG(COVER, INFO) << "Max cache size = " << _maxCacheSize;
LMS_LOG(COVER, INFO) << "Max file size = " << _maxFileSize;
LMS_LOG(COVER, INFO) << "Preferred file names: " << StringUtils::joinStrings(_preferredFileNames, ",");
#if LMS_SUPPORT_IMAGE_GM
GraphicsMagick::init(execPath);
GraphicsMagick::init(execPath);
#else
(void)execPath;
(void)execPath;
#endif
try
{
getDefault(512);
}
catch (const Image::ImageException& e)
{
throw LmsException("Cannot read default cover file '" + _defaultCoverPath.string() + "': " + e.what());
}
}
std::unique_ptr<IEncodedImage>
CoverService::getFromAvMediaFile(const Av::IAudioFile& input, ImageSize width) const
{
std::unique_ptr<IEncodedImage> image;
input.visitAttachedPictures([&](const Av::Picture& picture)
{
if (image)
return;
try
{
std::unique_ptr<IRawImage> rawImage {decodeImage(picture.data, picture.dataSize)};
rawImage->resize(width);
image = rawImage->encodeToJPEG(_jpegQuality);
}
catch (const Image::ImageException& e)
{
LMS_LOG(COVER, ERROR) << "Cannot read embedded cover: " << e.what();
}
});
return image;
}
std::unique_ptr<IEncodedImage>
CoverService::getFromCoverFile(const std::filesystem::path& p, ImageSize width) const
{
std::unique_ptr<IEncodedImage> image;
try
{
std::unique_ptr<IRawImage> rawImage {decodeImage(p)};
rawImage->resize(width);
image = rawImage->encodeToJPEG(_jpegQuality);
}
catch (const ImageException& e)
{
LMS_LOG(COVER, ERROR) << "Cannot read cover in file '" << p.string() << "': " << e.what();
}
return image;
}
std::shared_ptr<IEncodedImage>
CoverService::getDefault(ImageSize width)
{
{
std::shared_lock lock {_cacheMutex};
if (auto it {_defaultCoverCache.find(width)}; it != std::cend(_defaultCoverCache))
return it->second;
}
{
std::unique_lock lock {_cacheMutex};
if (auto it {_defaultCoverCache.find(width)}; it != std::cend(_defaultCoverCache))
return it->second;
std::shared_ptr<IEncodedImage> image {getFromCoverFile(_defaultCoverPath, width)};
_defaultCoverCache[width] = image;
LMS_LOG(COVER, DEBUG) << "Default cache entries = " << _defaultCoverCache.size();
return image;
}
}
std::unique_ptr<IEncodedImage>
CoverService::getFromDirectory(const std::filesystem::path& directory, ImageSize width) const
{
const std::multimap<std::string, std::filesystem::path> coverPaths {getCoverPaths(directory)};
auto tryLoadImageFromFilename = [&](std::string_view fileName)
{
std::unique_ptr<IEncodedImage> image;
auto range {coverPaths.equal_range(std::string {fileName})};
for (auto it {range.first}; it != range.second; ++it)
{
image = getFromCoverFile(it->second, width);
if (image)
break;
}
return image;
};
std::unique_ptr<IEncodedImage> image;
for (std::string_view filename : _preferredFileNames)
{
image = tryLoadImageFromFilename(filename);
if (image)
return image;
}
// Just pick one
for (const auto& [filename, coverPath] : coverPaths)
{
image = getFromCoverFile(coverPath, width);
if (image)
return image;
}
return image;
}
std::unique_ptr<IEncodedImage>
CoverService::getFromSameNamedFile(const std::filesystem::path& filePath, ImageSize width) const
{
std::unique_ptr<IEncodedImage> res;
std::filesystem::path coverPath {filePath};
for (const std::filesystem::path& extension : _fileExtensions)
{
coverPath.replace_extension(extension);
if (!checkCoverFile(coverPath))
continue;
res = getFromCoverFile(coverPath, width);
if (res)
break;
}
return res;
}
bool
CoverService::checkCoverFile(const std::filesystem::path& filePath) const
{
std::error_code ec;
if (!isFileSupported(filePath, _fileExtensions))
return false;
if (!std::filesystem::exists(filePath, ec))
return false;
if (!std::filesystem::is_regular_file(filePath, ec))
return false;
if (std::filesystem::file_size(filePath, ec) > _maxFileSize && !ec)
{
LMS_LOG(COVER, INFO) << "Cover file '" << filePath.string() << " is too big (" << std::filesystem::file_size(filePath, ec) << "), limit is " << _maxFileSize;
return false;
}
return true;
}
std::multimap<std::string, std::filesystem::path>
CoverService::getCoverPaths(const std::filesystem::path& directoryPath) const
{
std::multimap<std::string, std::filesystem::path> res;
std::error_code ec;
std::filesystem::directory_iterator itPath(directoryPath, ec);
std::filesystem::directory_iterator itEnd;
while (!ec && itPath != itEnd)
{
const std::filesystem::path& path {*itPath};
if (checkCoverFile(path))
res.emplace(std::filesystem::path{ path }.filename().replace_extension("").string(), path);
itPath.increment(ec);
}
return res;
}
std::unique_ptr<IEncodedImage>
CoverService::getFromTrack(const std::filesystem::path& p, ImageSize width) const
{
std::unique_ptr<IEncodedImage> image;
try
{
image = getFromAvMediaFile(*Av::parseAudioFile(p), width);
}
catch (Av::Exception& e)
{
LMS_LOG(COVER, ERROR) << "Cannot get covers from track " << p.string() << ": " << e.what();
}
return image;
}
std::shared_ptr<IEncodedImage>
CoverService::getFromTrack(Database::TrackId trackId, ImageSize width)
{
return getFromTrack(_db.getTLSSession(), trackId, width, true /* allow release fallback*/);
}
std::shared_ptr<IEncodedImage>
CoverService::getFromTrack(Database::Session& dbSession, Database::TrackId trackId, ImageSize width, bool allowReleaseFallback)
{
using namespace Database;
const CacheEntryDesc cacheEntryDesc {trackId, width};
std::shared_ptr<IEncodedImage> cover {loadFromCache(cacheEntryDesc)};
if (cover)
return cover;
if (const std::optional<TrackInfo> trackInfo {getTrackInfo(dbSession, trackId)})
{
if (trackInfo->hasCover)
cover = getFromTrack(trackInfo->trackPath, width);
if (!cover)
cover = getFromSameNamedFile(trackInfo->trackPath, width);
if (!cover && trackInfo->releaseId && allowReleaseFallback)
cover = getFromRelease(*trackInfo->releaseId, width);
if (!cover && trackInfo->isMultiDisc)
{
if (trackInfo->trackPath.parent_path().has_parent_path())
cover = getFromDirectory(trackInfo->trackPath.parent_path().parent_path(), width);
}
}
if (!cover)
cover = getDefault(width);
if (cover)
saveToCache(cacheEntryDesc, cover);
return cover;
}
std::shared_ptr<IEncodedImage>
CoverService::getFromRelease(Database::ReleaseId releaseId, ImageSize width)
{
using namespace Database;
const CacheEntryDesc cacheEntryDesc {releaseId, width};
std::shared_ptr<IEncodedImage> cover {loadFromCache(cacheEntryDesc)};
if (cover)
return cover;
struct ReleaseInfo
{
TrackId firstTrackId;
std::filesystem::path releaseDirectory;
};
Session& session {_db.getTLSSession()};
auto getReleaseInfo {[&]
{
std::optional<ReleaseInfo> res;
auto transaction {session.createSharedTransaction()};
const auto tracks {Track::find(session, Track::FindParameters {}.setRelease(releaseId).setRange({0, 1}).setSortMethod(TrackSortMethod::Release))};
if (!tracks.results.empty())
{
if (const Track::pointer track {Track::find(session, tracks.results.front())})
{
res = ReleaseInfo {};
res->firstTrackId = track->getId();
res->releaseDirectory = track->getPath().parent_path();
}
}
return res;
}};
if (const std::optional<ReleaseInfo> releaseInfo {getReleaseInfo()})
{
cover = getFromDirectory(releaseInfo->releaseDirectory, width);
if (!cover)
cover = getFromTrack(session, releaseInfo->firstTrackId, width, false /* no release fallback */);
}
if (!cover)
cover = getDefault(width);
if (cover)
saveToCache(cacheEntryDesc, cover);
return cover;
}
void
CoverService::flushCache()
{
std::unique_lock lock {_cacheMutex};
LMS_LOG(COVER, DEBUG) << "Cache stats: hits = " << _cacheHits << ", misses = " << _cacheMisses << ", nb entries = " << _cache.size() << ", size = " << _cacheSize;
_cacheHits = 0;
_cacheMisses = 0;
_cacheSize = 0;
_cache.clear();
}
void
CoverService::setJpegQuality(unsigned quality)
{
_jpegQuality = Utils::clamp<unsigned>(quality, 1, 100);
LMS_LOG(COVER, INFO) << "JPEG export quality = " << _jpegQuality;
}
void
CoverService::saveToCache(const CacheEntryDesc& entryDesc, std::shared_ptr<IEncodedImage> image)
{
std::unique_lock lock {_cacheMutex};
while (_cacheSize + image->getDataSize() > _maxCacheSize && !_cache.empty())
{
auto itRandom {Random::pickRandom(_cache)};
_cacheSize -= itRandom->second->getDataSize();
_cache.erase(itRandom);
}
_cacheSize += image->getDataSize();
_cache[entryDesc] = image;
}
std::shared_ptr<IEncodedImage>
CoverService::loadFromCache(const CacheEntryDesc& entryDesc)
{
std::shared_lock lock {_cacheMutex};
auto it {_cache.find(entryDesc)};
if (it == std::cend(_cache))
{
++_cacheMisses;
return nullptr;
}
++_cacheHits;
return it->second;
}
try
{
getDefault(512);
}
catch (const Image::ImageException& e)
{
throw LmsException("Cannot read default cover file '" + _defaultCoverPath.string() + "': " + e.what());
}
}
std::unique_ptr<IEncodedImage> CoverService::getFromAvMediaFile(const Av::IAudioFile& input, ImageSize width) const
{
std::unique_ptr<IEncodedImage> image;
input.visitAttachedPictures([&](const Av::Picture& picture)
{
if (image)
return;
try
{
std::unique_ptr<IRawImage> rawImage{ decodeImage(picture.data, picture.dataSize) };
rawImage->resize(width);
image = rawImage->encodeToJPEG(_jpegQuality);
}
catch (const Image::ImageException& e)
{
LMS_LOG(COVER, ERROR) << "Cannot read embedded cover: " << e.what();
}
});
return image;
}
std::unique_ptr<IEncodedImage> CoverService::getFromCoverFile(const std::filesystem::path& p, ImageSize width) const
{
std::unique_ptr<IEncodedImage> image;
try
{
std::unique_ptr<IRawImage> rawImage{ decodeImage(p) };
rawImage->resize(width);
image = rawImage->encodeToJPEG(_jpegQuality);
}
catch (const ImageException& e)
{
LMS_LOG(COVER, ERROR) << "Cannot read cover in file '" << p.string() << "': " << e.what();
}
return image;
}
std::shared_ptr<IEncodedImage> CoverService::getDefault(ImageSize width)
{
{
std::shared_lock lock{ _cacheMutex };
if (auto it{ _defaultCoverCache.find(width) }; it != std::cend(_defaultCoverCache))
return it->second;
}
{
std::unique_lock lock{ _cacheMutex };
if (auto it{ _defaultCoverCache.find(width) }; it != std::cend(_defaultCoverCache))
return it->second;
std::shared_ptr<IEncodedImage> image{ getFromCoverFile(_defaultCoverPath, width) };
_defaultCoverCache[width] = image;
LMS_LOG(COVER, DEBUG) << "Default cache entries = " << _defaultCoverCache.size();
return image;
}
}
std::unique_ptr<IEncodedImage> CoverService::getFromDirectory(const std::filesystem::path& directory, ImageSize width) const
{
const std::multimap<std::string, std::filesystem::path> coverPaths{ getCoverPaths(directory) };
auto tryLoadImageFromFilename = [&](std::string_view fileName)
{
std::unique_ptr<IEncodedImage> image;
auto range{ coverPaths.equal_range(std::string {fileName}) };
for (auto it{ range.first }; it != range.second; ++it)
{
image = getFromCoverFile(it->second, width);
if (image)
break;
}
return image;
};
std::unique_ptr<IEncodedImage> image;
for (std::string_view filename : _preferredFileNames)
{
image = tryLoadImageFromFilename(filename);
if (image)
return image;
}
// Just pick one
for (const auto& [filename, coverPath] : coverPaths)
{
image = getFromCoverFile(coverPath, width);
if (image)
return image;
}
return image;
}
std::unique_ptr<IEncodedImage> CoverService::getFromSameNamedFile(const std::filesystem::path& filePath, ImageSize width) const
{
std::unique_ptr<IEncodedImage> res;
std::filesystem::path coverPath{ filePath };
for (const std::filesystem::path& extension : _fileExtensions)
{
coverPath.replace_extension(extension);
if (!checkCoverFile(coverPath))
continue;
res = getFromCoverFile(coverPath, width);
if (res)
break;
}
return res;
}
bool CoverService::checkCoverFile(const std::filesystem::path& filePath) const
{
std::error_code ec;
if (!isFileSupported(filePath, _fileExtensions))
return false;
if (!std::filesystem::exists(filePath, ec))
return false;
if (!std::filesystem::is_regular_file(filePath, ec))
return false;
if (std::filesystem::file_size(filePath, ec) > _maxFileSize && !ec)
{
LMS_LOG(COVER, INFO) << "Cover file '" << filePath.string() << " is too big (" << std::filesystem::file_size(filePath, ec) << "), limit is " << _maxFileSize;
return false;
}
return true;
}
std::multimap<std::string, std::filesystem::path> CoverService::getCoverPaths(const std::filesystem::path& directoryPath) const
{
std::multimap<std::string, std::filesystem::path> res;
std::error_code ec;
std::filesystem::directory_iterator itPath(directoryPath, ec);
std::filesystem::directory_iterator itEnd;
while (!ec && itPath != itEnd)
{
const std::filesystem::path& path{ *itPath };
if (checkCoverFile(path))
res.emplace(std::filesystem::path{ path }.filename().replace_extension("").string(), path);
itPath.increment(ec);
}
return res;
}
std::unique_ptr<IEncodedImage> CoverService::getFromTrack(const std::filesystem::path& p, ImageSize width) const
{
std::unique_ptr<IEncodedImage> image;
try
{
image = getFromAvMediaFile(*Av::parseAudioFile(p), width);
}
catch (Av::Exception& e)
{
LMS_LOG(COVER, ERROR) << "Cannot get covers from track " << p.string() << ": " << e.what();
}
return image;
}
std::shared_ptr<IEncodedImage> CoverService::getFromTrack(Database::TrackId trackId, ImageSize width)
{
return getFromTrack(_db.getTLSSession(), trackId, width, true /* allow release fallback*/);
}
std::shared_ptr<IEncodedImage> CoverService::getFromTrack(Database::Session& dbSession, Database::TrackId trackId, ImageSize width, bool allowReleaseFallback)
{
using namespace Database;
const CacheEntryDesc cacheEntryDesc{ trackId, width };
std::shared_ptr<IEncodedImage> cover{ loadFromCache(cacheEntryDesc) };
if (cover)
return cover;
if (const std::optional<TrackInfo> trackInfo{ getTrackInfo(dbSession, trackId) })
{
if (trackInfo->hasCover)
cover = getFromTrack(trackInfo->trackPath, width);
if (!cover)
cover = getFromSameNamedFile(trackInfo->trackPath, width);
if (!cover && trackInfo->releaseId && allowReleaseFallback)
cover = getFromRelease(*trackInfo->releaseId, width);
if (!cover && trackInfo->isMultiDisc)
{
if (trackInfo->trackPath.parent_path().has_parent_path())
cover = getFromDirectory(trackInfo->trackPath.parent_path().parent_path(), width);
}
}
if (!cover)
cover = getDefault(width);
if (cover)
saveToCache(cacheEntryDesc, cover);
return cover;
}
std::shared_ptr<IEncodedImage> CoverService::getFromRelease(Database::ReleaseId releaseId, ImageSize width)
{
using namespace Database;
const CacheEntryDesc cacheEntryDesc{ releaseId, width };
std::shared_ptr<IEncodedImage> cover{ loadFromCache(cacheEntryDesc) };
if (cover)
return cover;
struct ReleaseInfo
{
TrackId firstTrackId;
std::filesystem::path releaseDirectory;
};
Session& session{ _db.getTLSSession() };
auto getReleaseInfo{ [&]
{
std::optional<ReleaseInfo> res;
auto transaction{ session.createSharedTransaction() };
const auto tracks{ Track::find(session, Track::FindParameters {}.setRelease(releaseId).setRange({0, 1}).setSortMethod(TrackSortMethod::Release)) };
if (!tracks.results.empty())
{
const Track::pointer& track{ tracks.results.front() };
res = ReleaseInfo{};
res->firstTrackId = track->getId();
res->releaseDirectory = track->getPath().parent_path();
}
return res;
} };
if (const std::optional<ReleaseInfo> releaseInfo{ getReleaseInfo() })
{
cover = getFromDirectory(releaseInfo->releaseDirectory, width);
if (!cover)
cover = getFromTrack(session, releaseInfo->firstTrackId, width, false /* no release fallback */);
}
if (!cover)
cover = getDefault(width);
if (cover)
saveToCache(cacheEntryDesc, cover);
return cover;
}
void CoverService::flushCache()
{
std::unique_lock lock{ _cacheMutex };
LMS_LOG(COVER, DEBUG) << "Cache stats: hits = " << _cacheHits << ", misses = " << _cacheMisses << ", nb entries = " << _cache.size() << ", size = " << _cacheSize;
_cacheHits = 0;
_cacheMisses = 0;
_cacheSize = 0;
_cache.clear();
}
void CoverService::setJpegQuality(unsigned quality)
{
_jpegQuality = Utils::clamp<unsigned>(quality, 1, 100);
LMS_LOG(COVER, INFO) << "JPEG export quality = " << _jpegQuality;
}
void CoverService::saveToCache(const CacheEntryDesc& entryDesc, std::shared_ptr<IEncodedImage> image)
{
std::unique_lock lock{ _cacheMutex };
while (_cacheSize + image->getDataSize() > _maxCacheSize && !_cache.empty())
{
auto itRandom{ Random::pickRandom(_cache) };
_cacheSize -= itRandom->second->getDataSize();
_cache.erase(itRandom);
}
_cacheSize += image->getDataSize();
_cache[entryDesc] = image;
}
std::shared_ptr<IEncodedImage> CoverService::loadFromCache(const CacheEntryDesc& entryDesc)
{
std::shared_lock lock{ _cacheMutex };
auto it{ _cache.find(entryDesc) };
if (it == std::cend(_cache))
{
++_cacheMisses;
return nullptr;
}
++_cacheHits;
return it->second;
}
} // namespace Cover
+243 -223
View File
@@ -33,278 +33,298 @@
namespace Database
{
namespace
{
template <typename ResultType>
Wt::Dbo::Query<ResultType> createQuery(Session& session, std::string_view itemToSelect, const Artist::FindParameters& params)
{
session.checkSharedLocked();
Artist::Artist(const std::string& name, const std::optional<UUID>& MBID)
: _name {std::string(name, 0 , _maxNameLength)},
_sortName {_name},
_MBID {MBID ? MBID->getAsString() : ""}
{
}
auto query{ session.getDboSession().query<ResultType>("SELECT DISTINCT " + std::string{ itemToSelect } + " FROM artist a") };
if (params.sortMethod == ArtistSortMethod::LastWritten
|| params.writtenAfter.isValid()
|| params.linkType
|| params.track.isValid()
|| params.release.isValid()
|| params.clusters.size() == 1)
{
query.join("track t ON t.id = t_a_l.track_id");
query.join("track_artist_link t_a_l ON t_a_l.artist_id = a.id");
}
Artist::pointer
Artist::create(Session& session, const std::string& name, const std::optional<UUID>& MBID)
{
return session.getDboSession().add(std::unique_ptr<Artist> {new Artist {name, MBID}});
}
if (params.linkType)
query.where("t_a_l.type = ?").bind(*params.linkType);
std::size_t
Artist::getCount(Session& session)
{
session.checkSharedLocked();
if (params.writtenAfter.isValid())
query.where("t.file_last_write > ?").bind(params.writtenAfter);
return session.getDboSession().query<int>("SELECT COUNT(*) FROM artist");
}
if (!params.keywords.empty())
{
std::vector<std::string> clauses;
std::vector<std::string> sortClauses;
std::vector<Artist::pointer>
Artist::find(Session& session, const std::string& name)
{
session.checkSharedLocked();
for (std::string_view keyword : params.keywords)
{
clauses.push_back("a.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'");
query.bind("%" + Utils::escapeLikeKeyword(keyword) + "%");
}
Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> res = session.getDboSession().find<Artist>()
.where("name = ?").bind(std::string {name, 0, _maxNameLength})
.orderBy("LENGTH(mbid) DESC"); // put mbid entries first
for (std::string_view keyword : params.keywords)
{
sortClauses.push_back("a.sort_name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'");
query.bind("%" + Utils::escapeLikeKeyword(keyword) + "%");
}
return std::vector<Artist::pointer>(res.begin(), res.end());
}
query.where("(" + StringUtils::joinStrings(clauses, " AND ") + ") OR (" + StringUtils::joinStrings(sortClauses, " AND ") + ")");
}
Artist::pointer
Artist::find(Session& session, const UUID& mbid)
{
session.checkSharedLocked();
return session.getDboSession().find<Artist>().where("mbid = ?").bind(std::string {mbid.getAsString()}).resultValue();
}
if (params.starringUser.isValid())
{
assert(params.feedbackBackend);
query.join("starred_artist s_a ON s_a.artist_id = a.id")
.where("s_a.user_id = ?").bind(params.starringUser)
.where("s_a.backend = ?").bind(*params.feedbackBackend)
.where("s_a.sync_state <> ?").bind(SyncState::PendingRemove);
}
Artist::pointer
Artist::find(Session& session, ArtistId id)
{
session.checkSharedLocked();
return session.getDboSession().find<Artist>().where("id = ?").bind(id).resultValue();
}
if (params.clusters.size() == 1)
{
query.join("track_cluster t_c ON t_c.track_id = t.id")
.where("t_c.cluster_id = ?").bind(params.clusters.front());
}
else if (params.clusters.size() > 1)
{
std::ostringstream oss;
oss << "a.id IN (SELECT DISTINCT a.id FROM artist a"
" INNER JOIN track t ON t.id = t_a_l.track_id"
" INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id"
" INNER JOIN cluster c ON c.id = t_c.cluster_id"
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
bool
Artist::exists(Session& session, ArtistId id)
{
session.checkSharedLocked();
return session.getDboSession().query<int>("SELECT 1 FROM artist").where("id = ?").bind(id).resultValue() == 1;
}
WhereClause clusterClause;
for (const ClusterId clusterId : params.clusters)
{
clusterClause.Or(WhereClause("c.id = ?"));
query.bind(clusterId);
}
static
Wt::Dbo::Query<ArtistId>
createQuery(Session& session, const Artist::FindParameters& params)
{
session.checkSharedLocked();
oss << " " << clusterClause.get();
oss << " GROUP BY t.id,a.id HAVING COUNT(DISTINCT c.id) = " << params.clusters.size() << ")";
auto query {session.getDboSession().query<ArtistId>("SELECT DISTINCT a.id FROM artist a")};
if (params.sortMethod == ArtistSortMethod::LastWritten
|| params.writtenAfter.isValid()
|| params.linkType
|| params.track.isValid()
|| params.release.isValid())
{
query.join("track t ON t.id = t_a_l.track_id");
query.join("track_artist_link t_a_l ON t_a_l.artist_id = a.id");
}
query.where(oss.str());
}
if (params.linkType)
query.where("t_a_l.type = ?").bind(*params.linkType);
if (params.track.isValid())
query.where("t.id = ?").bind(params.track);
if (params.writtenAfter.isValid())
query.where("t.file_last_write > ?").bind(params.writtenAfter);
if (params.release.isValid())
query.where("t.release_id = ?").bind(params.release);
if (!params.keywords.empty())
{
std::vector<std::string> clauses;
std::vector<std::string> sortClauses;
switch (params.sortMethod)
{
case ArtistSortMethod::None:
break;
case ArtistSortMethod::ByName:
query.orderBy("a.name COLLATE NOCASE");
break;
case ArtistSortMethod::BySortName:
query.orderBy("a.sort_name COLLATE NOCASE");
break;
case ArtistSortMethod::Random:
query.orderBy("RANDOM()");
break;
case ArtistSortMethod::LastWritten:
query.orderBy("t.file_last_write DESC");
break;
case ArtistSortMethod::StarredDateDesc:
assert(params.starringUser.isValid());
query.orderBy("s_a.date_time DESC");
break;
}
for (std::string_view keyword : params.keywords)
{
clauses.push_back("a.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'");
query.bind("%" + Utils::escapeLikeKeyword(keyword) + "%");
}
return query;
}
for (std::string_view keyword : params.keywords)
{
sortClauses.push_back("a.sort_name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'");
query.bind("%" + Utils::escapeLikeKeyword(keyword) + "%");
}
template <typename ResultType>
Wt::Dbo::Query<ResultType> createQuery(Session& session, const Artist::FindParameters& params)
{
std::string_view itemToSelect;
if constexpr (std::is_same_v<ResultType, ArtistId>)
itemToSelect = "a.id";
else if constexpr (std::is_same_v<ResultType, Wt::Dbo::ptr<Artist>>)
itemToSelect = "a";
else
static_assert("Unhandled type");
query.where("(" + StringUtils::joinStrings(clauses, " AND ") + ") OR (" + StringUtils::joinStrings(sortClauses, " AND ") + ")");
}
return createQuery<ResultType>(session, itemToSelect, params);
}
}
if (params.starringUser.isValid())
{
assert(params.scrobbler);
query.join("starred_artist s_a ON s_a.artist_id = a.id")
.where("s_a.user_id = ?").bind(params.starringUser)
.where("s_a.scrobbler = ?").bind(*params.scrobbler)
.where("s_a.scrobbling_state <> ?").bind(ScrobblingState::PendingRemove);
}
Artist::Artist(const std::string& name, const std::optional<UUID>& MBID)
: _name{ std::string(name, 0 , _maxNameLength) },
_sortName{ _name },
_MBID{ MBID ? MBID->getAsString() : "" }
{
}
if (!params.clusters.empty())
{
std::ostringstream oss;
oss << "a.id IN (SELECT DISTINCT a.id FROM artist a"
" INNER JOIN track t ON t.id = t_a_l.track_id"
" INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id"
" INNER JOIN cluster c ON c.id = t_c.cluster_id"
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
Artist::pointer Artist::create(Session& session, const std::string& name, const std::optional<UUID>& MBID)
{
return session.getDboSession().add(std::unique_ptr<Artist> {new Artist{ name, MBID }});
}
WhereClause clusterClause;
for (const ClusterId clusterId : params.clusters)
{
clusterClause.Or(WhereClause("c.id = ?"));
query.bind(clusterId);
}
std::size_t Artist::getCount(Session& session)
{
session.checkSharedLocked();
oss << " " << clusterClause.get();
oss << " GROUP BY t.id,a.id HAVING COUNT(DISTINCT c.id) = " << params.clusters.size() << ")";
return session.getDboSession().query<int>("SELECT COUNT(*) FROM artist");
}
query.where(oss.str());
}
std::vector<Artist::pointer> Artist::find(Session& session, const std::string& name)
{
session.checkSharedLocked();
if (params.track.isValid())
query.where("t.id = ?").bind(params.track);
Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> res = session.getDboSession().find<Artist>()
.where("name = ?").bind(std::string{ name, 0, _maxNameLength })
.orderBy("LENGTH(mbid) DESC"); // put mbid entries first
if (params.release.isValid())
query.where("t.release_id = ?").bind(params.release);
return std::vector<Artist::pointer>(res.begin(), res.end());
}
switch (params.sortMethod)
{
case ArtistSortMethod::None:
break;
case ArtistSortMethod::ByName:
query.orderBy("a.name COLLATE NOCASE");
break;
case ArtistSortMethod::BySortName:
query.orderBy("a.sort_name COLLATE NOCASE");
break;
case ArtistSortMethod::Random:
query.orderBy("RANDOM()");
break;
case ArtistSortMethod::LastWritten:
query.orderBy("t.file_last_write DESC");
break;
case ArtistSortMethod::StarredDateDesc:
assert(params.starringUser.isValid());
query.orderBy("s_a.date_time DESC");
break;
}
Artist::pointer Artist::find(Session& session, const UUID& mbid)
{
session.checkSharedLocked();
return session.getDboSession().find<Artist>().where("mbid = ?").bind(std::string{ mbid.getAsString() }).resultValue();
}
return query;
}
Artist::pointer Artist::find(Session& session, ArtistId id)
{
session.checkSharedLocked();
return session.getDboSession().find<Artist>().where("id = ?").bind(id).resultValue();
}
RangeResults<ArtistId>
Artist::findAllOrphans(Session& session, Range range)
{
session.checkSharedLocked();
auto query {session.getDboSession().query<ArtistId>("SELECT DISTINCT a.id FROM artist a WHERE NOT EXISTS(SELECT 1 FROM track t INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id WHERE t.id = t_a_l.track_id)")};
bool Artist::exists(Session& session, ArtistId id)
{
session.checkSharedLocked();
return session.getDboSession().query<int>("SELECT 1 FROM artist").where("id = ?").bind(id).resultValue() == 1;
}
return Utils::execQuery(query, range);
}
RangeResults<ArtistId>
Artist::find(Session& session, const FindParameters& params)
{
session.checkSharedLocked();
RangeResults<ArtistId> Artist::findOrphanIds(Session& session, Range range)
{
session.checkSharedLocked();
auto query{ session.getDboSession().query<ArtistId>("SELECT DISTINCT a.id FROM artist a WHERE NOT EXISTS(SELECT 1 FROM track t INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id WHERE t.id = t_a_l.track_id)") };
auto query {createQuery(session, params)};
return Utils::execQuery(query, params.range);
}
return Utils::execQuery(query, range);
}
RangeResults<ArtistId>
Artist::findSimilarArtists(EnumSet<TrackArtistLinkType> artistLinkTypes, Range range) const
{
assert(session());
RangeResults<ArtistId> Artist::findIds(Session& session, const FindParameters& params)
{
session.checkSharedLocked();
std::ostringstream oss;
oss <<
"SELECT a.id FROM artist a"
" INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id"
" INNER JOIN track t ON t.id = t_a_l.track_id"
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
" WHERE "
" t_c.cluster_id IN (SELECT c.id from cluster c"
" INNER JOIN track t ON c.id = t_c.cluster_id"
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
" INNER JOIN artist a ON a.id = t_a_l.artist_id"
" INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id"
" WHERE a.id = ?)"
" AND a.id <> ?";
auto query{ createQuery<ArtistId>(session, params) };
return Utils::execQuery(query, params.range);
}
if (!artistLinkTypes.empty())
{
oss << " AND t_a_l.type IN (";
RangeResults<Artist::pointer> Artist::find(Session& session, const FindParameters& params)
{
session.checkSharedLocked();
bool first {true};
for (TrackArtistLinkType type : artistLinkTypes)
{
(void) type;
if (!first)
oss << ", ";
oss << "?";
first = false;
}
oss << ")";
}
auto query{ createQuery<Wt::Dbo::ptr<Artist>>(session, params) };
return Utils::execQuery(query, params.range);
}
auto query {session()->query<ArtistId>(oss.str())
.bind(getId())
.bind(getId())
.groupBy("a.id")
.orderBy("COUNT(*) DESC, RANDOM()")};
RangeResults<ArtistId> Artist::findSimilarArtistIds(EnumSet<TrackArtistLinkType> artistLinkTypes, Range range) const
{
assert(session());
for (TrackArtistLinkType type : artistLinkTypes)
query.bind(type);
std::ostringstream oss;
oss <<
"SELECT a.id FROM artist a"
" INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id"
" INNER JOIN track t ON t.id = t_a_l.track_id"
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
" WHERE "
" t_c.cluster_id IN (SELECT DISTINCT c.id from cluster c"
" INNER JOIN track t ON c.id = t_c.cluster_id"
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
" INNER JOIN artist a ON a.id = t_a_l.artist_id"
" INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id"
" WHERE a.id = ?)"
" AND a.id <> ?";
return Utils::execQuery(query, range);
}
if (!artistLinkTypes.empty())
{
oss << " AND t_a_l.type IN (";
std::vector<std::vector<Cluster::pointer>>
Artist::getClusterGroups(std::vector<ClusterType::pointer> clusterTypes, std::size_t size) const
{
assert(session());
bool first{ true };
for (TrackArtistLinkType type : artistLinkTypes)
{
(void)type;
if (!first)
oss << ", ";
oss << "?";
first = false;
}
oss << ")";
}
WhereClause where;
auto query{ session()->query<ArtistId>(oss.str())
.bind(getId())
.bind(getId())
.groupBy("a.id")
.orderBy("COUNT(*) DESC, RANDOM()") };
std::ostringstream oss;
oss << "SELECT c FROM cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN cluster_type c_type ON c.cluster_type_id = c_type.id INNER JOIN artist a ON t_a_l.artist_id = a.id INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id";
for (TrackArtistLinkType type : artistLinkTypes)
query.bind(type);
where.And(WhereClause("a.id = ?")).bind(getId().toString());
{
WhereClause clusterClause;
for (auto clusterType : clusterTypes)
clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterType->getId().toString());
return Utils::execQuery(query, range);
}
where.And(clusterClause);
}
oss << " " << where.get();
oss << "GROUP BY c.id ORDER BY COUNT(DISTINCT c.id) DESC";
std::vector<std::vector<Cluster::pointer>> Artist::getClusterGroups(std::vector<ClusterType::pointer> clusterTypes, std::size_t size) const
{
assert(session());
Wt::Dbo::Query<Wt::Dbo::ptr<Cluster>> query = session()->query<Wt::Dbo::ptr<Cluster>>( oss.str() );
WhereClause where;
for (const std::string& bindArg : where.getBindArgs())
query.bind(bindArg);
std::ostringstream oss;
oss << "SELECT c FROM cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN cluster_type c_type ON c.cluster_type_id = c_type.id INNER JOIN artist a ON t_a_l.artist_id = a.id INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id";
Wt::Dbo::collection<Wt::Dbo::ptr<Cluster>> queryRes = query;
where.And(WhereClause("a.id = ?")).bind(getId().toString());
{
WhereClause clusterClause;
for (auto clusterType : clusterTypes)
clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterType->getId().toString());
std::map<ClusterTypeId, std::vector<Cluster::pointer>> clustersByType;
for (Cluster::pointer cluster : queryRes)
{
if (clustersByType[cluster->getType()->getId()].size() < size)
clustersByType[cluster->getType()->getId()].push_back(cluster);
}
where.And(clusterClause);
}
oss << " " << where.get();
oss << "GROUP BY c.id ORDER BY COUNT(DISTINCT c.id) DESC";
std::vector<std::vector<Cluster::pointer>> res;
for (const auto& [clusterTypeId, clusters] : clustersByType)
res.push_back(clusters);
Wt::Dbo::Query<Wt::Dbo::ptr<Cluster>> query = session()->query<Wt::Dbo::ptr<Cluster>>(oss.str());
return res;
}
for (const std::string& bindArg : where.getBindArgs())
query.bind(bindArg);
void
Artist::setSortName(const std::string& sortName)
{
_sortName = std::string(sortName, 0 , _maxNameLength);
}
Wt::Dbo::collection<Wt::Dbo::ptr<Cluster>> queryRes = query;
std::map<ClusterTypeId, std::vector<Cluster::pointer>> clustersByType;
for (Cluster::pointer cluster : queryRes)
{
if (clustersByType[cluster->getType()->getId()].size() < size)
clustersByType[cluster->getType()->getId()].push_back(cluster);
}
std::vector<std::vector<Cluster::pointer>> res;
for (const auto& [clusterTypeId, clusters] : clustersByType)
res.push_back(clusters);
return res;
}
void Artist::setSortName(const std::string& sortName)
{
_sortName = std::string(sortName, 0, _maxNameLength);
}
} // namespace Database
+23 -4
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;
};
}
@@ -68,7 +87,7 @@ namespace Database
LMS_LOG(DB, INFO) << "Creating connection pool on file " << dbPath.string();
auto connection{ std::make_unique<Connection>(dbPath.string()) };
// connection->setProperty("show-queries", "true");
// connection->setProperty("show-queries", "true");
auto connectionPool{ std::make_unique<Wt::Dbo::FixedSqlConnectionPool>(std::move(connection), connectionCount) };
connectionPool->setTimeout(std::chrono::seconds{ 10 });
+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:
+159 -123
View File
@@ -34,147 +34,170 @@
namespace Database
{
Wt::Dbo::Query<ReleaseId> createQuery(Session& session, const Release::FindParameters& params)
namespace
{
auto query{ session.getDboSession().query<ReleaseId>("SELECT DISTINCT r.id from release r") };
if (params.sortMethod == ReleaseSortMethod::LastWritten
|| params.sortMethod == ReleaseSortMethod::Date
|| params.sortMethod == ReleaseSortMethod::OriginalDate
|| params.sortMethod == ReleaseSortMethod::OriginalDateDesc
|| params.writtenAfter.isValid()
|| params.dateRange
|| params.artist.isValid())
template <typename ResultType>
Wt::Dbo::Query<ResultType> createQuery(Session& session, std::string_view itemToSelect, const Release::FindParameters& params)
{
query.join("track t ON t.release_id = r.id");
}
auto query{ session.getDboSession().query<ResultType>("SELECT DISTINCT " + std::string{ itemToSelect } + " from release r") };
if (params.writtenAfter.isValid())
query.where("t.file_last_write > ?").bind(params.writtenAfter);
if (params.dateRange)
{
query.where("t.date >= ?").bind(params.dateRange->begin);
query.where("t.date <= ?").bind(params.dateRange->end);
}
for (std::string_view keyword : params.keywords)
query.where("r.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + Utils::escapeLikeKeyword(keyword) + "%");
if (params.starringUser.isValid())
{
assert(params.scrobbler);
query.join("starred_release s_r ON s_r.release_id = r.id")
.where("s_r.user_id = ?").bind(params.starringUser)
.where("s_r.scrobbler = ?").bind(*params.scrobbler)
.where("s_r.scrobbling_state <> ?").bind(ScrobblingState::PendingRemove);
}
if (params.artist.isValid())
{
query.join("artist a ON a.id = t_a_l.artist_id")
.join("track_artist_link t_a_l ON t_a_l.track_id = t.id")
.where("a.id = ?").bind(params.artist);
if (!params.trackArtistLinkTypes.empty())
if (params.sortMethod == ReleaseSortMethod::LastWritten
|| params.sortMethod == ReleaseSortMethod::Date
|| params.sortMethod == ReleaseSortMethod::OriginalDate
|| params.sortMethod == ReleaseSortMethod::OriginalDateDesc
|| params.writtenAfter.isValid()
|| params.dateRange
|| params.artist.isValid()
|| params.clusters.size() == 1)
{
std::ostringstream oss;
bool first{ true };
for (TrackArtistLinkType linkType : params.trackArtistLinkTypes)
{
if (!first)
oss << " OR ";
oss << "t_a_l.type = ?";
query.bind(linkType);
first = false;
}
query.where(oss.str());
query.join("track t ON t.release_id = r.id");
}
if (!params.excludedTrackArtistLinkTypes.empty())
if (params.writtenAfter.isValid())
query.where("t.file_last_write > ?").bind(params.writtenAfter);
if (params.dateRange)
{
query.where("t.date >= ?").bind(params.dateRange->begin);
query.where("t.date <= ?").bind(params.dateRange->end);
}
for (std::string_view keyword : params.keywords)
query.where("r.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + Utils::escapeLikeKeyword(keyword) + "%");
if (params.starringUser.isValid())
{
assert(params.feedbackBackend);
query.join("starred_release s_r ON s_r.release_id = r.id")
.where("s_r.user_id = ?").bind(params.starringUser)
.where("s_r.backend = ?").bind(*params.feedbackBackend)
.where("s_r.sync_state <> ?").bind(SyncState::PendingRemove);
}
if (params.artist.isValid())
{
query.join("artist a ON a.id = t_a_l.artist_id")
.join("track_artist_link t_a_l ON t_a_l.track_id = t.id")
.where("a.id = ?").bind(params.artist);
if (!params.trackArtistLinkTypes.empty())
{
std::ostringstream oss;
bool first{ true };
for (TrackArtistLinkType linkType : params.trackArtistLinkTypes)
{
if (!first)
oss << " OR ";
oss << "t_a_l.type = ?";
query.bind(linkType);
first = false;
}
query.where(oss.str());
}
if (!params.excludedTrackArtistLinkTypes.empty())
{
std::ostringstream oss;
oss << "r.id NOT IN (SELECT DISTINCT r.id FROM release r"
" INNER JOIN artist a ON a.id = t_a_l.artist_id"
" INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id"
" INNER JOIN track t ON t.release_id = r.id"
" WHERE (a.id = ? AND (";
query.bind(params.artist);
bool first{ true };
for (const TrackArtistLinkType linkType : params.excludedTrackArtistLinkTypes)
{
if (!first)
oss << " OR ";
oss << "t_a_l.type = ?";
query.bind(linkType);
first = false;
}
oss << ")))";
query.where(oss.str());
}
}
if (params.clusters.size() == 1)
{
query.join("track_cluster t_c ON t_c.track_id = t.id")
.where("t_c.cluster_id = ?").bind(params.clusters.front());
}
else if (params.clusters.size() > 1)
{
std::ostringstream oss;
oss << "r.id NOT IN (SELECT DISTINCT r.id FROM release r"
" INNER JOIN artist a ON a.id = t_a_l.artist_id"
" INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id"
oss << "r.id IN (SELECT DISTINCT r.id FROM release r"
" INNER JOIN track t ON t.release_id = r.id"
" WHERE (a.id = ? AND (";
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
query.bind(params.artist);
bool first{ true };
for (const TrackArtistLinkType linkType : params.excludedTrackArtistLinkTypes)
WhereClause clusterClause;
for (const ClusterId clusterId : params.clusters)
{
if (!first)
oss << " OR ";
oss << "t_a_l.type = ?";
query.bind(linkType);
first = false;
clusterClause.Or(WhereClause("t_c.cluster_id = ?"));
query.bind(clusterId);
}
oss << ")))";
oss << " " << clusterClause.get();
oss << " GROUP BY t.id HAVING COUNT(*) = " << params.clusters.size() << ")";
query.where(oss.str());
}
}
if (!params.clusters.empty())
{
std::ostringstream oss;
oss << "r.id IN (SELECT DISTINCT r.id FROM release r"
" INNER JOIN track t ON t.release_id = r.id"
" INNER JOIN cluster c ON c.id = t_c.cluster_id"
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
if (params.primaryType)
query.where("primary_type = ?").bind(*params.primaryType);
if (!params.secondaryTypes.empty())
query.where("secondary_type = ?").bind(params.secondaryTypes);
WhereClause clusterClause;
for (const ClusterId clusterId : params.clusters)
switch (params.sortMethod)
{
clusterClause.Or(WhereClause("c.id = ?"));
query.bind(clusterId);
case ReleaseSortMethod::None:
break;
case ReleaseSortMethod::Name:
query.orderBy("r.name COLLATE NOCASE");
break;
case ReleaseSortMethod::Random:
query.orderBy("RANDOM()");
break;
case ReleaseSortMethod::LastWritten:
query.orderBy("t.file_last_write DESC");
break;
case ReleaseSortMethod::Date:
query.orderBy("t.date, r.name COLLATE NOCASE");
break;
case ReleaseSortMethod::OriginalDate:
query.orderBy("CASE WHEN t.original_date IS NULL THEN t.date ELSE t.original_date END, t.date, r.name COLLATE NOCASE");
break;
case ReleaseSortMethod::OriginalDateDesc:
query.orderBy("CASE WHEN t.original_date IS NULL THEN t.date ELSE t.original_date END DESC, t.date, r.name COLLATE NOCASE");
break;
case ReleaseSortMethod::StarredDateDesc:
assert(params.starringUser.isValid());
query.orderBy("s_r.date_time DESC");
break;
}
oss << " " << clusterClause.get();
oss << " GROUP BY t.id HAVING COUNT(*) = " << params.clusters.size() << ")";
query.where(oss.str());
return query;
}
if (params.primaryType)
query.where("primary_type = ?").bind(*params.primaryType);
if (!params.secondaryTypes.empty())
query.where("secondary_type = ?").bind(params.secondaryTypes);
switch (params.sortMethod)
template <typename ResultType>
Wt::Dbo::Query<ResultType> createQuery(Session& session, const Release::FindParameters& params)
{
case ReleaseSortMethod::None:
break;
case ReleaseSortMethod::Name:
query.orderBy("r.name COLLATE NOCASE");
break;
case ReleaseSortMethod::Random:
query.orderBy("RANDOM()");
break;
case ReleaseSortMethod::LastWritten:
query.orderBy("t.file_last_write DESC");
break;
case ReleaseSortMethod::Date:
query.orderBy("t.date, r.name COLLATE NOCASE");
break;
case ReleaseSortMethod::OriginalDate:
query.orderBy("CASE WHEN t.original_date IS NULL THEN t.date ELSE t.original_date END, t.date, r.name COLLATE NOCASE");
break;
case ReleaseSortMethod::OriginalDateDesc:
query.orderBy("CASE WHEN t.original_date IS NULL THEN t.date ELSE t.original_date END DESC, t.date, r.name COLLATE NOCASE");
break;
case ReleaseSortMethod::StarredDateDesc:
assert(params.starringUser.isValid());
query.orderBy("s_r.date_time DESC");
break;
}
std::string_view itemToSelect;
if constexpr (std::is_same_v<ResultType, ReleaseId>)
itemToSelect = "r.id";
else if constexpr (std::is_same_v<ResultType, Wt::Dbo::ptr<Release>>)
itemToSelect = "r";
else
static_assert("Unhandled type");
return query;
return createQuery<ResultType>(session, itemToSelect, params);
}
}
Release::Release(const std::string& name, const std::optional<UUID>& MBID)
@@ -233,7 +256,7 @@ namespace Database
return session.getDboSession().query<int>("SELECT COUNT(*) FROM release");
}
RangeResults<ReleaseId> Release::findOrderedByArtist(Session& session, Range range)
RangeResults<ReleaseId> Release::findIdsOrderedByArtist(Session& session, Range range)
{
session.checkSharedLocked();
@@ -248,7 +271,7 @@ namespace Database
return Utils::execQuery(query, range);
}
RangeResults<ReleaseId> Release::findOrphans(Session& session, Range range)
RangeResults<ReleaseId> Release::findOrphanIds(Session& session, Range range)
{
session.checkSharedLocked();
@@ -256,12 +279,19 @@ namespace Database
return Utils::execQuery(query, range);
}
RangeResults<ReleaseId> Release::find(Session& session, const FindParameters& params)
RangeResults<Release::pointer> Release::find(Session& session, const FindParameters& params)
{
session.checkSharedLocked();
auto query{ createQuery(session, params) };
auto query{ createQuery<Wt::Dbo::ptr<Release>>(session, params) };
return Utils::execQuery(query, params.range);
}
RangeResults<ReleaseId> Release::findIds(Session& session, const FindParameters& params)
{
session.checkSharedLocked();
auto query{ createQuery<ReleaseId>(session, params) };
return Utils::execQuery(query, params.range);
}
@@ -382,12 +412,18 @@ namespace Database
{
assert(session());
// Select the similar releases using the 5 most used clusters of the release
auto res{ session()->query<Wt::Dbo::ptr<Release>>(
"SELECT r FROM release r"
" INNER JOIN track t ON t.release_id = r.id"
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
" WHERE "
" t_c.cluster_id IN (SELECT c.id from cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN release r ON r.id = t.release_id WHERE r.id = ?)"
" t_c.cluster_id IN "
"(SELECT DISTINCT c.id FROM cluster c"
" INNER JOIN track t ON c.id = t_c.cluster_id"
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
" INNER JOIN release r ON r.id = t.release_id"
" WHERE r.id = ?)"
" AND r.id <> ?"
)
.bind(getId())
+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,47 +29,42 @@
namespace Database
{
StarredArtist::StarredArtist(ObjectPtr<Artist> artist, ObjectPtr<User> user, Scrobbler scrobbler)
: _scrobbler {scrobbler}
, _artist {getDboPtr(artist)}
, _user {getDboPtr(user)}
{
}
StarredArtist::StarredArtist(ObjectPtr<Artist> artist, ObjectPtr<User> user, FeedbackBackend backend)
: _backend{ backend }
, _artist{ getDboPtr(artist) }
, _user{ getDboPtr(user) }
{
}
StarredArtist::pointer
StarredArtist::create(Session& session, ObjectPtr<Artist> artist, ObjectPtr<User> user, Scrobbler scrobbler)
{
return session.getDboSession().add(std::unique_ptr<StarredArtist> {new StarredArtist {artist, user, scrobbler}});
}
StarredArtist::pointer StarredArtist::create(Session& session, ObjectPtr<Artist> artist, ObjectPtr<User> user, FeedbackBackend backend)
{
return session.getDboSession().add(std::unique_ptr<StarredArtist> {new StarredArtist{ artist, user, backend }});
}
std::size_t
StarredArtist::getCount(Session& session)
{
session.checkSharedLocked();
return session.getDboSession().query<int>("SELECT COUNT(*) FROM starred_artist");
}
std::size_t StarredArtist::getCount(Session& session)
{
session.checkSharedLocked();
return session.getDboSession().query<int>("SELECT COUNT(*) FROM starred_artist");
}
StarredArtist::pointer
StarredArtist::find(Session& session, StarredArtistId id)
{
session.checkSharedLocked();
return session.getDboSession().find<StarredArtist>().where("id = ?").bind(id).resultValue();
}
StarredArtist::pointer StarredArtist::find(Session& session, StarredArtistId id)
{
session.checkSharedLocked();
return session.getDboSession().find<StarredArtist>().where("id = ?").bind(id).resultValue();
}
StarredArtist::pointer
StarredArtist::find(Session& session, ArtistId artistId, UserId userId, Scrobbler scrobbler)
{
session.checkSharedLocked();
return session.getDboSession().find<StarredArtist>()
.where("artist_id = ?").bind(artistId)
.where("user_id = ?").bind(userId)
.where("scrobbler = ?").bind(scrobbler)
.resultValue();
}
StarredArtist::pointer StarredArtist::find(Session& session, ArtistId artistId, UserId userId, FeedbackBackend backend)
{
session.checkSharedLocked();
return session.getDboSession().find<StarredArtist>()
.where("artist_id = ?").bind(artistId)
.where("user_id = ?").bind(userId)
.where("backend = ?").bind(backend)
.resultValue();
}
void
StarredArtist::setDateTime(const Wt::WDateTime& dateTime)
{
_dateTime = Utils::normalizeDateTime(dateTime);
}
void StarredArtist::setDateTime(const Wt::WDateTime& dateTime)
{
_dateTime = Utils::normalizeDateTime(dateTime);
}
}
@@ -29,47 +29,42 @@
namespace Database
{
StarredRelease::StarredRelease(ObjectPtr<Release> release, ObjectPtr<User> user, Scrobbler scrobbler)
: _scrobbler {scrobbler}
, _release {getDboPtr(release)}
, _user {getDboPtr(user)}
{
}
StarredRelease::StarredRelease(ObjectPtr<Release> release, ObjectPtr<User> user, FeedbackBackend backend)
: _backend{ backend }
, _release{ getDboPtr(release) }
, _user{ getDboPtr(user) }
{
}
StarredRelease::pointer
StarredRelease::create(Session& session, ObjectPtr<Release> release, ObjectPtr<User> user, Scrobbler scrobbler)
{
return session.getDboSession().add(std::unique_ptr<StarredRelease>{new StarredRelease{release, user, scrobbler}});
}
StarredRelease::pointer StarredRelease::create(Session& session, ObjectPtr<Release> release, ObjectPtr<User> user, FeedbackBackend backend)
{
return session.getDboSession().add(std::unique_ptr<StarredRelease>{new StarredRelease{ release, user, backend }});
}
std::size_t
StarredRelease::getCount(Session& session)
{
session.checkSharedLocked();
return session.getDboSession().query<int>("SELECT COUNT(*) FROM starred_release");
}
std::size_t StarredRelease::getCount(Session& session)
{
session.checkSharedLocked();
return session.getDboSession().query<int>("SELECT COUNT(*) FROM starred_release");
}
StarredRelease::pointer
StarredRelease::find(Session& session, StarredReleaseId id)
{
session.checkSharedLocked();
return session.getDboSession().find<StarredRelease>().where("id = ?").bind(id).resultValue();
}
StarredRelease::pointer StarredRelease::find(Session& session, StarredReleaseId id)
{
session.checkSharedLocked();
return session.getDboSession().find<StarredRelease>().where("id = ?").bind(id).resultValue();
}
StarredRelease::pointer
StarredRelease::find(Session& session, ReleaseId releaseId, UserId userId, Scrobbler scrobbler)
{
session.checkSharedLocked();
return session.getDboSession().find<StarredRelease>()
.where("release_id = ?").bind(releaseId)
.where("user_id = ?").bind(userId)
.where("scrobbler = ?").bind(scrobbler)
.resultValue();
}
StarredRelease::pointer StarredRelease::find(Session& session, ReleaseId releaseId, UserId userId, FeedbackBackend backend)
{
session.checkSharedLocked();
return session.getDboSession().find<StarredRelease>()
.where("release_id = ?").bind(releaseId)
.where("user_id = ?").bind(userId)
.where("backend = ?").bind(backend)
.resultValue();
}
void
StarredRelease::setDateTime(const Wt::WDateTime& dateTime)
{
_dateTime = Utils::normalizeDateTime(dateTime);
}
void StarredRelease::setDateTime(const Wt::WDateTime& dateTime)
{
_dateTime = Utils::normalizeDateTime(dateTime);
}
}
@@ -29,64 +29,58 @@
namespace Database
{
StarredTrack::StarredTrack(ObjectPtr<Track> track, ObjectPtr<User> user, Scrobbler scrobbler)
: _scrobbler {scrobbler}
, _track {getDboPtr(track)}
, _user {getDboPtr(user)}
{
}
StarredTrack::StarredTrack(ObjectPtr<Track> track, ObjectPtr<User> user, FeedbackBackend backend)
: _backend{ backend }
, _track{ getDboPtr(track) }
, _user{ getDboPtr(user) }
{
}
StarredTrack::pointer
StarredTrack::create(Session& session, ObjectPtr<Track> track, ObjectPtr<User> user, Scrobbler scrobbler)
{
return session.getDboSession().add(std::unique_ptr<StarredTrack> {new StarredTrack{track, user, scrobbler}});
}
StarredTrack::pointer StarredTrack::create(Session& session, ObjectPtr<Track> track, ObjectPtr<User> user, FeedbackBackend backend)
{
return session.getDboSession().add(std::unique_ptr<StarredTrack> {new StarredTrack{ track, user, backend }});
}
std::size_t
StarredTrack::getCount(Session& session)
{
session.checkSharedLocked();
return session.getDboSession().query<int>("SELECT COUNT(*) FROM starred_track");
}
std::size_t StarredTrack::getCount(Session& session)
{
session.checkSharedLocked();
return session.getDboSession().query<int>("SELECT COUNT(*) FROM starred_track");
}
StarredTrack::pointer
StarredTrack::find(Session& session, StarredTrackId id)
{
session.checkSharedLocked();
return session.getDboSession().find<StarredTrack>().where("id = ?").bind(id).resultValue();
}
StarredTrack::pointer StarredTrack::find(Session& session, StarredTrackId id)
{
session.checkSharedLocked();
return session.getDboSession().find<StarredTrack>().where("id = ?").bind(id).resultValue();
}
StarredTrack::pointer
StarredTrack::find(Session& session, TrackId trackId, UserId userId, Scrobbler scrobbler)
{
session.checkSharedLocked();
return session.getDboSession().find<StarredTrack>()
.where("track_id = ?").bind(trackId)
.where("user_id = ?").bind(userId)
.where("scrobbler = ?").bind(scrobbler)
.resultValue();
}
StarredTrack::pointer StarredTrack::find(Session& session, TrackId trackId, UserId userId, FeedbackBackend backend)
{
session.checkSharedLocked();
return session.getDboSession().find<StarredTrack>()
.where("track_id = ?").bind(trackId)
.where("user_id = ?").bind(userId)
.where("backend = ?").bind(backend)
.resultValue();
}
RangeResults<StarredTrackId>
StarredTrack::find(Session& session, const FindParameters& params)
{
session.checkSharedLocked();
RangeResults<StarredTrackId> StarredTrack::find(Session& session, const FindParameters& params)
{
session.checkSharedLocked();
auto query {session.getDboSession().query<StarredTrackId>("SELECT DISTINCT s_t.id FROM starred_track s_t")};
auto query{ session.getDboSession().query<StarredTrackId>("SELECT DISTINCT s_t.id FROM starred_track s_t") };
if (params.scrobbler)
query.where("s_t.scrobbler = ?").bind(*params.scrobbler);
if (params.scrobblingState)
query.where("s_t.scrobbling_state = ?").bind(*params.scrobblingState);
if (params.user.isValid())
query.where("s_t.user_id = ?").bind(params.user);
if (params.backend)
query.where("s_t.backend = ?").bind(*params.backend);
if (params.syncState)
query.where("s_t.sync_state = ?").bind(*params.syncState);
if (params.user.isValid())
query.where("s_t.user_id = ?").bind(params.user);
return Utils::execQuery(query, params.range);
}
return Utils::execQuery(query, params.range);
}
void
StarredTrack::setDateTime(const Wt::WDateTime& dateTime)
{
_dateTime = Utils::normalizeDateTime(dateTime);
}
void StarredTrack::setDateTime(const Wt::WDateTime& dateTime)
{
_dateTime = Utils::normalizeDateTime(dateTime);
}
}
File diff suppressed because it is too large Load Diff
@@ -538,7 +538,7 @@ TrackList::getSimilarTracks(std::optional<std::size_t> offset, std::optional<std
"SELECT t FROM track t"
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
" WHERE "
" (t_c.cluster_id IN (SELECT c.id from cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN tracklist_entry p_e ON p_e.track_id = t.id INNER JOIN tracklist p ON p.id = p_e.tracklist_id WHERE p.id = ?)"
" (t_c.cluster_id IN (SELECT DISTINCT c.id from cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN tracklist_entry p_e ON p_e.track_id = t.id INNER JOIN tracklist p ON p.id = p_e.tracklist_id WHERE p.id = ?)"
" AND t.id NOT IN (SELECT tracklist_t.id FROM track tracklist_t INNER JOIN tracklist_entry t_e ON t_e.track_id = tracklist_t.id WHERE t_e.tracklist_id = ?))"
)
.bind(getId())
+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);
}
@@ -40,96 +40,97 @@
namespace Database
{
class Cluster;
class ClusterType;
class Release;
class Session;
class StarredArtist;
class Track;
class TrackArtistLink;
class User;
class Cluster;
class ClusterType;
class Release;
class Session;
class StarredArtist;
class Track;
class TrackArtistLink;
class User;
class Artist final : public Object<Artist, ArtistId>
{
public:
struct FindParameters
{
std::vector<ClusterId> clusters; // if non empty, at least one artist that belongs to these clusters
std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords (on either name field OR sort name field)
std::optional<TrackArtistLinkType> linkType; // if set, only artists that have produced at least one track with this link type
ArtistSortMethod sortMethod {ArtistSortMethod::None};
Range range;
Wt::WDateTime writtenAfter;
UserId starringUser; // only artists starred by this user
std::optional<Scrobbler> scrobbler; // and for this scrobbler
TrackId track; // artists involved in this track
ReleaseId release; // artists involved in this release
class Artist final : public Object<Artist, ArtistId>
{
public:
struct FindParameters
{
std::vector<ClusterId> clusters; // if non empty, at least one artist that belongs to these clusters
std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords (on either name field OR sort name field)
std::optional<TrackArtistLinkType> linkType; // if set, only artists that have produced at least one track with this link type
ArtistSortMethod sortMethod{ ArtistSortMethod::None };
Range range;
Wt::WDateTime writtenAfter;
UserId starringUser; // only artists starred by this user
std::optional<FeedbackBackend> feedbackBackend; // and for this feedback backend
TrackId track; // artists involved in this track
ReleaseId release; // artists involved in this release
FindParameters& setClusters(const std::vector<ClusterId>& _clusters) { clusters = _clusters; return *this; }
FindParameters& setKeywords(const std::vector<std::string_view>& _keywords) { keywords = _keywords; return *this; }
FindParameters& setLinkType(std::optional<TrackArtistLinkType> _linkType) { linkType = _linkType; return *this; }
FindParameters& setSortMethod(ArtistSortMethod _sortMethod) {sortMethod = _sortMethod; return *this; }
FindParameters& setRange(Range _range) {range = _range; return *this; }
FindParameters& setWrittenAfter(const Wt::WDateTime& _after) { writtenAfter = _after; return *this; }
FindParameters& setStarringUser(UserId _user, Scrobbler _scrobbler) { starringUser = _user; scrobbler = _scrobbler; return *this; }
FindParameters& setTrack(TrackId _track) { track = _track; return *this; }
FindParameters& setRelease(ReleaseId _release) { release = _release; return *this; }
};
FindParameters& setClusters(const std::vector<ClusterId>& _clusters) { clusters = _clusters; return *this; }
FindParameters& setKeywords(const std::vector<std::string_view>& _keywords) { keywords = _keywords; return *this; }
FindParameters& setLinkType(std::optional<TrackArtistLinkType> _linkType) { linkType = _linkType; return *this; }
FindParameters& setSortMethod(ArtistSortMethod _sortMethod) { sortMethod = _sortMethod; return *this; }
FindParameters& setRange(Range _range) { range = _range; return *this; }
FindParameters& setWrittenAfter(const Wt::WDateTime& _after) { writtenAfter = _after; return *this; }
FindParameters& setStarringUser(UserId _user, FeedbackBackend _feedbackBackend) { starringUser = _user; feedbackBackend = _feedbackBackend; return *this; }
FindParameters& setTrack(TrackId _track) { track = _track; return *this; }
FindParameters& setRelease(ReleaseId _release) { release = _release; return *this; }
};
Artist() = default;
Artist() = default;
// Accessors
static std::size_t getCount(Session& session);
static pointer find(Session& session, const UUID& MBID);
static pointer find(Session& session, ArtistId id);
static std::vector<pointer> find(Session& session, const std::string& name); // exact match on name field
static RangeResults<ArtistId> find(Session& session, const FindParameters& parameters);
static RangeResults<ArtistId> findAllOrphans(Session& session, Range range); // No track related
static bool exists(Session& session, ArtistId id);
// Accessors
static std::size_t getCount(Session& session);
static pointer find(Session& session, const UUID& MBID);
static pointer find(Session& session, ArtistId id);
static std::vector<pointer> find(Session& session, const std::string& name); // exact match on name field
static RangeResults<pointer> find(Session& session, const FindParameters& parameters);
static RangeResults<ArtistId> findIds(Session& session, const FindParameters& parameters);
static RangeResults<ArtistId> findOrphanIds(Session& session, Range range); // No track related
static bool exists(Session& session, ArtistId id);
// Accessors
const std::string& getName() const { return _name; }
const std::string& getSortName() const { return _sortName; }
std::optional<UUID> getMBID() const { return UUID::fromString(_MBID); }
// Accessors
const std::string& getName() const { return _name; }
const std::string& getSortName() const { return _sortName; }
std::optional<UUID> getMBID() const { return UUID::fromString(_MBID); }
// No artistLinkTypes means get them all
RangeResults<ArtistId> findSimilarArtists(EnumSet<TrackArtistLinkType> artistLinkTypes = {}, Range range = {}) const;
// No artistLinkTypes means get them all
RangeResults<ArtistId> findSimilarArtistIds(EnumSet<TrackArtistLinkType> artistLinkTypes = {}, Range range = {}) const;
// Get the cluster of the tracks made by this artist
// Each clusters are grouped by cluster type, sorted by the number of occurence
// size is the max number of cluster per cluster type
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(std::vector<ObjectPtr<ClusterType>> clusterTypes, std::size_t size) const;
// Get the cluster of the tracks made by this artist
// Each clusters are grouped by cluster type, sorted by the number of occurence
// size is the max number of cluster per cluster type
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(std::vector<ObjectPtr<ClusterType>> clusterTypes, std::size_t size) const;
void setName(std::string_view name) { _name = name; }
void setMBID(const std::optional<UUID>& mbid) { _MBID = mbid ? mbid->getAsString() : ""; }
void setSortName(const std::string& sortName);
void setName(std::string_view name) { _name = name; }
void setMBID(const std::optional<UUID>& mbid) { _MBID = mbid ? mbid->getAsString() : ""; }
void setSortName(const std::string& sortName);
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _name, "name");
Wt::Dbo::field(a, _sortName, "sort_name");
Wt::Dbo::field(a, _MBID, "mbid");
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _name, "name");
Wt::Dbo::field(a, _sortName, "sort_name");
Wt::Dbo::field(a, _MBID, "mbid");
Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "artist");
Wt::Dbo::hasMany(a, _starredArtists, Wt::Dbo::ManyToMany, "user_starred_artists", "", Wt::Dbo::OnDeleteCascade);
}
Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "artist");
Wt::Dbo::hasMany(a, _starredArtists, Wt::Dbo::ManyToMany, "user_starred_artists", "", Wt::Dbo::OnDeleteCascade);
}
private:
static constexpr std::size_t _maxNameLength {128};
private:
static constexpr std::size_t _maxNameLength{ 128 };
friend class Session;
// Create
Artist(const std::string& name, const std::optional<UUID>& MBID = {});
static pointer create(Session& session, const std::string& name, const std::optional<UUID>& UUID = {});
friend class Session;
// Create
Artist(const std::string& name, const std::optional<UUID>& MBID = {});
static pointer create(Session& session, const std::string& name, const std::optional<UUID>& UUID = {});
std::string _name;
std::string _sortName;
std::string _MBID; // Musicbrainz Identifier
std::string _name;
std::string _sortName;
std::string _MBID; // Musicbrainz Identifier
Wt::Dbo::collection<Wt::Dbo::ptr<TrackArtistLink>> _trackArtistLinks; // Tracks involving this artist
Wt::Dbo::collection<Wt::Dbo::ptr<StarredArtist>> _starredArtists; // starred entries for this artist
};
Wt::Dbo::collection<Wt::Dbo::ptr<TrackArtistLink>> _trackArtistLinks; // Tracks involving this artist
Wt::Dbo::collection<Wt::Dbo::ptr<StarredArtist>> _starredArtists; // starred entries for this artist
};
} // namespace Database
@@ -46,47 +46,50 @@ namespace Database
struct FindParameters
{
UserId user;
std::optional<Scrobbler> scrobbler;
std::optional<ScrobblingState> scrobblingState;
std::optional<ScrobblingBackend> backend;
std::optional<SyncState> syncState;
Range range;
FindParameters& setUser(UserId _user) { user = _user; return *this; }
FindParameters& setScrobbler(Scrobbler _scrobbler) { scrobbler = _scrobbler; return *this; }
FindParameters& setScrobblingState(ScrobblingState _scrobblingState) { scrobblingState = _scrobblingState; return *this; }
FindParameters& setScrobblingBackend(ScrobblingBackend _backend) { backend = _backend; return *this; }
FindParameters& setSyncState(SyncState _syncState) { syncState = _syncState; return *this; }
FindParameters& setRange(Range _range) { range = _range; return *this; }
};
// Accessors
static std::size_t getCount(Session& session);
static pointer find(Session& session, ListenId id);
static pointer find(Session& session, UserId userId, TrackId trackId, Scrobbler scrobbler, const Wt::WDateTime& dateTime);
static pointer find(Session& session, UserId userId, TrackId trackId, ScrobblingBackend backend, const Wt::WDateTime& dateTime);
static RangeResults<ListenId> find(Session& session, const FindParameters& parameters);
// Stats
static RangeResults<ArtistId> getTopArtists(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range = {});
static RangeResults<ReleaseId> getTopReleases(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, Range range = {});
static RangeResults<TrackId> getTopTracks(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, Range range = {});
static RangeResults<ArtistId> getTopArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range = {});
static RangeResults<ReleaseId> getTopReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, Range range = {});
static RangeResults<TrackId> getTopTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, Range range = {});
static RangeResults<ArtistId> getRecentArtists(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range = {});
static RangeResults<ReleaseId> getRecentReleases(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, Range range = {});
static RangeResults<TrackId> getRecentTracks(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, Range range = {});
static RangeResults<ArtistId> getRecentArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range = {});
static RangeResults<ReleaseId> getRecentReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, Range range = {});
static RangeResults<TrackId> getRecentTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, Range range = {});
static pointer getMostRecentListen(Session& session, UserId userId, Scrobbler scrobbler, ReleaseId releaseId);
static pointer getMostRecentListen(Session& session, UserId userId, Scrobbler scrobbler, TrackId releaseId);
static std::size_t getCount(Session& session, UserId userId, ScrobblingBackend backend, TrackId trackId);
static std::size_t getCount(Session& session, UserId userId, ScrobblingBackend backend, ReleaseId trackId);
ScrobblingState getScrobblingState() const { return _scrobblingState; }
static pointer getMostRecentListen(Session& session, UserId userId, ScrobblingBackend backend, ReleaseId releaseId);
static pointer getMostRecentListen(Session& session, UserId userId, ScrobblingBackend backend, TrackId releaseId);
SyncState getSyncState() const { return _syncState; }
ObjectPtr<User> getUser() const { return _user; }
ObjectPtr<Track> getTrack() const { return _track; }
const Wt::WDateTime& getDateTime() const { return _dateTime; }
void setScrobblingState(ScrobblingState state) { _scrobblingState = state; }
void setSyncState(SyncState state) { _syncState = state; }
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _dateTime, "date_time");
Wt::Dbo::field(a, _scrobbler, "scrobbler");
Wt::Dbo::field(a, _scrobblingState, "scrobbling_state");
Wt::Dbo::field(a, _backend, "backend"); // TODO rename
Wt::Dbo::field(a, _syncState, "sync_state"); // TODO rename
Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
@@ -94,12 +97,12 @@ namespace Database
private:
friend class Session;
Listen(ObjectPtr<User> user, ObjectPtr<Track> track, Scrobbler scrobbler, const Wt::WDateTime& dateTime);
static pointer create(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track, Scrobbler scrobbler, const Wt::WDateTime& dateTime);
Listen(ObjectPtr<User> user, ObjectPtr<Track> track, ScrobblingBackend backend, const Wt::WDateTime& dateTime);
static pointer create(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track, ScrobblingBackend backend, const Wt::WDateTime& dateTime);
Wt::WDateTime _dateTime;
Scrobbler _scrobbler;
ScrobblingState _scrobblingState{ ScrobblingState::PendingAdd };
Wt::WDateTime _dateTime;
ScrobblingBackend _backend;
SyncState _syncState{ SyncState::PendingAdd };
Wt::Dbo::ptr<User> _user;
Wt::Dbo::ptr<Track> _track;
@@ -57,7 +57,7 @@ namespace Database
Wt::WDateTime writtenAfter;
std::optional<DateRange> dateRange;
UserId starringUser; // only releases starred by this user
std::optional<Scrobbler> scrobbler; // and for this scrobbler
std::optional<FeedbackBackend> feedbackBackend; // and for this backend
ArtistId artist; // only releases that involved this user
EnumSet<TrackArtistLinkType> trackArtistLinkTypes; // and for these link types
EnumSet<TrackArtistLinkType> excludedTrackArtistLinkTypes; // but not for these link types
@@ -70,7 +70,7 @@ namespace Database
FindParameters& setRange(Range _range) { range = _range; return *this; }
FindParameters& setWrittenAfter(const Wt::WDateTime& _after) { writtenAfter = _after; return *this; }
FindParameters& setDateRange(const std::optional<DateRange>& _dateRange) { dateRange = _dateRange; return *this; }
FindParameters& setStarringUser(UserId _user, Scrobbler _scrobbler) { starringUser = _user; scrobbler = _scrobbler; return *this; }
FindParameters& setStarringUser(UserId _user, FeedbackBackend _feedbackBackend) { starringUser = _user; feedbackBackend = _feedbackBackend; return *this; }
FindParameters& setArtist(ArtistId _artist, EnumSet<TrackArtistLinkType> _trackArtistLinkTypes = {}, EnumSet<TrackArtistLinkType> _excludedTrackArtistLinkTypes = {})
{
artist = _artist;
@@ -88,9 +88,10 @@ namespace Database
static pointer find(Session& session, const UUID& MBID);
static std::vector<pointer> find(Session& session, const std::string& name);
static pointer find(Session& session, ReleaseId id);
static RangeResults<ReleaseId> find(Session& session, const FindParameters& parameters);
static RangeResults<ReleaseId> findOrphans(Session& session, Range range); // no track related
static RangeResults<ReleaseId> findOrderedByArtist(Session& session, Range range);
static RangeResults<pointer> find(Session& session, const FindParameters& parameters);
static RangeResults<ReleaseId> findIds(Session& session, const FindParameters& parameters);
static RangeResults<ReleaseId> findOrphanIds(Session& session, Range range); // not track related
static RangeResults<ReleaseId> findIdsOrderedByArtist(Session& session, Range range);
// Get the cluster of the tracks that belong to this release
// Each clusters are grouped by cluster type, sorted by the number of occurence (max to min)
@@ -104,7 +105,7 @@ namespace Database
std::optional<std::string> getCopyrightURL() const;
// Accessors
const std::string& getName() const { return _name; }
const std::string& getName() const { return _name; }
std::optional<UUID> getMBID() const { return UUID::fromString(_MBID); }
std::optional<std::size_t> getTotalDisc() const { return _totalDisc; }
std::size_t getDiscCount() const; // may not be total disc (if incomplete for example)
@@ -32,72 +32,74 @@ LMS_DECLARE_IDTYPE(ScanSettingsId)
namespace Database {
class ClusterType;
class Session;
class ClusterType;
class Session;
class ScanSettings final : public Object<ScanSettings, ScanSettingsId>
{
public:
// Do not modify values (just add)
enum class UpdatePeriod
{
Never = 0,
Daily,
Weekly,
Monthly,
Hourly,
};
class ScanSettings final : public Object<ScanSettings, ScanSettingsId>
{
public:
// Do not modify values (just add)
enum class UpdatePeriod
{
Never = 0,
Daily,
Weekly,
Monthly,
Hourly,
};
// Do not modify values (just add)
enum class RecommendationEngineType
{
Clusters = 0,
Features,
};
// Do not modify values (just add)
enum class SimilarityEngineType
{
Clusters = 0,
Features,
None,
};
static void init(Session& session);
static void init(Session& session);
static pointer get(Session& session);
static pointer get(Session& session);
// Getters
std::size_t getScanVersion() const { return _scanVersion; }
std::filesystem::path getMediaDirectory() const { return _mediaDirectory; }
Wt::WTime getUpdateStartTime() const { return _startTime; }
UpdatePeriod getUpdatePeriod() const { return _updatePeriod; }
std::vector<ObjectPtr<ClusterType>> getClusterTypes() const;
std::vector<std::filesystem::path> getAudioFileExtensions() const;
RecommendationEngineType getRecommendationEngineType() const { return _recommendationEngineType; }
// Getters
std::size_t getScanVersion() const { return _scanVersion; }
std::filesystem::path getMediaDirectory() const { return _mediaDirectory; }
Wt::WTime getUpdateStartTime() const { return _startTime; }
UpdatePeriod getUpdatePeriod() const { return _updatePeriod; }
std::vector<ObjectPtr<ClusterType>> getClusterTypes() const;
std::vector<std::filesystem::path> getAudioFileExtensions() const;
SimilarityEngineType getSimilarityEngineType() const { return _similarityEngineType; }
// Setters
void addAudioFileExtension(const std::filesystem::path& ext);
void setMediaDirectory(const std::filesystem::path& p);
void setUpdateStartTime(Wt::WTime t) { _startTime = t; }
void setUpdatePeriod(UpdatePeriod p) { _updatePeriod = p; }
void setClusterTypes(Session& session, const std::set<std::string>& clusterTypeNames);
void setRecommendationEngineType(RecommendationEngineType type) { _recommendationEngineType = type; }
void incScanVersion();
// Setters
void addAudioFileExtension(const std::filesystem::path& ext);
void setMediaDirectory(const std::filesystem::path& p);
void setUpdateStartTime(Wt::WTime t) { _startTime = t; }
void setUpdatePeriod(UpdatePeriod p) { _updatePeriod = p; }
void setClusterTypes(Session& session, const std::set<std::string>& clusterTypeNames);
void setSimilarityEngineType(SimilarityEngineType type) { _similarityEngineType = type; }
void incScanVersion();
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _scanVersion, "scan_version");
Wt::Dbo::field(a, _mediaDirectory, "media_directory");
Wt::Dbo::field(a, _startTime, "start_time");
Wt::Dbo::field(a, _updatePeriod, "update_period");
Wt::Dbo::field(a, _audioFileExtensions, "audio_file_extensions");
Wt::Dbo::field(a, _recommendationEngineType,"similarity_engine_type");
Wt::Dbo::hasMany(a, _clusterTypes, Wt::Dbo::ManyToOne, "scan_settings");
}
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _scanVersion, "scan_version");
Wt::Dbo::field(a, _mediaDirectory, "media_directory");
Wt::Dbo::field(a, _startTime, "start_time");
Wt::Dbo::field(a, _updatePeriod, "update_period");
Wt::Dbo::field(a, _audioFileExtensions, "audio_file_extensions");
Wt::Dbo::field(a, _similarityEngineType, "similarity_engine_type");
Wt::Dbo::hasMany(a, _clusterTypes, Wt::Dbo::ManyToOne, "scan_settings");
}
private:
int _scanVersion {};
std::string _mediaDirectory;
Wt::WTime _startTime = Wt::WTime {0,0,0};
UpdatePeriod _updatePeriod {UpdatePeriod::Never};
RecommendationEngineType _recommendationEngineType {RecommendationEngineType::Clusters};
std::string _audioFileExtensions {".alac .mp3 .ogg .oga .aac .m4a .m4b .flac .wav .wma .aif .aiff .ape .mpc .shn .opus .wv"};
Wt::Dbo::collection<Wt::Dbo::ptr<ClusterType>> _clusterTypes;
};
private:
int _scanVersion{};
std::string _mediaDirectory;
Wt::WTime _startTime = Wt::WTime{ 0,0,0 };
UpdatePeriod _updatePeriod{ UpdatePeriod::Never };
SimilarityEngineType _similarityEngineType{ SimilarityEngineType::Clusters };
std::string _audioFileExtensions{ ".alac .mp3 .ogg .oga .aac .m4a .m4b .flac .wav .wma .aif .aiff .ape .mpc .shn .opus .wv" };
Wt::Dbo::collection<Wt::Dbo::ptr<ClusterType>> _clusterTypes;
};
} // namespace Database
@@ -61,6 +61,7 @@ namespace Database
void checkSharedLocked();
void analyze();
void optimize();
void prepareTables(); // need to run only once at startup
@@ -30,53 +30,53 @@
namespace Database
{
class Artist;
class Session;
class User;
class Artist;
class Session;
class User;
class StarredArtist final : public Object<StarredArtist, StarredArtistId>
{
public:
StarredArtist() = default;
class StarredArtist final : public Object<StarredArtist, StarredArtistId>
{
public:
StarredArtist() = default;
// Search utility
static std::size_t getCount(Session& session);
static pointer find(Session& session, StarredArtistId id);
static pointer find(Session& session, ArtistId artistId, UserId userId, Scrobbler scrobbler);
// Search utility
static std::size_t getCount(Session& session);
static pointer find(Session& session, StarredArtistId id);
static pointer find(Session& session, ArtistId artistId, UserId userId, FeedbackBackend backend);
// Accessors
ObjectPtr<Artist> getArtist() const { return _artist; }
ObjectPtr<User> getUser() const { return _user; }
Scrobbler getScrobbler() const { return _scrobbler; }
const Wt::WDateTime& getDateTime() const { return _dateTime; }
ScrobblingState getScrobblingState() const { return _scrobblingState; }
// Accessors
ObjectPtr<Artist> getArtist() const { return _artist; }
ObjectPtr<User> getUser() const { return _user; }
FeedbackBackend getFeedbackBackend() const { return _backend; }
const Wt::WDateTime& getDateTime() const { return _dateTime; }
SyncState getSyncState() const { return _syncState; }
// Setters
void setDateTime(const Wt::WDateTime& dateTime);
void setScrobblingState(ScrobblingState state) { _scrobblingState = state; }
// Setters
void setDateTime(const Wt::WDateTime& dateTime);
void setSyncState(SyncState state) { _syncState = state; }
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _scrobbler, "scrobbler");
Wt::Dbo::field(a, _scrobblingState, "scrobbling_state");
Wt::Dbo::field(a, _dateTime, "date_time");
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _backend, "backend");
Wt::Dbo::field(a, _syncState, "sync_state");
Wt::Dbo::field(a, _dateTime, "date_time");
Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
}
Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
}
private:
friend class Session;
StarredArtist(ObjectPtr<Artist> artist, ObjectPtr<User> user, Scrobbler scrobbler);
static pointer create(Session& session, ObjectPtr<Artist> artist, ObjectPtr<User> user, Scrobbler scrobbler);
private:
friend class Session;
StarredArtist(ObjectPtr<Artist> artist, ObjectPtr<User> user, FeedbackBackend scrobblingbackend);
static pointer create(Session& session, ObjectPtr<Artist> artist, ObjectPtr<User> user, FeedbackBackend scrobblingbackend);
Scrobbler _scrobbler; // for which scrobbler
ScrobblingState _scrobblingState {ScrobblingState::PendingAdd};
Wt::WDateTime _dateTime; // when it was starred
FeedbackBackend _backend; // for which backend
SyncState _syncState{ SyncState::PendingAdd };
Wt::WDateTime _dateTime; // when it was starred
Wt::Dbo::ptr<Artist> _artist;
Wt::Dbo::ptr<User> _user;
};
Wt::Dbo::ptr<Artist> _artist;
Wt::Dbo::ptr<User> _user;
};
} // namespace Database
@@ -30,53 +30,53 @@
namespace Database
{
class Release;
class Session;
class User;
class Release;
class Session;
class User;
class StarredRelease final : public Object<StarredRelease, StarredReleaseId>
{
public:
StarredRelease() = default;
class StarredRelease final : public Object<StarredRelease, StarredReleaseId>
{
public:
StarredRelease() = default;
// Search utility
static std::size_t getCount(Session& session);
static pointer find(Session& session, StarredReleaseId id);
static pointer find(Session& session, ReleaseId releaseId, UserId userId, Scrobbler scrobbler);
// Search utility
static std::size_t getCount(Session& session);
static pointer find(Session& session, StarredReleaseId id);
static pointer find(Session& session, ReleaseId releaseId, UserId userId, FeedbackBackend backend);
// Accessors
ObjectPtr<Release> getRelease() const { return _release; }
ObjectPtr<User> getUser() const { return _user; }
Scrobbler getScrobbler() const { return _scrobbler; }
const Wt::WDateTime& getDateTime() const { return _dateTime; }
ScrobblingState getScrobblingState() const { return _scrobblingState; }
// Accessors
ObjectPtr<Release> getRelease() const { return _release; }
ObjectPtr<User> getUser() const { return _user; }
FeedbackBackend getFeedbackBackend() const { return _backend; }
const Wt::WDateTime& getDateTime() const { return _dateTime; }
SyncState getSyncState() const { return _syncState; }
// Setters
void setDateTime(const Wt::WDateTime& dateTime);
void setScrobblingState(ScrobblingState state) { _scrobblingState = state; }
// Setters
void setDateTime(const Wt::WDateTime& dateTime);
void setSyncState(SyncState state) { _syncState = state; }
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _scrobbler, "scrobbler");
Wt::Dbo::field(a, _scrobblingState, "scrobbling_state");
Wt::Dbo::field(a, _dateTime, "date_time");
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _backend, "backend");
Wt::Dbo::field(a, _syncState, "sync_state");
Wt::Dbo::field(a, _dateTime, "date_time");
Wt::Dbo::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
}
Wt::Dbo::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
}
private:
friend class Session;
StarredRelease(ObjectPtr<Release> release, ObjectPtr<User> user, Scrobbler scrobbler);
static pointer create(Session& session, ObjectPtr<Release> release, ObjectPtr<User> user, Scrobbler scrobbler);
private:
friend class Session;
StarredRelease(ObjectPtr<Release> release, ObjectPtr<User> user, FeedbackBackend backend);
static pointer create(Session& session, ObjectPtr<Release> release, ObjectPtr<User> user, FeedbackBackend backend);
Scrobbler _scrobbler; // for which scrobbler
ScrobblingState _scrobblingState {ScrobblingState::PendingAdd};
Wt::WDateTime _dateTime; // when it was starred
FeedbackBackend _backend; // for which backend
SyncState _syncState{ SyncState::PendingAdd };
Wt::WDateTime _dateTime; // when it was starred
Wt::Dbo::ptr<Release> _release;
Wt::Dbo::ptr<User> _user;
};
Wt::Dbo::ptr<Release> _release;
Wt::Dbo::ptr<User> _user;
};
} // namespace Database
@@ -31,66 +31,66 @@
namespace Database
{
class Track;
class Session;
class User;
class Track;
class Session;
class User;
class StarredTrack final : public Object<StarredTrack, StarredTrackId>
{
public:
StarredTrack() = default;
class StarredTrack final : public Object<StarredTrack, StarredTrackId>
{
public:
StarredTrack() = default;
struct FindParameters
{
std::optional<Scrobbler> scrobbler; // for this scrobbler
std::optional<ScrobblingState> scrobblingState; // and these states
UserId user; // and this user
Range range;
struct FindParameters
{
std::optional<FeedbackBackend> backend; // for this backend
std::optional<SyncState> syncState; // and these states
UserId user; // and this user
Range range;
FindParameters& setScrobbler(Scrobbler _scrobbler, ScrobblingState _scrobblingState) { scrobbler = _scrobbler; scrobblingState = _scrobblingState; return *this; }
FindParameters& setUser(UserId _user) {user = _user; return *this; }
FindParameters& setRange(Range _range) {range = _range; return *this; }
};
FindParameters& setFeedbackBackend(FeedbackBackend _backend, SyncState _syncState) { backend = _backend; syncState = _syncState; return *this; }
FindParameters& setUser(UserId _user) { user = _user; return *this; }
FindParameters& setRange(Range _range) { range = _range; return *this; }
};
// Search utility
static std::size_t getCount(Session& session);
static pointer find(Session& session, StarredTrackId id);
static pointer find(Session& session, TrackId trackId, UserId userId, Scrobbler scrobbler);
static RangeResults<StarredTrackId> find(Session& session, const FindParameters& findParams);
// Search utility
static std::size_t getCount(Session& session);
static pointer find(Session& session, StarredTrackId id);
static pointer find(Session& session, TrackId trackId, UserId userId, FeedbackBackend backend);
static RangeResults<StarredTrackId> find(Session& session, const FindParameters& findParams);
// Accessors
ObjectPtr<Track> getTrack() const { return _track; }
ObjectPtr<User> getUser() const { return _user; }
Scrobbler getScrobbler() const { return _scrobbler; }
const Wt::WDateTime& getDateTime() const { return _dateTime; }
ScrobblingState getScrobblingState() const { return _scrobblingState; }
// Accessors
ObjectPtr<Track> getTrack() const { return _track; }
ObjectPtr<User> getUser() const { return _user; }
FeedbackBackend getBackend() const { return _backend; }
const Wt::WDateTime& getDateTime() const { return _dateTime; }
SyncState getSyncState() const { return _syncState; }
// Setters
void setDateTime(const Wt::WDateTime& dateTime);
void setScrobblingState(ScrobblingState state) { _scrobblingState = state; }
// Setters
void setDateTime(const Wt::WDateTime& dateTime);
void setSyncState(SyncState state) { _syncState = state; }
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _scrobbler, "scrobbler");
Wt::Dbo::field(a, _scrobblingState, "scrobbling_state");
Wt::Dbo::field(a, _dateTime, "date_time");
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _backend, "backend");
Wt::Dbo::field(a, _syncState, "sync_state");
Wt::Dbo::field(a, _dateTime, "date_time");
Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
}
Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
}
private:
friend class Session;
StarredTrack(ObjectPtr<Track> track, ObjectPtr<User> user, Scrobbler scrobbler);
static pointer create(Session& session, ObjectPtr<Track> track, ObjectPtr<User> user, Scrobbler scrobbler);
private:
friend class Session;
StarredTrack(ObjectPtr<Track> track, ObjectPtr<User> user, FeedbackBackend backend);
static pointer create(Session& session, ObjectPtr<Track> track, ObjectPtr<User> user, FeedbackBackend backend);
Scrobbler _scrobbler; // for which scrobbler
ScrobblingState _scrobblingState {ScrobblingState::PendingAdd};
Wt::WDateTime _dateTime; // when it was starred
FeedbackBackend _backend; // for which backend
SyncState _syncState{ SyncState::PendingAdd };
Wt::WDateTime _dateTime; // when it was starred
Wt::Dbo::ptr<Track> _track;
Wt::Dbo::ptr<User> _user;
};
Wt::Dbo::ptr<Track> _track;
Wt::Dbo::ptr<User> _user;
};
} // namespace Database
@@ -21,11 +21,10 @@
#include <chrono>
#include <filesystem>
#include <iostream>
#include <ostream>
#include <optional>
#include <string>
#include <string_view>
#include <unordered_set>
#include <utility>
#include <vector>
@@ -61,23 +60,23 @@ class Track final : public Object<Track, TrackId>
public:
struct FindParameters
{
std::vector<ClusterId> clusters; // if non empty, tracks that belong to these clusters
std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords
std::string name; // if non empty, must match this name
TrackSortMethod sortMethod {TrackSortMethod::None};
Range range;
Wt::WDateTime writtenAfter;
UserId starringUser; // only tracks starred by this user
std::optional<Scrobbler> scrobbler; // and for this scrobbler
ArtistId artist; // only tracks that involve this artist
std::string artistName; // only tracks that involve this artist name
EnumSet<TrackArtistLinkType> trackArtistLinkTypes; // and for these link types
bool nonRelease {}; // only tracks that do not belong to a release
ReleaseId release; // matching this release
std::string releaseName; // matching this release name
TrackListId trackList; // matching this trackList
std::optional<int> trackNumber; // matching this track number
bool distinct {true};
std::vector<ClusterId> clusters; // if non empty, tracks that belong to these clusters
std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords
std::string name; // if non empty, must match this name
TrackSortMethod sortMethod {TrackSortMethod::None};
Range range;
Wt::WDateTime writtenAfter;
UserId starringUser; // only tracks starred by this user
std::optional<FeedbackBackend> feedbackBackend; // and for this feedback backend
ArtistId artist; // only tracks that involve this artist
std::string artistName; // only tracks that involve this artist name
EnumSet<TrackArtistLinkType> trackArtistLinkTypes; // and for these link types
bool nonRelease {}; // only tracks that do not belong to a release
ReleaseId release; // matching this release
std::string releaseName; // matching this release name
TrackListId trackList; // matching this trackList
std::optional<int> trackNumber; // matching this track number
bool distinct {true};
FindParameters& setClusters(const std::vector<ClusterId>& _clusters) { clusters = _clusters; return *this; }
FindParameters& setKeywords(const std::vector<std::string_view>& _keywords) { keywords = _keywords; return *this; }
@@ -85,7 +84,7 @@ class Track final : public Object<Track, TrackId>
FindParameters& setSortMethod(TrackSortMethod _method) { sortMethod = _method; return *this; }
FindParameters& setRange(Range _range) { range = _range; return *this; }
FindParameters& setWrittenAfter(const Wt::WDateTime& _after) { writtenAfter = _after; return *this; }
FindParameters& setStarringUser(UserId _user, Scrobbler _scrobbler) { starringUser = _user; scrobbler = _scrobbler; return *this; }
FindParameters& setStarringUser(UserId _user, FeedbackBackend _feedbackBackend) { starringUser = _user; feedbackBackend = _feedbackBackend; return *this; }
FindParameters& setArtist(ArtistId _artist, EnumSet<TrackArtistLinkType> _trackArtistLinkTypes = {}) { artist = _artist; trackArtistLinkTypes = _trackArtistLinkTypes; return *this; }
FindParameters& setArtistName(std::string_view _artistName, EnumSet<TrackArtistLinkType> _trackArtistLinkTypes = {}) { artistName = _artistName; trackArtistLinkTypes = _trackArtistLinkTypes; return *this; }
FindParameters& setNonRelease(bool _nonRelease) { nonRelease = _nonRelease; return *this; }
@@ -111,12 +110,13 @@ class Track final : public Object<Track, TrackId>
static bool exists(Session& session, TrackId id);
static std::vector<pointer> findByRecordingMBID(Session& session, const UUID& MBID);
static std::vector<pointer> findByMBID(Session& session, const UUID& MBID);
static RangeResults<TrackId> findSimilarTracks(Session& session, const std::vector<TrackId>& trackIds, Range range);
static RangeResults<TrackId> findSimilarTrackIds(Session& session, const std::vector<TrackId>& trackIds, Range range);
static RangeResults<TrackId> find(Session& session, const FindParameters& parameters);
static RangeResults<TrackId> findIds(Session& session, const FindParameters& parameters);
static RangeResults<pointer> find(Session& session, const FindParameters& parameters);
static RangeResults<PathResult> findPaths(Session& session, Range range);
static RangeResults<TrackId> findTrackMBIDDuplicates(Session& session, Range range);
static RangeResults<TrackId> findWithRecordingMBIDAndMissingFeatures(Session& session, Range range);
static RangeResults<TrackId> findIdsTrackMBIDDuplicates(Session& session, Range range);
static RangeResults<TrackId> findIdsWithRecordingMBIDAndMissingFeatures(Session& session, Range range);
// Accessors
void setScanVersion(std::size_t version) { _scanVersion = version; }
@@ -46,7 +46,7 @@ namespace Database
{
Range range;
std::vector<T> results;
bool moreResults;
bool moreResults{};
RangeResults getSubRange(Range subRange)
{
@@ -160,13 +160,19 @@ namespace Database
void visitAllowedAudioBitrates(std::function<void(Bitrate)>);
bool isAudioBitrateAllowed(Bitrate bitrate);
enum class Scrobbler
enum class ScrobblingBackend
{
Internal = 0,
ListenBrainz = 1,
};
enum class ScrobblingState
enum class FeedbackBackend
{
Internal = 0,
ListenBrainz = 1,
};
enum class SyncState
{
PendingAdd = 0,
Synchronized = 1,
@@ -47,20 +47,23 @@ namespace Database {
struct FindParameters
{
std::optional<Scrobbler> scrobbler;
Range range;
std::optional<ScrobblingBackend> scrobblingBackend;
std::optional<FeedbackBackend> feedbackBackend;
Range range;
FindParameters& setScrobbler(Scrobbler _scrobbler) { scrobbler = _scrobbler; return *this; }
FindParameters& setFeedbackBackend(FeedbackBackend _feedbackBackend) { feedbackBackend = _feedbackBackend; return *this; }
FindParameters& setScrobblingBackend(ScrobblingBackend _scrobblingBackend) { scrobblingBackend = _scrobblingBackend; return *this; }
FindParameters& setRange(Range _range) { range = _range; return *this; }
};
static inline const std::size_t MinNameLength{ 3 };
static inline const std::size_t MaxNameLength{ 15 };
static inline const AudioFormat defaultSubsonicTranscodeFormat{ AudioFormat::OGG_OPUS };
static inline const Bitrate defaultSubsonicTranscodeBitrate{ 128000 };
static inline const UITheme defaultUITheme{ UITheme::Dark };
static inline const SubsonicArtistListMode defaultSubsonicArtistListMode{ SubsonicArtistListMode::AllArtists };
static inline const Scrobbler defaultScrobbler{ Scrobbler::Internal };
static inline constexpr std::size_t MinNameLength{ 3 };
static inline constexpr std::size_t MaxNameLength{ 15 };
static inline constexpr AudioFormat defaultSubsonicTranscodeFormat{ AudioFormat::OGG_OPUS };
static inline constexpr Bitrate defaultSubsonicTranscodeBitrate{ 128000 };
static inline constexpr UITheme defaultUITheme{ UITheme::Dark };
static inline constexpr SubsonicArtistListMode defaultSubsonicArtistListMode{ SubsonicArtistListMode::AllArtists };
static inline constexpr ScrobblingBackend defaultScrobblingBackend{ ScrobblingBackend::Internal };
static inline constexpr FeedbackBackend defaultFeedbackBackend{ FeedbackBackend::Internal };
User() = default;
@@ -88,7 +91,8 @@ namespace Database {
void setUITheme(UITheme uiTheme) { _uiTheme = uiTheme; }
void clearAuthTokens();
void setSubsonicArtistListMode(SubsonicArtistListMode mode) { _subsonicArtistListMode = mode; }
void setScrobbler(Scrobbler scrobbler) { _scrobbler = scrobbler; }
void setFeedbackBackend(FeedbackBackend feedbackBackend) { _feedbackBackend = feedbackBackend; }
void setScrobblingBackend(ScrobblingBackend scrobblingBackend) { _scrobblingBackend = scrobblingBackend; }
void setListenBrainzToken(const std::optional<UUID>& MBID) { _listenbrainzToken = MBID ? MBID->getAsString() : ""; }
// read
@@ -102,7 +106,8 @@ namespace Database {
bool isRadioSet() const { return _radio; }
UITheme getUITheme() const { return _uiTheme; }
SubsonicArtistListMode getSubsonicArtistListMode() const { return _subsonicArtistListMode; }
Scrobbler getScrobbler() const { return _scrobbler; }
FeedbackBackend getFeedbackBackend() const { return _feedbackBackend; }
ScrobblingBackend getScrobblingBackend() const { return _scrobblingBackend; }
std::optional<UUID> getListenBrainzToken() const { return UUID::fromString(_listenbrainzToken); }
template<class Action>
@@ -117,7 +122,8 @@ namespace Database {
Wt::Dbo::field(a, _subsonicDefaultTranscodeBitrate, "subsonic_default_transcode_bitrate");
Wt::Dbo::field(a, _subsonicArtistListMode, "subsonic_artist_list_mode");
Wt::Dbo::field(a, _uiTheme, "ui_theme");
Wt::Dbo::field(a, _scrobbler, "scrobbler");
Wt::Dbo::field(a, _feedbackBackend, "feedback_backend");
Wt::Dbo::field(a, _scrobblingBackend, "scrobbling_backend");
Wt::Dbo::field(a, _listenbrainzToken, "listenbrainz_token");
// UI player settings
@@ -138,7 +144,8 @@ namespace Database {
std::string _passwordHash;
Wt::WDateTime _lastLogin;
UITheme _uiTheme{ defaultUITheme };
Scrobbler _scrobbler{ defaultScrobbler };
FeedbackBackend _feedbackBackend{ defaultFeedbackBackend };
ScrobblingBackend _scrobblingBackend{ defaultScrobblingBackend };
std::string _listenbrainzToken; // Musicbrainz Identifier
// Admin defined settings
+315 -307
View File
@@ -23,415 +23,423 @@ using namespace Database;
TEST_F(DatabaseFixture, Artist)
{
{
auto transaction {session.createSharedTransaction()};
EXPECT_FALSE(Artist::exists(session, 35));
EXPECT_FALSE(Artist::exists(session, 0));
EXPECT_FALSE(Artist::exists(session, 1));
EXPECT_EQ(Artist::getCount(session), 0);
}
{
auto transaction{ session.createSharedTransaction() };
EXPECT_FALSE(Artist::exists(session, 35));
EXPECT_FALSE(Artist::exists(session, 0));
EXPECT_FALSE(Artist::exists(session, 1));
EXPECT_EQ(Artist::getCount(session), 0);
}
ScopedArtist artist {session, "MyArtist"};
ScopedArtist artist{ session, "MyArtist" };
{
auto transaction {session.createSharedTransaction()};
{
auto transaction{ session.createSharedTransaction() };
EXPECT_TRUE(artist.get());
EXPECT_FALSE(!artist.get());
EXPECT_EQ(artist.get()->getId(), artist.getId());
EXPECT_TRUE(artist.get());
EXPECT_FALSE(!artist.get());
EXPECT_EQ(artist.get()->getId(), artist.getId());
EXPECT_TRUE(Artist::exists(session, artist.getId()));
EXPECT_EQ(Artist::getCount(session), 1);
}
EXPECT_TRUE(Artist::exists(session, artist.getId()));
EXPECT_EQ(Artist::getCount(session), 1);
}
{
auto transaction {session.createSharedTransaction()};
{
auto transaction{ session.createSharedTransaction() };
auto artists {Artist::find(session, Artist::FindParameters {})};
ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId());
auto artists{ Artist::findIds(session, Artist::FindParameters {}) };
ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId());
artists = Artist::findAllOrphans(session, Range {});
ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId());
}
artists = Artist::findOrphanIds(session, Range{});
ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId());
}
{
auto transaction{ session.createSharedTransaction() };
auto artists{ Artist::find(session, Artist::FindParameters {}) };
ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results.front()->getId(), artist.getId());
}
}
TEST_F(DatabaseFixture, Artist_singleTrack)
{
ScopedTrack track {session, "MyTrack"};
ScopedArtist artist {session, "MyArtist"};
ScopedTrack track{ session, "MyTrack" };
ScopedArtist artist{ session, "MyArtist" };
{
auto transaction {session.createUniqueTransaction()};
{
auto transaction{ session.createUniqueTransaction() };
track.get().modify()->setName("MyTrackName");
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
}
track.get().modify()->setName("MyTrackName");
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
}
{
auto transaction {session.createSharedTransaction()};
EXPECT_TRUE(Artist::findAllOrphans(session, Range {}).results.empty());
}
{
auto transaction{ session.createSharedTransaction() };
EXPECT_TRUE(Artist::findOrphanIds(session, Range{}).results.empty());
}
{
auto transaction {session.createSharedTransaction()};
{
auto transaction{ session.createSharedTransaction() };
auto artists {track->getArtists({TrackArtistLinkType::Artist})};
ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.front()->getId(), artist.getId());
auto artists{ track->getArtists({TrackArtistLinkType::Artist}) };
ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.front()->getId(), artist.getId());
ASSERT_EQ(track->getArtistLinks().size(), 1);
auto artistLink {track->getArtistLinks().front()};
EXPECT_EQ(artistLink->getTrack()->getId(), track.getId());
EXPECT_EQ(artistLink->getArtist()->getId(), artist.getId());
ASSERT_EQ(track->getArtistLinks().size(), 1);
auto artistLink{ track->getArtistLinks().front() };
EXPECT_EQ(artistLink->getTrack()->getId(), track.getId());
EXPECT_EQ(artistLink->getArtist()->getId(), artist.getId());
ASSERT_EQ(track->getArtists({TrackArtistLinkType::Artist}).size(), 1);
EXPECT_TRUE(track->getArtists({TrackArtistLinkType::ReleaseArtist}).empty());
EXPECT_EQ(track->getArtists({}).size(), 1);
}
ASSERT_EQ(track->getArtists({ TrackArtistLinkType::Artist }).size(), 1);
EXPECT_TRUE(track->getArtists({ TrackArtistLinkType::ReleaseArtist }).empty());
EXPECT_EQ(track->getArtists({}).size(), 1);
}
{
auto transaction {session.createSharedTransaction()};
{
auto transaction{ session.createSharedTransaction() };
auto artists {track->getArtistIds({TrackArtistLinkType::Artist})};
ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.front(), artist.getId());
auto artists{ track->getArtistIds({TrackArtistLinkType::Artist}) };
ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.front(), artist.getId());
ASSERT_EQ(track->getArtistIds({TrackArtistLinkType::Artist}).size(), 1);
EXPECT_TRUE(track->getArtistIds({TrackArtistLinkType::ReleaseArtist}).empty());
EXPECT_EQ(track->getArtistIds({}).size(), 1);
}
ASSERT_EQ(track->getArtistIds({ TrackArtistLinkType::Artist }).size(), 1);
EXPECT_TRUE(track->getArtistIds({ TrackArtistLinkType::ReleaseArtist }).empty());
EXPECT_EQ(track->getArtistIds({}).size(), 1);
}
{
auto transaction {session.createSharedTransaction()};
auto tracks {Track::find(session, Track::FindParameters{}.setName("MyTrackName").setArtistName("MyArtist"))};
ASSERT_EQ(tracks.results.size(), 1);
EXPECT_EQ(tracks.results.front(), track.getId());
}
{
auto transaction {session.createSharedTransaction()};
auto tracks {Track::find(session, Track::FindParameters{}.setName("MyTrackName").setArtistName("MyArtistFoo"))};
EXPECT_EQ(tracks.results.size(), 0);
}
{
auto transaction {session.createSharedTransaction()};
auto tracks {Track::find(session, Track::FindParameters{}.setName("MyTrackNameFoo").setArtistName("MyArtist"))};
EXPECT_EQ(tracks.results.size(), 0);
}
{
auto transaction {session.createSharedTransaction()};
auto artists {Artist::find(session, Artist::FindParameters{}.setTrack(track->getId()))};
ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId());
}
{
auto transaction{ session.createSharedTransaction() };
auto tracks{ Track::findIds(session, Track::FindParameters{}.setName("MyTrackName").setArtistName("MyArtist")) };
ASSERT_EQ(tracks.results.size(), 1);
EXPECT_EQ(tracks.results.front(), track.getId());
}
{
auto transaction{ session.createSharedTransaction() };
auto tracks{ Track::findIds(session, Track::FindParameters{}.setName("MyTrackName").setArtistName("MyArtistFoo")) };
EXPECT_EQ(tracks.results.size(), 0);
}
{
auto transaction{ session.createSharedTransaction() };
auto tracks{ Track::findIds(session, Track::FindParameters{}.setName("MyTrackNameFoo").setArtistName("MyArtist")) };
EXPECT_EQ(tracks.results.size(), 0);
}
{
auto transaction{ session.createSharedTransaction() };
auto artists{ Artist::findIds(session, Artist::FindParameters{}.setTrack(track->getId())) };
ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId());
}
}
TEST_F(DatabaseFixture, Artist_singleTracktMultiRoles)
{
ScopedTrack track {session, "MyTrack"};
ScopedArtist artist {session, "MyArtist"};
{
auto transaction {session.createUniqueTransaction()};
ScopedTrack track{ session, "MyTrack" };
ScopedArtist artist{ session, "MyArtist" };
{
auto transaction{ session.createUniqueTransaction() };
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::ReleaseArtist);
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Writer);
}
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::ReleaseArtist);
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Writer);
}
{
auto transaction {session.createSharedTransaction()};
EXPECT_TRUE(Artist::findAllOrphans(session, Range {}).results.empty());
}
{
auto transaction{ session.createSharedTransaction() };
EXPECT_TRUE(Artist::findOrphanIds(session, Range{}).results.empty());
}
{
auto transaction {session.createSharedTransaction()};
EXPECT_EQ(Artist::find(session, Artist::FindParameters {}).results.size(), 1);
EXPECT_EQ(Artist::find(session, Artist::FindParameters {}.setLinkType(TrackArtistLinkType::Artist)).results.size(), 1);
EXPECT_EQ(Artist::find(session, Artist::FindParameters {}.setLinkType(TrackArtistLinkType::ReleaseArtist)).results.size(), 1);
EXPECT_EQ(Artist::find(session, Artist::FindParameters {}.setLinkType(TrackArtistLinkType::Writer)).results.size(), 1);
EXPECT_EQ(Artist::find(session, Artist::FindParameters {}.setLinkType(TrackArtistLinkType::Composer)).results.size(), 0);
}
{
auto transaction{ session.createSharedTransaction() };
EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}).results.size(), 1);
EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setLinkType(TrackArtistLinkType::Artist)).results.size(), 1);
EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setLinkType(TrackArtistLinkType::ReleaseArtist)).results.size(), 1);
EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setLinkType(TrackArtistLinkType::Writer)).results.size(), 1);
EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setLinkType(TrackArtistLinkType::Composer)).results.size(), 0);
}
{
auto transaction {session.createSharedTransaction()};
{
auto transaction{ session.createSharedTransaction() };
auto artists {track->getArtists({TrackArtistLinkType::Artist})};
ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.front()->getId(), artist.getId());
auto artists{ track->getArtists({TrackArtistLinkType::Artist}) };
ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.front()->getId(), artist.getId());
artists = track->getArtists({TrackArtistLinkType::ReleaseArtist});
ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.front()->getId(), artist.getId());
artists = track->getArtists({ TrackArtistLinkType::ReleaseArtist });
ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.front()->getId(), artist.getId());
EXPECT_EQ(track->getArtistLinks().size(), 3);
EXPECT_EQ(track->getArtistLinks().size(), 3);
auto tracks {Track::find(session, Track::FindParameters {}.setArtist(artist.getId()))};
EXPECT_EQ(tracks.results.size(), 1);
auto tracks{ Track::findIds(session, Track::FindParameters {}.setArtist(artist.getId())) };
EXPECT_EQ(tracks.results.size(), 1);
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::ReleaseArtist}));
EXPECT_EQ(tracks.results.size(), 1);
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::Artist}));
EXPECT_EQ(tracks.results.size(), 1);
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::Writer}));
EXPECT_EQ(tracks.results.size(), 1);
tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::ReleaseArtist }));
EXPECT_EQ(tracks.results.size(), 1);
tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Artist }));
EXPECT_EQ(tracks.results.size(), 1);
tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Writer }));
EXPECT_EQ(tracks.results.size(), 1);
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::Composer}));
EXPECT_EQ(tracks.results.size(), 0);
}
tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Composer }));
EXPECT_EQ(tracks.results.size(), 0);
}
{
auto transaction {session.createSharedTransaction()};
EnumSet<TrackArtistLinkType> types{ TrackArtistLink::findUsedTypes(session, artist.getId()) };
EXPECT_TRUE(types.contains(TrackArtistLinkType::ReleaseArtist));
EXPECT_TRUE(types.contains(TrackArtistLinkType::Artist));
EXPECT_TRUE(types.contains(TrackArtistLinkType::Writer));
EXPECT_FALSE(types.contains(TrackArtistLinkType::Composer));
}
{
auto transaction{ session.createSharedTransaction() };
EnumSet<TrackArtistLinkType> types{ TrackArtistLink::findUsedTypes(session, artist.getId()) };
EXPECT_TRUE(types.contains(TrackArtistLinkType::ReleaseArtist));
EXPECT_TRUE(types.contains(TrackArtistLinkType::Artist));
EXPECT_TRUE(types.contains(TrackArtistLinkType::Writer));
EXPECT_FALSE(types.contains(TrackArtistLinkType::Composer));
}
}
TEST_F(DatabaseFixture, Artist_singleTrackMultiArtists)
{
ScopedTrack track {session, "track"};
ScopedArtist artist1 {session, "artist1"};
ScopedArtist artist2 {session, "artist2"};
ASSERT_NE(artist1.getId(), artist2.getId());
ScopedTrack track{ session, "track" };
ScopedArtist artist1{ session, "artist1" };
ScopedArtist artist2{ session, "artist2" };
ASSERT_NE(artist1.getId(), artist2.getId());
{
auto transaction {session.createUniqueTransaction()};
{
auto transaction{ session.createUniqueTransaction() };
TrackArtistLink::create(session, track.get(), artist1.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, track.get(), artist2.get(), TrackArtistLinkType::Artist);
}
TrackArtistLink::create(session, track.get(), artist1.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, track.get(), artist2.get(), TrackArtistLinkType::Artist);
}
{
auto transaction {session.createSharedTransaction()};
EXPECT_TRUE(Artist::findAllOrphans(session, Range {}).results.empty());
}
{
auto transaction{ session.createSharedTransaction() };
EXPECT_TRUE(Artist::findOrphanIds(session, Range{}).results.empty());
}
{
auto transaction {session.createSharedTransaction()};
{
auto transaction{ session.createSharedTransaction() };
auto artists {track->getArtists({TrackArtistLinkType::Artist})};
ASSERT_EQ(artists.size(), 2);
EXPECT_TRUE((artists[0]->getId() == artist1.getId() && artists[1]->getId() == artist2.getId())
|| (artists[0]->getId() == artist2.getId() && artists[1]->getId() == artist1.getId()));
auto artists{ track->getArtists({TrackArtistLinkType::Artist}) };
ASSERT_EQ(artists.size(), 2);
EXPECT_TRUE((artists[0]->getId() == artist1.getId() && artists[1]->getId() == artist2.getId())
|| (artists[0]->getId() == artist2.getId() && artists[1]->getId() == artist1.getId()));
EXPECT_EQ(track->getArtists({}).size(), 2);
EXPECT_EQ(track->getArtists({TrackArtistLinkType::Artist}).size(), 2);
EXPECT_TRUE(track->getArtists({TrackArtistLinkType::ReleaseArtist}).empty());
EXPECT_EQ(Artist::find(session, Artist::FindParameters {}).results.size(), 2);
EXPECT_EQ(Artist::find(session, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::Random)).results.size(), 2);
}
EXPECT_EQ(track->getArtists({}).size(), 2);
EXPECT_EQ(track->getArtists({ TrackArtistLinkType::Artist }).size(), 2);
EXPECT_TRUE(track->getArtists({ TrackArtistLinkType::ReleaseArtist }).empty());
EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}).results.size(), 2);
EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setSortMethod(ArtistSortMethod::Random)).results.size(), 2);
}
{
auto transaction {session.createSharedTransaction()};
{
auto transaction{ session.createSharedTransaction() };
auto tracks {Track::find(session, Track::FindParameters {}.setArtist(artist1->getId()))};
ASSERT_EQ(tracks.results.size(), 1);
EXPECT_EQ(tracks.results.front(), track->getId());
auto tracks{ Track::findIds(session, Track::FindParameters {}.setArtist(artist1->getId())) };
ASSERT_EQ(tracks.results.size(), 1);
EXPECT_EQ(tracks.results.front(), track->getId());
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist2->getId()));
ASSERT_EQ(tracks.results.size(), 1);
EXPECT_EQ(tracks.results.front(), track->getId());
tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist2->getId()));
ASSERT_EQ(tracks.results.size(), 1);
EXPECT_EQ(tracks.results.front(), track->getId());
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist1->getId(), {TrackArtistLinkType::ReleaseArtist}));
EXPECT_EQ(tracks.results.size(), 0);
tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist1->getId(), { TrackArtistLinkType::ReleaseArtist }));
EXPECT_EQ(tracks.results.size(), 0);
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist1->getId(), {TrackArtistLinkType::Artist}));
EXPECT_EQ(tracks.results.size(), 1);
tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist1->getId(), { TrackArtistLinkType::Artist }));
EXPECT_EQ(tracks.results.size(), 1);
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist2->getId(), {TrackArtistLinkType::ReleaseArtist}));
EXPECT_EQ(tracks.results.size(), 0);
tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist2->getId(), { TrackArtistLinkType::ReleaseArtist }));
EXPECT_EQ(tracks.results.size(), 0);
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist2->getId(), {TrackArtistLinkType::Artist}));
EXPECT_EQ(tracks.results.size(), 1);
}
tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist2->getId(), { TrackArtistLinkType::Artist }));
EXPECT_EQ(tracks.results.size(), 1);
}
}
TEST_F(DatabaseFixture, Artist_findByName)
{
ScopedArtist artist {session, "AAA"};
ScopedTrack track {session, "MyTrack"}; // filters does not work on orphans
ScopedArtist artist{ session, "AAA" };
ScopedTrack track{ session, "MyTrack" }; // filters does not work on orphans
{
auto transaction {session.createUniqueTransaction()};
artist.get().modify()->setSortName("ZZZ");
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
}
{
auto transaction{ session.createUniqueTransaction() };
artist.get().modify()->setSortName("ZZZ");
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
}
{
auto transaction {session.createSharedTransaction()};
{
auto transaction{ session.createSharedTransaction() };
EXPECT_TRUE(Artist::find(session, Artist::FindParameters {}.setKeywords({"N"})).results.empty());
EXPECT_TRUE(Artist::findIds(session, Artist::FindParameters{}.setKeywords({ "N" })).results.empty());
const auto artistsByAAA {Artist::find(session, Artist::FindParameters {}.setKeywords({"A"}))};
ASSERT_EQ(artistsByAAA.results.size(), 1);
EXPECT_EQ(artistsByAAA.results.front(), artist.getId());
const auto artistsByAAA{ Artist::findIds(session, Artist::FindParameters {}.setKeywords({"A"})) };
ASSERT_EQ(artistsByAAA.results.size(), 1);
EXPECT_EQ(artistsByAAA.results.front(), artist.getId());
const auto artistsByZZZ {Artist::Artist::find(session, Artist::FindParameters {}.setKeywords({"Z"}))};
ASSERT_EQ(artistsByZZZ.results.size(), 1);
EXPECT_EQ(artistsByZZZ.results.front(), artist.getId());
const auto artistsByZZZ{ Artist::Artist::findIds(session, Artist::FindParameters {}.setKeywords({"Z"})) };
ASSERT_EQ(artistsByZZZ.results.size(), 1);
EXPECT_EQ(artistsByZZZ.results.front(), artist.getId());
EXPECT_TRUE(Artist::find(session, "NNN").empty());
EXPECT_EQ(Artist::find(session, "AAA").size(), 1);
}
EXPECT_TRUE(Artist::find(session, "NNN").empty());
EXPECT_EQ(Artist::find(session, "AAA").size(), 1);
}
}
TEST_F(DatabaseFixture, Artist_findByNameEscaped)
{
ScopedArtist artist1 {session, "MyArtist%"};
ScopedArtist artist2 {session, "%MyArtist"};
ScopedArtist artist3 {session, "%_MyArtist"};
ScopedArtist artist1{ session, R"(MyArtist%)" };
ScopedArtist artist2{ session, R"(%MyArtist)" };
ScopedArtist artist3{ session, R"(%_MyArtist)" };
ScopedArtist artist4 {session, "MyArtist%foo"};
ScopedArtist artist5 {session, "foo%MyArtist"};
ScopedArtist artist6 {session, "%AMyArtist"};
ScopedArtist artist4{ session, R"(MyArtist%foo)" };
ScopedArtist artist5{ session, R"(foo%MyArtist)" };
ScopedArtist artist6{ session, R"(%AMyArtist)" };
{
auto transaction {session.createSharedTransaction()};
{
const auto artists {Artist::find(session, "MyArtist%")};
ASSERT_TRUE(artists.size() == 1);
EXPECT_EQ(artists.front()->getId(), artist1.getId());
EXPECT_TRUE(Artist::find(session, "MyArtistFoo").empty());
}
{
const auto artists {Artist::find(session, "%MyArtist")};
ASSERT_TRUE(artists.size() == 1);
EXPECT_EQ(artists.front()->getId(), artist2.getId());
EXPECT_TRUE(Artist::find(session, "FooMyArtist").empty());
}
{
const auto artists {Artist::find(session, "%_MyArtist")};
ASSERT_TRUE(artists.size() == 1);
ASSERT_EQ(artists.front()->getId(), artist3.getId());
EXPECT_TRUE(Artist::find(session, "%CMyArtist").empty());
}
}
{
auto transaction{ session.createSharedTransaction() };
{
const auto artists{ Artist::find(session, R"(MyArtist%)") };
ASSERT_TRUE(artists.size() == 1);
EXPECT_EQ(artists.front()->getId(), artist1.getId());
EXPECT_TRUE(Artist::find(session, R"(MyArtistFoo)").empty());
}
{
const auto artists{ Artist::find(session, R"(%MyArtist)") };
ASSERT_TRUE(artists.size() == 1);
EXPECT_EQ(artists.front()->getId(), artist2.getId());
EXPECT_TRUE(Artist::find(session, R"(FooMyArtist)").empty());
}
{
const auto artists{ Artist::find(session, R"(%_MyArtist)") };
ASSERT_TRUE(artists.size() == 1);
ASSERT_EQ(artists.front()->getId(), artist3.getId());
EXPECT_TRUE(Artist::find(session, R"(%CMyArtist)").empty());
}
}
{
auto transaction {session.createSharedTransaction()};
{
const auto artists {Artist::find(session, Artist::FindParameters {}.setKeywords({"MyArtist"}))};
EXPECT_EQ(artists.results.size(), 6);
}
{
auto transaction{ session.createSharedTransaction() };
{
const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setKeywords({"MyArtist"})) };
EXPECT_EQ(artists.results.size(), 6);
}
{
const auto artists {Artist::find(session, Artist::FindParameters {}.setKeywords({"MyArtist%"}).setSortMethod(ArtistSortMethod::ByName))};
ASSERT_EQ(artists.results.size(), 2);
EXPECT_EQ(artists.results[0], artist1.getId());
EXPECT_EQ(artists.results[1], artist4.getId());
}
{
const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setKeywords({"MyArtist%"}).setSortMethod(ArtistSortMethod::ByName)) };
ASSERT_EQ(artists.results.size(), 2);
EXPECT_EQ(artists.results[0], artist1.getId());
EXPECT_EQ(artists.results[1], artist4.getId());
}
{
const auto artists {Artist::find(session, Artist::FindParameters {}.setKeywords({"%MyArtist"}).setSortMethod(ArtistSortMethod::ByName))};
ASSERT_EQ(artists.results.size(), 2);
EXPECT_EQ(artists.results[0], artist2.getId());
EXPECT_EQ(artists.results[1], artist5.getId());
}
{
const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setKeywords({"%MyArtist"}).setSortMethod(ArtistSortMethod::ByName)) };
ASSERT_EQ(artists.results.size(), 2);
EXPECT_EQ(artists.results[0], artist2.getId());
EXPECT_EQ(artists.results[1], artist5.getId());
}
{
const auto artists {Artist::find(session, Artist::FindParameters {}.setKeywords({"_MyArtist"}).setSortMethod(ArtistSortMethod::ByName))};
ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results[0], artist3.getId());
}
}
{
const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setKeywords({"_MyArtist"}).setSortMethod(ArtistSortMethod::ByName)) };
ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results[0], artist3.getId());
}
}
}
TEST_F(DatabaseFixture, Artist_sortMethod)
{
ScopedArtist artistA {session, "artistA"};
ScopedArtist artistB {session, "artistB"};
ScopedArtist artistA{ session, "artistA" };
ScopedArtist artistB{ session, "artistB" };
{
auto transaction {session.createUniqueTransaction()};
{
auto transaction{ session.createUniqueTransaction() };
artistA.get().modify()->setSortName("sortNameB");
artistB.get().modify()->setSortName("sortNameA");
}
artistA.get().modify()->setSortName("sortNameB");
artistB.get().modify()->setSortName("sortNameA");
}
{
auto transaction {session.createSharedTransaction()};
{
auto transaction{ session.createSharedTransaction() };
auto allArtistsByName {Artist::find(session, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::ByName))};
auto allArtistsBySortName {Artist::find(session, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::BySortName))};
auto allArtistsByName{ Artist::findIds(session, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::ByName)) };
auto allArtistsBySortName{ Artist::findIds(session, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::BySortName)) };
ASSERT_EQ(allArtistsByName.results.size(), 2);
EXPECT_EQ(allArtistsByName.results.front(), artistA.getId());
EXPECT_EQ(allArtistsByName.results.back(), artistB.getId());
ASSERT_EQ(allArtistsByName.results.size(), 2);
EXPECT_EQ(allArtistsByName.results.front(), artistA.getId());
EXPECT_EQ(allArtistsByName.results.back(), artistB.getId());
ASSERT_EQ(allArtistsBySortName.results.size(), 2);
EXPECT_EQ(allArtistsBySortName.results.front(), artistB.getId());
EXPECT_EQ(allArtistsBySortName.results.back(), artistA.getId());
}
ASSERT_EQ(allArtistsBySortName.results.size(), 2);
EXPECT_EQ(allArtistsBySortName.results.front(), artistB.getId());
EXPECT_EQ(allArtistsBySortName.results.back(), artistA.getId());
}
}
TEST_F(DatabaseFixture, Artist_nonReleaseTracks)
{
ScopedArtist artist {session, "artist"};
ScopedTrack track1 {session, "MyTrack1"};
ScopedTrack track2 {session, "MyTrack2"};
ScopedRelease release{session, "MyRelease"};
ScopedArtist artist{ session, "artist" };
ScopedTrack track1{ session, "MyTrack1" };
ScopedTrack track2{ session, "MyTrack2" };
ScopedRelease release{ session, "MyRelease" };
{
auto transaction {session.createSharedTransaction()};
{
auto transaction{ session.createSharedTransaction() };
auto tracks {Track::find(session, Track::FindParameters {}.setNonRelease(true).setArtist(artist->getId()))};
EXPECT_EQ(tracks.results.size(), 0);
}
auto tracks{ Track::findIds(session, Track::FindParameters {}.setNonRelease(true).setArtist(artist->getId())) };
EXPECT_EQ(tracks.results.size(), 0);
}
{
auto transaction {session.createUniqueTransaction()};
{
auto transaction{ session.createUniqueTransaction() };
TrackArtistLink::create(session, track1.get(), artist.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, track2.get(), artist.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, track1.get(), artist.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, track2.get(), artist.get(), TrackArtistLinkType::Artist);
track1.get().modify()->setRelease(release.get());
}
track1.get().modify()->setRelease(release.get());
}
{
auto transaction {session.createSharedTransaction()};
{
auto transaction{ session.createSharedTransaction() };
const auto tracks {Track::find(session, Track::FindParameters {}.setArtist(artist.getId()).setNonRelease(true))};
ASSERT_EQ(tracks.results.size(), 1);
EXPECT_EQ(tracks.results.front(), track2.getId());
}
const auto tracks{ Track::findIds(session, Track::FindParameters {}.setArtist(artist.getId()).setNonRelease(true)) };
ASSERT_EQ(tracks.results.size(), 1);
EXPECT_EQ(tracks.results.front(), track2.getId());
}
}
TEST_F(DatabaseFixture, Artist_findByRelease)
{
ScopedArtist artist {session, "artist"};
ScopedTrack track {session, "MyTrack"};
ScopedRelease release{session, "MyRelease"};
ScopedArtist artist{ session, "artist" };
ScopedTrack track{ session, "MyTrack" };
ScopedRelease release{ session, "MyRelease" };
{
auto transaction {session.createSharedTransaction()};
const auto artists {Artist::find(session, Artist::FindParameters {}.setRelease(release.getId()))};
EXPECT_EQ(artists.results.size(), 0);
}
{
auto transaction{ session.createSharedTransaction() };
const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setRelease(release.getId())) };
EXPECT_EQ(artists.results.size(), 0);
}
{
auto transaction {session.createUniqueTransaction()};
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
}
{
auto transaction{ session.createUniqueTransaction() };
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
}
{
auto transaction {session.createSharedTransaction()};
const auto artists {Artist::find(session, Artist::FindParameters {}.setRelease(release.getId()))};
EXPECT_EQ(artists.results.size(), 0);
}
{
auto transaction{ session.createSharedTransaction() };
const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setRelease(release.getId())) };
EXPECT_EQ(artists.results.size(), 0);
}
{
auto transaction {session.createUniqueTransaction()};
track.get().modify()->setRelease(release.get());
}
{
auto transaction{ session.createUniqueTransaction() };
track.get().modify()->setRelease(release.get());
}
{
auto transaction {session.createSharedTransaction()};
const auto artists {Artist::find(session, Artist::FindParameters {}.setRelease(release.getId()))};
ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId());
}
{
auto transaction{ session.createSharedTransaction() };
const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setRelease(release.getId())) };
ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId());
}
}
+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());
@@ -25,85 +25,85 @@ using namespace Database;
TEST_F(DatabaseFixture, MultiTracksSingleArtistSingleRelease)
{
constexpr std::size_t nbTracks {10};
std::list<ScopedTrack> tracks;
ScopedArtist artist {session, "MyArtst"};
ScopedRelease release {session, "MyRelease"};
constexpr std::size_t nbTracks{ 10 };
std::list<ScopedTrack> tracks;
ScopedArtist artist{ session, "MyArtst" };
ScopedRelease release{ session, "MyRelease" };
for (std::size_t i {}; i < nbTracks; ++i)
{
tracks.emplace_back(session, "MyTrack" + std::to_string(i));
for (std::size_t i{}; i < nbTracks; ++i)
{
tracks.emplace_back(session, "MyTrack" + std::to_string(i));
auto transaction {session.createUniqueTransaction()};
auto transaction{ session.createUniqueTransaction() };
TrackArtistLink::create(session, tracks.back().get(), artist.get(), TrackArtistLinkType::Artist);
tracks.back().get().modify()->setRelease(release.get());
}
TrackArtistLink::create(session, tracks.back().get(), artist.get(), TrackArtistLinkType::Artist);
tracks.back().get().modify()->setRelease(release.get());
}
{
auto transaction {session.createSharedTransaction()};
EXPECT_TRUE(Release::findOrphans(session, Range {}).results.empty());
EXPECT_TRUE(Artist::findAllOrphans(session, Range {}).results.empty());
}
{
auto transaction{ session.createSharedTransaction() };
EXPECT_TRUE(Release::findOrphanIds(session, Range{}).results.empty());
EXPECT_TRUE(Artist::findOrphanIds(session, Range{}).results.empty());
}
{
auto transaction {session.createSharedTransaction()};
{
auto transaction{ session.createSharedTransaction() };
auto releases {Release::find(session, Release::FindParameters {}.setArtist(artist.getId()))};
ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results.front(), release.getId());
auto releases{ Release::findIds(session, Release::FindParameters {}.setArtist(artist.getId())) };
ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results.front(), release.getId());
const auto releaseTracks {Track::find(session, Track::FindParameters {}.setRelease(release.getId()))};
EXPECT_EQ(releaseTracks.results.size(), nbTracks);
}
const auto releaseTracks{ Track::find(session, Track::FindParameters {}.setRelease(release.getId())) };
EXPECT_EQ(releaseTracks.results.size(), nbTracks);
}
}
TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtist)
{
ScopedTrack track {session, "MyTrack"};
ScopedRelease release {session, "MyRelease"};
ScopedArtist artist {session, "MyArtist"};
ScopedTrack track{ session, "MyTrack" };
ScopedRelease release{ session, "MyRelease" };
ScopedArtist artist{ session, "MyArtist" };
{
auto transaction {session.createUniqueTransaction()};
{
auto transaction{ session.createUniqueTransaction() };
auto trackArtistLink {TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist)};
track.get().modify()->setRelease(release.get());
}
auto trackArtistLink{ TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist) };
track.get().modify()->setRelease(release.get());
}
{
auto transaction {session.createUniqueTransaction()};
{
auto transaction{ session.createUniqueTransaction() };
auto releases {Release::find(session, Release::FindParameters {}.setArtist(artist.getId()))};
ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results.front(), release.getId());
auto releases{ Release::findIds(session, Release::FindParameters {}.setArtist(artist.getId())) };
ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results.front(), release.getId());
auto artists {release->getArtists()};
ASSERT_EQ(artists.size(), 1);
ASSERT_EQ(artists.front()->getId(), artist.getId());
}
auto artists{ release->getArtists() };
ASSERT_EQ(artists.size(), 1);
ASSERT_EQ(artists.front()->getId(), artist.getId());
}
}
TEST_F(DatabaseFixture, SingleUser)
{
{
auto transaction {session.createSharedTransaction()};
EXPECT_TRUE(User::find(session, User::FindParameters {}).results.empty());
EXPECT_EQ(User::getCount(session), 0);
}
{
auto transaction{ session.createSharedTransaction() };
EXPECT_TRUE(User::find(session, User::FindParameters{}).results.empty());
EXPECT_EQ(User::getCount(session), 0);
}
ScopedUser user {session, "MyUser"};
ScopedUser user{ session, "MyUser" };
{
auto transaction {session.createSharedTransaction()};
{
auto transaction{ session.createSharedTransaction() };
EXPECT_EQ(User::find(session, User::FindParameters {}).results.size(), 1);
EXPECT_EQ(User::getCount(session), 1);
}
EXPECT_EQ(User::find(session, User::FindParameters{}).results.size(), 1);
EXPECT_EQ(User::getCount(session), 1);
}
}
int main(int argc, char **argv)
int main(int argc, char** argv)
{
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
+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());
}
File diff suppressed because it is too large Load Diff
@@ -26,102 +26,100 @@ using ScopedStarredArtist = ScopedEntity<Database::StarredArtist>;
TEST_F(DatabaseFixture, StarredArtist)
{
ScopedArtist artist {session, "MyArtist"};
ScopedUser user {session, "MyUser"};
ScopedUser user2 {session, "MyUser2"};
ScopedArtist artist{ session, "MyArtist" };
ScopedUser user{ session, "MyUser" };
ScopedUser user2{ session, "MyUser2" };
{
auto transaction {session.createSharedTransaction()};
{
auto transaction{ session.createSharedTransaction() };
auto starredArtist {StarredArtist::find(session, artist->getId(), user->getId(), Scrobbler::Internal)};
EXPECT_FALSE(starredArtist);
EXPECT_EQ(StarredArtist::getCount(session), 0);
auto starredArtist{ StarredArtist::find(session, artist->getId(), user->getId(), FeedbackBackend::Internal) };
EXPECT_FALSE(starredArtist);
EXPECT_EQ(StarredArtist::getCount(session), 0);
auto artists {Artist::find(session, Artist::FindParameters {})};
EXPECT_EQ(artists.results.size(), 1);
}
auto artists{ Artist::findIds(session, Artist::FindParameters {}) };
EXPECT_EQ(artists.results.size(), 1);
}
ScopedStarredArtist starredArtist {session, artist.lockAndGet(), user.lockAndGet(), Scrobbler::Internal};
{
auto transaction {session.createSharedTransaction()};
ScopedStarredArtist starredArtist{ session, artist.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal };
{
auto transaction{ session.createSharedTransaction() };
auto gotArtist {StarredArtist::find(session, artist->getId(), user->getId(), Scrobbler::Internal)};
EXPECT_EQ(gotArtist->getId(), starredArtist->getId());
EXPECT_EQ(StarredArtist::getCount(session), 1);
}
auto gotArtist{ StarredArtist::find(session, artist->getId(), user->getId(), FeedbackBackend::Internal) };
EXPECT_EQ(gotArtist->getId(), starredArtist->getId());
EXPECT_EQ(StarredArtist::getCount(session), 1);
}
{
auto transaction {session.createSharedTransaction()};
{
auto transaction{ session.createSharedTransaction() };
auto artists {Artist::find(session, Artist::FindParameters {})};
EXPECT_EQ(artists.results.size(), 1);
auto artists{ Artist::findIds(session, Artist::FindParameters {}) };
EXPECT_EQ(artists.results.size(), 1);
artists = Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal));
EXPECT_EQ(artists.results.size(), 1);
artists = Artist::findIds(session, Artist::FindParameters{}.setStarringUser(user.getId(), FeedbackBackend::Internal));
EXPECT_EQ(artists.results.size(), 1);
artists = Artist::find(session, Artist::FindParameters {}.setStarringUser(user2.getId(), Scrobbler::Internal));
EXPECT_EQ(artists.results.size(), 0);
}
artists = Artist::findIds(session, Artist::FindParameters{}.setStarringUser(user2.getId(), FeedbackBackend::Internal));
EXPECT_EQ(artists.results.size(), 0);
}
}
TEST_F(DatabaseFixture, StarredArtist_PendingDestroy)
{
ScopedArtist artist {session, "MyArtist"};
ScopedUser user {session, "MyUser"};
ScopedStarredArtist starredArtist {session, artist.lockAndGet(), user.lockAndGet(), Scrobbler::Internal};
ScopedArtist artist{ session, "MyArtist" };
ScopedUser user{ session, "MyUser" };
ScopedStarredArtist starredArtist{ session, artist.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal };
{
auto transaction {session.createUniqueTransaction()};
{
auto transaction{ session.createUniqueTransaction() };
auto artists {Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal))};
EXPECT_EQ(artists.results.size(), 1);
auto artists{ Artist::findIds(session, Artist::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal)) };
EXPECT_EQ(artists.results.size(), 1);
starredArtist.get().modify()->setScrobblingState(ScrobblingState::PendingRemove);
artists = Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal));
EXPECT_EQ(artists.results.size(), 0);
}
starredArtist.get().modify()->setSyncState(SyncState::PendingRemove);
artists = Artist::findIds(session, Artist::FindParameters{}.setStarringUser(user.getId(), FeedbackBackend::Internal));
EXPECT_EQ(artists.results.size(), 0);
}
}
TEST_F(DatabaseFixture, StarredArtist_dateTime)
{
ScopedArtist artist1 {session, "MyArtist1"};
ScopedArtist artist2 {session, "MyArtist2"};
ScopedUser user {session, "MyUser"};
ScopedArtist artist1{ session, "MyArtist1" };
ScopedArtist artist2{ session, "MyArtist2" };
ScopedUser user{ session, "MyUser" };
ScopedStarredArtist starredArtist1 {session, artist1.lockAndGet(), user.lockAndGet(), Scrobbler::Internal};
ScopedStarredArtist starredArtist2 {session, artist2.lockAndGet(), user.lockAndGet(), Scrobbler::Internal};
ScopedStarredArtist starredArtist1{ session, artist1.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal };
ScopedStarredArtist starredArtist2{ session, artist2.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal };
const Wt::WDateTime dateTime {Wt::WDate {1950, 1, 2}, Wt::WTime {12, 30, 1}};
const Wt::WDateTime dateTime{ Wt::WDate {1950, 1, 2}, Wt::WTime {12, 30, 1} };
{
auto transaction {session.createSharedTransaction()};
{
auto transaction{ session.createSharedTransaction() };
auto artists {Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal))};
EXPECT_EQ(artists.results.size(), 2);
}
auto artists{ Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal)) };
EXPECT_EQ(artists.results.size(), 2);
}
{
auto transaction {session.createUniqueTransaction()};
{
auto transaction{ session.createUniqueTransaction() };
starredArtist1.get().modify()->setDateTime(dateTime);
starredArtist2.get().modify()->setDateTime(dateTime.addSecs(-1));
starredArtist1.get().modify()->setDateTime(dateTime);
starredArtist2.get().modify()->setDateTime(dateTime.addSecs(-1));
auto artists {Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal)
.setSortMethod(ArtistSortMethod::StarredDateDesc))};
ASSERT_EQ(artists.results.size(), 2);
EXPECT_EQ(artists.results[0], starredArtist1->getArtist()->getId());
EXPECT_EQ(artists.results[1], starredArtist2->getArtist()->getId());
}
{
auto transaction {session.createUniqueTransaction()};
auto artists{ Artist::findIds(session, Artist::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal).setSortMethod(ArtistSortMethod::StarredDateDesc)) };
ASSERT_EQ(artists.results.size(), 2);
EXPECT_EQ(artists.results[0], starredArtist1->getArtist()->getId());
EXPECT_EQ(artists.results[1], starredArtist2->getArtist()->getId());
}
{
auto transaction{ session.createUniqueTransaction() };
starredArtist1.get().modify()->setDateTime(dateTime);
starredArtist2.get().modify()->setDateTime(dateTime.addSecs(1));
starredArtist1.get().modify()->setDateTime(dateTime);
starredArtist2.get().modify()->setDateTime(dateTime.addSecs(1));
auto artists {Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal)
.setSortMethod(ArtistSortMethod::StarredDateDesc))};
ASSERT_EQ(artists.results.size(), 2);
EXPECT_EQ(artists.results[0], starredArtist2->getArtist()->getId());
EXPECT_EQ(artists.results[1], starredArtist1->getArtist()->getId());
}
auto artists{ Artist::findIds(session, Artist::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal).setSortMethod(ArtistSortMethod::StarredDateDesc)) };
ASSERT_EQ(artists.results.size(), 2);
EXPECT_EQ(artists.results[0], starredArtist2->getArtist()->getId());
EXPECT_EQ(artists.results[1], starredArtist1->getArtist()->getId());
}
}
@@ -26,102 +26,100 @@ using ScopedStarredRelease = ScopedEntity<Database::StarredRelease>;
TEST_F(DatabaseFixture, StarredRelease)
{
ScopedRelease release {session, "MyRelease"};
ScopedUser user {session, "MyUser"};
ScopedUser user2 {session, "MyUser2"};
ScopedRelease release{ session, "MyRelease" };
ScopedUser user{ session, "MyUser" };
ScopedUser user2{ session, "MyUser2" };
{
auto transaction {session.createSharedTransaction()};
{
auto transaction{ session.createSharedTransaction() };
auto starredRelease {StarredRelease::find(session, release->getId(), user->getId(), Scrobbler::Internal)};
EXPECT_FALSE(starredRelease);
EXPECT_EQ(StarredRelease::getCount(session), 0);
auto starredRelease{ StarredRelease::find(session, release->getId(), user->getId(), FeedbackBackend::Internal) };
EXPECT_FALSE(starredRelease);
EXPECT_EQ(StarredRelease::getCount(session), 0);
auto releases {Release::find(session, Release::FindParameters {})};
EXPECT_EQ(releases.results.size(), 1);
}
auto releases{ Release::find(session, Release::FindParameters {}) };
EXPECT_EQ(releases.results.size(), 1);
}
ScopedStarredRelease starredRelease {session, release.lockAndGet(), user.lockAndGet(), Scrobbler::Internal};
{
auto transaction {session.createSharedTransaction()};
ScopedStarredRelease starredRelease{ session, release.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal };
{
auto transaction{ session.createSharedTransaction() };
auto gotRelease {StarredRelease::find(session, release->getId(), user->getId(), Scrobbler::Internal)};
EXPECT_EQ(gotRelease->getId(), starredRelease->getId());
EXPECT_EQ(StarredRelease::getCount(session), 1);
}
auto gotRelease{ StarredRelease::find(session, release->getId(), user->getId(), FeedbackBackend::Internal) };
EXPECT_EQ(gotRelease->getId(), starredRelease->getId());
EXPECT_EQ(StarredRelease::getCount(session), 1);
}
{
auto transaction {session.createSharedTransaction()};
{
auto transaction{ session.createSharedTransaction() };
auto releases {Release::find(session, Release::FindParameters {})};
EXPECT_EQ(releases.results.size(), 1);
auto releases{ Release::find(session, Release::FindParameters {}) };
EXPECT_EQ(releases.results.size(), 1);
releases = Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal));
EXPECT_EQ(releases.results.size(), 1);
releases = Release::find(session, Release::FindParameters{}.setStarringUser(user.getId(), FeedbackBackend::Internal));
EXPECT_EQ(releases.results.size(), 1);
releases = Release::find(session, Release::FindParameters {}.setStarringUser(user2.getId(), Scrobbler::Internal));
EXPECT_EQ(releases.results.size(), 0);
}
releases = Release::find(session, Release::FindParameters{}.setStarringUser(user2.getId(), FeedbackBackend::Internal));
EXPECT_EQ(releases.results.size(), 0);
}
}
TEST_F(DatabaseFixture, Starredrelease_PendingDestroy)
{
ScopedRelease release {session, "MyRelease"};
ScopedUser user {session, "MyUser"};
ScopedStarredRelease starredRelease {session, release.lockAndGet(), user.lockAndGet(), Scrobbler::Internal};
ScopedRelease release{ session, "MyRelease" };
ScopedUser user{ session, "MyUser" };
ScopedStarredRelease starredRelease{ session, release.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal };
{
auto transaction {session.createUniqueTransaction()};
{
auto transaction{ session.createUniqueTransaction() };
auto releases {Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal))};
EXPECT_EQ(releases.results.size(), 1);
auto releases{ Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal)) };
EXPECT_EQ(releases.results.size(), 1);
starredRelease.get().modify()->setScrobblingState(ScrobblingState::PendingRemove);
releases = Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal));
EXPECT_EQ(releases.results.size(), 0);
}
starredRelease.get().modify()->setSyncState(SyncState::PendingRemove);
releases = Release::find(session, Release::FindParameters{}.setStarringUser(user.getId(), FeedbackBackend::Internal));
EXPECT_EQ(releases.results.size(), 0);
}
}
TEST_F(DatabaseFixture, StarredRelease_dateTime)
{
ScopedRelease release1 {session, "MyRelease1"};
ScopedRelease release2 {session, "MyRelease2"};
ScopedUser user {session, "MyUser"};
ScopedRelease release1{ session, "MyRelease1" };
ScopedRelease release2{ session, "MyRelease2" };
ScopedUser user{ session, "MyUser" };
ScopedStarredRelease starredRelease1 {session, release1.lockAndGet(), user.lockAndGet(), Scrobbler::Internal};
ScopedStarredRelease starredRelease2 {session, release2.lockAndGet(), user.lockAndGet(), Scrobbler::Internal};
ScopedStarredRelease starredRelease1{ session, release1.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal };
ScopedStarredRelease starredRelease2{ session, release2.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal };
const Wt::WDateTime dateTime {Wt::WDate {1950, 1, 2}, Wt::WTime {12, 30, 1}};
const Wt::WDateTime dateTime{ Wt::WDate {1950, 1, 2}, Wt::WTime {12, 30, 1} };
{
auto transaction {session.createSharedTransaction()};
{
auto transaction{ session.createSharedTransaction() };
auto releases {Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal))};
EXPECT_EQ(releases.results.size(), 2);
}
auto releases{ Release::findIds(session, Release::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal)) };
EXPECT_EQ(releases.results.size(), 2);
}
{
auto transaction {session.createUniqueTransaction()};
{
auto transaction{ session.createUniqueTransaction() };
starredRelease1.get().modify()->setDateTime(dateTime);
starredRelease2.get().modify()->setDateTime(dateTime.addSecs(-1));
starredRelease1.get().modify()->setDateTime(dateTime);
starredRelease2.get().modify()->setDateTime(dateTime.addSecs(-1));
auto releases {Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal)
.setSortMethod(ReleaseSortMethod::StarredDateDesc))};
ASSERT_EQ(releases.results.size(), 2);
EXPECT_EQ(releases.results[0], starredRelease1->getRelease()->getId());
EXPECT_EQ(releases.results[1], starredRelease2->getRelease()->getId());
}
{
auto transaction {session.createUniqueTransaction()};
auto releases{ Release::findIds(session, Release::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal).setSortMethod(ReleaseSortMethod::StarredDateDesc)) };
ASSERT_EQ(releases.results.size(), 2);
EXPECT_EQ(releases.results[0], starredRelease1->getRelease()->getId());
EXPECT_EQ(releases.results[1], starredRelease2->getRelease()->getId());
}
{
auto transaction{ session.createUniqueTransaction() };
starredRelease1.get().modify()->setDateTime(dateTime);
starredRelease2.get().modify()->setDateTime(dateTime.addSecs(1));
starredRelease1.get().modify()->setDateTime(dateTime);
starredRelease2.get().modify()->setDateTime(dateTime.addSecs(1));
auto releases {Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal)
.setSortMethod(ReleaseSortMethod::StarredDateDesc))};
ASSERT_EQ(releases.results.size(), 2);
EXPECT_EQ(releases.results[0], starredRelease2->getRelease()->getId());
EXPECT_EQ(releases.results[1], starredRelease1->getRelease()->getId());
}
auto releases{ Release::findIds(session, Release::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal).setSortMethod(ReleaseSortMethod::StarredDateDesc)) };
ASSERT_EQ(releases.results.size(), 2);
EXPECT_EQ(releases.results[0], starredRelease2->getRelease()->getId());
EXPECT_EQ(releases.results[1], starredRelease1->getRelease()->getId());
}
}
@@ -26,102 +26,100 @@ using ScopedStarredTrack = ScopedEntity<Database::StarredTrack>;
TEST_F(DatabaseFixture, StarredTrack)
{
ScopedTrack track {session, "MyTrack"};
ScopedUser user {session, "MyUser"};
ScopedUser user2 {session, "MyUser2"};
ScopedTrack track {session, "MyTrack"};
ScopedUser user {session, "MyUser"};
ScopedUser user2 {session, "MyUser2"};
{
auto transaction {session.createSharedTransaction()};
{
auto transaction {session.createSharedTransaction()};
auto starredTrack {StarredTrack::find(session, track->getId(), user->getId(), Scrobbler::Internal)};
EXPECT_FALSE(starredTrack);
EXPECT_EQ(StarredTrack::getCount(session), 0);
auto starredTrack {StarredTrack::find(session, track->getId(), user->getId(), FeedbackBackend::Internal)};
EXPECT_FALSE(starredTrack);
EXPECT_EQ(StarredTrack::getCount(session), 0);
auto tracks {Track::find(session, Track::FindParameters {})};
EXPECT_EQ(tracks.results.size(), 1);
}
auto tracks {Track::findIds(session, Track::FindParameters {})};
EXPECT_EQ(tracks.results.size(), 1);
}
ScopedStarredTrack starredTrack {session, track.lockAndGet(), user.lockAndGet(), Scrobbler::Internal};
{
auto transaction {session.createSharedTransaction()};
ScopedStarredTrack starredTrack {session, track.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal};
{
auto transaction {session.createSharedTransaction()};
auto gotTrack {StarredTrack::find(session, track->getId(), user->getId(), Scrobbler::Internal)};
EXPECT_EQ(gotTrack->getId(), starredTrack->getId());
EXPECT_EQ(StarredTrack::getCount(session), 1);
}
auto gotTrack {StarredTrack::find(session, track->getId(), user->getId(), FeedbackBackend::Internal)};
EXPECT_EQ(gotTrack->getId(), starredTrack->getId());
EXPECT_EQ(StarredTrack::getCount(session), 1);
}
{
auto transaction {session.createSharedTransaction()};
{
auto transaction {session.createSharedTransaction()};
auto tracks {Track::find(session, Track::FindParameters {})};
EXPECT_EQ(tracks.results.size(), 1);
auto tracks {Track::findIds(session, Track::FindParameters {})};
EXPECT_EQ(tracks.results.size(), 1);
tracks = Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal));
EXPECT_EQ(tracks.results.size(), 1);
tracks = Track::findIds(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal));
EXPECT_EQ(tracks.results.size(), 1);
tracks = Track::find(session, Track::FindParameters {}.setStarringUser(user2.getId(), Scrobbler::Internal));
EXPECT_EQ(tracks.results.size(), 0);
}
tracks = Track::findIds(session, Track::FindParameters {}.setStarringUser(user2.getId(), FeedbackBackend::Internal));
EXPECT_EQ(tracks.results.size(), 0);
}
}
TEST_F(DatabaseFixture, Starredtrack_PendingDestroy)
{
ScopedTrack track {session, "MyTrack"};
ScopedUser user {session, "MyUser"};
ScopedStarredTrack starredTrack {session, track.lockAndGet(), user.lockAndGet(), Scrobbler::Internal};
ScopedTrack track {session, "MyTrack"};
ScopedUser user {session, "MyUser"};
ScopedStarredTrack starredTrack {session, track.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal};
{
auto transaction {session.createUniqueTransaction()};
{
auto transaction {session.createUniqueTransaction()};
auto tracks {Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal))};
EXPECT_EQ(tracks.results.size(), 1);
auto tracks {Track::findIds(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal))};
EXPECT_EQ(tracks.results.size(), 1);
starredTrack.get().modify()->setScrobblingState(ScrobblingState::PendingRemove);
tracks = Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal));
EXPECT_EQ(tracks.results.size(), 0);
}
starredTrack.get().modify()->setSyncState(SyncState::PendingRemove);
tracks = Track::findIds(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal));
EXPECT_EQ(tracks.results.size(), 0);
}
}
TEST_F(DatabaseFixture, StarredTrack_dateTime)
{
ScopedTrack track1 {session, "MyTrack1"};
ScopedTrack track2 {session, "MyTrack2"};
ScopedUser user {session, "MyUser"};
ScopedTrack track1 {session, "MyTrack1"};
ScopedTrack track2 {session, "MyTrack2"};
ScopedUser user {session, "MyUser"};
ScopedStarredTrack starredTrack1 {session, track1.lockAndGet(), user.lockAndGet(), Scrobbler::Internal};
ScopedStarredTrack starredTrack2 {session, track2.lockAndGet(), user.lockAndGet(), Scrobbler::Internal};
ScopedStarredTrack starredTrack1 {session, track1.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal};
ScopedStarredTrack starredTrack2 {session, track2.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal};
const Wt::WDateTime dateTime {Wt::WDate {1950, 1, 2}, Wt::WTime {12, 30, 1}};
const Wt::WDateTime dateTime {Wt::WDate {1950, 1, 2}, Wt::WTime {12, 30, 1}};
{
auto transaction {session.createSharedTransaction()};
{
auto transaction {session.createSharedTransaction()};
auto tracks {Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal))};
EXPECT_EQ(tracks.results.size(), 2);
}
auto tracks {Track::findIds(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal))};
EXPECT_EQ(tracks.results.size(), 2);
}
{
auto transaction {session.createUniqueTransaction()};
{
auto transaction {session.createUniqueTransaction()};
starredTrack1.get().modify()->setDateTime(dateTime);
starredTrack2.get().modify()->setDateTime(dateTime.addSecs(-1));
starredTrack1.get().modify()->setDateTime(dateTime);
starredTrack2.get().modify()->setDateTime(dateTime.addSecs(-1));
auto tracks {Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal)
.setSortMethod(TrackSortMethod::StarredDateDesc))};
ASSERT_EQ(tracks.results.size(), 2);
EXPECT_EQ(tracks.results[0], starredTrack1->getTrack()->getId());
EXPECT_EQ(tracks.results[1], starredTrack2->getTrack()->getId());
}
{
auto transaction {session.createUniqueTransaction()};
auto tracks {Track::findIds(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal).setSortMethod(TrackSortMethod::StarredDateDesc))};
ASSERT_EQ(tracks.results.size(), 2);
EXPECT_EQ(tracks.results[0], starredTrack1->getTrack()->getId());
EXPECT_EQ(tracks.results[1], starredTrack2->getTrack()->getId());
}
{
auto transaction {session.createUniqueTransaction()};
starredTrack1.get().modify()->setDateTime(dateTime);
starredTrack2.get().modify()->setDateTime(dateTime.addSecs(1));
starredTrack1.get().modify()->setDateTime(dateTime);
starredTrack2.get().modify()->setDateTime(dateTime.addSecs(1));
auto tracks {Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal)
.setSortMethod(TrackSortMethod::StarredDateDesc))};
ASSERT_EQ(tracks.results.size(), 2);
EXPECT_EQ(tracks.results[0], starredTrack2->getTrack()->getId());
EXPECT_EQ(tracks.results[1], starredTrack1->getTrack()->getId());
}
auto tracks {Track::findIds(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal).setSortMethod(TrackSortMethod::StarredDateDesc))};
ASSERT_EQ(tracks.results.size(), 2);
EXPECT_EQ(tracks.results[0], starredTrack2->getTrack()->getId());
EXPECT_EQ(tracks.results[1], starredTrack1->getTrack()->getId());
}
}
+101 -100
View File
@@ -25,137 +25,138 @@ using namespace Database;
TEST_F(DatabaseFixture, Track)
{
{
auto transaction {session.createSharedTransaction()};
EXPECT_EQ(Track::find(session, Track::FindParameters {}).results.size(), 0);
EXPECT_EQ(Track::getCount(session), 0);
EXPECT_FALSE(Track::exists(session, 0));
}
{
auto transaction{ session.createSharedTransaction() };
EXPECT_EQ(Track::find(session, Track::FindParameters{}).results.size(), 0);
EXPECT_EQ(Track::findIds(session, Track::FindParameters{}).results.size(), 0);
EXPECT_EQ(Track::getCount(session), 0);
EXPECT_FALSE(Track::exists(session, 0));
}
ScopedTrack track {session, "MyTrackFile"};
ScopedTrack track{ session, "MyTrackFile" };
{
auto transaction {session.createSharedTransaction()};
{
auto transaction{ session.createSharedTransaction() };
EXPECT_EQ(Track::find(session, Track::FindParameters {}).results.size(), 1);
EXPECT_EQ(Track::getCount(session), 1);
EXPECT_TRUE(Track::exists(session, track.getId()));
auto myTrack {Track::find(session, track.getId())};
ASSERT_TRUE(myTrack);
EXPECT_EQ(myTrack->getId(), track.getId());
}
EXPECT_EQ(Track::find(session, Track::FindParameters{}).results.size(), 1);
EXPECT_EQ(Track::getCount(session), 1);
EXPECT_TRUE(Track::exists(session, track.getId()));
auto myTrack{ Track::find(session, track.getId()) };
ASSERT_TRUE(myTrack);
EXPECT_EQ(myTrack->getId(), track.getId());
}
}
TEST_F(DatabaseFixture, MultipleTracks)
{
ScopedTrack track1 {session, "MyTrackFile1"};
ScopedTrack track2 {session, "MyTrackFile2"};
ScopedTrack track1{ session, "MyTrackFile1" };
ScopedTrack track2{ session, "MyTrackFile2" };
{
auto transaction {session.createSharedTransaction()};
{
auto transaction{ session.createSharedTransaction() };
EXPECT_TRUE(track1.getId() != track2.getId());
EXPECT_TRUE(track1.get() != track2.get());
EXPECT_FALSE(track1.get() == track2.get());
}
EXPECT_TRUE(track1.getId() != track2.getId());
EXPECT_TRUE(track1.get() != track2.get());
EXPECT_FALSE(track1.get() == track2.get());
}
}
TEST_F(DatabaseFixture, MultipleTracksSearchByFilter)
{
ScopedTrack track1 {session, ""};
ScopedTrack track2 {session, ""};
ScopedTrack track3 {session, ""};
ScopedTrack track4 {session, ""};
ScopedTrack track5 {session, ""};
ScopedTrack track6 {session, ""};
ScopedTrack track1{ session, "" };
ScopedTrack track2{ session, "" };
ScopedTrack track3{ session, "" };
ScopedTrack track4{ session, "" };
ScopedTrack track5{ session, "" };
ScopedTrack track6{ session, "" };
{
auto transaction {session.createUniqueTransaction()};
track1.get().modify()->setName("MyTrack");
track2.get().modify()->setName("MyTrack%");
track3.get().modify()->setName("MyTrack%Foo");
track4.get().modify()->setName("%MyTrack");
track5.get().modify()->setName("Foo%MyTrack");
track6.get().modify()->setName("M_Track");
}
{
auto transaction{ session.createUniqueTransaction() };
track1.get().modify()->setName("MyTrack");
track2.get().modify()->setName("MyTrack%");
track3.get().modify()->setName("MyTrack%Foo");
track4.get().modify()->setName("%MyTrack");
track5.get().modify()->setName("Foo%MyTrack");
track6.get().modify()->setName("M_Track");
}
{
auto transaction {session.createSharedTransaction()};
{
auto transaction{ session.createSharedTransaction() };
{
const auto tracks {Track::find(session, Track::FindParameters {}.setKeywords({"Track"}))};
EXPECT_EQ(tracks.results.size(), 6);
}
{
const auto tracks {Track::find(session, Track::FindParameters {}.setKeywords({"MyTrack"}))};
EXPECT_EQ(tracks.results.size(), 5);
EXPECT_TRUE(std::none_of(std::cbegin(tracks.results), std::cend(tracks.results), [&](const TrackId trackId) { return trackId == track6.getId(); }));
}
{
const auto tracks {Track::find(session, Track::FindParameters {}.setKeywords({"MyTrack%"}))};
ASSERT_EQ(tracks.results.size(), 2);
EXPECT_EQ(tracks.results[0], track2.getId());
EXPECT_EQ(tracks.results[1], track3.getId());
}
{
const auto tracks {Track::find(session, Track::FindParameters {}.setKeywords({"%MyTrack"}))};
ASSERT_EQ(tracks.results.size(), 2);
EXPECT_EQ(tracks.results[0], track4.getId());
EXPECT_EQ(tracks.results[1], track5.getId());
}
}
{
const auto tracks{ Track::findIds(session, Track::FindParameters {}.setKeywords({"Track"})) };
EXPECT_EQ(tracks.results.size(), 6);
}
{
const auto tracks{ Track::findIds(session, Track::FindParameters {}.setKeywords({"MyTrack"})) };
EXPECT_EQ(tracks.results.size(), 5);
EXPECT_TRUE(std::none_of(std::cbegin(tracks.results), std::cend(tracks.results), [&](const TrackId trackId) { return trackId == track6.getId(); }));
}
{
const auto tracks{ Track::findIds(session, Track::FindParameters {}.setKeywords({"MyTrack%"})) };
ASSERT_EQ(tracks.results.size(), 2);
EXPECT_EQ(tracks.results[0], track2.getId());
EXPECT_EQ(tracks.results[1], track3.getId());
}
{
const auto tracks{ Track::findIds(session, Track::FindParameters {}.setKeywords({"%MyTrack"})) };
ASSERT_EQ(tracks.results.size(), 2);
EXPECT_EQ(tracks.results[0], track4.getId());
EXPECT_EQ(tracks.results[1], track5.getId());
}
}
}
TEST_F(DatabaseFixture, Track_date)
{
ScopedTrack track {session, "MyTrack"};
ScopedTrack track{ session, "MyTrack" };
{
auto transaction {session.createSharedTransaction()};
EXPECT_EQ(track->getYear(), std::nullopt);
EXPECT_EQ(track->getOriginalYear(), std::nullopt);
}
{
auto transaction{ session.createSharedTransaction() };
EXPECT_EQ(track->getYear(), std::nullopt);
EXPECT_EQ(track->getOriginalYear(), std::nullopt);
}
{
auto transaction {session.createUniqueTransaction()};
track.get().modify()->setDate(Wt::WDate {1995, 5, 5});
track.get().modify()->setOriginalDate(Wt::WDate {1994, 2, 2});
}
{
auto transaction{ session.createUniqueTransaction() };
track.get().modify()->setDate(Wt::WDate{ 1995, 5, 5 });
track.get().modify()->setOriginalDate(Wt::WDate{ 1994, 2, 2 });
}
{
auto transaction {session.createSharedTransaction()};
EXPECT_EQ(track->getYear(), 1995);
EXPECT_EQ(track->getOriginalYear(), 1994);
}
{
auto transaction{ session.createSharedTransaction() };
EXPECT_EQ(track->getYear(), 1995);
EXPECT_EQ(track->getOriginalYear(), 1994);
}
}
TEST_F(DatabaseFixture, Track_writtenAfter)
{
ScopedTrack track {session, "MyTrack"};
ScopedTrack track{ session, "MyTrack" };
const Wt::WDateTime dateTime {Wt::WDate {1950, 1, 1}, Wt::WTime {12, 30, 20}};
const Wt::WDateTime dateTime{ Wt::WDate {1950, 1, 1}, Wt::WTime {12, 30, 20} };
{
auto transaction {session.createUniqueTransaction()};
track.get().modify()->setLastWriteTime(dateTime);
}
{
auto transaction{ session.createUniqueTransaction() };
track.get().modify()->setLastWriteTime(dateTime);
}
{
auto transaction {session.createSharedTransaction()};
const auto tracks {Track::find(session, Track::FindParameters {})};
EXPECT_EQ(tracks.results.size(), 1);
}
{
auto transaction{ session.createSharedTransaction() };
const auto tracks{ Track::findIds(session, Track::FindParameters {}) };
EXPECT_EQ(tracks.results.size(), 1);
}
{
auto transaction {session.createSharedTransaction()};
const auto tracks {Track::find(session, Track::FindParameters {}.setWrittenAfter(dateTime.addSecs(-1)))};
EXPECT_EQ(tracks.results.size(), 1);
}
{
auto transaction{ session.createSharedTransaction() };
const auto tracks{ Track::findIds(session, Track::FindParameters {}.setWrittenAfter(dateTime.addSecs(-1))) };
EXPECT_EQ(tracks.results.size(), 1);
}
{
auto transaction {session.createSharedTransaction()};
const auto tracks {Track::find(session, Track::FindParameters {}.setWrittenAfter(dateTime.addSecs(+1)))};
EXPECT_EQ(tracks.results.size(), 0);
}
{
auto transaction{ session.createSharedTransaction() };
const auto tracks{ Track::findIds(session, Track::FindParameters {}.setWrittenAfter(dateTime.addSecs(+1))) };
EXPECT_EQ(tracks.results.size(), 0);
}
}
@@ -49,10 +49,10 @@ TEST_F(DatabaseFixture, SingleTrackListSingleTrack)
{
auto transaction {session.createSharedTransaction()};
auto tracks {Track::find(session, Track::FindParameters {}.setTrackList(trackList1.getId()))};
auto tracks {Track::findIds(session, Track::FindParameters {}.setTrackList(trackList1.getId()))};
EXPECT_EQ(tracks.results.size(), 0);
tracks = Track::find(session, Track::FindParameters {}.setTrackList(trackList2.getId()));
tracks = Track::findIds(session, Track::FindParameters {}.setTrackList(trackList2.getId()));
EXPECT_EQ(tracks.results.size(), 0);
}
@@ -65,11 +65,11 @@ TEST_F(DatabaseFixture, SingleTrackListSingleTrack)
{
auto transaction {session.createSharedTransaction()};
auto tracks {Track::find(session, Track::FindParameters {}.setTrackList(trackList1.getId()))};
auto tracks {Track::findIds(session, Track::FindParameters {}.setTrackList(trackList1.getId()))};
ASSERT_EQ(tracks.results.size(), 1);
EXPECT_EQ(tracks.results.front(), track.getId());
tracks = Track::find(session, Track::FindParameters {}.setTrackList(trackList2.getId()));
tracks = Track::findIds(session, Track::FindParameters {}.setTrackList(trackList2.getId()));
EXPECT_EQ(tracks.results.size(), 0);
}
}
+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
@@ -30,89 +30,86 @@
namespace Recommendation
{
using namespace Database;
using namespace Database;
std::unique_ptr<IPlaylistGeneratorService>
createPlaylistGeneratorService(Db& db, Recommendation::IRecommendationService& recommendationService)
{
return std::make_unique<PlaylistGeneratorService>(db, recommendationService);
}
std::unique_ptr<IPlaylistGeneratorService> createPlaylistGeneratorService(Db& db, Recommendation::IRecommendationService& recommendationService)
{
return std::make_unique<PlaylistGeneratorService>(db, recommendationService);
}
PlaylistGeneratorService::PlaylistGeneratorService(Db& db, Recommendation::IRecommendationService& recommendationService)
: _db {db}
, _recommendationService {recommendationService}
{
_constraints.push_back(std::make_unique<PlaylistGeneratorConstraint::ConsecutiveArtists>(_db));
_constraints.push_back(std::make_unique<PlaylistGeneratorConstraint::ConsecutiveReleases>(_db));
_constraints.push_back(std::make_unique<PlaylistGeneratorConstraint::DuplicateTracks>());
}
PlaylistGeneratorService::PlaylistGeneratorService(Db& db, Recommendation::IRecommendationService& recommendationService)
: _db{ db }
, _recommendationService{ recommendationService }
{
_constraints.push_back(std::make_unique<PlaylistGeneratorConstraint::ConsecutiveArtists>(_db));
_constraints.push_back(std::make_unique<PlaylistGeneratorConstraint::ConsecutiveReleases>(_db));
_constraints.push_back(std::make_unique<PlaylistGeneratorConstraint::DuplicateTracks>());
}
std::vector<TrackId>
PlaylistGeneratorService::extendPlaylist(TrackListId tracklistId, std::size_t maxCount) const
{
LMS_LOG(RECOMMENDATION, DEBUG) << "Requested to extend playlist by " << maxCount << " similar tracks";
std::vector<TrackId> PlaylistGeneratorService::extendPlaylist(TrackListId tracklistId, std::size_t maxCount) const
{
LMS_LOG(RECOMMENDATION, DEBUG) << "Requested to extend playlist by " << maxCount << " similar tracks";
// supposed to be ordered from most similar to least similar
std::vector<TrackId> similarTracks {_recommendationService.findSimilarTracks(tracklistId, maxCount * 2)}; // ask for more tracks than we need as it will be easier to respect constraints
// supposed to be ordered from most similar to least similar
std::vector<TrackId> similarTracks{ _recommendationService.findSimilarTracks(tracklistId, maxCount * 2) }; // ask for more tracks than we need as it will be easier to respect constraints
const std::vector<TrackId> startingTracks {getTracksFromTrackList(tracklistId)};
const std::vector<TrackId> startingTracks{ getTracksFromTrackList(tracklistId) };
std::vector<TrackId> finalResult = startingTracks;
finalResult.reserve(startingTracks.size() + maxCount);
std::vector<TrackId> finalResult = startingTracks;
finalResult.reserve(startingTracks.size() + maxCount);
std::vector<float> scores;
for (std::size_t i {}; i < maxCount; ++i)
{
if (similarTracks.empty())
break;
std::vector<float> scores;
for (std::size_t i{}; i < maxCount; ++i)
{
if (similarTracks.empty())
break;
scores.resize(similarTracks.size(), {});
scores.resize(similarTracks.size(), {});
// select the similar track that has the best score
for (std::size_t trackIndex {}; trackIndex < similarTracks.size(); ++trackIndex)
{
using namespace Database::Debug;
// select the similar track that has the best score
for (std::size_t trackIndex{}; trackIndex < similarTracks.size(); ++trackIndex)
{
using namespace Database::Debug;
finalResult.push_back(similarTracks[trackIndex]);
finalResult.push_back(similarTracks[trackIndex]);
scores[trackIndex] = 0;
for (const auto& constraint : _constraints)
scores[trackIndex] += constraint->computeScore(finalResult, finalResult.size() - 1);
scores[trackIndex] = 0;
for (const auto& constraint : _constraints)
scores[trackIndex] += constraint->computeScore(finalResult, finalResult.size() - 1);
finalResult.pop_back();
finalResult.pop_back();
// early exit if we consider we found a track with no constraint violation (since similarTracks sorted from most to least similar)
if (scores[trackIndex] < 0.01)
break;
}
// early exit if we consider we found a track with no constraint violation (since similarTracks sorted from most to least similar)
if (scores[trackIndex] < 0.01)
break;
}
// get the best score
const std::size_t bestScoreIndex {static_cast<std::size_t>(std::distance(std::cbegin(scores), std::min_element(std::cbegin(scores), std::cend(scores))))};
// get the best score
const std::size_t bestScoreIndex{ static_cast<std::size_t>(std::distance(std::cbegin(scores), std::min_element(std::cbegin(scores), std::cend(scores)))) };
finalResult.push_back(similarTracks[bestScoreIndex]);
similarTracks.erase(std::begin(similarTracks) + bestScoreIndex);
}
finalResult.push_back(similarTracks[bestScoreIndex]);
similarTracks.erase(std::begin(similarTracks) + bestScoreIndex);
}
// for now, just get some more similar tracks
return std::vector(std::cbegin(finalResult) + startingTracks.size(), std::cend(finalResult));
}
// for now, just get some more similar tracks
return std::vector(std::cbegin(finalResult) + startingTracks.size(), std::cend(finalResult));
}
TrackContainer
PlaylistGeneratorService::getTracksFromTrackList(Database::TrackListId tracklistId) const
{
TrackContainer tracks;
TrackContainer PlaylistGeneratorService::getTracksFromTrackList(Database::TrackListId tracklistId) const
{
TrackContainer tracks;
Session& dbSession {_db.getTLSSession()};
auto transaction {dbSession.createSharedTransaction()};
Session& dbSession{ _db.getTLSSession() };
auto transaction{ dbSession.createSharedTransaction() };
Track::FindParameters params;
params.setTrackList(tracklistId);
params.setSortMethod(TrackSortMethod::TrackList);
params.setDistinct(false);
Track::FindParameters params;
params.setTrackList(tracklistId);
params.setSortMethod(TrackSortMethod::TrackList);
params.setDistinct(false);
for (const TrackId trackId : Track::find(dbSession, params).results)
tracks.push_back(trackId);
for (const TrackId trackId : Track::findIds(dbSession, params).results)
tracks.push_back(trackId);
return tracks;
}
return tracks;
}
}
@@ -90,6 +90,8 @@ namespace Recommendation
if (itEngine == std::cend(_engines))
continue;
LMS_LOG(RECOMMENDATION, DEBUG) << "Trying engine '" << engineTypeToString(engineType) << "' to get similar tracks";
const IEngine& engine {*itEngine->second};
res = engine.findSimilarTracks(trackIds, maxCount);
if (!res.empty())
@@ -114,6 +116,8 @@ namespace Recommendation
if (itEngine == std::cend(_engines))
continue;
LMS_LOG(RECOMMENDATION, DEBUG) << "Trying engine '" << engineTypeToString(engineType) << "' to get similar releases";
const IEngine& engine {*itEngine->second};
res = engine.getSimilarReleases(releaseId, maxCount);
if (!res.empty())
@@ -140,7 +144,7 @@ namespace Recommendation
if (itEngine == std::cend(_engines))
continue;
LMS_LOG(RECOMMENDATION, DEBUG) << "Trying engine '" << engineTypeToString(engineType) << "'";
LMS_LOG(RECOMMENDATION, DEBUG) << "Trying engine '" << engineTypeToString(engineType) << "' to get similar artists";
const IEngine& engine {*itEngine->second};
res = engine.getSimilarArtists(artistId, linkTypes, maxCount);
@@ -155,12 +159,12 @@ namespace Recommendation
}
static
Database::ScanSettings::RecommendationEngineType
getRecommendationEngineType(Database::Session& session)
Database::ScanSettings::SimilarityEngineType
getSimilarityEngineType(Database::Session& session)
{
auto transaction {session.createSharedTransaction()};
return Database::ScanSettings::get(session)->getRecommendationEngineType();
return Database::ScanSettings::get(session)->getSimilarityEngineType();
}
void
@@ -180,20 +184,24 @@ namespace Recommendation
_engines.clear();
}
switch (getRecommendationEngineType(_db.getTLSSession()))
switch (getSimilarityEngineType(_db.getTLSSession()))
{
case ScanSettings::RecommendationEngineType::Clusters:
case ScanSettings::SimilarityEngineType::Clusters:
_enginePriorities = {EngineType::Clusters};
enginesToLoad.try_emplace(EngineType::Clusters, createClustersEngine(_db));
break;
case ScanSettings::RecommendationEngineType::Features:
case ScanSettings::SimilarityEngineType::Features:
_enginePriorities = {EngineType::Features, EngineType::Clusters};
// not same order since clusters is faster to load
enginesToLoad.try_emplace(EngineType::Clusters, createClustersEngine(_db));
enginesToLoad.try_emplace(EngineType::Features, createFeaturesEngine(_db));
break;
case ScanSettings::SimilarityEngineType::None:
_enginePriorities.clear();
break;
}
assert(_pendingEngines.empty());
@@ -44,7 +44,7 @@ namespace Recommendation {
Session& dbSession{ _db.getTLSSession() };
auto transaction{ dbSession.createSharedTransaction() };
const auto similarTrackIds{ Track::findSimilarTracks(dbSession, trackIds, Range {0, maxCount}) };
const auto similarTrackIds{ Track::findSimilarTrackIds(dbSession, trackIds, Range {0, maxCount}) };
return std::move(similarTrackIds.results);
}
@@ -104,7 +104,7 @@ namespace Recommendation {
if (!artist)
return {};
const auto similarArtistIds{ artist->findSimilarArtists(artistLinkTypes, Range {0, maxCount}) };
const auto similarArtistIds{ artist->findSimilarArtistIds(artistLinkTypes, Range {0, maxCount}) };
return std::move(similarArtistIds.results);
}
@@ -37,7 +37,7 @@ namespace Scanner
Session& session {_db.getTLSSession()};
auto transaction {session.createSharedTransaction()};
const RangeResults<TrackId> tracks = Track::findTrackMBIDDuplicates(session, Range {});
const RangeResults<TrackId> tracks = Track::findIdsTrackMBIDDuplicates(session, Range {});
for (const TrackId trackId : tracks.results)
{
const Track::pointer track {Track::find(session, trackId)};
@@ -137,7 +137,7 @@ namespace Scanner
Session& session {_db.getTLSSession()};
auto transaction {session.createUniqueTransaction()};
auto artistIds {Artist::findAllOrphans(session, Range {})};
auto artistIds {Artist::findOrphanIds(session, Range {})};
for (const ArtistId artistId : artistIds.results)
{
Artist::pointer artist {Artist::find(session, artistId)};
@@ -153,10 +153,11 @@ namespace Scanner
LMS_LOG(DBUPDATER, DEBUG) << "Checking orphan releases...";
// TODO, by batch
Session& session {_db.getTLSSession()};
auto transaction {session.createUniqueTransaction()};
auto releases {Release::findOrphans(session, Range {})};
auto releases {Release::findOrphanIds(session, Range {})};
for (const ReleaseId releaseId : releases.results)
{
Release::pointer release {Release::find(session, releaseId)};
@@ -307,6 +307,10 @@ namespace Scanner
context.currentStepStats.processedElems++;
_progressCallback(context.currentStepStats);
// optimize the database during scan (if we import a very large database, it may be too late to do it once at end)
if ((context.stats.nbChanges() % 5'000) == 0)
_db.getTLSSession().optimize();
}
return true;
@@ -381,7 +381,7 @@ ScannerService::readSettings()
std::transform(std::cbegin(fileExtensions), std::end(fileExtensions), std::back_inserter(newSettings.supportedExtensions),
[](const std::filesystem::path& extension) { return std::filesystem::path{ StringUtils::stringToLower(extension.string()) }; });
}
newSettings.recommendationServiceType = scanSettings->getRecommendationEngineType();
newSettings.similarityServiceType = scanSettings->getSimilarityEngineType();
newSettings.mediaDirectory = scanSettings->getMediaDirectory();
const auto clusterTypes = scanSettings->getClusterTypes();
@@ -34,7 +34,7 @@ namespace Scanner
Wt::WTime startTime;
Database::ScanSettings::UpdatePeriod updatePeriod {Database::ScanSettings::UpdatePeriod::Never};
std::vector<std::filesystem::path> supportedExtensions;
Database::ScanSettings::RecommendationEngineType recommendationServiceType;
Database::ScanSettings::SimilarityEngineType similarityServiceType;
std::filesystem::path mediaDirectory;
bool skipDuplicateMBID {};
std::set<std::string> clusterTypeNames;
@@ -45,7 +45,7 @@ namespace Scanner
&& startTime == rhs.startTime
&& updatePeriod == rhs.updatePeriod
&& supportedExtensions == rhs.supportedExtensions
&& recommendationServiceType == rhs.recommendationServiceType
&& similarityServiceType == rhs.similarityServiceType
&& mediaDirectory == rhs.mediaDirectory
&& skipDuplicateMBID == rhs.skipDuplicateMBID
&& clusterTypeNames == rhs.clusterTypeNames;
+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
File diff suppressed because it is too large Load Diff
@@ -52,7 +52,7 @@ namespace Scrobbling::ListenBrainz
private:
void enqueListen(const Listen& listen, const Wt::WDateTime& timePoint);
bool saveListen(const TimedListen& listen, Database::ScrobblingState scrobblinState);
bool saveListen(const TimedListen& listen, Database::SyncState scrobblinState);
void enquePendingListens();
@@ -19,12 +19,11 @@
#pragma once
#include "utils/UUID.hpp"
#include "services/database/UserId.hpp"
#include "utils/Logger.hpp"
#include "utils/UUID.hpp"
#define LOG(sev) LMS_LOG(SCROBBLING, sev) << "[listenbrainz] - "
#define LOG(sev) LMS_LOG(SCROBBLING, sev) << "[listenbrainz] "
namespace Database
{
@@ -34,6 +33,5 @@ namespace Database
namespace Scrobbling::ListenBrainz::Utils
{
std::optional<UUID> getListenBrainzToken(Database::Session& session, Database::UserId userId);
std::string parseValidateToken(std::string_view msgBody);
}
@@ -55,59 +55,19 @@ namespace Scrobbling
using ArtistContainer = Database::RangeResults<Database::ArtistId>;
using ReleaseContainer = Database::RangeResults<Database::ReleaseId>;
using TrackContainer = Database::RangeResults<Database::TrackId>;
// From most recent to oldest
virtual ArtistContainer getRecentArtists(Database::UserId userId,
const std::vector<Database::ClusterId>& clusterIds,
std::optional<Database::TrackArtistLinkType> linkType,
Database::Range range) = 0;
virtual ReleaseContainer getRecentReleases(Database::UserId userId,
const std::vector<Database::ClusterId>& clusterIds,
Database::Range range) = 0;
virtual TrackContainer getRecentTracks(Database::UserId userId,
const std::vector<Database::ClusterId>& clusterIds,
Database::Range range) = 0;
virtual ArtistContainer getRecentArtists(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, std::optional<Database::TrackArtistLinkType> linkType, Database::Range range) = 0;
virtual ReleaseContainer getRecentReleases(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) = 0;
virtual TrackContainer getRecentTracks(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) = 0;
virtual Wt::WDateTime getLastListenDateTime(Database::UserId userId, Database::ReleaseId releaseId) = 0;
virtual Wt::WDateTime getLastListenDateTime(Database::UserId userId, Database::TrackId trackId) = 0;
// Top
virtual ArtistContainer getTopArtists(Database::UserId userId,
const std::vector<Database::ClusterId>& clusterIds,
std::optional<Database::TrackArtistLinkType> linkType,
Database::Range) = 0;
virtual ReleaseContainer getTopReleases(Database::UserId userId,
const std::vector<Database::ClusterId>& clusterIds,
Database::Range range) = 0;
virtual TrackContainer getTopTracks(Database::UserId userId,
const std::vector<Database::ClusterId>& clusterIds,
Database::Range range) = 0;
// Star
virtual void star(Database::UserId userId, Database::ArtistId artistId) = 0;
virtual void unstar(Database::UserId userId, Database::ArtistId artistId) = 0;
virtual bool isStarred(Database::UserId userId, Database::ArtistId artistId) = 0;
virtual Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::ArtistId artistId) = 0;
virtual ArtistContainer getStarredArtists(Database::UserId userId,
const std::vector<Database::ClusterId>& clusterIds,
std::optional<Database::TrackArtistLinkType> linkType,
Database::ArtistSortMethod sortMethod,
Database::Range range) = 0;
virtual void star(Database::UserId userId, Database::ReleaseId releaseId) = 0;
virtual void unstar(Database::UserId userId, Database::ReleaseId releaseId) = 0;
virtual bool isStarred(Database::UserId userId, Database::ReleaseId artistId) = 0;
virtual Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::ReleaseId artistId) = 0;
virtual ReleaseContainer getStarredReleases(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) = 0;
virtual void star(Database::UserId userId, Database::TrackId trackId) = 0;
virtual void unstar(Database::UserId userId, Database::TrackId trackId) = 0;
virtual bool isStarred(Database::UserId userId, Database::TrackId artistId) = 0;
virtual Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::TrackId artistId) = 0;
virtual TrackContainer getStarredTracks(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) = 0;
virtual ArtistContainer getTopArtists(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, std::optional<Database::TrackArtistLinkType> linkType, Database::Range) = 0;
virtual ReleaseContainer getTopReleases(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) = 0;
virtual TrackContainer getTopTracks(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) = 0;
};
std::unique_ptr<IScrobblingService> createScrobblingService(boost::asio::io_service& ioService, Database::Db& db);
+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 };
};
}
+17 -3
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 }
{
}
@@ -273,13 +286,13 @@ namespace API::Subsonic
const ResponseFormat format{ getParameterAs<std::string>(request.getParameterMap(), "f").value_or("xml") == "json" ? ResponseFormat::json : ResponseFormat::xml };
ProtocolVersion protocolVersion{ defaultServerProtocolVersion };
try
{
// We need to parse client a soon as possible to make sure to answer with the right protocol version
protocolVersion = getServerProtocolVersion(getMandatoryParameterAs<std::string>(request.getParameterMap(), "c"));
RequestContext requestContext{ buildRequestContext(request) };
auto itEntryPoint{ requestEntryPoints.find(requestPath) };
if (itEntryPoint != requestEntryPoints.end())
{
@@ -364,8 +377,9 @@ namespace API::Subsonic
const Wt::Http::ParameterMap& parameters{ request.getParameterMap() };
const ClientInfo clientInfo{ getClientInfo(parameters) };
const Database::UserId userId{ authenticateUser(request, clientInfo) };
bool enableOpenSubsonic{ _openSubsonicDisabledClients.find(clientInfo.name) == std::cend(_openSubsonicDisabledClients) };
return { parameters, _db.getTLSSession(), userId, clientInfo, getServerProtocolVersion(clientInfo.name) };
return { parameters, _db.getTLSSession(), userId, clientInfo, getServerProtocolVersion(clientInfo.name), enableOpenSubsonic };
}
Database::UserId SubsonicResource::authenticateUser(const Wt::Http::Request& request, const ClientInfo& clientInfo)
@@ -19,6 +19,7 @@
#pragma once
#include <string>
#include <unordered_set>
#include <unordered_map>
#include <Wt/WResource.h>
@@ -51,6 +52,7 @@ namespace API::Subsonic
Database::UserId authenticateUser(const Wt::Http::Request& request, const ClientInfo& clientInfo);
const std::unordered_map<std::string, ProtocolVersion> _serverProtocolVersionsByClient;
const std::unordered_set<std::string> _openSubsonicDisabledClients;
Database::Db& _db;
};
@@ -148,6 +148,7 @@ namespace API::Subsonic
}
// OpenSubsonic mandatory fields
// No big deal to send them even for legacy clients
responseNode.setAttribute("type", "lms");
responseNode.setAttribute("serverVersion", serverVersion);
responseNode.setAttribute("openSubsonic", true);
@@ -25,6 +25,7 @@
#include "services/database/Session.hpp"
#include "services/database/Track.hpp"
#include "services/database/User.hpp"
#include "services/feedback/IFeedbackService.hpp"
#include "services/scrobbling/IScrobblingService.hpp"
#include "responses/Album.hpp"
#include "responses/Artist.hpp"
@@ -36,8 +37,9 @@ namespace API::Subsonic
{
using namespace Database;
namespace {
Response handleGetAlbumListRequestCommon(const RequestContext& context, bool id3)
namespace
{
Response handleGetAlbumListRequestCommon(RequestContext& context, bool id3)
{
// Mandatory params
const std::string type{ getMandatoryParameterAs<std::string>(context.parameters, "type") };
@@ -49,7 +51,8 @@ namespace API::Subsonic
const Range range{ offset, size };
RangeResults<ReleaseId> releases;
Scrobbling::IScrobblingService& scrobbling{ *Service<Scrobbling::IScrobblingService>::get() };
Scrobbling::IScrobblingService& scrobblingService{ *Service<Scrobbling::IScrobblingService>::get() };
Feedback::IFeedbackService& feedbackService{ *Service<Feedback::IFeedbackService>::get() };
auto transaction{ context.dbSession.createSharedTransaction() };
@@ -63,11 +66,11 @@ namespace API::Subsonic
params.setSortMethod(ReleaseSortMethod::Name);
params.setRange(range);
releases = Release::find(context.dbSession, params);
releases = Release::findIds(context.dbSession, params);
}
else if (type == "alphabeticalByArtist")
{
releases = Release::findOrderedByArtist(context.dbSession, range);
releases = Release::findIdsOrderedByArtist(context.dbSession, range);
}
else if (type == "byGenre")
{
@@ -83,7 +86,7 @@ namespace API::Subsonic
params.setSortMethod(ReleaseSortMethod::Name);
params.setRange(range);
releases = Release::find(context.dbSession, params);
releases = Release::findIds(context.dbSession, params);
}
}
}
@@ -97,11 +100,11 @@ namespace API::Subsonic
params.setRange(range);
params.setDateRange(DateRange::fromYearRange(fromYear, toYear));
releases = Release::find(context.dbSession, params);
releases = Release::findIds(context.dbSession, params);
}
else if (type == "frequent")
{
releases = scrobbling.getTopReleases(context.userId, {}, range);
releases = scrobblingService.getTopReleases(context.userId, {}, range);
}
else if (type == "newest")
{
@@ -109,7 +112,7 @@ namespace API::Subsonic
params.setSortMethod(ReleaseSortMethod::LastWritten);
params.setRange(range);
releases = Release::find(context.dbSession, params);
releases = Release::findIds(context.dbSession, params);
}
else if (type == "random")
{
@@ -119,15 +122,15 @@ namespace API::Subsonic
params.setSortMethod(ReleaseSortMethod::Random);
params.setRange({ 0, size });
releases = Release::find(context.dbSession, params);
releases = Release::findIds(context.dbSession, params);
}
else if (type == "recent")
{
releases = scrobbling.getRecentReleases(context.userId, {}, range);
releases = scrobblingService.getRecentReleases(context.userId, {}, range);
}
else if (type == "starred")
{
releases = scrobbling.getStarredReleases(context.userId, {}, range);
releases = feedbackService.getStarredReleases(context.userId, {}, range);
}
else
throw NotImplementedGenericError{};
@@ -138,7 +141,7 @@ namespace API::Subsonic
for (const ReleaseId releaseId : releases.results)
{
const Release::pointer release{ Release::find(context.dbSession, releaseId) };
albumListNode.addArrayChild("album", createAlbumNode(release, context.dbSession, user, id3));
albumListNode.addArrayChild("album", createAlbumNode(context, release, user, id3));
}
return response;
@@ -155,24 +158,24 @@ namespace API::Subsonic
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
Response::Node& starredNode{ response.createNode(id3 ? Response::Node::Key{ "starred2" } : Response::Node::Key{ "starred" }) };
Scrobbling::IScrobblingService& scrobbling{ *Service<Scrobbling::IScrobblingService>::get() };
Feedback::IFeedbackService& feedbackService{ *Service<Feedback::IFeedbackService>::get() };
for (const ArtistId artistId : scrobbling.getStarredArtists(context.userId, {} /* clusters */, std::nullopt /* linkType */, ArtistSortMethod::BySortName, Range{}).results)
for (const ArtistId artistId : feedbackService.getStarredArtists(context.userId, {} /* clusters */, std::nullopt /* linkType */, ArtistSortMethod::BySortName, Range{}).results)
{
if (auto artist{ Artist::find(context.dbSession, artistId) })
starredNode.addArrayChild("artist", createArtistNode(artist, context.dbSession, user, id3));
starredNode.addArrayChild("artist", createArtistNode(context, artist, user, id3));
}
for (const ReleaseId releaseId : scrobbling.getStarredReleases(context.userId, {} /* clusters */, Range{}).results)
for (const ReleaseId releaseId : feedbackService.getStarredReleases(context.userId, {} /* clusters */, Range{}).results)
{
if (auto release{ Release::find(context.dbSession, releaseId) })
starredNode.addArrayChild("album", createAlbumNode(release, context.dbSession, user, id3));
starredNode.addArrayChild("album", createAlbumNode(context, release, user, id3));
}
for (const TrackId trackId : scrobbling.getStarredTracks(context.userId, {} /* clusters */, Range{}).results)
for (const TrackId trackId : feedbackService.getStarredTracks(context.userId, {} /* clusters */, Range{}).results)
{
if (auto track{ Track::find(context.dbSession, trackId) })
starredNode.addArrayChild("song", createSongNode(track, context.dbSession, user));
starredNode.addArrayChild("song", createSongNode(context, track, user));
}
return response;
@@ -201,16 +204,13 @@ namespace API::Subsonic
if (!user)
throw UserNotAuthorizedError{};
const auto trackIds{ Track::find(context.dbSession, Track::FindParameters {}.setSortMethod(TrackSortMethod::Random).setRange({0, size})) };
const auto tracks{ Track::find(context.dbSession, Track::FindParameters {}.setSortMethod(TrackSortMethod::Random).setRange({0, size})) };
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
Response::Node& randomSongsNode{ response.createNode("randomSongs") };
for (const TrackId trackId : trackIds.results)
{
const Track::pointer track{ Track::find(context.dbSession, trackId) };
randomSongsNode.addArrayChild("song", createSongNode(track, context.dbSession, user));
}
for (const Track::pointer& track : tracks.results)
randomSongsNode.addArrayChild("song", createSongNode(context, track, user));
return response;
}
@@ -247,12 +247,9 @@ namespace API::Subsonic
params.setClusters({ cluster->getId() });
params.setRange({ offset, size });
auto trackIds{ Track::find(context.dbSession, params) };
for (const TrackId trackId : trackIds.results)
{
const Track::pointer track{ Track::find(context.dbSession, trackId) };
songsByGenreNode.addArrayChild("song", createSongNode(track, context.dbSession, user));
}
const auto tracks{ Track::find(context.dbSession, params) };
for (const Track::pointer& track : tracks.results)
songsByGenreNode.addArrayChild("song", createSongNode(context, track, user));
return response;
}
@@ -49,7 +49,7 @@ namespace API::Subsonic
{
const TrackBookmark::pointer bookmark{ TrackBookmark::find(context.dbSession, bookmarkId) };
Response::Node bookmarkNode{ createBookmarkNode(bookmark) };
bookmarkNode.addArrayChild("entry", createSongNode(bookmark->getTrack(), context.dbSession, user));
bookmarkNode.addArrayChild("entry", createSongNode(context, bookmark->getTrack(), user));
bookmarksNode.addArrayChild("bookmark", std::move(bookmarkNode));
}

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