-
${releases}
-
-
-
- ${link-type class="form-select"}
-
-
- ${artists}
+
+
+
+ ${releases class="nav-link"}
+
+
+ ${artists class="nav-link"}
+
+
+ ${tracks class="nav-link"}
+
+
+
+ ${results}
+
+
+
+
+
+ ${link-type class="form-select"}
-
${tracks}
+ ${artists}
diff --git a/approot/settings.xml b/approot/settings.xml
index c15db5649..507ec6862 100644
--- a/approot/settings.xml
+++ b/approot/settings.xml
@@ -28,9 +28,9 @@
- ${tr:Lms.Settings.scrobbling.listenbrainz-token}
+ ${tr:Lms.Settings.backend.listenbrainz-token}
${listenbrainz-token class="form-control"}
diff --git a/conf/lms.conf b/conf/lms.conf
index 4454eff44..07e7943fc 100644
--- a/conf/lms.conf
+++ b/conf/lms.conf
@@ -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";
diff --git a/src/libs/av/impl/TranscodeResourceHandler.cpp b/src/libs/av/impl/TranscodeResourceHandler.cpp
index 6535f806f..753213c46 100644
--- a/src/libs/av/impl/TranscodeResourceHandler.cpp
+++ b/src/libs/av/impl/TranscodeResourceHandler.cpp
@@ -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
(&_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();
diff --git a/src/libs/services/CMakeLists.txt b/src/libs/services/CMakeLists.txt
index fd4d31602..70f360edd 100644
--- a/src/libs/services/CMakeLists.txt
+++ b/src/libs/services/CMakeLists.txt
@@ -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)
diff --git a/src/libs/services/cover/impl/CoverService.cpp b/src/libs/services/cover/impl/CoverService.cpp
index 86526dabf..0912828e8 100644
--- a/src/libs/services/cover/impl/CoverService.cpp
+++ b/src/libs/services/cover/impl/CoverService.cpp
@@ -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 releaseId;
- };
- std::optional
- getTrackInfo(Database::Session& dbSession, Database::TrackId trackId)
- {
- std::optional res;
-
- auto transaction {dbSession.createSharedTransaction()};
-
- const Database::Track::pointer track {Database::Track::find(dbSession, trackId)};
- if (!track)
- return res;
-
- res = TrackInfo {};
-
- res->hasCover = track->hasCover();
- res->trackPath = track->getPath();
-
- if (const Database::Release::pointer& release {track->getRelease()})
- {
- res->releaseId = release->getId();
- if (release->getTotalDisc() > 1)
- res->isMultiDisc = true;
- }
-
- return res;
- }
-
- std::vector constructPreferredFileNames()
- {
- std::vector res;
-
- Service::get()->visitStrings("cover-preferred-file-names",
- [&res](std::string_view fileName)
- {
- res.emplace_back(fileName);
- }, {"cover", "front"});
-
- return res;
- }
-}
-
-namespace Cover {
-
-using namespace Image;
-
-static
-bool
-isFileSupported(const std::filesystem::path& file, const std::vector& extensions)
-{
- return (std::find(std::cbegin(extensions), std::cend(extensions), file.extension()) != std::cend(extensions));
-}
-
-std::unique_ptr
-createCoverService(Database::Db& db, const std::filesystem::path& execPath, const std::filesystem::path& defaultCoverPath)
-{
- return std::make_unique(db, execPath, defaultCoverPath);
-}
-
-CoverService::CoverService(Database::Db& db,
- const std::filesystem::path& execPath,
- const std::filesystem::path& defaultCoverPath)
- : _db {db}
- , _defaultCoverPath {defaultCoverPath}
- , _maxCacheSize {Service::get()->getULong("cover-max-cache-size", 30) * 1000 * 1000}
- , _maxFileSize {Service::get()->getULong("cover-max-file-size", 10) * 1000 * 1000}
- , _preferredFileNames {constructPreferredFileNames()}
-
-{
- setJpegQuality(Service::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, ",");
+ namespace
+ {
+ struct TrackInfo
+ {
+ bool hasCover{};
+ bool isMultiDisc{};
+ std::filesystem::path trackPath;
+ std::optional releaseId;
+ };
+
+ std::optional getTrackInfo(Database::Session& dbSession, Database::TrackId trackId)
+ {
+ std::optional res;
+
+ auto transaction{ dbSession.createSharedTransaction() };
+
+ const Database::Track::pointer track{ Database::Track::find(dbSession, trackId) };
+ if (!track)
+ return res;
+
+ res = TrackInfo{};
+
+ res->hasCover = track->hasCover();
+ res->trackPath = track->getPath();
+
+ if (const Database::Release::pointer & release{ track->getRelease() })
+ {
+ res->releaseId = release->getId();
+ if (release->getTotalDisc() > 1)
+ res->isMultiDisc = true;
+ }
+
+ return res;
+ }
+
+ std::vector constructPreferredFileNames()
+ {
+ std::vector res;
+
+ Service::get()->visitStrings("cover-preferred-file-names",
+ [&res](std::string_view fileName)
+ {
+ res.emplace_back(fileName);
+ }, { "cover", "front" });
+
+ return res;
+ }
+
+ bool isFileSupported(const std::filesystem::path& file, const std::vector& extensions)
+ {
+ return (std::find(std::cbegin(extensions), std::cend(extensions), file.extension()) != std::cend(extensions));
+ }
+ }
+
+ std::unique_ptr createCoverService(Database::Db& db, const std::filesystem::path& execPath, const std::filesystem::path& defaultCoverPath)
+ {
+ return std::make_unique(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::get()->getULong("cover-max-cache-size", 30) * 1000 * 1000 }
+ , _maxFileSize{ Service::get()->getULong("cover-max-file-size", 10) * 1000 * 1000 }
+ , _preferredFileNames{ constructPreferredFileNames() }
+
+ {
+ setJpegQuality(Service::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, ",");
#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
-CoverService::getFromAvMediaFile(const Av::IAudioFile& input, ImageSize width) const
-{
- std::unique_ptr image;
-
- input.visitAttachedPictures([&](const Av::Picture& picture)
- {
- if (image)
- return;
-
- try
- {
- std::unique_ptr 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
-CoverService::getFromCoverFile(const std::filesystem::path& p, ImageSize width) const
-{
- std::unique_ptr image;
-
- try
- {
- std::unique_ptr 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
-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 image {getFromCoverFile(_defaultCoverPath, width)};
- _defaultCoverCache[width] = image;
- LMS_LOG(COVER, DEBUG) << "Default cache entries = " << _defaultCoverCache.size();
-
- return image;
- }
-}
-
-std::unique_ptr
-CoverService::getFromDirectory(const std::filesystem::path& directory, ImageSize width) const
-{
- const std::multimap coverPaths {getCoverPaths(directory)};
-
- auto tryLoadImageFromFilename = [&](std::string_view fileName)
- {
- std::unique_ptr 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 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
-CoverService::getFromSameNamedFile(const std::filesystem::path& filePath, ImageSize width) const
-{
- std::unique_ptr 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
-CoverService::getCoverPaths(const std::filesystem::path& directoryPath) const
-{
- std::multimap 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
-CoverService::getFromTrack(const std::filesystem::path& p, ImageSize width) const
-{
- std::unique_ptr 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
-CoverService::getFromTrack(Database::TrackId trackId, ImageSize width)
-{
- return getFromTrack(_db.getTLSSession(), trackId, width, true /* allow release fallback*/);
-}
-
-std::shared_ptr
-CoverService::getFromTrack(Database::Session& dbSession, Database::TrackId trackId, ImageSize width, bool allowReleaseFallback)
-{
- using namespace Database;
-
- const CacheEntryDesc cacheEntryDesc {trackId, width};
-
- std::shared_ptr cover {loadFromCache(cacheEntryDesc)};
- if (cover)
- return cover;
-
- if (const std::optional 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
-CoverService::getFromRelease(Database::ReleaseId releaseId, ImageSize width)
-{
- using namespace Database;
- const CacheEntryDesc cacheEntryDesc {releaseId, width};
-
- std::shared_ptr cover {loadFromCache(cacheEntryDesc)};
- if (cover)
- return cover;
-
- struct ReleaseInfo
- {
- TrackId firstTrackId;
- std::filesystem::path releaseDirectory;
- };
-
- Session& session {_db.getTLSSession()};
-
- auto getReleaseInfo {[&]
- {
- std::optional 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 {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(quality, 1, 100);
-
- LMS_LOG(COVER, INFO) << "JPEG export quality = " << _jpegQuality;
-}
-
-void
-CoverService::saveToCache(const CacheEntryDesc& entryDesc, std::shared_ptr 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
-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 CoverService::getFromAvMediaFile(const Av::IAudioFile& input, ImageSize width) const
+ {
+ std::unique_ptr image;
+
+ input.visitAttachedPictures([&](const Av::Picture& picture)
+ {
+ if (image)
+ return;
+
+ try
+ {
+ std::unique_ptr 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 CoverService::getFromCoverFile(const std::filesystem::path& p, ImageSize width) const
+ {
+ std::unique_ptr image;
+
+ try
+ {
+ std::unique_ptr 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 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 image{ getFromCoverFile(_defaultCoverPath, width) };
+ _defaultCoverCache[width] = image;
+ LMS_LOG(COVER, DEBUG) << "Default cache entries = " << _defaultCoverCache.size();
+
+ return image;
+ }
+ }
+
+ std::unique_ptr CoverService::getFromDirectory(const std::filesystem::path& directory, ImageSize width) const
+ {
+ const std::multimap coverPaths{ getCoverPaths(directory) };
+
+ auto tryLoadImageFromFilename = [&](std::string_view fileName)
+ {
+ std::unique_ptr 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 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 CoverService::getFromSameNamedFile(const std::filesystem::path& filePath, ImageSize width) const
+ {
+ std::unique_ptr 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 CoverService::getCoverPaths(const std::filesystem::path& directoryPath) const
+ {
+ std::multimap 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 CoverService::getFromTrack(const std::filesystem::path& p, ImageSize width) const
+ {
+ std::unique_ptr 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 CoverService::getFromTrack(Database::TrackId trackId, ImageSize width)
+ {
+ return getFromTrack(_db.getTLSSession(), trackId, width, true /* allow release fallback*/);
+ }
+
+ std::shared_ptr CoverService::getFromTrack(Database::Session& dbSession, Database::TrackId trackId, ImageSize width, bool allowReleaseFallback)
+ {
+ using namespace Database;
+
+ const CacheEntryDesc cacheEntryDesc{ trackId, width };
+
+ std::shared_ptr cover{ loadFromCache(cacheEntryDesc) };
+ if (cover)
+ return cover;
+
+ if (const std::optional 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 CoverService::getFromRelease(Database::ReleaseId releaseId, ImageSize width)
+ {
+ using namespace Database;
+ const CacheEntryDesc cacheEntryDesc{ releaseId, width };
+
+ std::shared_ptr cover{ loadFromCache(cacheEntryDesc) };
+ if (cover)
+ return cover;
+
+ struct ReleaseInfo
+ {
+ TrackId firstTrackId;
+ std::filesystem::path releaseDirectory;
+ };
+
+ Session& session{ _db.getTLSSession() };
+
+ auto getReleaseInfo{ [&]
+ {
+ std::optional 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{ 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(quality, 1, 100);
+
+ LMS_LOG(COVER, INFO) << "JPEG export quality = " << _jpegQuality;
+ }
+
+ void CoverService::saveToCache(const CacheEntryDesc& entryDesc, std::shared_ptr 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 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
diff --git a/src/libs/services/database/impl/Artist.cpp b/src/libs/services/database/impl/Artist.cpp
index 1bbfbfeb2..c9ca60a47 100644
--- a/src/libs/services/database/impl/Artist.cpp
+++ b/src/libs/services/database/impl/Artist.cpp
@@ -33,278 +33,298 @@
namespace Database
{
-
-Artist::Artist(const std::string& name, const std::optional& MBID)
-: _name {std::string(name, 0 , _maxNameLength)},
-_sortName {_name},
-_MBID {MBID ? MBID->getAsString() : ""}
-{
-}
-
-Artist::pointer
-Artist::create(Session& session, const std::string& name, const std::optional& MBID)
-{
- return session.getDboSession().add(std::unique_ptr {new Artist {name, MBID}});
-}
-
-std::size_t
-Artist::getCount(Session& session)
-{
- session.checkSharedLocked();
-
- return session.getDboSession().query("SELECT COUNT(*) FROM artist");
-}
-
-std::vector
-Artist::find(Session& session, const std::string& name)
-{
- session.checkSharedLocked();
-
- Wt::Dbo::collection> res = session.getDboSession().find()
- .where("name = ?").bind(std::string {name, 0, _maxNameLength})
- .orderBy("LENGTH(mbid) DESC"); // put mbid entries first
-
- return std::vector(res.begin(), res.end());
-}
-
-Artist::pointer
-Artist::find(Session& session, const UUID& mbid)
-{
- session.checkSharedLocked();
- return session.getDboSession().find().where("mbid = ?").bind(std::string {mbid.getAsString()}).resultValue();
-}
-
-Artist::pointer
-Artist::find(Session& session, ArtistId id)
-{
- session.checkSharedLocked();
- return session.getDboSession().find().where("id = ?").bind(id).resultValue();
-}
-
-bool
-Artist::exists(Session& session, ArtistId id)
-{
- session.checkSharedLocked();
- return session.getDboSession().query("SELECT 1 FROM artist").where("id = ?").bind(id).resultValue() == 1;
-}
-
-static
-Wt::Dbo::Query
-createQuery(Session& session, const Artist::FindParameters& params)
-{
- session.checkSharedLocked();
-
- auto query {session.getDboSession().query("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");
- }
-
- if (params.linkType)
- query.where("t_a_l.type = ?").bind(*params.linkType);
-
- if (params.writtenAfter.isValid())
- query.where("t.file_last_write > ?").bind(params.writtenAfter);
-
- if (!params.keywords.empty())
- {
- std::vector clauses;
- std::vector sortClauses;
-
- for (std::string_view keyword : params.keywords)
- {
- clauses.push_back("a.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'");
- query.bind("%" + Utils::escapeLikeKeyword(keyword) + "%");
- }
-
- for (std::string_view keyword : params.keywords)
- {
- sortClauses.push_back("a.sort_name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'");
- query.bind("%" + Utils::escapeLikeKeyword(keyword) + "%");
- }
-
- query.where("(" + StringUtils::joinStrings(clauses, " AND ") + ") OR (" + StringUtils::joinStrings(sortClauses, " AND ") + ")");
- }
-
- 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);
- }
-
- 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";
-
- WhereClause clusterClause;
- for (const ClusterId clusterId : params.clusters)
- {
- clusterClause.Or(WhereClause("c.id = ?"));
- query.bind(clusterId);
- }
-
- oss << " " << clusterClause.get();
- oss << " GROUP BY t.id,a.id HAVING COUNT(DISTINCT c.id) = " << params.clusters.size() << ")";
-
- query.where(oss.str());
- }
-
- if (params.track.isValid())
- query.where("t.id = ?").bind(params.track);
-
- if (params.release.isValid())
- query.where("t.release_id = ?").bind(params.release);
-
- 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;
- }
-
- return query;
-}
-
-RangeResults
-Artist::findAllOrphans(Session& session, Range range)
-{
- session.checkSharedLocked();
- auto query {session.getDboSession().query("SELECT DISTINCT a.id FROM artist a WHERE NOT EXISTS(SELECT 1 FROM track t INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id WHERE t.id = t_a_l.track_id)")};
-
- return Utils::execQuery(query, range);
-}
-
-RangeResults
-Artist::find(Session& session, const FindParameters& params)
-{
- session.checkSharedLocked();
-
- auto query {createQuery(session, params)};
- return Utils::execQuery(query, params.range);
-}
-
-RangeResults
-Artist::findSimilarArtists(EnumSet artistLinkTypes, Range range) const
-{
- assert(session());
-
- 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 <> ?";
-
- if (!artistLinkTypes.empty())
- {
- oss << " AND t_a_l.type IN (";
-
- bool first {true};
- for (TrackArtistLinkType type : artistLinkTypes)
- {
- (void) type;
- if (!first)
- oss << ", ";
- oss << "?";
- first = false;
- }
- oss << ")";
- }
-
- auto query {session()->query(oss.str())
- .bind(getId())
- .bind(getId())
- .groupBy("a.id")
- .orderBy("COUNT(*) DESC, RANDOM()")};
-
- for (TrackArtistLinkType type : artistLinkTypes)
- query.bind(type);
-
- return Utils::execQuery(query, range);
-}
-
-std::vector>
-Artist::getClusterGroups(std::vector clusterTypes, std::size_t size) const
-{
- assert(session());
-
- WhereClause where;
-
- 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";
-
- where.And(WhereClause("a.id = ?")).bind(getId().toString());
- {
- WhereClause clusterClause;
- for (auto clusterType : clusterTypes)
- clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterType->getId().toString());
-
- where.And(clusterClause);
- }
- oss << " " << where.get();
- oss << "GROUP BY c.id ORDER BY COUNT(DISTINCT c.id) DESC";
-
- Wt::Dbo::Query> query = session()->query>( oss.str() );
-
- for (const std::string& bindArg : where.getBindArgs())
- query.bind(bindArg);
-
- Wt::Dbo::collection> queryRes = query;
-
- std::map> clustersByType;
- for (Cluster::pointer cluster : queryRes)
- {
- if (clustersByType[cluster->getType()->getId()].size() < size)
- clustersByType[cluster->getType()->getId()].push_back(cluster);
- }
-
- std::vector> 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
+ {
+ template
+ Wt::Dbo::Query createQuery(Session& session, std::string_view itemToSelect, const Artist::FindParameters& params)
+ {
+ session.checkSharedLocked();
+
+ auto query{ session.getDboSession().query("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");
+ }
+
+ if (params.linkType)
+ query.where("t_a_l.type = ?").bind(*params.linkType);
+
+ if (params.writtenAfter.isValid())
+ query.where("t.file_last_write > ?").bind(params.writtenAfter);
+
+ if (!params.keywords.empty())
+ {
+ std::vector clauses;
+ std::vector sortClauses;
+
+ for (std::string_view keyword : params.keywords)
+ {
+ clauses.push_back("a.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'");
+ query.bind("%" + Utils::escapeLikeKeyword(keyword) + "%");
+ }
+
+ for (std::string_view keyword : params.keywords)
+ {
+ sortClauses.push_back("a.sort_name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'");
+ query.bind("%" + Utils::escapeLikeKeyword(keyword) + "%");
+ }
+
+ query.where("(" + StringUtils::joinStrings(clauses, " AND ") + ") OR (" + StringUtils::joinStrings(sortClauses, " AND ") + ")");
+ }
+
+ 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);
+ }
+
+ 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";
+
+ WhereClause clusterClause;
+ for (const ClusterId clusterId : params.clusters)
+ {
+ clusterClause.Or(WhereClause("c.id = ?"));
+ query.bind(clusterId);
+ }
+
+ oss << " " << clusterClause.get();
+ oss << " GROUP BY t.id,a.id HAVING COUNT(DISTINCT c.id) = " << params.clusters.size() << ")";
+
+ query.where(oss.str());
+ }
+
+ if (params.track.isValid())
+ query.where("t.id = ?").bind(params.track);
+
+ if (params.release.isValid())
+ query.where("t.release_id = ?").bind(params.release);
+
+ 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;
+ }
+
+ return query;
+ }
+
+ template
+ Wt::Dbo::Query createQuery(Session& session, const Artist::FindParameters& params)
+ {
+ std::string_view itemToSelect;
+
+ if constexpr (std::is_same_v)
+ itemToSelect = "a.id";
+ else if constexpr (std::is_same_v>)
+ itemToSelect = "a";
+ else
+ static_assert("Unhandled type");
+
+ return createQuery(session, itemToSelect, params);
+ }
+ }
+
+ Artist::Artist(const std::string& name, const std::optional& MBID)
+ : _name{ std::string(name, 0 , _maxNameLength) },
+ _sortName{ _name },
+ _MBID{ MBID ? MBID->getAsString() : "" }
+ {
+ }
+
+ Artist::pointer Artist::create(Session& session, const std::string& name, const std::optional& MBID)
+ {
+ return session.getDboSession().add(std::unique_ptr {new Artist{ name, MBID }});
+ }
+
+ std::size_t Artist::getCount(Session& session)
+ {
+ session.checkSharedLocked();
+
+ return session.getDboSession().query("SELECT COUNT(*) FROM artist");
+ }
+
+ std::vector Artist::find(Session& session, const std::string& name)
+ {
+ session.checkSharedLocked();
+
+ Wt::Dbo::collection> res = session.getDboSession().find()
+ .where("name = ?").bind(std::string{ name, 0, _maxNameLength })
+ .orderBy("LENGTH(mbid) DESC"); // put mbid entries first
+
+ return std::vector(res.begin(), res.end());
+ }
+
+ Artist::pointer Artist::find(Session& session, const UUID& mbid)
+ {
+ session.checkSharedLocked();
+ return session.getDboSession().find().where("mbid = ?").bind(std::string{ mbid.getAsString() }).resultValue();
+ }
+
+ Artist::pointer Artist::find(Session& session, ArtistId id)
+ {
+ session.checkSharedLocked();
+ return session.getDboSession().find().where("id = ?").bind(id).resultValue();
+ }
+
+ bool Artist::exists(Session& session, ArtistId id)
+ {
+ session.checkSharedLocked();
+ return session.getDboSession().query("SELECT 1 FROM artist").where("id = ?").bind(id).resultValue() == 1;
+ }
+
+
+ RangeResults Artist::findOrphanIds(Session& session, Range range)
+ {
+ session.checkSharedLocked();
+ auto query{ session.getDboSession().query("SELECT DISTINCT a.id FROM artist a WHERE NOT EXISTS(SELECT 1 FROM track t INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id WHERE t.id = t_a_l.track_id)") };
+
+ return Utils::execQuery(query, range);
+ }
+
+ RangeResults Artist::findIds(Session& session, const FindParameters& params)
+ {
+ session.checkSharedLocked();
+
+ auto query{ createQuery(session, params) };
+ return Utils::execQuery(query, params.range);
+ }
+
+ RangeResults Artist::find(Session& session, const FindParameters& params)
+ {
+ session.checkSharedLocked();
+
+ auto query{ createQuery>(session, params) };
+ return Utils::execQuery(query, params.range);
+ }
+
+ RangeResults Artist::findSimilarArtistIds(EnumSet artistLinkTypes, Range range) const
+ {
+ assert(session());
+
+ 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 <> ?";
+
+ if (!artistLinkTypes.empty())
+ {
+ oss << " AND t_a_l.type IN (";
+
+ bool first{ true };
+ for (TrackArtistLinkType type : artistLinkTypes)
+ {
+ (void)type;
+ if (!first)
+ oss << ", ";
+ oss << "?";
+ first = false;
+ }
+ oss << ")";
+ }
+
+ auto query{ session()->query(oss.str())
+ .bind(getId())
+ .bind(getId())
+ .groupBy("a.id")
+ .orderBy("COUNT(*) DESC, RANDOM()") };
+
+ for (TrackArtistLinkType type : artistLinkTypes)
+ query.bind(type);
+
+ return Utils::execQuery(query, range);
+ }
+
+ std::vector> Artist::getClusterGroups(std::vector clusterTypes, std::size_t size) const
+ {
+ assert(session());
+
+ WhereClause where;
+
+ 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";
+
+ where.And(WhereClause("a.id = ?")).bind(getId().toString());
+ {
+ WhereClause clusterClause;
+ for (auto clusterType : clusterTypes)
+ clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterType->getId().toString());
+
+ where.And(clusterClause);
+ }
+ oss << " " << where.get();
+ oss << "GROUP BY c.id ORDER BY COUNT(DISTINCT c.id) DESC";
+
+ Wt::Dbo::Query> query = session()->query>(oss.str());
+
+ for (const std::string& bindArg : where.getBindArgs())
+ query.bind(bindArg);
+
+ Wt::Dbo::collection> queryRes = query;
+
+ std::map> clustersByType;
+ for (Cluster::pointer cluster : queryRes)
+ {
+ if (clustersByType[cluster->getType()->getId()].size() < size)
+ clustersByType[cluster->getType()->getId()].push_back(cluster);
+ }
+
+ std::vector> 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
diff --git a/src/libs/services/database/impl/Db.cpp b/src/libs/services/database/impl/Db.cpp
index bcac0eaa4..c21791657 100644
--- a/src/libs/services/database/impl/Db.cpp
+++ b/src/libs/services/database/impl/Db.cpp
@@ -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 clone() const override
{
- return std::make_unique(_dbPath);
+ return std::make_unique(*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(dbPath.string()) };
- // connection->setProperty("show-queries", "true");
+ // connection->setProperty("show-queries", "true");
auto connectionPool{ std::make_unique(std::move(connection), connectionCount) };
connectionPool->setTimeout(std::chrono::seconds{ 10 });
diff --git a/src/libs/services/database/impl/Listen.cpp b/src/libs/services/database/impl/Listen.cpp
index d77ef8271..d7af57d09 100644
--- a/src/libs/services/database/impl/Listen.cpp
+++ b/src/libs/services/database/impl/Listen.cpp
@@ -29,14 +29,14 @@ namespace
{
using namespace Database;
- Wt::Dbo::Query createArtistsQuery(Wt::Dbo::Session& session, UserId userId, Scrobbler scrobbler, const std::vector& clusterIds, std::optional linkType)
+ Wt::Dbo::Query createArtistsQuery(Wt::Dbo::Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, std::optional linkType)
{
auto query{ session.query("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 createReleasesQuery(Wt::Dbo::Session& session, UserId userId, Scrobbler scrobbler, const std::vector& clusterIds)
+ Wt::Dbo::Query createReleasesQuery(Wt::Dbo::Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds)
{
auto query{ session.query("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 createTracksQuery(Wt::Dbo::Session& session, UserId userId, Scrobbler scrobbler, const std::vector& clusterIds)
+ Wt::Dbo::Query createTracksQuery(Wt::Dbo::Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds)
{
auto query{ session.query("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, ObjectPtr track, Scrobbler scrobbler, const Wt::WDateTime& dateTime)
+ Listen::Listen(ObjectPtr user, ObjectPtr 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, ObjectPtr track, Scrobbler scrobbler, const Wt::WDateTime& dateTime)
+ Listen::pointer Listen::create(Session& session, ObjectPtr user, ObjectPtr track, ScrobblingBackend backend, const Wt::WDateTime& dateTime)
{
- return session.getDboSession().add(std::unique_ptr {new Listen{ user, track, scrobbler, dateTime }});
+ session.checkUniqueLocked();
+ return session.getDboSession().add(std::unique_ptr {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()
.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 Listen::getTopArtists(Session& session, UserId userId, Scrobbler scrobbler, const std::vector& clusterIds, std::optional linkType, Range range)
+ RangeResults Listen::getTopArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, std::optional 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 Listen::getTopReleases(Session& session, UserId userId, Scrobbler scrobbler, const std::vector& clusterIds, Range range)
+ RangeResults Listen::getTopReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& 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 Listen::getTopTracks(Session& session, UserId userId, Scrobbler scrobbler, const std::vector& clusterIds, Range range)
+ RangeResults Listen::getTopTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& 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 Listen::getRecentArtists(Session& session, UserId userId, Scrobbler scrobbler, const std::vector& clusterIds, std::optional linkType, Range range)
+ RangeResults Listen::getRecentArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, std::optional 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 Listen::getRecentReleases(Session& session, UserId userId, Scrobbler scrobbler, const std::vector& clusterIds, Range range)
+ RangeResults Listen::getRecentReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& 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 Listen::getRecentTracks(Session& session, UserId userId, Scrobbler scrobbler, const std::vector& clusterIds, Range range)
+ RangeResults Listen::getRecentTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& 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("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("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>("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>("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();
diff --git a/src/libs/services/database/impl/Migration.cpp b/src/libs/services/database/impl/Migration.cpp
index 6c11b8879..da002ba0e 100644
--- a/src/libs/services/database/impl/Migration.cpp
+++ b/src/libs/services/database/impl/Migration.cpp
@@ -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(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(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(User::defaultSubsonicTranscodeFormat))
- + ", " + std::to_string(User::defaultSubsonicTranscodeBitrate)
- + ", " + std::to_string(static_cast(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(/*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(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 userScrobblers;
- auto getScrobbler{ [&](IdType::ValueType userId)
- {
- auto itScrobbler {userScrobblers.find(userId)};
- if (itScrobbler != std::cend(userScrobblers))
- return itScrobbler->second;
-
- auto query {session.getDboSession().query("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 ;
-
- std::vector starredEntries;
- auto query {session.getDboSession().query("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;
-
- std::vector listens;
- auto query {session.getDboSession().query("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 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},
};
{
diff --git a/src/libs/services/database/impl/Migration.hpp b/src/libs/services/database/impl/Migration.hpp
index 68a70ade4..55b1e679b 100644
--- a/src/libs/services/database/impl/Migration.hpp
+++ b/src/libs/services/database/impl/Migration.hpp
@@ -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:
diff --git a/src/libs/services/database/impl/Release.cpp b/src/libs/services/database/impl/Release.cpp
index 109c24b7a..8021dc98d 100644
--- a/src/libs/services/database/impl/Release.cpp
+++ b/src/libs/services/database/impl/Release.cpp
@@ -34,147 +34,170 @@
namespace Database
{
-
- Wt::Dbo::Query createQuery(Session& session, const Release::FindParameters& params)
+ namespace
{
- auto query{ session.getDboSession().query("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
+ Wt::Dbo::Query 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("SELECT DISTINCT " + std::string{ itemToSelect } + " 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()
+ || params.clusters.size() == 1)
+ {
+ query.join("track t ON t.release_id = r.id");
+ }
- if (params.writtenAfter.isValid())
- query.where("t.file_last_write > ?").bind(params.writtenAfter);
+ 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.dateRange)
+ {
+ query.where("t.date >= ?").bind(params.dateRange->begin);
+ query.where("t.date <= ?").bind(params.dateRange->end);
+ }
- 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);
- }
+ for (std::string_view keyword : params.keywords)
+ query.where("r.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + Utils::escapeLikeKeyword(keyword) + "%");
- 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.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.trackArtistLinkTypes.empty())
+ if (params.artist.isValid())
{
- std::ostringstream oss;
+ 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);
- bool first{ true };
- for (TrackArtistLinkType linkType : params.trackArtistLinkTypes)
+ if (!params.trackArtistLinkTypes.empty())
{
- if (!first)
- oss << " OR ";
- oss << "t_a_l.type = ?";
- query.bind(linkType);
+ 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());
+ }
- first = false;
+ 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());
}
- query.where(oss.str());
}
- if (!params.excludedTrackArtistLinkTypes.empty())
+ if (params.clusters.size() == 1)
+ {
+ query.join("track_cluster t_c ON t_c.track_id = t.id")
+ .where("t_c.cluster_id = ?").bind(params.clusters.front());
+ }
+ else if (params.clusters.size() > 1)
{
std::ostringstream oss;
- oss << "r.id 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 (";
-
- query.bind(params.artist);
+ " INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
- 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
+ Wt::Dbo::Query 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)
+ itemToSelect = "r.id";
+ else if constexpr (std::is_same_v>)
+ itemToSelect = "r";
+ else
+ static_assert("Unhandled type");
+
+ return createQuery(session, itemToSelect, params);
}
-
- return query;
}
Release::Release(const std::string& name, const std::optional& MBID)
@@ -233,7 +256,7 @@ namespace Database
return session.getDboSession().query("SELECT COUNT(*) FROM release");
}
- RangeResults Release::findOrderedByArtist(Session& session, Range range)
+ RangeResults Release::findIdsOrderedByArtist(Session& session, Range range)
{
session.checkSharedLocked();
@@ -248,7 +271,7 @@ namespace Database
return Utils::execQuery(query, range);
}
- RangeResults Release::findOrphans(Session& session, Range range)
+ RangeResults Release::findOrphanIds(Session& session, Range range)
{
session.checkSharedLocked();
@@ -256,12 +279,19 @@ namespace Database
return Utils::execQuery(query, range);
}
- RangeResults Release::find(Session& session, const FindParameters& params)
+ RangeResults Release::find(Session& session, const FindParameters& params)
{
session.checkSharedLocked();
- auto query{ createQuery(session, params) };
+ auto query{ createQuery>(session, params) };
+ return Utils::execQuery(query, params.range);
+ }
+
+ RangeResults