SubsonicAPI: optimized search for tracks

This commit is contained in:
emeric
2023-11-04 17:36:59 +01:00
parent c4d2d20447
commit bd59dc3bc9
27 changed files with 3162 additions and 3301 deletions
+396 -416
View File
@@ -34,445 +34,425 @@
#include "utils/String.hpp" #include "utils/String.hpp"
#include "utils/Utils.hpp" #include "utils/Utils.hpp"
namespace namespace Cover
{ {
struct TrackInfo
{
bool hasCover {};
bool isMultiDisc {};
std::filesystem::path trackPath;
std::optional<Database::ReleaseId> releaseId;
};
std::optional<TrackInfo> namespace
getTrackInfo(Database::Session& dbSession, Database::TrackId trackId) {
{ struct TrackInfo
std::optional<TrackInfo> res; {
bool hasCover{};
bool isMultiDisc{};
std::filesystem::path trackPath;
std::optional<Database::ReleaseId> releaseId;
};
auto transaction {dbSession.createSharedTransaction()}; std::optional<TrackInfo> getTrackInfo(Database::Session& dbSession, Database::TrackId trackId)
{
std::optional<TrackInfo> res;
const Database::Track::pointer track {Database::Track::find(dbSession, trackId)}; auto transaction{ dbSession.createSharedTransaction() };
if (!track)
return res;
res = TrackInfo {}; const Database::Track::pointer track{ Database::Track::find(dbSession, trackId) };
if (!track)
return res;
res->hasCover = track->hasCover(); res = TrackInfo{};
res->trackPath = track->getPath();
if (const Database::Release::pointer& release {track->getRelease()}) res->hasCover = track->hasCover();
{ res->trackPath = track->getPath();
res->releaseId = release->getId();
if (release->getTotalDisc() > 1)
res->isMultiDisc = true;
}
return res; if (const Database::Release::pointer & release{ track->getRelease() })
} {
res->releaseId = release->getId();
if (release->getTotalDisc() > 1)
res->isMultiDisc = true;
}
std::vector<std::string> constructPreferredFileNames() return res;
{ }
std::vector<std::string> res;
Service<IConfig>::get()->visitStrings("cover-preferred-file-names", std::vector<std::string> constructPreferredFileNames()
[&res](std::string_view fileName) {
{ std::vector<std::string> res;
res.emplace_back(fileName);
}, {"cover", "front"});
return res; Service<IConfig>::get()->visitStrings("cover-preferred-file-names",
} [&res](std::string_view fileName)
} {
res.emplace_back(fileName);
}, { "cover", "front" });
namespace Cover { return res;
}
using namespace Image; bool isFileSupported(const std::filesystem::path& file, const std::vector<std::filesystem::path>& extensions)
{
return (std::find(std::cbegin(extensions), std::cend(extensions), file.extension()) != std::cend(extensions));
}
}
static std::unique_ptr<ICoverService> createCoverService(Database::Db& db, const std::filesystem::path& execPath, const std::filesystem::path& defaultCoverPath)
bool {
isFileSupported(const std::filesystem::path& file, const std::vector<std::filesystem::path>& extensions) return std::make_unique<CoverService>(db, execPath, defaultCoverPath);
{ }
return (std::find(std::cbegin(extensions), std::cend(extensions), file.extension()) != std::cend(extensions));
}
std::unique_ptr<ICoverService> using namespace Image;
createCoverService(Database::Db& db, const std::filesystem::path& execPath, const std::filesystem::path& defaultCoverPath)
{
return std::make_unique<CoverService>(db, execPath, defaultCoverPath);
}
CoverService::CoverService(Database::Db& db, CoverService::CoverService(Database::Db& db,
const std::filesystem::path& execPath, const std::filesystem::path& execPath,
const std::filesystem::path& defaultCoverPath) const std::filesystem::path& defaultCoverPath)
: _db {db} : _db{ db }
, _defaultCoverPath {defaultCoverPath} , _defaultCoverPath{ defaultCoverPath }
, _maxCacheSize {Service<IConfig>::get()->getULong("cover-max-cache-size", 30) * 1000 * 1000} , _maxCacheSize{ Service<IConfig>::get()->getULong("cover-max-cache-size", 30) * 1000 * 1000 }
, _maxFileSize {Service<IConfig>::get()->getULong("cover-max-file-size", 10) * 1000 * 1000} , _maxFileSize{ Service<IConfig>::get()->getULong("cover-max-file-size", 10) * 1000 * 1000 }
, _preferredFileNames {constructPreferredFileNames()} , _preferredFileNames{ constructPreferredFileNames() }
{ {
setJpegQuality(Service<IConfig>::get()->getULong("cover-jpeg-quality", 75)); setJpegQuality(Service<IConfig>::get()->getULong("cover-jpeg-quality", 75));
LMS_LOG(COVER, INFO) << "Default cover path = '" << _defaultCoverPath.string() << "'"; LMS_LOG(COVER, INFO) << "Default cover path = '" << _defaultCoverPath.string() << "'";
LMS_LOG(COVER, INFO) << "Max cache size = " << _maxCacheSize; LMS_LOG(COVER, INFO) << "Max cache size = " << _maxCacheSize;
LMS_LOG(COVER, INFO) << "Max file size = " << _maxFileSize; LMS_LOG(COVER, INFO) << "Max file size = " << _maxFileSize;
LMS_LOG(COVER, INFO) << "Preferred file names: " << StringUtils::joinStrings(_preferredFileNames, ","); LMS_LOG(COVER, INFO) << "Preferred file names: " << StringUtils::joinStrings(_preferredFileNames, ",");
#if LMS_SUPPORT_IMAGE_GM #if LMS_SUPPORT_IMAGE_GM
GraphicsMagick::init(execPath); GraphicsMagick::init(execPath);
#else #else
(void)execPath; (void)execPath;
#endif #endif
try try
{ {
getDefault(512); getDefault(512);
} }
catch (const Image::ImageException& e) catch (const Image::ImageException& e)
{ {
throw LmsException("Cannot read default cover file '" + _defaultCoverPath.string() + "': " + e.what()); throw LmsException("Cannot read default cover file '" + _defaultCoverPath.string() + "': " + e.what());
} }
} }
std::unique_ptr<IEncodedImage> std::unique_ptr<IEncodedImage> CoverService::getFromAvMediaFile(const Av::IAudioFile& input, ImageSize width) const
CoverService::getFromAvMediaFile(const Av::IAudioFile& input, ImageSize width) const {
{ std::unique_ptr<IEncodedImage> image;
std::unique_ptr<IEncodedImage> image;
input.visitAttachedPictures([&](const Av::Picture& picture)
input.visitAttachedPictures([&](const Av::Picture& picture) {
{ if (image)
if (image) return;
return;
try
try {
{ std::unique_ptr<IRawImage> rawImage{ decodeImage(picture.data, picture.dataSize) };
std::unique_ptr<IRawImage> rawImage {decodeImage(picture.data, picture.dataSize)}; rawImage->resize(width);
rawImage->resize(width); image = rawImage->encodeToJPEG(_jpegQuality);
image = rawImage->encodeToJPEG(_jpegQuality); }
} catch (const Image::ImageException& e)
catch (const Image::ImageException& e) {
{ LMS_LOG(COVER, ERROR) << "Cannot read embedded cover: " << e.what();
LMS_LOG(COVER, ERROR) << "Cannot read embedded cover: " << e.what(); }
} });
});
return image;
return image; }
}
std::unique_ptr<IEncodedImage> CoverService::getFromCoverFile(const std::filesystem::path& p, ImageSize width) const
std::unique_ptr<IEncodedImage> {
CoverService::getFromCoverFile(const std::filesystem::path& p, ImageSize width) const std::unique_ptr<IEncodedImage> image;
{
std::unique_ptr<IEncodedImage> image; try
{
try std::unique_ptr<IRawImage> rawImage{ decodeImage(p) };
{ rawImage->resize(width);
std::unique_ptr<IRawImage> rawImage {decodeImage(p)}; image = rawImage->encodeToJPEG(_jpegQuality);
rawImage->resize(width); }
image = rawImage->encodeToJPEG(_jpegQuality); catch (const ImageException& e)
} {
catch (const ImageException& e) LMS_LOG(COVER, ERROR) << "Cannot read cover in file '" << p.string() << "': " << e.what();
{ }
LMS_LOG(COVER, ERROR) << "Cannot read cover in file '" << p.string() << "': " << e.what();
} return image;
}
return image;
} std::shared_ptr<IEncodedImage> CoverService::getDefault(ImageSize width)
{
std::shared_ptr<IEncodedImage> {
CoverService::getDefault(ImageSize width) std::shared_lock lock{ _cacheMutex };
{
{ if (auto it{ _defaultCoverCache.find(width) }; it != std::cend(_defaultCoverCache))
std::shared_lock lock {_cacheMutex}; return it->second;
}
if (auto it {_defaultCoverCache.find(width)}; it != std::cend(_defaultCoverCache))
return it->second; {
} std::unique_lock lock{ _cacheMutex };
{ if (auto it{ _defaultCoverCache.find(width) }; it != std::cend(_defaultCoverCache))
std::unique_lock lock {_cacheMutex}; return it->second;
if (auto it {_defaultCoverCache.find(width)}; it != std::cend(_defaultCoverCache)) std::shared_ptr<IEncodedImage> image{ getFromCoverFile(_defaultCoverPath, width) };
return it->second; _defaultCoverCache[width] = image;
LMS_LOG(COVER, DEBUG) << "Default cache entries = " << _defaultCoverCache.size();
std::shared_ptr<IEncodedImage> image {getFromCoverFile(_defaultCoverPath, width)};
_defaultCoverCache[width] = image; return image;
LMS_LOG(COVER, DEBUG) << "Default cache entries = " << _defaultCoverCache.size(); }
}
return image;
} std::unique_ptr<IEncodedImage> CoverService::getFromDirectory(const std::filesystem::path& directory, ImageSize width) const
} {
const std::multimap<std::string, std::filesystem::path> coverPaths{ getCoverPaths(directory) };
std::unique_ptr<IEncodedImage>
CoverService::getFromDirectory(const std::filesystem::path& directory, ImageSize width) const auto tryLoadImageFromFilename = [&](std::string_view fileName)
{ {
const std::multimap<std::string, std::filesystem::path> coverPaths {getCoverPaths(directory)}; std::unique_ptr<IEncodedImage> image;
auto tryLoadImageFromFilename = [&](std::string_view fileName) auto range{ coverPaths.equal_range(std::string {fileName}) };
{ for (auto it{ range.first }; it != range.second; ++it)
std::unique_ptr<IEncodedImage> image; {
image = getFromCoverFile(it->second, width);
auto range {coverPaths.equal_range(std::string {fileName})}; if (image)
for (auto it {range.first}; it != range.second; ++it) break;
{ }
image = getFromCoverFile(it->second, width); return image;
if (image) };
break;
} std::unique_ptr<IEncodedImage> image;
return image;
}; for (std::string_view filename : _preferredFileNames)
{
std::unique_ptr<IEncodedImage> image; image = tryLoadImageFromFilename(filename);
if (image)
for (std::string_view filename : _preferredFileNames) return image;
{ }
image = tryLoadImageFromFilename(filename);
if (image) // Just pick one
return image; for (const auto& [filename, coverPath] : coverPaths)
} {
image = getFromCoverFile(coverPath, width);
// Just pick one if (image)
for (const auto& [filename, coverPath] : coverPaths) return image;
{ }
image = getFromCoverFile(coverPath, width);
if (image) return image;
return image; }
}
std::unique_ptr<IEncodedImage> CoverService::getFromSameNamedFile(const std::filesystem::path& filePath, ImageSize width) const
return image; {
} std::unique_ptr<IEncodedImage> res;
std::unique_ptr<IEncodedImage> std::filesystem::path coverPath{ filePath };
CoverService::getFromSameNamedFile(const std::filesystem::path& filePath, ImageSize width) const for (const std::filesystem::path& extension : _fileExtensions)
{ {
std::unique_ptr<IEncodedImage> res; coverPath.replace_extension(extension);
std::filesystem::path coverPath {filePath}; if (!checkCoverFile(coverPath))
for (const std::filesystem::path& extension : _fileExtensions) continue;
{
coverPath.replace_extension(extension); res = getFromCoverFile(coverPath, width);
if (res)
if (!checkCoverFile(coverPath)) break;
continue; }
res = getFromCoverFile(coverPath, width); return res;
if (res) }
break;
} bool CoverService::checkCoverFile(const std::filesystem::path& filePath) const
{
return res; std::error_code ec;
}
if (!isFileSupported(filePath, _fileExtensions))
bool return false;
CoverService::checkCoverFile(const std::filesystem::path& filePath) const
{ if (!std::filesystem::exists(filePath, ec))
std::error_code ec; return false;
if (!isFileSupported(filePath, _fileExtensions)) if (!std::filesystem::is_regular_file(filePath, ec))
return false; return false;
if (!std::filesystem::exists(filePath, ec)) if (std::filesystem::file_size(filePath, ec) > _maxFileSize && !ec)
return false; {
LMS_LOG(COVER, INFO) << "Cover file '" << filePath.string() << " is too big (" << std::filesystem::file_size(filePath, ec) << "), limit is " << _maxFileSize;
if (!std::filesystem::is_regular_file(filePath, ec)) return false;
return false; }
if (std::filesystem::file_size(filePath, ec) > _maxFileSize && !ec) return true;
{ }
LMS_LOG(COVER, INFO) << "Cover file '" << filePath.string() << " is too big (" << std::filesystem::file_size(filePath, ec) << "), limit is " << _maxFileSize;
return false; std::multimap<std::string, std::filesystem::path> CoverService::getCoverPaths(const std::filesystem::path& directoryPath) const
} {
std::multimap<std::string, std::filesystem::path> res;
return true; std::error_code ec;
}
std::filesystem::directory_iterator itPath(directoryPath, ec);
std::multimap<std::string, std::filesystem::path> std::filesystem::directory_iterator itEnd;
CoverService::getCoverPaths(const std::filesystem::path& directoryPath) const while (!ec && itPath != itEnd)
{ {
std::multimap<std::string, std::filesystem::path> res; const std::filesystem::path& path{ *itPath };
std::error_code ec;
if (checkCoverFile(path))
std::filesystem::directory_iterator itPath(directoryPath, ec); res.emplace(std::filesystem::path{ path }.filename().replace_extension("").string(), path);
std::filesystem::directory_iterator itEnd;
while (!ec && itPath != itEnd) itPath.increment(ec);
{ }
const std::filesystem::path& path {*itPath};
return res;
if (checkCoverFile(path)) }
res.emplace(std::filesystem::path{ path }.filename().replace_extension("").string(), path);
std::unique_ptr<IEncodedImage> CoverService::getFromTrack(const std::filesystem::path& p, ImageSize width) const
itPath.increment(ec); {
} std::unique_ptr<IEncodedImage> image;
return res; try
} {
image = getFromAvMediaFile(*Av::parseAudioFile(p), width);
std::unique_ptr<IEncodedImage> }
CoverService::getFromTrack(const std::filesystem::path& p, ImageSize width) const catch (Av::Exception& e)
{ {
std::unique_ptr<IEncodedImage> image; LMS_LOG(COVER, ERROR) << "Cannot get covers from track " << p.string() << ": " << e.what();
}
try
{ return image;
image = getFromAvMediaFile(*Av::parseAudioFile(p), width); }
}
catch (Av::Exception& e) std::shared_ptr<IEncodedImage> CoverService::getFromTrack(Database::TrackId trackId, ImageSize width)
{ {
LMS_LOG(COVER, ERROR) << "Cannot get covers from track " << p.string() << ": " << e.what(); return getFromTrack(_db.getTLSSession(), trackId, width, true /* allow release fallback*/);
} }
return image; std::shared_ptr<IEncodedImage> CoverService::getFromTrack(Database::Session& dbSession, Database::TrackId trackId, ImageSize width, bool allowReleaseFallback)
} {
using namespace Database;
std::shared_ptr<IEncodedImage>
CoverService::getFromTrack(Database::TrackId trackId, ImageSize width) const CacheEntryDesc cacheEntryDesc{ trackId, width };
{
return getFromTrack(_db.getTLSSession(), trackId, width, true /* allow release fallback*/); std::shared_ptr<IEncodedImage> cover{ loadFromCache(cacheEntryDesc) };
} if (cover)
return cover;
std::shared_ptr<IEncodedImage>
CoverService::getFromTrack(Database::Session& dbSession, Database::TrackId trackId, ImageSize width, bool allowReleaseFallback) if (const std::optional<TrackInfo> trackInfo{ getTrackInfo(dbSession, trackId) })
{ {
using namespace Database; if (trackInfo->hasCover)
cover = getFromTrack(trackInfo->trackPath, width);
const CacheEntryDesc cacheEntryDesc {trackId, width};
if (!cover)
std::shared_ptr<IEncodedImage> cover {loadFromCache(cacheEntryDesc)}; cover = getFromSameNamedFile(trackInfo->trackPath, width);
if (cover)
return cover; if (!cover && trackInfo->releaseId && allowReleaseFallback)
cover = getFromRelease(*trackInfo->releaseId, width);
if (const std::optional<TrackInfo> trackInfo {getTrackInfo(dbSession, trackId)})
{ if (!cover && trackInfo->isMultiDisc)
if (trackInfo->hasCover) {
cover = getFromTrack(trackInfo->trackPath, width); if (trackInfo->trackPath.parent_path().has_parent_path())
cover = getFromDirectory(trackInfo->trackPath.parent_path().parent_path(), width);
if (!cover) }
cover = getFromSameNamedFile(trackInfo->trackPath, width); }
if (!cover && trackInfo->releaseId && allowReleaseFallback) if (!cover)
cover = getFromRelease(*trackInfo->releaseId, width); cover = getDefault(width);
if (!cover && trackInfo->isMultiDisc) if (cover)
{ saveToCache(cacheEntryDesc, cover);
if (trackInfo->trackPath.parent_path().has_parent_path())
cover = getFromDirectory(trackInfo->trackPath.parent_path().parent_path(), width); return cover;
} }
}
std::shared_ptr<IEncodedImage> CoverService::getFromRelease(Database::ReleaseId releaseId, ImageSize width)
if (!cover) {
cover = getDefault(width); using namespace Database;
const CacheEntryDesc cacheEntryDesc{ releaseId, width };
if (cover)
saveToCache(cacheEntryDesc, cover); std::shared_ptr<IEncodedImage> cover{ loadFromCache(cacheEntryDesc) };
if (cover)
return cover; return cover;
}
struct ReleaseInfo
std::shared_ptr<IEncodedImage> {
CoverService::getFromRelease(Database::ReleaseId releaseId, ImageSize width) TrackId firstTrackId;
{ std::filesystem::path releaseDirectory;
using namespace Database; };
const CacheEntryDesc cacheEntryDesc {releaseId, width};
Session& session{ _db.getTLSSession() };
std::shared_ptr<IEncodedImage> cover {loadFromCache(cacheEntryDesc)};
if (cover) auto getReleaseInfo{ [&]
return cover; {
std::optional<ReleaseInfo> res;
struct ReleaseInfo
{ auto transaction{ session.createSharedTransaction() };
TrackId firstTrackId;
std::filesystem::path releaseDirectory; const auto tracks{ Track::find(session, Track::FindParameters {}.setRelease(releaseId).setRange({0, 1}).setSortMethod(TrackSortMethod::Release)) };
};
if (!tracks.results.empty())
Session& session {_db.getTLSSession()}; {
const Track::pointer& track{ tracks.results.front() };
auto getReleaseInfo {[&] res = ReleaseInfo{};
{ res->firstTrackId = track->getId();
std::optional<ReleaseInfo> res; res->releaseDirectory = track->getPath().parent_path();
}
auto transaction {session.createSharedTransaction()};
return res;
const auto tracks {Track::find(session, Track::FindParameters {}.setRelease(releaseId).setRange({0, 1}).setSortMethod(TrackSortMethod::Release))}; } };
if (!tracks.results.empty()) if (const std::optional<ReleaseInfo> releaseInfo{ getReleaseInfo() })
{ {
if (const Track::pointer track {Track::find(session, tracks.results.front())}) cover = getFromDirectory(releaseInfo->releaseDirectory, width);
{ if (!cover)
res = ReleaseInfo {}; cover = getFromTrack(session, releaseInfo->firstTrackId, width, false /* no release fallback */);
res->firstTrackId = track->getId(); }
res->releaseDirectory = track->getPath().parent_path();
} if (!cover)
} cover = getDefault(width);
return res; if (cover)
}}; saveToCache(cacheEntryDesc, cover);
if (const std::optional<ReleaseInfo> releaseInfo {getReleaseInfo()}) return cover;
{ }
cover = getFromDirectory(releaseInfo->releaseDirectory, width);
if (!cover) void CoverService::flushCache()
cover = getFromTrack(session, releaseInfo->firstTrackId, width, false /* no release fallback */); {
} std::unique_lock lock{ _cacheMutex };
if (!cover) LMS_LOG(COVER, DEBUG) << "Cache stats: hits = " << _cacheHits << ", misses = " << _cacheMisses << ", nb entries = " << _cache.size() << ", size = " << _cacheSize;
cover = getDefault(width); _cacheHits = 0;
_cacheMisses = 0;
if (cover) _cacheSize = 0;
saveToCache(cacheEntryDesc, cover); _cache.clear();
}
return cover;
} void CoverService::setJpegQuality(unsigned quality)
{
void _jpegQuality = Utils::clamp<unsigned>(quality, 1, 100);
CoverService::flushCache()
{ LMS_LOG(COVER, INFO) << "JPEG export quality = " << _jpegQuality;
std::unique_lock lock {_cacheMutex}; }
LMS_LOG(COVER, DEBUG) << "Cache stats: hits = " << _cacheHits << ", misses = " << _cacheMisses << ", nb entries = " << _cache.size() << ", size = " << _cacheSize; void CoverService::saveToCache(const CacheEntryDesc& entryDesc, std::shared_ptr<IEncodedImage> image)
_cacheHits = 0; {
_cacheMisses = 0; std::unique_lock lock{ _cacheMutex };
_cacheSize = 0;
_cache.clear(); while (_cacheSize + image->getDataSize() > _maxCacheSize && !_cache.empty())
} {
auto itRandom{ Random::pickRandom(_cache) };
void _cacheSize -= itRandom->second->getDataSize();
CoverService::setJpegQuality(unsigned quality) _cache.erase(itRandom);
{ }
_jpegQuality = Utils::clamp<unsigned>(quality, 1, 100);
_cacheSize += image->getDataSize();
LMS_LOG(COVER, INFO) << "JPEG export quality = " << _jpegQuality; _cache[entryDesc] = image;
} }
void std::shared_ptr<IEncodedImage> CoverService::loadFromCache(const CacheEntryDesc& entryDesc)
CoverService::saveToCache(const CacheEntryDesc& entryDesc, std::shared_ptr<IEncodedImage> image) {
{ std::shared_lock lock{ _cacheMutex };
std::unique_lock lock {_cacheMutex};
auto it{ _cache.find(entryDesc) };
while (_cacheSize + image->getDataSize() > _maxCacheSize && !_cache.empty()) if (it == std::cend(_cache))
{ {
auto itRandom {Random::pickRandom(_cache)}; ++_cacheMisses;
_cacheSize -= itRandom->second->getDataSize(); return nullptr;
_cache.erase(itRandom); }
}
++_cacheHits;
_cacheSize += image->getDataSize(); return it->second;
_cache[entryDesc] = image; }
}
std::shared_ptr<IEncodedImage>
CoverService::loadFromCache(const CacheEntryDesc& entryDesc)
{
std::shared_lock lock {_cacheMutex};
auto it {_cache.find(entryDesc)};
if (it == std::cend(_cache))
{
++_cacheMisses;
return nullptr;
}
++_cacheHits;
return it->second;
}
} // namespace Cover } // namespace Cover
+31 -8
View File
@@ -39,11 +39,12 @@ namespace Database
{ {
namespace namespace
{ {
Wt::Dbo::Query<TrackId> createQuery(Session& session, const Track::FindParameters& params) template <typename ResultType>
Wt::Dbo::Query<ResultType> createQuery(Session& session, std::string_view itemToSelect, const Track::FindParameters& params)
{ {
session.checkSharedLocked(); session.checkSharedLocked();
auto query{ session.getDboSession().query<TrackId>(params.distinct ? "SELECT DISTINCT t.id FROM track t" : "SELECT t.id FROM track t") }; auto query{ session.getDboSession().query<ResultType>(params.distinct ? "SELECT DISTINCT " + std::string{ itemToSelect } + " FROM track t" : "SELECT t.id FROM track t") };
assert(params.keywords.empty() || params.name.empty()); assert(params.keywords.empty() || params.name.empty());
for (std::string_view keyword : params.keywords) for (std::string_view keyword : params.keywords)
@@ -168,6 +169,21 @@ namespace Database
return query; return query;
} }
template <typename ResultType>
Wt::Dbo::Query<ResultType> createQuery(Session& session, const Track::FindParameters& params)
{
std::string_view itemToSelect;
if constexpr (std::is_same_v<ResultType, TrackId>)
itemToSelect = "t.id";
else if constexpr (std::is_same_v<ResultType, Wt::Dbo::ptr<Track>>)
itemToSelect = "t";
else
static_assert("Unhandled type");
return createQuery<ResultType>(session, itemToSelect, params);
}
} }
Track::Track(const std::filesystem::path& p) Track::Track(const std::filesystem::path& p)
@@ -257,7 +273,7 @@ namespace Database
return res; return res;
} }
RangeResults<TrackId> Track::findTrackMBIDDuplicates(Session& session, Range range) RangeResults<TrackId> Track::findIdsTrackMBIDDuplicates(Session& session, Range range)
{ {
session.checkSharedLocked(); session.checkSharedLocked();
@@ -267,7 +283,7 @@ namespace Database
return Utils::execQuery(query, range); return Utils::execQuery(query, range);
} }
RangeResults<TrackId> Track::findWithRecordingMBIDAndMissingFeatures(Session& session, Range range) RangeResults<TrackId> Track::findIdsWithRecordingMBIDAndMissingFeatures(Session& session, Range range)
{ {
session.checkSharedLocked(); session.checkSharedLocked();
@@ -295,16 +311,23 @@ namespace Database
return std::vector<ClusterId>(res.begin(), res.end()); return std::vector<ClusterId>(res.begin(), res.end());
} }
RangeResults<TrackId> Track::find(Session& session, const FindParameters& parameters) RangeResults<TrackId> Track::findIds(Session& session, const FindParameters& parameters)
{ {
session.checkSharedLocked(); session.checkSharedLocked();
auto query{ createQuery(session, parameters) }; auto query{ createQuery<TrackId>(session, parameters) };
return Utils::execQuery(query, parameters.range); return Utils::execQuery(query, parameters.range);
} }
RangeResults<TrackId> Track::findSimilarTracks(Session& session, const std::vector<TrackId>& tracks, Range range) RangeResults<Track::pointer> Track::find(Session& session, const FindParameters& parameters)
{
session.checkSharedLocked();
auto query{ createQuery<Wt::Dbo::ptr<Track>>(session, parameters) };
return Utils::execQuery(query, parameters.range);
}
RangeResults<TrackId> Track::findSimilarTrackIds(Session& session, const std::vector<TrackId>& tracks, Range range)
{ {
assert(!tracks.empty()); assert(!tracks.empty());
session.checkSharedLocked(); session.checkSharedLocked();
@@ -21,11 +21,10 @@
#include <chrono> #include <chrono>
#include <filesystem> #include <filesystem>
#include <iostream> #include <ostream>
#include <optional> #include <optional>
#include <string> #include <string>
#include <string_view> #include <string_view>
#include <unordered_set>
#include <utility> #include <utility>
#include <vector> #include <vector>
@@ -111,12 +110,13 @@ class Track final : public Object<Track, TrackId>
static bool exists(Session& session, TrackId id); static bool exists(Session& session, TrackId id);
static std::vector<pointer> findByRecordingMBID(Session& session, const UUID& MBID); static std::vector<pointer> findByRecordingMBID(Session& session, const UUID& MBID);
static std::vector<pointer> findByMBID(Session& session, const UUID& MBID); static std::vector<pointer> findByMBID(Session& session, const UUID& MBID);
static RangeResults<TrackId> findSimilarTracks(Session& session, const std::vector<TrackId>& trackIds, Range range); static RangeResults<TrackId> findSimilarTrackIds(Session& session, const std::vector<TrackId>& trackIds, Range range);
static RangeResults<TrackId> find(Session& session, const FindParameters& parameters); static RangeResults<TrackId> findIds(Session& session, const FindParameters& parameters);
static RangeResults<pointer> find(Session& session, const FindParameters& parameters);
static RangeResults<PathResult> findPaths(Session& session, Range range); static RangeResults<PathResult> findPaths(Session& session, Range range);
static RangeResults<TrackId> findTrackMBIDDuplicates(Session& session, Range range); static RangeResults<TrackId> findIdsTrackMBIDDuplicates(Session& session, Range range);
static RangeResults<TrackId> findWithRecordingMBIDAndMissingFeatures(Session& session, Range range); static RangeResults<TrackId> findIdsWithRecordingMBIDAndMissingFeatures(Session& session, Range range);
// Accessors // Accessors
void setScanVersion(std::size_t version) { _scanVersion = version; } void setScanVersion(std::size_t version) { _scanVersion = version; }
+306 -307
View File
@@ -23,415 +23,414 @@ using namespace Database;
TEST_F(DatabaseFixture, Artist) TEST_F(DatabaseFixture, Artist)
{ {
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
EXPECT_FALSE(Artist::exists(session, 35)); EXPECT_FALSE(Artist::exists(session, 35));
EXPECT_FALSE(Artist::exists(session, 0)); EXPECT_FALSE(Artist::exists(session, 0));
EXPECT_FALSE(Artist::exists(session, 1)); EXPECT_FALSE(Artist::exists(session, 1));
EXPECT_EQ(Artist::getCount(session), 0); EXPECT_EQ(Artist::getCount(session), 0);
} }
ScopedArtist artist {session, "MyArtist"}; ScopedArtist artist{ session, "MyArtist" };
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
EXPECT_TRUE(artist.get()); EXPECT_TRUE(artist.get());
EXPECT_FALSE(!artist.get()); EXPECT_FALSE(!artist.get());
EXPECT_EQ(artist.get()->getId(), artist.getId()); EXPECT_EQ(artist.get()->getId(), artist.getId());
EXPECT_TRUE(Artist::exists(session, artist.getId())); EXPECT_TRUE(Artist::exists(session, artist.getId()));
EXPECT_EQ(Artist::getCount(session), 1); EXPECT_EQ(Artist::getCount(session), 1);
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
auto artists {Artist::find(session, Artist::FindParameters {})}; auto artists{ Artist::find(session, Artist::FindParameters {}) };
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId()); EXPECT_EQ(artists.results.front(), artist.getId());
artists = Artist::findAllOrphans(session, Range {}); artists = Artist::findAllOrphans(session, Range{});
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId()); EXPECT_EQ(artists.results.front(), artist.getId());
} }
} }
TEST_F(DatabaseFixture, Artist_singleTrack) TEST_F(DatabaseFixture, Artist_singleTrack)
{ {
ScopedTrack track {session, "MyTrack"}; ScopedTrack track{ session, "MyTrack" };
ScopedArtist artist {session, "MyArtist"}; ScopedArtist artist{ session, "MyArtist" };
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction{ session.createUniqueTransaction() };
track.get().modify()->setName("MyTrackName"); track.get().modify()->setName("MyTrackName");
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
EXPECT_TRUE(Artist::findAllOrphans(session, Range {}).results.empty()); EXPECT_TRUE(Artist::findAllOrphans(session, Range{}).results.empty());
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
auto artists {track->getArtists({TrackArtistLinkType::Artist})}; auto artists{ track->getArtists({TrackArtistLinkType::Artist}) };
ASSERT_EQ(artists.size(), 1); ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.front()->getId(), artist.getId()); EXPECT_EQ(artists.front()->getId(), artist.getId());
ASSERT_EQ(track->getArtistLinks().size(), 1); ASSERT_EQ(track->getArtistLinks().size(), 1);
auto artistLink {track->getArtistLinks().front()}; auto artistLink{ track->getArtistLinks().front() };
EXPECT_EQ(artistLink->getTrack()->getId(), track.getId()); EXPECT_EQ(artistLink->getTrack()->getId(), track.getId());
EXPECT_EQ(artistLink->getArtist()->getId(), artist.getId()); EXPECT_EQ(artistLink->getArtist()->getId(), artist.getId());
ASSERT_EQ(track->getArtists({TrackArtistLinkType::Artist}).size(), 1); ASSERT_EQ(track->getArtists({ TrackArtistLinkType::Artist }).size(), 1);
EXPECT_TRUE(track->getArtists({TrackArtistLinkType::ReleaseArtist}).empty()); EXPECT_TRUE(track->getArtists({ TrackArtistLinkType::ReleaseArtist }).empty());
EXPECT_EQ(track->getArtists({}).size(), 1); EXPECT_EQ(track->getArtists({}).size(), 1);
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
auto artists {track->getArtistIds({TrackArtistLinkType::Artist})}; auto artists{ track->getArtistIds({TrackArtistLinkType::Artist}) };
ASSERT_EQ(artists.size(), 1); ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.front(), artist.getId()); EXPECT_EQ(artists.front(), artist.getId());
ASSERT_EQ(track->getArtistIds({TrackArtistLinkType::Artist}).size(), 1); ASSERT_EQ(track->getArtistIds({ TrackArtistLinkType::Artist }).size(), 1);
EXPECT_TRUE(track->getArtistIds({TrackArtistLinkType::ReleaseArtist}).empty()); EXPECT_TRUE(track->getArtistIds({ TrackArtistLinkType::ReleaseArtist }).empty());
EXPECT_EQ(track->getArtistIds({}).size(), 1); EXPECT_EQ(track->getArtistIds({}).size(), 1);
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
auto tracks {Track::find(session, Track::FindParameters{}.setName("MyTrackName").setArtistName("MyArtist"))}; auto tracks{ Track::findIds(session, Track::FindParameters{}.setName("MyTrackName").setArtistName("MyArtist")) };
ASSERT_EQ(tracks.results.size(), 1); ASSERT_EQ(tracks.results.size(), 1);
EXPECT_EQ(tracks.results.front(), track.getId()); EXPECT_EQ(tracks.results.front(), track.getId());
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
auto tracks {Track::find(session, Track::FindParameters{}.setName("MyTrackName").setArtistName("MyArtistFoo"))}; auto tracks{ Track::findIds(session, Track::FindParameters{}.setName("MyTrackName").setArtistName("MyArtistFoo")) };
EXPECT_EQ(tracks.results.size(), 0); EXPECT_EQ(tracks.results.size(), 0);
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
auto tracks {Track::find(session, Track::FindParameters{}.setName("MyTrackNameFoo").setArtistName("MyArtist"))}; auto tracks{ Track::findIds(session, Track::FindParameters{}.setName("MyTrackNameFoo").setArtistName("MyArtist")) };
EXPECT_EQ(tracks.results.size(), 0); EXPECT_EQ(tracks.results.size(), 0);
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
auto artists {Artist::find(session, Artist::FindParameters{}.setTrack(track->getId()))}; auto artists{ Artist::find(session, Artist::FindParameters{}.setTrack(track->getId())) };
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId()); EXPECT_EQ(artists.results.front(), artist.getId());
} }
} }
TEST_F(DatabaseFixture, Artist_singleTracktMultiRoles) TEST_F(DatabaseFixture, Artist_singleTracktMultiRoles)
{ {
ScopedTrack track {session, "MyTrack"}; ScopedTrack track{ session, "MyTrack" };
ScopedArtist artist {session, "MyArtist"}; ScopedArtist artist{ session, "MyArtist" };
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction{ session.createUniqueTransaction() };
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::ReleaseArtist); TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::ReleaseArtist);
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Writer); TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Writer);
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
EXPECT_TRUE(Artist::findAllOrphans(session, Range {}).results.empty()); EXPECT_TRUE(Artist::findAllOrphans(session, Range{}).results.empty());
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
EXPECT_EQ(Artist::find(session, Artist::FindParameters {}).results.size(), 1); EXPECT_EQ(Artist::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::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::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::Writer)).results.size(), 1);
EXPECT_EQ(Artist::find(session, Artist::FindParameters {}.setLinkType(TrackArtistLinkType::Composer)).results.size(), 0); EXPECT_EQ(Artist::find(session, Artist::FindParameters{}.setLinkType(TrackArtistLinkType::Composer)).results.size(), 0);
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
auto artists {track->getArtists({TrackArtistLinkType::Artist})}; auto artists{ track->getArtists({TrackArtistLinkType::Artist}) };
ASSERT_EQ(artists.size(), 1); ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.front()->getId(), artist.getId()); EXPECT_EQ(artists.front()->getId(), artist.getId());
artists = track->getArtists({TrackArtistLinkType::ReleaseArtist}); artists = track->getArtists({ TrackArtistLinkType::ReleaseArtist });
ASSERT_EQ(artists.size(), 1); ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.front()->getId(), artist.getId()); EXPECT_EQ(artists.front()->getId(), artist.getId());
EXPECT_EQ(track->getArtistLinks().size(), 3); EXPECT_EQ(track->getArtistLinks().size(), 3);
auto tracks {Track::find(session, Track::FindParameters {}.setArtist(artist.getId()))}; auto tracks{ Track::findIds(session, Track::FindParameters {}.setArtist(artist.getId())) };
EXPECT_EQ(tracks.results.size(), 1); EXPECT_EQ(tracks.results.size(), 1);
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::ReleaseArtist})); tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::ReleaseArtist }));
EXPECT_EQ(tracks.results.size(), 1); EXPECT_EQ(tracks.results.size(), 1);
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::Artist})); tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Artist }));
EXPECT_EQ(tracks.results.size(), 1); EXPECT_EQ(tracks.results.size(), 1);
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::Writer})); tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Writer }));
EXPECT_EQ(tracks.results.size(), 1); EXPECT_EQ(tracks.results.size(), 1);
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::Composer})); tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Composer }));
EXPECT_EQ(tracks.results.size(), 0); EXPECT_EQ(tracks.results.size(), 0);
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
EnumSet<TrackArtistLinkType> types{ TrackArtistLink::findUsedTypes(session, artist.getId()) }; EnumSet<TrackArtistLinkType> types{ TrackArtistLink::findUsedTypes(session, artist.getId()) };
EXPECT_TRUE(types.contains(TrackArtistLinkType::ReleaseArtist)); EXPECT_TRUE(types.contains(TrackArtistLinkType::ReleaseArtist));
EXPECT_TRUE(types.contains(TrackArtistLinkType::Artist)); EXPECT_TRUE(types.contains(TrackArtistLinkType::Artist));
EXPECT_TRUE(types.contains(TrackArtistLinkType::Writer)); EXPECT_TRUE(types.contains(TrackArtistLinkType::Writer));
EXPECT_FALSE(types.contains(TrackArtistLinkType::Composer)); EXPECT_FALSE(types.contains(TrackArtistLinkType::Composer));
} }
} }
TEST_F(DatabaseFixture, Artist_singleTrackMultiArtists) TEST_F(DatabaseFixture, Artist_singleTrackMultiArtists)
{ {
ScopedTrack track {session, "track"}; ScopedTrack track{ session, "track" };
ScopedArtist artist1 {session, "artist1"}; ScopedArtist artist1{ session, "artist1" };
ScopedArtist artist2 {session, "artist2"}; ScopedArtist artist2{ session, "artist2" };
ASSERT_NE(artist1.getId(), artist2.getId()); ASSERT_NE(artist1.getId(), artist2.getId());
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction{ session.createUniqueTransaction() };
TrackArtistLink::create(session, track.get(), artist1.get(), TrackArtistLinkType::Artist); TrackArtistLink::create(session, track.get(), artist1.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, track.get(), artist2.get(), TrackArtistLinkType::Artist); TrackArtistLink::create(session, track.get(), artist2.get(), TrackArtistLinkType::Artist);
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
EXPECT_TRUE(Artist::findAllOrphans(session, Range {}).results.empty()); EXPECT_TRUE(Artist::findAllOrphans(session, Range{}).results.empty());
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
auto artists {track->getArtists({TrackArtistLinkType::Artist})}; auto artists{ track->getArtists({TrackArtistLinkType::Artist}) };
ASSERT_EQ(artists.size(), 2); ASSERT_EQ(artists.size(), 2);
EXPECT_TRUE((artists[0]->getId() == artist1.getId() && artists[1]->getId() == artist2.getId()) EXPECT_TRUE((artists[0]->getId() == artist1.getId() && artists[1]->getId() == artist2.getId())
|| (artists[0]->getId() == artist2.getId() && artists[1]->getId() == artist1.getId())); || (artists[0]->getId() == artist2.getId() && artists[1]->getId() == artist1.getId()));
EXPECT_EQ(track->getArtists({}).size(), 2); EXPECT_EQ(track->getArtists({}).size(), 2);
EXPECT_EQ(track->getArtists({TrackArtistLinkType::Artist}).size(), 2); EXPECT_EQ(track->getArtists({ TrackArtistLinkType::Artist }).size(), 2);
EXPECT_TRUE(track->getArtists({TrackArtistLinkType::ReleaseArtist}).empty()); EXPECT_TRUE(track->getArtists({ TrackArtistLinkType::ReleaseArtist }).empty());
EXPECT_EQ(Artist::find(session, Artist::FindParameters {}).results.size(), 2); EXPECT_EQ(Artist::find(session, Artist::FindParameters{}).results.size(), 2);
EXPECT_EQ(Artist::find(session, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::Random)).results.size(), 2); EXPECT_EQ(Artist::find(session, Artist::FindParameters{}.setSortMethod(ArtistSortMethod::Random)).results.size(), 2);
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
auto tracks {Track::find(session, Track::FindParameters {}.setArtist(artist1->getId()))}; auto tracks{ Track::findIds(session, Track::FindParameters {}.setArtist(artist1->getId())) };
ASSERT_EQ(tracks.results.size(), 1); ASSERT_EQ(tracks.results.size(), 1);
EXPECT_EQ(tracks.results.front(), track->getId()); EXPECT_EQ(tracks.results.front(), track->getId());
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist2->getId())); tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist2->getId()));
ASSERT_EQ(tracks.results.size(), 1); ASSERT_EQ(tracks.results.size(), 1);
EXPECT_EQ(tracks.results.front(), track->getId()); EXPECT_EQ(tracks.results.front(), track->getId());
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist1->getId(), {TrackArtistLinkType::ReleaseArtist})); tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist1->getId(), { TrackArtistLinkType::ReleaseArtist }));
EXPECT_EQ(tracks.results.size(), 0); EXPECT_EQ(tracks.results.size(), 0);
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist1->getId(), {TrackArtistLinkType::Artist})); tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist1->getId(), { TrackArtistLinkType::Artist }));
EXPECT_EQ(tracks.results.size(), 1); EXPECT_EQ(tracks.results.size(), 1);
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist2->getId(), {TrackArtistLinkType::ReleaseArtist})); tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist2->getId(), { TrackArtistLinkType::ReleaseArtist }));
EXPECT_EQ(tracks.results.size(), 0); EXPECT_EQ(tracks.results.size(), 0);
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist2->getId(), {TrackArtistLinkType::Artist})); tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist2->getId(), { TrackArtistLinkType::Artist }));
EXPECT_EQ(tracks.results.size(), 1); EXPECT_EQ(tracks.results.size(), 1);
} }
} }
TEST_F(DatabaseFixture, Artist_findByName) TEST_F(DatabaseFixture, Artist_findByName)
{ {
ScopedArtist artist {session, "AAA"}; ScopedArtist artist{ session, "AAA" };
ScopedTrack track {session, "MyTrack"}; // filters does not work on orphans ScopedTrack track{ session, "MyTrack" }; // filters does not work on orphans
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction{ session.createUniqueTransaction() };
artist.get().modify()->setSortName("ZZZ"); artist.get().modify()->setSortName("ZZZ");
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
EXPECT_TRUE(Artist::find(session, Artist::FindParameters {}.setKeywords({"N"})).results.empty()); EXPECT_TRUE(Artist::find(session, Artist::FindParameters{}.setKeywords({ "N" })).results.empty());
const auto artistsByAAA {Artist::find(session, Artist::FindParameters {}.setKeywords({"A"}))}; const auto artistsByAAA{ Artist::find(session, Artist::FindParameters {}.setKeywords({"A"})) };
ASSERT_EQ(artistsByAAA.results.size(), 1); ASSERT_EQ(artistsByAAA.results.size(), 1);
EXPECT_EQ(artistsByAAA.results.front(), artist.getId()); EXPECT_EQ(artistsByAAA.results.front(), artist.getId());
const auto artistsByZZZ {Artist::Artist::find(session, Artist::FindParameters {}.setKeywords({"Z"}))}; const auto artistsByZZZ{ Artist::Artist::find(session, Artist::FindParameters {}.setKeywords({"Z"})) };
ASSERT_EQ(artistsByZZZ.results.size(), 1); ASSERT_EQ(artistsByZZZ.results.size(), 1);
EXPECT_EQ(artistsByZZZ.results.front(), artist.getId()); EXPECT_EQ(artistsByZZZ.results.front(), artist.getId());
EXPECT_TRUE(Artist::find(session, "NNN").empty()); EXPECT_TRUE(Artist::find(session, "NNN").empty());
EXPECT_EQ(Artist::find(session, "AAA").size(), 1); EXPECT_EQ(Artist::find(session, "AAA").size(), 1);
} }
} }
TEST_F(DatabaseFixture, Artist_findByNameEscaped) TEST_F(DatabaseFixture, Artist_findByNameEscaped)
{ {
ScopedArtist artist1 {session, "MyArtist%"}; ScopedArtist artist1{ session, "MyArtist%" };
ScopedArtist artist2 {session, "%MyArtist"}; ScopedArtist artist2{ session, "%MyArtist" };
ScopedArtist artist3 {session, "%_MyArtist"}; ScopedArtist artist3{ session, "%_MyArtist" };
ScopedArtist artist4 {session, "MyArtist%foo"}; ScopedArtist artist4{ session, "MyArtist%foo" };
ScopedArtist artist5 {session, "foo%MyArtist"}; ScopedArtist artist5{ session, "foo%MyArtist" };
ScopedArtist artist6 {session, "%AMyArtist"}; ScopedArtist artist6{ session, "%AMyArtist" };
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
{ {
const auto artists {Artist::find(session, "MyArtist%")}; const auto artists{ Artist::find(session, "MyArtist%") };
ASSERT_TRUE(artists.size() == 1); ASSERT_TRUE(artists.size() == 1);
EXPECT_EQ(artists.front()->getId(), artist1.getId()); EXPECT_EQ(artists.front()->getId(), artist1.getId());
EXPECT_TRUE(Artist::find(session, "MyArtistFoo").empty()); EXPECT_TRUE(Artist::find(session, "MyArtistFoo").empty());
} }
{ {
const auto artists {Artist::find(session, "%MyArtist")}; const auto artists{ Artist::find(session, "%MyArtist") };
ASSERT_TRUE(artists.size() == 1); ASSERT_TRUE(artists.size() == 1);
EXPECT_EQ(artists.front()->getId(), artist2.getId()); EXPECT_EQ(artists.front()->getId(), artist2.getId());
EXPECT_TRUE(Artist::find(session, "FooMyArtist").empty()); EXPECT_TRUE(Artist::find(session, "FooMyArtist").empty());
} }
{ {
const auto artists {Artist::find(session, "%_MyArtist")}; const auto artists{ Artist::find(session, "%_MyArtist") };
ASSERT_TRUE(artists.size() == 1); ASSERT_TRUE(artists.size() == 1);
ASSERT_EQ(artists.front()->getId(), artist3.getId()); ASSERT_EQ(artists.front()->getId(), artist3.getId());
EXPECT_TRUE(Artist::find(session, "%CMyArtist").empty()); EXPECT_TRUE(Artist::find(session, "%CMyArtist").empty());
} }
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
{ {
const auto artists {Artist::find(session, Artist::FindParameters {}.setKeywords({"MyArtist"}))}; const auto artists{ Artist::find(session, Artist::FindParameters {}.setKeywords({"MyArtist"})) };
EXPECT_EQ(artists.results.size(), 6); EXPECT_EQ(artists.results.size(), 6);
} }
{ {
const auto artists {Artist::find(session, Artist::FindParameters {}.setKeywords({"MyArtist%"}).setSortMethod(ArtistSortMethod::ByName))}; const auto artists{ Artist::find(session, Artist::FindParameters {}.setKeywords({"MyArtist%"}).setSortMethod(ArtistSortMethod::ByName)) };
ASSERT_EQ(artists.results.size(), 2); ASSERT_EQ(artists.results.size(), 2);
EXPECT_EQ(artists.results[0], artist1.getId()); EXPECT_EQ(artists.results[0], artist1.getId());
EXPECT_EQ(artists.results[1], artist4.getId()); EXPECT_EQ(artists.results[1], artist4.getId());
} }
{ {
const auto artists {Artist::find(session, Artist::FindParameters {}.setKeywords({"%MyArtist"}).setSortMethod(ArtistSortMethod::ByName))}; const auto artists{ Artist::find(session, Artist::FindParameters {}.setKeywords({"%MyArtist"}).setSortMethod(ArtistSortMethod::ByName)) };
ASSERT_EQ(artists.results.size(), 2); ASSERT_EQ(artists.results.size(), 2);
EXPECT_EQ(artists.results[0], artist2.getId()); EXPECT_EQ(artists.results[0], artist2.getId());
EXPECT_EQ(artists.results[1], artist5.getId()); EXPECT_EQ(artists.results[1], artist5.getId());
} }
{ {
const auto artists {Artist::find(session, Artist::FindParameters {}.setKeywords({"_MyArtist"}).setSortMethod(ArtistSortMethod::ByName))}; const auto artists{ Artist::find(session, Artist::FindParameters {}.setKeywords({"_MyArtist"}).setSortMethod(ArtistSortMethod::ByName)) };
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results[0], artist3.getId()); EXPECT_EQ(artists.results[0], artist3.getId());
} }
} }
} }
TEST_F(DatabaseFixture, Artist_sortMethod) TEST_F(DatabaseFixture, Artist_sortMethod)
{ {
ScopedArtist artistA {session, "artistA"}; ScopedArtist artistA{ session, "artistA" };
ScopedArtist artistB {session, "artistB"}; ScopedArtist artistB{ session, "artistB" };
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction{ session.createUniqueTransaction() };
artistA.get().modify()->setSortName("sortNameB"); artistA.get().modify()->setSortName("sortNameB");
artistB.get().modify()->setSortName("sortNameA"); artistB.get().modify()->setSortName("sortNameA");
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
auto allArtistsByName {Artist::find(session, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::ByName))}; auto allArtistsByName{ Artist::find(session, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::ByName)) };
auto allArtistsBySortName {Artist::find(session, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::BySortName))}; auto allArtistsBySortName{ Artist::find(session, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::BySortName)) };
ASSERT_EQ(allArtistsByName.results.size(), 2); ASSERT_EQ(allArtistsByName.results.size(), 2);
EXPECT_EQ(allArtistsByName.results.front(), artistA.getId()); EXPECT_EQ(allArtistsByName.results.front(), artistA.getId());
EXPECT_EQ(allArtistsByName.results.back(), artistB.getId()); EXPECT_EQ(allArtistsByName.results.back(), artistB.getId());
ASSERT_EQ(allArtistsBySortName.results.size(), 2); ASSERT_EQ(allArtistsBySortName.results.size(), 2);
EXPECT_EQ(allArtistsBySortName.results.front(), artistB.getId()); EXPECT_EQ(allArtistsBySortName.results.front(), artistB.getId());
EXPECT_EQ(allArtistsBySortName.results.back(), artistA.getId()); EXPECT_EQ(allArtistsBySortName.results.back(), artistA.getId());
} }
} }
TEST_F(DatabaseFixture, Artist_nonReleaseTracks) TEST_F(DatabaseFixture, Artist_nonReleaseTracks)
{ {
ScopedArtist artist {session, "artist"}; ScopedArtist artist{ session, "artist" };
ScopedTrack track1 {session, "MyTrack1"}; ScopedTrack track1{ session, "MyTrack1" };
ScopedTrack track2 {session, "MyTrack2"}; ScopedTrack track2{ session, "MyTrack2" };
ScopedRelease release{session, "MyRelease"}; ScopedRelease release{ session, "MyRelease" };
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
auto tracks {Track::find(session, Track::FindParameters {}.setNonRelease(true).setArtist(artist->getId()))}; auto tracks{ Track::findIds(session, Track::FindParameters {}.setNonRelease(true).setArtist(artist->getId())) };
EXPECT_EQ(tracks.results.size(), 0); EXPECT_EQ(tracks.results.size(), 0);
} }
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction{ session.createUniqueTransaction() };
TrackArtistLink::create(session, track1.get(), artist.get(), TrackArtistLinkType::Artist); TrackArtistLink::create(session, track1.get(), artist.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, track2.get(), artist.get(), TrackArtistLinkType::Artist); TrackArtistLink::create(session, track2.get(), artist.get(), TrackArtistLinkType::Artist);
track1.get().modify()->setRelease(release.get()); track1.get().modify()->setRelease(release.get());
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
const auto tracks {Track::find(session, Track::FindParameters {}.setArtist(artist.getId()).setNonRelease(true))}; const auto tracks{ Track::findIds(session, Track::FindParameters {}.setArtist(artist.getId()).setNonRelease(true)) };
ASSERT_EQ(tracks.results.size(), 1); ASSERT_EQ(tracks.results.size(), 1);
EXPECT_EQ(tracks.results.front(), track2.getId()); EXPECT_EQ(tracks.results.front(), track2.getId());
} }
} }
TEST_F(DatabaseFixture, Artist_findByRelease) TEST_F(DatabaseFixture, Artist_findByRelease)
{ {
ScopedArtist artist {session, "artist"}; ScopedArtist artist{ session, "artist" };
ScopedTrack track {session, "MyTrack"}; ScopedTrack track{ session, "MyTrack" };
ScopedRelease release{session, "MyRelease"}; ScopedRelease release{ session, "MyRelease" };
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
const auto artists {Artist::find(session, Artist::FindParameters {}.setRelease(release.getId()))}; const auto artists{ Artist::find(session, Artist::FindParameters {}.setRelease(release.getId())) };
EXPECT_EQ(artists.results.size(), 0); EXPECT_EQ(artists.results.size(), 0);
} }
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction{ session.createUniqueTransaction() };
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
const auto artists {Artist::find(session, Artist::FindParameters {}.setRelease(release.getId()))}; const auto artists{ Artist::find(session, Artist::FindParameters {}.setRelease(release.getId())) };
EXPECT_EQ(artists.results.size(), 0); EXPECT_EQ(artists.results.size(), 0);
} }
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction{ session.createUniqueTransaction() };
track.get().modify()->setRelease(release.get()); track.get().modify()->setRelease(release.get());
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
const auto artists {Artist::find(session, Artist::FindParameters {}.setRelease(release.getId()))}; const auto artists{ Artist::find(session, Artist::FindParameters {}.setRelease(release.getId())) };
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId()); EXPECT_EQ(artists.results.front(), artist.getId());
} }
} }
+8 -8
View File
@@ -133,11 +133,11 @@ TEST_F(DatabaseFixture, Cluster_singleTrack)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto tracks{ Track::find(session, Track::FindParameters {}.setClusters({cluster1.getId()})) }; auto tracks{ Track::findIds(session, Track::FindParameters {}.setClusters({cluster1.getId()})) };
ASSERT_EQ(tracks.results.size(), 1); ASSERT_EQ(tracks.results.size(), 1);
EXPECT_EQ(tracks.results.front(), track.getId()); EXPECT_EQ(tracks.results.front(), track.getId());
tracks = Track::find(session, Track::FindParameters{}.setClusters({ cluster2.getId() })); tracks = Track::findIds(session, Track::FindParameters{}.setClusters({ cluster2.getId() }));
EXPECT_TRUE(tracks.results.empty()); EXPECT_TRUE(tracks.results.empty());
} }
@@ -167,7 +167,7 @@ TEST_F(DatabaseFixture, Cluster_singleTrackWithSeveralClusters)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
const auto tracks{ Track::find(session, Track::FindParameters{}.setClusters(clusterIds)) }; const auto tracks{ Track::findIds(session, Track::FindParameters{}.setClusters(clusterIds)) };
EXPECT_TRUE(tracks.results.empty()); EXPECT_TRUE(tracks.results.empty());
} }
@@ -180,7 +180,7 @@ TEST_F(DatabaseFixture, Cluster_singleTrackWithSeveralClusters)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
const auto tracks{ Track::find(session, Track::FindParameters{}.setClusters(clusterIds)) }; const auto tracks{ Track::findIds(session, Track::FindParameters{}.setClusters(clusterIds)) };
EXPECT_TRUE(tracks.results.empty()); EXPECT_TRUE(tracks.results.empty());
} }
@@ -193,7 +193,7 @@ TEST_F(DatabaseFixture, Cluster_singleTrackWithSeveralClusters)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
const auto tracks{ Track::find(session, Track::FindParameters{}.setClusters(clusterIds)) }; const auto tracks{ Track::findIds(session, Track::FindParameters{}.setClusters(clusterIds)) };
ASSERT_FALSE(tracks.results.empty()); ASSERT_FALSE(tracks.results.empty());
EXPECT_EQ(tracks.results.front(), track.getId()); EXPECT_EQ(tracks.results.front(), track.getId());
} }
@@ -550,7 +550,7 @@ TEST_F(DatabaseFixture, MultipleTracksSingleClusterSimilarity)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
const auto similarTracks{ Track::findSimilarTracks(session, {tracks.front().getId()}, Range {}) }; const auto similarTracks{ Track::findSimilarTrackIds(session, {tracks.front().getId()}, Range {}) };
EXPECT_EQ(similarTracks.results.size(), tracks.size() - 1); EXPECT_EQ(similarTracks.results.size(), tracks.size() - 1);
for (const TrackId similarTrackId : similarTracks.results) for (const TrackId similarTrackId : similarTracks.results)
{ {
@@ -591,14 +591,14 @@ TEST_F(DatabaseFixture, MultipleTracksMultipleClustersSimilarity)
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
{ {
auto similarTracks{ Track::findSimilarTracks(session, {tracks.back().getId()}, Range {0, 4}) }; auto similarTracks{ Track::findSimilarTrackIds(session, {tracks.back().getId()}, Range {0, 4}) };
EXPECT_EQ(similarTracks.results.size(), 4); EXPECT_EQ(similarTracks.results.size(), 4);
for (const TrackId similarTrackId : similarTracks.results) for (const TrackId similarTrackId : similarTracks.results)
EXPECT_TRUE(std::find_if(std::next(std::cbegin(tracks), 5), std::next(std::cend(tracks), -1), [&](const auto& track) { return similarTrackId == track.getId(); }) != std::cend(tracks)); EXPECT_TRUE(std::find_if(std::next(std::cbegin(tracks), 5), std::next(std::cend(tracks), -1), [&](const auto& track) { return similarTrackId == track.getId(); }) != std::cend(tracks));
} }
{ {
auto similarTracks{ Track::findSimilarTracks(session, {tracks.front().getId()}, Range {}) }; auto similarTracks{ Track::findSimilarTrackIds(session, {tracks.front().getId()}, Range {}) };
EXPECT_EQ(similarTracks.results.size(), tracks.size() - 1); EXPECT_EQ(similarTracks.results.size(), tracks.size() - 1);
for (const TrackId similarTrackId : similarTracks.results) for (const TrackId similarTrackId : similarTracks.results)
EXPECT_TRUE(std::find_if(std::next(std::cbegin(tracks), 1), std::cend(tracks), [&](const auto& track) { return similarTrackId == track.getId(); }) != std::cend(tracks)); EXPECT_TRUE(std::find_if(std::next(std::cbegin(tracks), 1), std::cend(tracks), [&](const auto& track) { return similarTrackId == track.getId(); }) != std::cend(tracks));
+9 -9
View File
@@ -78,7 +78,7 @@ TEST_F(DatabaseFixture, Release_singleTrack)
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
EXPECT_TRUE(Release::findOrphanIds(session, Range{}).results.empty()); EXPECT_TRUE(Release::findOrphanIds(session, Range{}).results.empty());
const auto tracks{ Track::find(session, Track::FindParameters {}.setRelease(release.getId())) }; const auto tracks{ Track::findIds(session, Track::FindParameters {}.setRelease(release.getId())) };
ASSERT_EQ(tracks.results.size(), 1); ASSERT_EQ(tracks.results.size(), 1);
EXPECT_EQ(tracks.results.front(), track.getId()); EXPECT_EQ(tracks.results.front(), track.getId());
} }
@@ -92,18 +92,18 @@ TEST_F(DatabaseFixture, Release_singleTrack)
{ {
auto transaction{ session.createUniqueTransaction() }; auto transaction{ session.createUniqueTransaction() };
auto tracks{ Track::find(session, Track::FindParameters{}.setName("MyTrackName").setReleaseName("MyReleaseName")) }; auto tracks{ Track::findIds(session, Track::FindParameters{}.setName("MyTrackName").setReleaseName("MyReleaseName")) };
ASSERT_EQ(tracks.results.size(), 1); ASSERT_EQ(tracks.results.size(), 1);
EXPECT_EQ(tracks.results.front(), track.getId()); EXPECT_EQ(tracks.results.front(), track.getId());
} }
{ {
auto transaction{ session.createUniqueTransaction() }; auto transaction{ session.createUniqueTransaction() };
auto tracks{ Track::find(session, Track::FindParameters{}.setName("MyTrackName").setReleaseName("MyReleaseFoo")) }; auto tracks{ Track::findIds(session, Track::FindParameters{}.setName("MyTrackName").setReleaseName("MyReleaseFoo")) };
EXPECT_EQ(tracks.results.size(), 0); EXPECT_EQ(tracks.results.size(), 0);
} }
{ {
auto transaction{ session.createUniqueTransaction() }; auto transaction{ session.createUniqueTransaction() };
auto tracks{ Track::find(session, Track::FindParameters{}.setName("MyTrackFoo").setReleaseName("MyReleaseName")) }; auto tracks{ Track::findIds(session, Track::FindParameters{}.setName("MyTrackFoo").setReleaseName("MyReleaseName")) };
EXPECT_EQ(tracks.results.size(), 0); EXPECT_EQ(tracks.results.size(), 0);
} }
} }
@@ -111,7 +111,7 @@ TEST_F(DatabaseFixture, Release_singleTrack)
{ {
auto transaction{ session.createUniqueTransaction() }; auto transaction{ session.createUniqueTransaction() };
const auto tracks{ Track::find(session, Track::FindParameters {}.setRelease(release.getId())) }; const auto tracks{ Track::findIds(session, Track::FindParameters {}.setRelease(release.getId())) };
EXPECT_TRUE(tracks.results.empty()); EXPECT_TRUE(tracks.results.empty());
auto releases{ Release::findOrphanIds(session, Range {}) }; auto releases{ Release::findOrphanIds(session, Range {}) };
@@ -285,8 +285,8 @@ TEST_F(DatabaseFixture, MultiTracksSingleReleaseFirstTrack)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
EXPECT_TRUE(Track::find(session, Track::FindParameters{}.setRelease(release1.getId())).results.empty()); EXPECT_TRUE(Track::findIds(session, Track::FindParameters{}.setRelease(release1.getId())).results.empty());
EXPECT_TRUE(Track::find(session, Track::FindParameters{}.setRelease(release2.getId())).results.empty()); EXPECT_TRUE(Track::findIds(session, Track::FindParameters{}.setRelease(release2.getId())).results.empty());
} }
{ {
@@ -310,13 +310,13 @@ TEST_F(DatabaseFixture, MultiTracksSingleReleaseFirstTrack)
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
{ {
const auto tracks{ Track::find(session, Track::FindParameters {}.setRelease(release1.getId()).setSortMethod(TrackSortMethod::Release)) }; const auto tracks{ Track::findIds(session, Track::FindParameters {}.setRelease(release1.getId()).setSortMethod(TrackSortMethod::Release)) };
ASSERT_FALSE(tracks.results.empty()); ASSERT_FALSE(tracks.results.empty());
EXPECT_EQ(tracks.results.front(), track1A.getId()); EXPECT_EQ(tracks.results.front(), track1A.getId());
} }
{ {
const auto tracks{ Track::find(session, Track::FindParameters {}.setRelease(release2.getId()).setSortMethod(TrackSortMethod::Release)) }; const auto tracks{ Track::findIds(session, Track::FindParameters {}.setRelease(release2.getId()).setSortMethod(TrackSortMethod::Release)) };
ASSERT_FALSE(tracks.results.empty()); ASSERT_FALSE(tracks.results.empty());
EXPECT_EQ(tracks.results.front(), track2B.getId()); EXPECT_EQ(tracks.results.front(), track2B.getId());
} }
@@ -37,7 +37,7 @@ TEST_F(DatabaseFixture, StarredTrack)
EXPECT_FALSE(starredTrack); EXPECT_FALSE(starredTrack);
EXPECT_EQ(StarredTrack::getCount(session), 0); EXPECT_EQ(StarredTrack::getCount(session), 0);
auto tracks {Track::find(session, Track::FindParameters {})}; auto tracks {Track::findIds(session, Track::FindParameters {})};
EXPECT_EQ(tracks.results.size(), 1); EXPECT_EQ(tracks.results.size(), 1);
} }
@@ -53,13 +53,13 @@ TEST_F(DatabaseFixture, StarredTrack)
{ {
auto transaction {session.createSharedTransaction()}; auto transaction {session.createSharedTransaction()};
auto tracks {Track::find(session, Track::FindParameters {})}; auto tracks {Track::findIds(session, Track::FindParameters {})};
EXPECT_EQ(tracks.results.size(), 1); EXPECT_EQ(tracks.results.size(), 1);
tracks = Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal)); tracks = Track::findIds(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal));
EXPECT_EQ(tracks.results.size(), 1); EXPECT_EQ(tracks.results.size(), 1);
tracks = Track::find(session, Track::FindParameters {}.setStarringUser(user2.getId(), FeedbackBackend::Internal)); tracks = Track::findIds(session, Track::FindParameters {}.setStarringUser(user2.getId(), FeedbackBackend::Internal));
EXPECT_EQ(tracks.results.size(), 0); EXPECT_EQ(tracks.results.size(), 0);
} }
} }
@@ -73,11 +73,11 @@ TEST_F(DatabaseFixture, Starredtrack_PendingDestroy)
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction {session.createUniqueTransaction()};
auto tracks {Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal))}; auto tracks {Track::findIds(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal))};
EXPECT_EQ(tracks.results.size(), 1); EXPECT_EQ(tracks.results.size(), 1);
starredTrack.get().modify()->setSyncState(SyncState::PendingRemove); starredTrack.get().modify()->setSyncState(SyncState::PendingRemove);
tracks = Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal)); tracks = Track::findIds(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal));
EXPECT_EQ(tracks.results.size(), 0); EXPECT_EQ(tracks.results.size(), 0);
} }
} }
@@ -96,7 +96,7 @@ TEST_F(DatabaseFixture, StarredTrack_dateTime)
{ {
auto transaction {session.createSharedTransaction()}; auto transaction {session.createSharedTransaction()};
auto tracks {Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal))}; auto tracks {Track::findIds(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal))};
EXPECT_EQ(tracks.results.size(), 2); EXPECT_EQ(tracks.results.size(), 2);
} }
@@ -106,7 +106,7 @@ TEST_F(DatabaseFixture, StarredTrack_dateTime)
starredTrack1.get().modify()->setDateTime(dateTime); starredTrack1.get().modify()->setDateTime(dateTime);
starredTrack2.get().modify()->setDateTime(dateTime.addSecs(-1)); starredTrack2.get().modify()->setDateTime(dateTime.addSecs(-1));
auto tracks {Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal).setSortMethod(TrackSortMethod::StarredDateDesc))}; auto tracks {Track::findIds(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal).setSortMethod(TrackSortMethod::StarredDateDesc))};
ASSERT_EQ(tracks.results.size(), 2); ASSERT_EQ(tracks.results.size(), 2);
EXPECT_EQ(tracks.results[0], starredTrack1->getTrack()->getId()); EXPECT_EQ(tracks.results[0], starredTrack1->getTrack()->getId());
EXPECT_EQ(tracks.results[1], starredTrack2->getTrack()->getId()); EXPECT_EQ(tracks.results[1], starredTrack2->getTrack()->getId());
@@ -117,7 +117,7 @@ TEST_F(DatabaseFixture, StarredTrack_dateTime)
starredTrack1.get().modify()->setDateTime(dateTime); starredTrack1.get().modify()->setDateTime(dateTime);
starredTrack2.get().modify()->setDateTime(dateTime.addSecs(1)); starredTrack2.get().modify()->setDateTime(dateTime.addSecs(1));
auto tracks {Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal).setSortMethod(TrackSortMethod::StarredDateDesc))}; auto tracks {Track::findIds(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal).setSortMethod(TrackSortMethod::StarredDateDesc))};
ASSERT_EQ(tracks.results.size(), 2); ASSERT_EQ(tracks.results.size(), 2);
EXPECT_EQ(tracks.results[0], starredTrack2->getTrack()->getId()); EXPECT_EQ(tracks.results[0], starredTrack2->getTrack()->getId());
EXPECT_EQ(tracks.results[1], starredTrack1->getTrack()->getId()); EXPECT_EQ(tracks.results[1], starredTrack1->getTrack()->getId());
+101 -100
View File
@@ -25,137 +25,138 @@ using namespace Database;
TEST_F(DatabaseFixture, Track) TEST_F(DatabaseFixture, Track)
{ {
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
EXPECT_EQ(Track::find(session, Track::FindParameters {}).results.size(), 0); EXPECT_EQ(Track::find(session, Track::FindParameters{}).results.size(), 0);
EXPECT_EQ(Track::getCount(session), 0); EXPECT_EQ(Track::findIds(session, Track::FindParameters{}).results.size(), 0);
EXPECT_FALSE(Track::exists(session, 0)); EXPECT_EQ(Track::getCount(session), 0);
} EXPECT_FALSE(Track::exists(session, 0));
}
ScopedTrack track {session, "MyTrackFile"}; ScopedTrack track{ session, "MyTrackFile" };
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
EXPECT_EQ(Track::find(session, Track::FindParameters {}).results.size(), 1); EXPECT_EQ(Track::find(session, Track::FindParameters{}).results.size(), 1);
EXPECT_EQ(Track::getCount(session), 1); EXPECT_EQ(Track::getCount(session), 1);
EXPECT_TRUE(Track::exists(session, track.getId())); EXPECT_TRUE(Track::exists(session, track.getId()));
auto myTrack {Track::find(session, track.getId())}; auto myTrack{ Track::find(session, track.getId()) };
ASSERT_TRUE(myTrack); ASSERT_TRUE(myTrack);
EXPECT_EQ(myTrack->getId(), track.getId()); EXPECT_EQ(myTrack->getId(), track.getId());
} }
} }
TEST_F(DatabaseFixture, MultipleTracks) TEST_F(DatabaseFixture, MultipleTracks)
{ {
ScopedTrack track1 {session, "MyTrackFile1"}; ScopedTrack track1{ session, "MyTrackFile1" };
ScopedTrack track2 {session, "MyTrackFile2"}; ScopedTrack track2{ session, "MyTrackFile2" };
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
EXPECT_TRUE(track1.getId() != track2.getId()); EXPECT_TRUE(track1.getId() != track2.getId());
EXPECT_TRUE(track1.get() != track2.get()); EXPECT_TRUE(track1.get() != track2.get());
EXPECT_FALSE(track1.get() == track2.get()); EXPECT_FALSE(track1.get() == track2.get());
} }
} }
TEST_F(DatabaseFixture, MultipleTracksSearchByFilter) TEST_F(DatabaseFixture, MultipleTracksSearchByFilter)
{ {
ScopedTrack track1 {session, ""}; ScopedTrack track1{ session, "" };
ScopedTrack track2 {session, ""}; ScopedTrack track2{ session, "" };
ScopedTrack track3 {session, ""}; ScopedTrack track3{ session, "" };
ScopedTrack track4 {session, ""}; ScopedTrack track4{ session, "" };
ScopedTrack track5 {session, ""}; ScopedTrack track5{ session, "" };
ScopedTrack track6 {session, ""}; ScopedTrack track6{ session, "" };
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction{ session.createUniqueTransaction() };
track1.get().modify()->setName("MyTrack"); track1.get().modify()->setName("MyTrack");
track2.get().modify()->setName("MyTrack%"); track2.get().modify()->setName("MyTrack%");
track3.get().modify()->setName("MyTrack%Foo"); track3.get().modify()->setName("MyTrack%Foo");
track4.get().modify()->setName("%MyTrack"); track4.get().modify()->setName("%MyTrack");
track5.get().modify()->setName("Foo%MyTrack"); track5.get().modify()->setName("Foo%MyTrack");
track6.get().modify()->setName("M_Track"); track6.get().modify()->setName("M_Track");
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
{ {
const auto tracks {Track::find(session, Track::FindParameters {}.setKeywords({"Track"}))}; const auto tracks{ Track::findIds(session, Track::FindParameters {}.setKeywords({"Track"})) };
EXPECT_EQ(tracks.results.size(), 6); EXPECT_EQ(tracks.results.size(), 6);
} }
{ {
const auto tracks {Track::find(session, Track::FindParameters {}.setKeywords({"MyTrack"}))}; const auto tracks{ Track::findIds(session, Track::FindParameters {}.setKeywords({"MyTrack"})) };
EXPECT_EQ(tracks.results.size(), 5); EXPECT_EQ(tracks.results.size(), 5);
EXPECT_TRUE(std::none_of(std::cbegin(tracks.results), std::cend(tracks.results), [&](const TrackId trackId) { return trackId == track6.getId(); })); EXPECT_TRUE(std::none_of(std::cbegin(tracks.results), std::cend(tracks.results), [&](const TrackId trackId) { return trackId == track6.getId(); }));
} }
{ {
const auto tracks {Track::find(session, Track::FindParameters {}.setKeywords({"MyTrack%"}))}; const auto tracks{ Track::findIds(session, Track::FindParameters {}.setKeywords({"MyTrack%"})) };
ASSERT_EQ(tracks.results.size(), 2); ASSERT_EQ(tracks.results.size(), 2);
EXPECT_EQ(tracks.results[0], track2.getId()); EXPECT_EQ(tracks.results[0], track2.getId());
EXPECT_EQ(tracks.results[1], track3.getId()); EXPECT_EQ(tracks.results[1], track3.getId());
} }
{ {
const auto tracks {Track::find(session, Track::FindParameters {}.setKeywords({"%MyTrack"}))}; const auto tracks{ Track::findIds(session, Track::FindParameters {}.setKeywords({"%MyTrack"})) };
ASSERT_EQ(tracks.results.size(), 2); ASSERT_EQ(tracks.results.size(), 2);
EXPECT_EQ(tracks.results[0], track4.getId()); EXPECT_EQ(tracks.results[0], track4.getId());
EXPECT_EQ(tracks.results[1], track5.getId()); EXPECT_EQ(tracks.results[1], track5.getId());
} }
} }
} }
TEST_F(DatabaseFixture, Track_date) TEST_F(DatabaseFixture, Track_date)
{ {
ScopedTrack track {session, "MyTrack"}; ScopedTrack track{ session, "MyTrack" };
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
EXPECT_EQ(track->getYear(), std::nullopt); EXPECT_EQ(track->getYear(), std::nullopt);
EXPECT_EQ(track->getOriginalYear(), std::nullopt); EXPECT_EQ(track->getOriginalYear(), std::nullopt);
} }
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction{ session.createUniqueTransaction() };
track.get().modify()->setDate(Wt::WDate {1995, 5, 5}); track.get().modify()->setDate(Wt::WDate{ 1995, 5, 5 });
track.get().modify()->setOriginalDate(Wt::WDate {1994, 2, 2}); track.get().modify()->setOriginalDate(Wt::WDate{ 1994, 2, 2 });
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
EXPECT_EQ(track->getYear(), 1995); EXPECT_EQ(track->getYear(), 1995);
EXPECT_EQ(track->getOriginalYear(), 1994); EXPECT_EQ(track->getOriginalYear(), 1994);
} }
} }
TEST_F(DatabaseFixture, Track_writtenAfter) TEST_F(DatabaseFixture, Track_writtenAfter)
{ {
ScopedTrack track {session, "MyTrack"}; ScopedTrack track{ session, "MyTrack" };
const Wt::WDateTime dateTime {Wt::WDate {1950, 1, 1}, Wt::WTime {12, 30, 20}}; const Wt::WDateTime dateTime{ Wt::WDate {1950, 1, 1}, Wt::WTime {12, 30, 20} };
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction{ session.createUniqueTransaction() };
track.get().modify()->setLastWriteTime(dateTime); track.get().modify()->setLastWriteTime(dateTime);
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
const auto tracks {Track::find(session, Track::FindParameters {})}; const auto tracks{ Track::findIds(session, Track::FindParameters {}) };
EXPECT_EQ(tracks.results.size(), 1); EXPECT_EQ(tracks.results.size(), 1);
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
const auto tracks {Track::find(session, Track::FindParameters {}.setWrittenAfter(dateTime.addSecs(-1)))}; const auto tracks{ Track::findIds(session, Track::FindParameters {}.setWrittenAfter(dateTime.addSecs(-1))) };
EXPECT_EQ(tracks.results.size(), 1); EXPECT_EQ(tracks.results.size(), 1);
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
const auto tracks {Track::find(session, Track::FindParameters {}.setWrittenAfter(dateTime.addSecs(+1)))}; const auto tracks{ Track::findIds(session, Track::FindParameters {}.setWrittenAfter(dateTime.addSecs(+1))) };
EXPECT_EQ(tracks.results.size(), 0); EXPECT_EQ(tracks.results.size(), 0);
} }
} }
@@ -49,10 +49,10 @@ TEST_F(DatabaseFixture, SingleTrackListSingleTrack)
{ {
auto transaction {session.createSharedTransaction()}; auto transaction {session.createSharedTransaction()};
auto tracks {Track::find(session, Track::FindParameters {}.setTrackList(trackList1.getId()))}; auto tracks {Track::findIds(session, Track::FindParameters {}.setTrackList(trackList1.getId()))};
EXPECT_EQ(tracks.results.size(), 0); EXPECT_EQ(tracks.results.size(), 0);
tracks = Track::find(session, Track::FindParameters {}.setTrackList(trackList2.getId())); tracks = Track::findIds(session, Track::FindParameters {}.setTrackList(trackList2.getId()));
EXPECT_EQ(tracks.results.size(), 0); EXPECT_EQ(tracks.results.size(), 0);
} }
@@ -65,11 +65,11 @@ TEST_F(DatabaseFixture, SingleTrackListSingleTrack)
{ {
auto transaction {session.createSharedTransaction()}; auto transaction {session.createSharedTransaction()};
auto tracks {Track::find(session, Track::FindParameters {}.setTrackList(trackList1.getId()))}; auto tracks {Track::findIds(session, Track::FindParameters {}.setTrackList(trackList1.getId()))};
ASSERT_EQ(tracks.results.size(), 1); ASSERT_EQ(tracks.results.size(), 1);
EXPECT_EQ(tracks.results.front(), track.getId()); EXPECT_EQ(tracks.results.front(), track.getId());
tracks = Track::find(session, Track::FindParameters {}.setTrackList(trackList2.getId())); tracks = Track::findIds(session, Track::FindParameters {}.setTrackList(trackList2.getId()));
EXPECT_EQ(tracks.results.size(), 0); EXPECT_EQ(tracks.results.size(), 0);
} }
} }
@@ -179,7 +179,7 @@ namespace Feedback
Session& session{ _db.getTLSSession() }; Session& session{ _db.getTLSSession() };
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
return Track::find(session, params); return Track::findIds(session, params);
} }
} // ns Feedback } // ns Feedback
@@ -30,89 +30,86 @@
namespace Recommendation namespace Recommendation
{ {
using namespace Database; using namespace Database;
std::unique_ptr<IPlaylistGeneratorService> std::unique_ptr<IPlaylistGeneratorService> createPlaylistGeneratorService(Db& db, Recommendation::IRecommendationService& recommendationService)
createPlaylistGeneratorService(Db& db, Recommendation::IRecommendationService& recommendationService) {
{ return std::make_unique<PlaylistGeneratorService>(db, recommendationService);
return std::make_unique<PlaylistGeneratorService>(db, recommendationService); }
}
PlaylistGeneratorService::PlaylistGeneratorService(Db& db, Recommendation::IRecommendationService& recommendationService) PlaylistGeneratorService::PlaylistGeneratorService(Db& db, Recommendation::IRecommendationService& recommendationService)
: _db {db} : _db{ db }
, _recommendationService {recommendationService} , _recommendationService{ recommendationService }
{ {
_constraints.push_back(std::make_unique<PlaylistGeneratorConstraint::ConsecutiveArtists>(_db)); _constraints.push_back(std::make_unique<PlaylistGeneratorConstraint::ConsecutiveArtists>(_db));
_constraints.push_back(std::make_unique<PlaylistGeneratorConstraint::ConsecutiveReleases>(_db)); _constraints.push_back(std::make_unique<PlaylistGeneratorConstraint::ConsecutiveReleases>(_db));
_constraints.push_back(std::make_unique<PlaylistGeneratorConstraint::DuplicateTracks>()); _constraints.push_back(std::make_unique<PlaylistGeneratorConstraint::DuplicateTracks>());
} }
std::vector<TrackId> std::vector<TrackId> PlaylistGeneratorService::extendPlaylist(TrackListId tracklistId, std::size_t maxCount) const
PlaylistGeneratorService::extendPlaylist(TrackListId tracklistId, std::size_t maxCount) const {
{ LMS_LOG(RECOMMENDATION, DEBUG) << "Requested to extend playlist by " << maxCount << " similar tracks";
LMS_LOG(RECOMMENDATION, DEBUG) << "Requested to extend playlist by " << maxCount << " similar tracks";
// supposed to be ordered from most similar to least similar // supposed to be ordered from most similar to least similar
std::vector<TrackId> similarTracks {_recommendationService.findSimilarTracks(tracklistId, maxCount * 2)}; // ask for more tracks than we need as it will be easier to respect constraints std::vector<TrackId> similarTracks{ _recommendationService.findSimilarTracks(tracklistId, maxCount * 2) }; // ask for more tracks than we need as it will be easier to respect constraints
const std::vector<TrackId> startingTracks {getTracksFromTrackList(tracklistId)}; const std::vector<TrackId> startingTracks{ getTracksFromTrackList(tracklistId) };
std::vector<TrackId> finalResult = startingTracks; std::vector<TrackId> finalResult = startingTracks;
finalResult.reserve(startingTracks.size() + maxCount); finalResult.reserve(startingTracks.size() + maxCount);
std::vector<float> scores; std::vector<float> scores;
for (std::size_t i {}; i < maxCount; ++i) for (std::size_t i{}; i < maxCount; ++i)
{ {
if (similarTracks.empty()) if (similarTracks.empty())
break; break;
scores.resize(similarTracks.size(), {}); scores.resize(similarTracks.size(), {});
// select the similar track that has the best score // select the similar track that has the best score
for (std::size_t trackIndex {}; trackIndex < similarTracks.size(); ++trackIndex) for (std::size_t trackIndex{}; trackIndex < similarTracks.size(); ++trackIndex)
{ {
using namespace Database::Debug; using namespace Database::Debug;
finalResult.push_back(similarTracks[trackIndex]); finalResult.push_back(similarTracks[trackIndex]);
scores[trackIndex] = 0; scores[trackIndex] = 0;
for (const auto& constraint : _constraints) for (const auto& constraint : _constraints)
scores[trackIndex] += constraint->computeScore(finalResult, finalResult.size() - 1); scores[trackIndex] += constraint->computeScore(finalResult, finalResult.size() - 1);
finalResult.pop_back(); finalResult.pop_back();
// early exit if we consider we found a track with no constraint violation (since similarTracks sorted from most to least similar) // early exit if we consider we found a track with no constraint violation (since similarTracks sorted from most to least similar)
if (scores[trackIndex] < 0.01) if (scores[trackIndex] < 0.01)
break; break;
} }
// get the best score // get the best score
const std::size_t bestScoreIndex {static_cast<std::size_t>(std::distance(std::cbegin(scores), std::min_element(std::cbegin(scores), std::cend(scores))))}; const std::size_t bestScoreIndex{ static_cast<std::size_t>(std::distance(std::cbegin(scores), std::min_element(std::cbegin(scores), std::cend(scores)))) };
finalResult.push_back(similarTracks[bestScoreIndex]); finalResult.push_back(similarTracks[bestScoreIndex]);
similarTracks.erase(std::begin(similarTracks) + bestScoreIndex); similarTracks.erase(std::begin(similarTracks) + bestScoreIndex);
} }
// for now, just get some more similar tracks // for now, just get some more similar tracks
return std::vector(std::cbegin(finalResult) + startingTracks.size(), std::cend(finalResult)); return std::vector(std::cbegin(finalResult) + startingTracks.size(), std::cend(finalResult));
} }
TrackContainer TrackContainer PlaylistGeneratorService::getTracksFromTrackList(Database::TrackListId tracklistId) const
PlaylistGeneratorService::getTracksFromTrackList(Database::TrackListId tracklistId) const {
{ TrackContainer tracks;
TrackContainer tracks;
Session& dbSession {_db.getTLSSession()}; Session& dbSession{ _db.getTLSSession() };
auto transaction {dbSession.createSharedTransaction()}; auto transaction{ dbSession.createSharedTransaction() };
Track::FindParameters params; Track::FindParameters params;
params.setTrackList(tracklistId); params.setTrackList(tracklistId);
params.setSortMethod(TrackSortMethod::TrackList); params.setSortMethod(TrackSortMethod::TrackList);
params.setDistinct(false); params.setDistinct(false);
for (const TrackId trackId : Track::find(dbSession, params).results) for (const TrackId trackId : Track::findIds(dbSession, params).results)
tracks.push_back(trackId); tracks.push_back(trackId);
return tracks; return tracks;
} }
} }
@@ -44,7 +44,7 @@ namespace Recommendation {
Session& dbSession{ _db.getTLSSession() }; Session& dbSession{ _db.getTLSSession() };
auto transaction{ dbSession.createSharedTransaction() }; auto transaction{ dbSession.createSharedTransaction() };
const auto similarTrackIds{ Track::findSimilarTracks(dbSession, trackIds, Range {0, maxCount}) }; const auto similarTrackIds{ Track::findSimilarTrackIds(dbSession, trackIds, Range {0, maxCount}) };
return std::move(similarTrackIds.results); return std::move(similarTrackIds.results);
} }
@@ -37,7 +37,7 @@ namespace Scanner
Session& session {_db.getTLSSession()}; Session& session {_db.getTLSSession()};
auto transaction {session.createSharedTransaction()}; auto transaction {session.createSharedTransaction()};
const RangeResults<TrackId> tracks = Track::findTrackMBIDDuplicates(session, Range {}); const RangeResults<TrackId> tracks = Track::findIdsTrackMBIDDuplicates(session, Range {});
for (const TrackId trackId : tracks.results) for (const TrackId trackId : tracks.results)
{ {
const Track::pointer track {Track::find(session, trackId)}; const Track::pointer track {Track::find(session, trackId)};
File diff suppressed because it is too large Load Diff
@@ -204,16 +204,13 @@ namespace API::Subsonic
if (!user) if (!user)
throw UserNotAuthorizedError{}; throw UserNotAuthorizedError{};
const auto trackIds{ Track::find(context.dbSession, Track::FindParameters {}.setSortMethod(TrackSortMethod::Random).setRange({0, size})) }; const auto tracks{ Track::find(context.dbSession, Track::FindParameters {}.setSortMethod(TrackSortMethod::Random).setRange({0, size})) };
Response response{ Response::createOkResponse(context.serverProtocolVersion) }; Response response{ Response::createOkResponse(context.serverProtocolVersion) };
Response::Node& randomSongsNode{ response.createNode("randomSongs") }; Response::Node& randomSongsNode{ response.createNode("randomSongs") };
for (const TrackId trackId : trackIds.results) for (const Track::pointer& track : tracks.results)
{
const Track::pointer track{ Track::find(context.dbSession, trackId) };
randomSongsNode.addArrayChild("song", createSongNode(context, track, user)); randomSongsNode.addArrayChild("song", createSongNode(context, track, user));
}
return response; return response;
} }
@@ -250,12 +247,9 @@ namespace API::Subsonic
params.setClusters({ cluster->getId() }); params.setClusters({ cluster->getId() });
params.setRange({ offset, size }); params.setRange({ offset, size });
auto trackIds{ Track::find(context.dbSession, params) }; const auto tracks{ Track::find(context.dbSession, params) };
for (const TrackId trackId : trackIds.results) for (const Track::pointer& track : tracks.results)
{
const Track::pointer track{ Track::find(context.dbSession, trackId) };
songsByGenreNode.addArrayChild("song", createSongNode(context, track, user)); songsByGenreNode.addArrayChild("song", createSongNode(context, track, user));
}
return response; return response;
} }
@@ -194,7 +194,7 @@ namespace API::Subsonic
params.setRange({ 0, meanTrackCountPerArtist }); params.setRange({ 0, meanTrackCountPerArtist });
params.setSortMethod(TrackSortMethod::Random); params.setSortMethod(TrackSortMethod::Random);
const auto artistTracks{ Track::find(context.dbSession, params) }; const auto artistTracks{ Track::findIds(context.dbSession, params) };
tracks.insert(std::end(tracks), tracks.insert(std::end(tracks),
std::begin(artistTracks.results), std::begin(artistTracks.results),
std::end(artistTracks.results)); std::end(artistTracks.results));
@@ -225,7 +225,7 @@ namespace API::Subsonic
params.setRange({ 0, meanTrackCountPerRelease }); params.setRange({ 0, meanTrackCountPerRelease });
params.setSortMethod(TrackSortMethod::Random); params.setSortMethod(TrackSortMethod::Random);
const auto releaseTracks{ Track::find(context.dbSession, params) }; const auto releaseTracks{ Track::findIds(context.dbSession, params) };
tracks.insert(std::end(tracks), tracks.insert(std::end(tracks),
std::begin(releaseTracks.results), std::begin(releaseTracks.results),
std::end(releaseTracks.results)); std::end(releaseTracks.results));
@@ -349,11 +349,8 @@ namespace API::Subsonic
directoryNode.setAttribute("name", Utils::makeNameFilesystemCompatible(release->getName())); directoryNode.setAttribute("name", Utils::makeNameFilesystemCompatible(release->getName()));
const auto tracks{ Track::find(context.dbSession, Track::FindParameters {}.setRelease(*releaseId).setSortMethod(TrackSortMethod::Release)) }; const auto tracks{ Track::find(context.dbSession, Track::FindParameters {}.setRelease(*releaseId).setSortMethod(TrackSortMethod::Release)) };
for (const TrackId trackId : tracks.results) for (const Track::pointer& track : tracks.results)
{
const Track::pointer track{ Track::find(context.dbSession, trackId) };
directoryNode.addArrayChild("child", createSongNode(context, track, user)); directoryNode.addArrayChild("child", createSongNode(context, track, user));
}
} }
else else
throw BadParameterGenericError{ "id" }; throw BadParameterGenericError{ "id" };
@@ -432,11 +429,8 @@ namespace API::Subsonic
Response::Node albumNode{ createAlbumNode(context, release, user, true /* id3 */) }; Response::Node albumNode{ createAlbumNode(context, release, user, true /* id3 */) };
const auto tracks{ Track::find(context.dbSession, Track::FindParameters {}.setRelease(id).setSortMethod(TrackSortMethod::Release)) }; const auto tracks{ Track::find(context.dbSession, Track::FindParameters {}.setRelease(id).setSortMethod(TrackSortMethod::Release)) };
for (const TrackId trackId : tracks.results) for (const Track::pointer& track : tracks.results)
{
const Track::pointer track{ Track::find(context.dbSession, trackId) };
albumNode.addArrayChild("song", createSongNode(context, track, user)); albumNode.addArrayChild("song", createSongNode(context, track, user));
}
response.addNode("album", std::move(albumNode)); response.addNode("album", std::move(albumNode));
@@ -86,8 +86,7 @@ namespace API::Subsonic
params.setKeywords(keywords); params.setKeywords(keywords);
params.setRange({ albumOffset, albumCount }); params.setRange({ albumOffset, albumCount });
RangeResults<Release::pointer> releases{ Release::find(context.dbSession, params) }; for (const Release::pointer& release : Release::find(context.dbSession, params).results)
for (const Release::pointer& release : releases.results)
searchResult2Node.addArrayChild("album", createAlbumNode(context, release, user, id3)); searchResult2Node.addArrayChild("album", createAlbumNode(context, release, user, id3));
} }
@@ -97,12 +96,8 @@ namespace API::Subsonic
params.setKeywords(keywords); params.setKeywords(keywords);
params.setRange({ songOffset, songCount }); params.setRange({ songOffset, songCount });
RangeResults<TrackId> trackIds{ Track::find(context.dbSession, params) }; for (const Track::pointer& track : Track::find(context.dbSession, params).results)
for (const TrackId trackId : trackIds.results)
{
const auto track{ Track::find(context.dbSession, trackId) };
searchResult2Node.addArrayChild("song", createSongNode(context, track, user)); searchResult2Node.addArrayChild("song", createSongNode(context, track, user));
}
} }
return response; return response;
@@ -110,6 +110,7 @@ namespace API::Subsonic
trackResponse.setAttribute("playCount", Listen::getCount(context.dbSession, user->getId(), user->getScrobblingBackend(), track->getId())); trackResponse.setAttribute("playCount", Listen::getCount(context.dbSession, user->getId(), user->getScrobblingBackend(), track->getId()));
trackResponse.setAttribute("path", getTrackPath(track)); trackResponse.setAttribute("path", getTrackPath(track));
{ {
// TODO, store this in DB
std::error_code ec; std::error_code ec;
const auto fileSize{ std::filesystem::file_size(track->getPath(), ec) }; const auto fileSize{ std::filesystem::file_size(track->getPath(), ec) };
if (!ec) if (!ec)
+751 -783
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -369,12 +369,12 @@ Artist::addSomeNonReleaseTracks()
params.setNonRelease(true); params.setNonRelease(true);
const auto tracks {Track::find(LmsApp->getDbSession(), params)}; const auto tracks {Track::find(LmsApp->getDbSession(), params)};
for (const TrackId trackId : tracks.results) for (const Track::pointer& track : tracks.results)
{ {
// TODO handle this with range
if (_trackContainer->getCount() == _tracksMaxCount) if (_trackContainer->getCount() == _tracksMaxCount)
break; break;
const Track::pointer track {Track::find(LmsApp->getDbSession(), trackId)};
_trackContainer->add(TrackListHelpers::createEntry(track, _playQueueController, _filters)); _trackContainer->add(TrackListHelpers::createEntry(track, _playQueueController, _filters));
areTracksAdded = true; areTracksAdded = true;
+113 -121
View File
@@ -29,153 +29,145 @@
namespace UserInterface namespace UserInterface
{ {
static namespace
std::vector<Database::TrackId> {
getArtistsTracks(Database::Session& session, const std::vector<Database::ArtistId>& artistsId, const std::vector<Database::ClusterId>& clusters, std::size_t maxTrackCount) std::vector<Database::TrackId> getArtistsTracks(Database::Session& session, const std::vector<Database::ArtistId>& artistsId, const std::vector<Database::ClusterId>& clusters, std::size_t maxTrackCount)
{ {
assert(maxTrackCount); assert(maxTrackCount);
std::vector<Database::TrackId> res; std::vector<Database::TrackId> res;
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
for (const Database::ArtistId artistId : artistsId) for (const Database::ArtistId artistId : artistsId)
{ {
Database::Track::FindParameters params; Database::Track::FindParameters params;
params.setArtist(artistId); params.setArtist(artistId);
params.setSortMethod(Database::TrackSortMethod::DateDescAndRelease); params.setSortMethod(Database::TrackSortMethod::DateDescAndRelease);
params.setClusters(clusters); params.setClusters(clusters);
params.setRange({0, maxTrackCount - res.size()}); params.setRange({ 0, maxTrackCount - res.size() });
const auto tracks {Database::Track::find(session, params)}; const auto tracks{ Database::Track::findIds(session, params) };
res.reserve(res.size() + tracks.results.size()); res.reserve(res.size() + tracks.results.size());
res.insert(std::end(res), std::cbegin(tracks.results), std::cend(tracks.results)); res.insert(std::end(res), std::cbegin(tracks.results), std::cend(tracks.results));
if (res.size() == maxTrackCount) if (res.size() == maxTrackCount)
break; break;
} }
return res; return res;
} }
static std::vector<Database::TrackId> getReleasesTracks(Database::Session& session, const std::vector<Database::ReleaseId>& releasesId, const std::vector<Database::ClusterId>& clusters, std::size_t maxTrackCount)
std::vector<Database::TrackId> {
getReleasesTracks(Database::Session& session, const std::vector<Database::ReleaseId>& releasesId, const std::vector<Database::ClusterId>& clusters, std::size_t maxTrackCount) using namespace Database;
{ assert(maxTrackCount);
using namespace Database;
assert(maxTrackCount);
std::vector<TrackId> res; std::vector<TrackId> res;
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
for (const ReleaseId releaseId : releasesId) for (const ReleaseId releaseId : releasesId)
{ {
Database::Track::FindParameters params; Database::Track::FindParameters params;
params.setRelease(releaseId); params.setRelease(releaseId);
params.setSortMethod(Database::TrackSortMethod::Release); params.setSortMethod(Database::TrackSortMethod::Release);
params.setClusters(clusters); params.setClusters(clusters);
params.setRange({0, maxTrackCount - res.size()}); params.setRange({ 0, maxTrackCount - res.size() });
const auto tracks {Database::Track::find(session, params)}; const auto tracks{ Database::Track::findIds(session, params) };
res.reserve(res.size() + tracks.results.size()); res.reserve(res.size() + tracks.results.size());
res.insert(std::end(res), std::cbegin(tracks.results), std::cend(tracks.results)); res.insert(std::end(res), std::cbegin(tracks.results), std::cend(tracks.results));
if (res.size() == maxTrackCount) if (res.size() == maxTrackCount)
break; break;
} }
return res; return res;
} }
static std::vector<Database::TrackId> getTrackListTracks(Database::Session& session, Database::TrackListId trackListId, const std::vector<Database::ClusterId>& clusters, std::size_t maxTrackCount)
std::vector<Database::TrackId> {
getTrackListTracks(Database::Session& session, Database::TrackListId trackListId, const std::vector<Database::ClusterId>& clusters, std::size_t maxTrackCount) using namespace Database;
{ assert(maxTrackCount);
using namespace Database;
assert(maxTrackCount);
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
Database::Track::FindParameters params; Database::Track::FindParameters params;
params.setTrackList(trackListId); params.setTrackList(trackListId);
params.setClusters(clusters); params.setClusters(clusters);
params.setRange({0, maxTrackCount}); params.setRange({ 0, maxTrackCount });
params.setSortMethod(TrackSortMethod::TrackList); params.setSortMethod(TrackSortMethod::TrackList);
params.setDistinct(false); params.setDistinct(false);
return Database::Track::find(session, params).results; return Database::Track::findIds(session, params).results;
} }
}
PlayQueueController::PlayQueueController(Filters& filters, PlayQueue& playQueue) PlayQueueController::PlayQueueController(Filters& filters, PlayQueue& playQueue)
: _filters {filters} : _filters{ filters }
, _playQueue {playQueue} , _playQueue{ playQueue }
{ {
} }
void void PlayQueueController::processCommand(Command command, const std::vector<Database::ArtistId>& artistIds)
PlayQueueController::processCommand(Command command, const std::vector<Database::ArtistId>& artistIds) {
{ const std::vector<Database::TrackId> tracks{ getArtistsTracks(LmsApp->getDbSession(), artistIds, _filters.getClusterIds(), _maxTrackCountToEnqueue) };
const std::vector<Database::TrackId> tracks {getArtistsTracks(LmsApp->getDbSession(), artistIds, _filters.getClusterIds(), _maxTrackCountToEnqueue)}; processCommand(command, tracks);
processCommand(command, tracks); }
}
void void PlayQueueController::processCommand(Command command, const std::vector<Database::ReleaseId>& releaseIds)
PlayQueueController::processCommand(Command command, const std::vector<Database::ReleaseId>& releaseIds) {
{ const std::vector<Database::TrackId> tracks{ getReleasesTracks(LmsApp->getDbSession(), releaseIds, _filters.getClusterIds(), _maxTrackCountToEnqueue) };
const std::vector<Database::TrackId> tracks {getReleasesTracks(LmsApp->getDbSession(), releaseIds, _filters.getClusterIds(), _maxTrackCountToEnqueue)}; processCommand(command, tracks);
processCommand(command, tracks); }
}
void void PlayQueueController::processCommand(Command command, const std::vector<Database::TrackId>& trackIds)
PlayQueueController::processCommand(Command command, const std::vector<Database::TrackId>& trackIds) {
{ // consider things are already filtered here, and _maxTrackCount honored playqueue side...
// consider things are already filtered here, and _maxTrackCount honored playqueue side... switch (command)
switch (command) {
{ case Command::Play:
case Command::Play: _playQueue.play(trackIds);
_playQueue.play(trackIds); break;
break; case Command::PlayNext:
case Command::PlayNext: _playQueue.playNext(trackIds);
_playQueue.playNext(trackIds); break;
break; case Command::PlayShuffled:
case Command::PlayShuffled: _playQueue.playShuffled(trackIds);
_playQueue.playShuffled(trackIds); break;
break; case Command::PlayOrAddLast:
case Command::PlayOrAddLast: _playQueue.playOrAddLast(trackIds);
_playQueue.playOrAddLast(trackIds); break;
break; }
} }
}
void void PlayQueueController::processCommand(Command command, Database::TrackListId trackListId)
PlayQueueController::processCommand(Command command, Database::TrackListId trackListId) {
{ const std::vector<Database::TrackId> tracks{ getTrackListTracks(LmsApp->getDbSession(), trackListId, _filters.getClusterIds(), _maxTrackCountToEnqueue) };
const std::vector<Database::TrackId> tracks {getTrackListTracks(LmsApp->getDbSession(), trackListId, _filters.getClusterIds(), _maxTrackCountToEnqueue)}; processCommand(command, tracks);
processCommand(command, tracks); }
}
void void PlayQueueController::playTrackInRelease(Database::TrackId trackId)
PlayQueueController::playTrackInRelease(Database::TrackId trackId) {
{ Database::ReleaseId releaseId;
Database::ReleaseId releaseId; {
{ auto transaction{ LmsApp->getDbSession().createSharedTransaction() };
auto transaction {LmsApp->getDbSession().createSharedTransaction()}; const Database::Track::pointer track{ Database::Track::find(LmsApp->getDbSession(), trackId) };
const Database::Track::pointer track {Database::Track::find(LmsApp->getDbSession(), trackId)}; if (!track || !track->getRelease())
if (!track || !track->getRelease()) return;
return;
releaseId = track->getRelease()->getId(); releaseId = track->getRelease()->getId();
} }
const std::vector<Database::TrackId> tracks {getReleasesTracks(LmsApp->getDbSession(), {releaseId}, _filters.getClusterIds(), _maxTrackCountToEnqueue)}; const std::vector<Database::TrackId> tracks{ getReleasesTracks(LmsApp->getDbSession(), {releaseId}, _filters.getClusterIds(), _maxTrackCountToEnqueue) };
auto itTrack {std::find(std::cbegin(tracks), std::cend(tracks), trackId)}; auto itTrack{ std::find(std::cbegin(tracks), std::cend(tracks), trackId) };
if (itTrack == std::cend(tracks)) if (itTrack == std::cend(tracks))
return; return;
const std::size_t index {static_cast<std::size_t>(std::distance(std::cbegin(tracks), itTrack))}; const std::size_t index{ static_cast<std::size_t>(std::distance(std::cbegin(tracks), itTrack)) };
_playQueue.playAtIndex(tracks, index); _playQueue.playAtIndex(tracks, index);
} }
} // namespace UserInterface } // namespace UserInterface
+482 -495
View File
@@ -50,501 +50,488 @@
using namespace Database; using namespace Database;
namespace UserInterface { namespace UserInterface
void
showReleaseInfoModal(Database::ReleaseId releaseId)
{ {
auto transaction {LmsApp->getDbSession().createSharedTransaction()}; namespace
{
const Database::Release::pointer release {Database::Release::find(LmsApp->getDbSession(), releaseId)}; void showReleaseInfoModal(Database::ReleaseId releaseId)
if (!release) {
return; auto transaction{ LmsApp->getDbSession().createSharedTransaction() };
auto releaseInfo {std::make_unique<Template>(Wt::WString::tr("Lms.Explore.Release.template.release-info"))}; const Database::Release::pointer release{ Database::Release::find(LmsApp->getDbSession(), releaseId) };
Wt::WWidget* releaseInfoPtr {releaseInfo.get()}; if (!release)
releaseInfo->addFunction("tr", &Wt::WTemplate::Functions::tr); return;
if (auto primaryReleaseType {release->getPrimaryType()}) auto releaseInfo{ std::make_unique<Template>(Wt::WString::tr("Lms.Explore.Release.template.release-info")) };
{ Wt::WWidget* releaseInfoPtr{ releaseInfo.get() };
releaseInfo->setCondition("if-has-release-type", true); releaseInfo->addFunction("tr", &Wt::WTemplate::Functions::tr);
releaseInfo->bindString("release-type", ReleaseHelpers::buildReleaseTypeString(*primaryReleaseType, release->getSecondaryTypes()));
} if (auto primaryReleaseType{ release->getPrimaryType() })
{
std::map<Wt::WString, std::set<ArtistId>> artistMap; releaseInfo->setCondition("if-has-release-type", true);
releaseInfo->bindString("release-type", ReleaseHelpers::buildReleaseTypeString(*primaryReleaseType, release->getSecondaryTypes()));
auto addArtists = [&](TrackArtistLinkType linkType, const char* type) }
{
Artist::FindParameters params; std::map<Wt::WString, std::set<ArtistId>> artistMap;
params.setRelease(releaseId);
params.setLinkType(linkType); auto addArtists = [&](TrackArtistLinkType linkType, const char* type)
const auto artistIds {Artist::find(LmsApp->getDbSession(), params)}; {
if (artistIds.results.empty()) Artist::FindParameters params;
return; params.setRelease(releaseId);
params.setLinkType(linkType);
Wt::WString typeStr {Wt::WString::trn(type, artistIds.results.size())}; const auto artistIds{ Artist::find(LmsApp->getDbSession(), params) };
for (ArtistId artistId : artistIds.results) if (artistIds.results.empty())
artistMap[typeStr].insert(artistId); return;
};
Wt::WString typeStr{ Wt::WString::trn(type, artistIds.results.size()) };
auto addPerformerArtists = [&] for (ArtistId artistId : artistIds.results)
{ artistMap[typeStr].insert(artistId);
TrackArtistLink::FindParameters params; };
params.setRelease(releaseId);
params.setLinkType(TrackArtistLinkType::Performer); auto addPerformerArtists = [&]
const auto links {TrackArtistLink::find(LmsApp->getDbSession(), params)}; {
if (links.results.empty()) TrackArtistLink::FindParameters params;
return; params.setRelease(releaseId);
params.setLinkType(TrackArtistLinkType::Performer);
for (const TrackArtistLinkId linkId : links.results) const auto links{ TrackArtistLink::find(LmsApp->getDbSession(), params) };
{ if (links.results.empty())
const TrackArtistLink::pointer link {TrackArtistLink::find(LmsApp->getDbSession(), linkId)}; return;
if (!link)
continue; for (const TrackArtistLinkId linkId : links.results)
{
artistMap[std::string {link->getSubType()}].insert(link->getArtist()->getId()); const TrackArtistLink::pointer link{ TrackArtistLink::find(LmsApp->getDbSession(), linkId) };
} if (!link)
}; continue;
addArtists(TrackArtistLinkType::Composer, "Lms.Explore.Artists.linktype-composer"); artistMap[std::string{ link->getSubType() }].insert(link->getArtist()->getId());
addArtists(TrackArtistLinkType::Conductor, "Lms.Explore.Artists.linktype-conductor"); }
addArtists(TrackArtistLinkType::Lyricist, "Lms.Explore.Artists.linktype-lyricist"); };
addArtists(TrackArtistLinkType::Mixer, "Lms.Explore.Artists.linktype-mixer");
addArtists(TrackArtistLinkType::Remixer, "Lms.Explore.Artists.linktype-remixer"); addArtists(TrackArtistLinkType::Composer, "Lms.Explore.Artists.linktype-composer");
addArtists(TrackArtistLinkType::Producer, "Lms.Explore.Artists.linktype-producer"); addArtists(TrackArtistLinkType::Conductor, "Lms.Explore.Artists.linktype-conductor");
addPerformerArtists(); addArtists(TrackArtistLinkType::Lyricist, "Lms.Explore.Artists.linktype-lyricist");
addArtists(TrackArtistLinkType::Mixer, "Lms.Explore.Artists.linktype-mixer");
if (auto itRolelessPerformers {artistMap.find("")}; itRolelessPerformers != std::cend(artistMap)) addArtists(TrackArtistLinkType::Remixer, "Lms.Explore.Artists.linktype-remixer");
{ addArtists(TrackArtistLinkType::Producer, "Lms.Explore.Artists.linktype-producer");
Wt::WString performersStr {Wt::WString::trn("Lms.Explore.Artists.linktype-performer", itRolelessPerformers->second.size())}; addPerformerArtists();
artistMap[performersStr] = std::move(itRolelessPerformers->second);
artistMap.erase(itRolelessPerformers); if (auto itRolelessPerformers{ artistMap.find("") }; itRolelessPerformers != std::cend(artistMap))
} {
Wt::WString performersStr{ Wt::WString::trn("Lms.Explore.Artists.linktype-performer", itRolelessPerformers->second.size()) };
if (!artistMap.empty()) artistMap[performersStr] = std::move(itRolelessPerformers->second);
{ artistMap.erase(itRolelessPerformers);
releaseInfo->setCondition("if-has-artist", true); }
Wt::WContainerWidget* artistTable {releaseInfo->bindNew<Wt::WContainerWidget>("artist-table")};
if (!artistMap.empty())
for (const auto& [role, artistIds] : artistMap) {
{ releaseInfo->setCondition("if-has-artist", true);
std::unique_ptr<Wt::WContainerWidget> artistContainer {Utils::createArtistAnchorList(std::vector (std::cbegin(artistIds), std::cend(artistIds)))}; Wt::WContainerWidget* artistTable{ releaseInfo->bindNew<Wt::WContainerWidget>("artist-table") };
auto artistsEntry {std::make_unique<Template>(Wt::WString::tr("Lms.Explore.template.info.artists"))};
artistsEntry->bindString("type", role); for (const auto& [role, artistIds] : artistMap)
artistsEntry->bindWidget("artist-container", std::move(artistContainer)); {
artistTable->addWidget(std::move(artistsEntry)); std::unique_ptr<Wt::WContainerWidget> artistContainer{ Utils::createArtistAnchorList(std::vector(std::cbegin(artistIds), std::cend(artistIds))) };
} auto artistsEntry{ std::make_unique<Template>(Wt::WString::tr("Lms.Explore.template.info.artists")) };
} artistsEntry->bindString("type", role);
artistsEntry->bindWidget("artist-container", std::move(artistContainer));
// TODO: save in DB and mean all this artistTable->addWidget(std::move(artistsEntry));
for (TrackId trackId : Track::find(LmsApp->getDbSession(), Track::FindParameters {}.setRelease(releaseId).setRange(Range {0, 1})).results) }
{ }
const Track::pointer track {Track::find(LmsApp->getDbSession(), trackId)};
if (!track) // TODO: save in DB and mean all this
continue; for (const Track::pointer& track : Track::find(LmsApp->getDbSession(), Track::FindParameters{}.setRelease(releaseId).setRange(Range{ 0, 1 })).results)
{
if (const auto audioFile {Av::parseAudioFile(track->getPath())}) if (const auto audioFile{ Av::parseAudioFile(track->getPath()) })
{ {
const std::optional<Av::StreamInfo> audioStream {audioFile->getBestStreamInfo()}; const std::optional<Av::StreamInfo> audioStream{ audioFile->getBestStreamInfo() };
if (audioStream) if (audioStream)
{ {
releaseInfo->setCondition("if-has-codec", true); releaseInfo->setCondition("if-has-codec", true);
releaseInfo->bindString("codec", audioStream->codec); releaseInfo->bindString("codec", audioStream->codec);
if (audioStream->bitrate) if (audioStream->bitrate)
{ {
releaseInfo->setCondition("if-has-bitrate", true); releaseInfo->setCondition("if-has-bitrate", true);
releaseInfo->bindString("bitrate", std::to_string(audioStream->bitrate / 1000) + " kbps"); releaseInfo->bindString("bitrate", std::to_string(audioStream->bitrate / 1000) + " kbps");
break; break;
} }
} }
} }
} }
Wt::WPushButton* okBtn {releaseInfo->bindNew<Wt::WPushButton>("ok-btn", Wt::WString::tr("Lms.ok"))}; Wt::WPushButton* okBtn{ releaseInfo->bindNew<Wt::WPushButton>("ok-btn", Wt::WString::tr("Lms.ok")) };
okBtn->clicked().connect([=] okBtn->clicked().connect([=]
{ {
LmsApp->getModalManager().dispose(releaseInfoPtr); LmsApp->getModalManager().dispose(releaseInfoPtr);
}); });
LmsApp->getModalManager().show(std::move(releaseInfo)); LmsApp->getModalManager().show(std::move(releaseInfo));
} }
Release::Release(Filters& filters, PlayQueueController& playQueueController) std::optional<ReleaseId> extractReleaseIdFromInternalPath()
: Template {Wt::WString::tr("Lms.Explore.Release.template")} {
, _filters {filters} if (wApp->internalPathMatches("/release/mbid/"))
, _playQueueController {playQueueController} {
{ const auto mbid{ UUID::fromString(wApp->internalPathNextPart("/release/mbid/")) };
addFunction("tr", &Wt::WTemplate::Functions::tr); if (mbid)
addFunction("id", &Wt::WTemplate::Functions::id); {
auto transaction{ LmsApp->getDbSession().createSharedTransaction() };
wApp->internalPathChanged().connect(this, [this] if (const Database::Release::pointer release{ Database::Release::find(LmsApp->getDbSession(), *mbid) })
{ return release->getId();
refreshView(); }
});
return std::nullopt;
_filters.updated().connect([this] }
{
_needForceRefresh = true; return StringUtils::readAs<ReleaseId::ValueType>(wApp->internalPathNextPart("/release/"));
refreshView(); }
}); }
refreshView(); Release::Release(Filters& filters, PlayQueueController& playQueueController)
} : Template{ Wt::WString::tr("Lms.Explore.Release.template") }
, _filters{ filters }
static , _playQueueController{ playQueueController }
std::optional<ReleaseId> {
extractReleaseIdFromInternalPath() addFunction("tr", &Wt::WTemplate::Functions::tr);
{ addFunction("id", &Wt::WTemplate::Functions::id);
if (wApp->internalPathMatches("/release/mbid/"))
{ wApp->internalPathChanged().connect(this, [this]
const auto mbid {UUID::fromString(wApp->internalPathNextPart("/release/mbid/"))}; {
if (mbid) refreshView();
{ });
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
if (const Database::Release::pointer release {Database::Release::find(LmsApp->getDbSession(), *mbid)}) _filters.updated().connect([this]
return release->getId(); {
} _needForceRefresh = true;
refreshView();
return std::nullopt; });
}
refreshView();
return StringUtils::readAs<ReleaseId::ValueType>(wApp->internalPathNextPart("/release/")); }
}
void Release::refreshView()
{
void if (!wApp->internalPathMatches("/release/"))
Release::refreshView() return;
{
if (!wApp->internalPathMatches("/release/")) const auto releaseId{ extractReleaseIdFromInternalPath() };
return;
// consider everything is up to date is the same release is being rendered
const auto releaseId {extractReleaseIdFromInternalPath()}; if (!_needForceRefresh && releaseId && *releaseId == _releaseId)
return;
// consider everything is up to date is the same release is being rendered
if (!_needForceRefresh && releaseId && *releaseId == _releaseId) clear();
return; _releaseId = {};
_needForceRefresh = false;
clear();
_releaseId = {}; if (!releaseId)
_needForceRefresh = false; throw ReleaseNotFoundException{};
if (!releaseId) auto similarReleasesIds{ Service<Recommendation::IRecommendationService>::get()->getSimilarReleases(*releaseId, 6) };
throw ReleaseNotFoundException {};
auto transaction{ LmsApp->getDbSession().createSharedTransaction() };
auto similarReleasesIds {Service<Recommendation::IRecommendationService>::get()->getSimilarReleases(*releaseId, 6)};
const Database::Release::pointer release{ Database::Release::find(LmsApp->getDbSession(), *releaseId) };
auto transaction {LmsApp->getDbSession().createSharedTransaction()}; if (!release)
throw ReleaseNotFoundException{};
const Database::Release::pointer release {Database::Release::find(LmsApp->getDbSession(), *releaseId)};
if (!release) LmsApp->setTitle(release->getName());
throw ReleaseNotFoundException {}; _releaseId = *releaseId;
LmsApp->setTitle(release->getName()); refreshCopyright(release);
_releaseId = *releaseId; refreshLinks(release);
refreshSimilarReleases(similarReleasesIds);
refreshCopyright(release);
refreshLinks(release); bindString("name", Wt::WString::fromUTF8(release->getName()), Wt::TextFormat::Plain);
refreshSimilarReleases(similarReleasesIds);
Wt::WString year{ ReleaseHelpers::buildReleaseYearString(release->getReleaseDate(), release->getOriginalReleaseDate()) };
bindString("name", Wt::WString::fromUTF8(release->getName()), Wt::TextFormat::Plain); if (!year.empty())
{
Wt::WString year {ReleaseHelpers::buildReleaseYearString(release->getReleaseDate(), release->getOriginalReleaseDate())}; setCondition("if-has-year", true);
if (!year.empty()) bindString("year", year, Wt::TextFormat::Plain);
{ }
setCondition("if-has-year", true);
bindString("year", year, Wt::TextFormat::Plain); bindString("duration", Utils::durationToString(release->getDuration()), Wt::TextFormat::Plain);
}
refreshReleaseArtists(release);
bindString("duration", Utils::durationToString(release->getDuration()), Wt::TextFormat::Plain);
bindWidget<Wt::WImage>("cover", Utils::createCover(release->getId(), CoverResource::Size::Large));
refreshReleaseArtists(release);
Wt::WContainerWidget* clusterContainers{ bindNew<Wt::WContainerWidget>("clusters") };
bindWidget<Wt::WImage>("cover", Utils::createCover(release->getId(), CoverResource::Size::Large)); {
const auto clusterTypes{ ScanSettings::get(LmsApp->getDbSession())->getClusterTypes() };
Wt::WContainerWidget* clusterContainers {bindNew<Wt::WContainerWidget>("clusters")}; const auto clusterGroups{ release->getClusterGroups(clusterTypes, 3) };
{
const auto clusterTypes {ScanSettings::get(LmsApp->getDbSession())->getClusterTypes()}; for (const auto& clusters : clusterGroups)
const auto clusterGroups {release->getClusterGroups(clusterTypes, 3)}; {
for (const Database::Cluster::pointer& cluster : clusters)
for (const auto& clusters : clusterGroups) {
{ const ClusterId clusterId{ cluster->getId() };
for (const Database::Cluster::pointer& cluster : clusters) Wt::WInteractWidget* entry{ clusterContainers->addWidget(Utils::createCluster(clusterId)) };
{ entry->clicked().connect([=]
const ClusterId clusterId {cluster->getId()}; {
Wt::WInteractWidget* entry {clusterContainers->addWidget(Utils::createCluster(clusterId))}; _filters.add(clusterId);
entry->clicked().connect([=] });
{ }
_filters.add(clusterId); }
}); }
}
} bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.Explore.play"), Wt::TextFormat::XHTML)
} ->clicked().connect([=]
{
bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.Explore.play"), Wt::TextFormat::XHTML) _playQueueController.processCommand(PlayQueueController::Command::Play, { _releaseId });
->clicked().connect([=] });
{
_playQueueController.processCommand(PlayQueueController::Command::Play, {_releaseId}); bindNew<Wt::WPushButton>("play-shuffled", Wt::WString::tr("Lms.Explore.play-shuffled"), Wt::TextFormat::Plain)
}); ->clicked().connect([=]
{
bindNew<Wt::WPushButton>("play-shuffled", Wt::WString::tr("Lms.Explore.play-shuffled"), Wt::TextFormat::Plain) _playQueueController.processCommand(PlayQueueController::Command::PlayShuffled, { _releaseId });
->clicked().connect([=] });
{
_playQueueController.processCommand(PlayQueueController::Command::PlayShuffled, {_releaseId}); bindNew<Wt::WPushButton>("play-next", Wt::WString::tr("Lms.Explore.play-next"), Wt::TextFormat::Plain)
}); ->clicked().connect([=]
{
bindNew<Wt::WPushButton>("play-next", Wt::WString::tr("Lms.Explore.play-next"), Wt::TextFormat::Plain) _playQueueController.processCommand(PlayQueueController::Command::PlayNext, { _releaseId });
->clicked().connect([=] });
{
_playQueueController.processCommand(PlayQueueController::Command::PlayNext, {_releaseId}); bindNew<Wt::WPushButton>("play-last", Wt::WString::tr("Lms.Explore.play-last"), Wt::TextFormat::Plain)
}); ->clicked().connect([=]
{
bindNew<Wt::WPushButton>("play-last", Wt::WString::tr("Lms.Explore.play-last"), Wt::TextFormat::Plain) _playQueueController.processCommand(PlayQueueController::Command::PlayOrAddLast, { _releaseId });
->clicked().connect([=] });
{
_playQueueController.processCommand(PlayQueueController::Command::PlayOrAddLast, {_releaseId}); bindNew<Wt::WPushButton>("download", Wt::WString::tr("Lms.Explore.download"))
}); ->setLink(Wt::WLink{ std::make_unique<DownloadReleaseResource>(_releaseId) });
bindNew<Wt::WPushButton>("download", Wt::WString::tr("Lms.Explore.download")) bindNew<Wt::WPushButton>("release-info", Wt::WString::tr("Lms.Explore.release-info"))
->setLink(Wt::WLink {std::make_unique<DownloadReleaseResource>(_releaseId)}); ->clicked().connect([=]
{
bindNew<Wt::WPushButton>("release-info", Wt::WString::tr("Lms.Explore.release-info")) showReleaseInfoModal(_releaseId);
->clicked().connect([=] });
{
showReleaseInfoModal(_releaseId); {
}); auto isStarred{ [=] { return Service<Feedback::IFeedbackService>::get()->isStarred(LmsApp->getUserId(), _releaseId); } };
{ Wt::WPushButton* starBtn{ bindNew<Wt::WPushButton>("star", Wt::WString::tr(isStarred() ? "Lms.Explore.unstar" : "Lms.Explore.star")) };
auto isStarred {[=] { return Service<Feedback::IFeedbackService>::get()->isStarred(LmsApp->getUserId(), _releaseId); }}; starBtn->clicked().connect([=]
{
Wt::WPushButton* starBtn {bindNew<Wt::WPushButton>("star", Wt::WString::tr(isStarred() ? "Lms.Explore.unstar" : "Lms.Explore.star"))}; if (isStarred())
starBtn->clicked().connect([=] {
{ Service<Feedback::IFeedbackService>::get()->unstar(LmsApp->getUserId(), _releaseId);
if (isStarred()) starBtn->setText(Wt::WString::tr("Lms.Explore.star"));
{ }
Service<Feedback::IFeedbackService>::get()->unstar(LmsApp->getUserId(), _releaseId); else
starBtn->setText(Wt::WString::tr("Lms.Explore.star")); {
} Service<Feedback::IFeedbackService>::get()->star(LmsApp->getUserId(), _releaseId);
else starBtn->setText(Wt::WString::tr("Lms.Explore.unstar"));
{ }
Service<Feedback::IFeedbackService>::get()->star(LmsApp->getUserId(), _releaseId); });
starBtn->setText(Wt::WString::tr("Lms.Explore.unstar")); }
}
}); Wt::WContainerWidget* rootContainer{ bindNew<Wt::WContainerWidget>("container") };
}
const bool variousArtists{ release->hasVariousArtists() };
Wt::WContainerWidget* rootContainer {bindNew<Wt::WContainerWidget>("container")}; const auto totalDisc{ release->getTotalDisc() };
const std::size_t discCount{ release->getDiscCount() };
const bool variousArtists {release->hasVariousArtists()}; const bool isReleaseMultiDisc{ (discCount > 1) || (totalDisc && *totalDisc > 1) };
const auto totalDisc {release->getTotalDisc()};
const std::size_t discCount {release->getDiscCount()}; // Expect to be called in asc order
const bool isReleaseMultiDisc {(discCount > 1) || (totalDisc && *totalDisc > 1)}; std::map<std::size_t, Wt::WContainerWidget*> trackContainers;
auto getOrAddDiscContainer = [&](std::size_t discNumber, const std::string& discSubtitle) -> Wt::WContainerWidget*
// Expect to be called in asc order {
std::map<std::size_t, Wt::WContainerWidget*> trackContainers; {
auto getOrAddDiscContainer = [&](std::size_t discNumber, const std::string& discSubtitle) -> Wt::WContainerWidget* auto it = trackContainers.find(discNumber);
{ if (it != std::cend(trackContainers))
{ return it->second;
auto it = trackContainers.find(discNumber); }
if (it != std::cend(trackContainers))
return it->second; Wt::WTemplate* disc{ rootContainer->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Release.template.entry-disc")) };
}
if (discSubtitle.empty())
Wt::WTemplate* disc {rootContainer->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Release.template.entry-disc"))}; disc->bindNew<Wt::WText>("disc-title", Wt::WString::tr("Lms.Explore.Release.disc").arg(discNumber));
else
if (discSubtitle.empty()) disc->bindString("disc-title", Wt::WString::fromUTF8(discSubtitle), Wt::TextFormat::Plain);
disc->bindNew<Wt::WText>("disc-title", Wt::WString::tr("Lms.Explore.Release.disc").arg(discNumber));
else Wt::WContainerWidget* tracksContainer{ disc->bindNew<Wt::WContainerWidget>("tracks") };
disc->bindString("disc-title", Wt::WString::fromUTF8(discSubtitle), Wt::TextFormat::Plain); trackContainers[discNumber] = tracksContainer;
Wt::WContainerWidget* tracksContainer {disc->bindNew<Wt::WContainerWidget>("tracks")}; return tracksContainer;
trackContainers[discNumber] = tracksContainer; };
return tracksContainer; Wt::WContainerWidget* noDiscTracksContainer{};
}; auto getOrAddNoDiscContainer = [&]
{
Wt::WContainerWidget* noDiscTracksContainer{}; if (noDiscTracksContainer)
auto getOrAddNoDiscContainer = [&] return noDiscTracksContainer;
{
if (noDiscTracksContainer) Wt::WTemplate* disc{ rootContainer->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Release.template.entry-nodisc")) };
return noDiscTracksContainer; noDiscTracksContainer = disc->bindNew<Wt::WContainerWidget>("tracks");
Wt::WTemplate* disc {rootContainer->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Release.template.entry-nodisc"))}; return noDiscTracksContainer;
noDiscTracksContainer = disc->bindNew<Wt::WContainerWidget>("tracks"); };
return noDiscTracksContainer; Database::Track::FindParameters params;
}; params.setRelease(_releaseId);
params.setSortMethod(Database::TrackSortMethod::Release);
Database::Track::FindParameters params; params.setClusters(_filters.getClusterIds());
params.setRelease(_releaseId);
params.setSortMethod(Database::TrackSortMethod::Release); for (const Database::Track::pointer& track : Database::Track::find(LmsApp->getDbSession(), params).results)
params.setClusters(_filters.getClusterIds()); {
const Database::TrackId trackId{ track->getId() };
const auto tracks {Database::Track::find(LmsApp->getDbSession(), params)}; const auto discNumber{ track->getDiscNumber() };
for (const Database::TrackId trackId : tracks.results)
{ Wt::WContainerWidget* container;
const Database::Track::pointer track {Database::Track::find(LmsApp->getDbSession(), trackId)}; if (isReleaseMultiDisc && discNumber)
container = getOrAddDiscContainer(*discNumber, track->getDiscSubtitle());
const auto discNumber {track->getDiscNumber()}; else
container = getOrAddNoDiscContainer();
Wt::WContainerWidget* container;
if (isReleaseMultiDisc && discNumber) Template* entry{ container->addNew<Template>(Wt::WString::tr("Lms.Explore.Release.template.entry")) };
container = getOrAddDiscContainer(*discNumber, track->getDiscSubtitle());
else entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain);
container = getOrAddNoDiscContainer();
const auto artists{ track->getArtistIds({TrackArtistLinkType::Artist}) };
Template* entry {container->addNew<Template>(Wt::WString::tr("Lms.Explore.Release.template.entry"))}; if (variousArtists && !artists.empty())
{
entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain); entry->setCondition("if-has-artists", true);
entry->bindWidget("artists", Utils::createArtistDisplayNameWithAnchors(track->getArtistDisplayName(), artists));
const auto artists {track->getArtistIds({TrackArtistLinkType::Artist})}; entry->bindWidget("artists-md", Utils::createArtistDisplayNameWithAnchors(track->getArtistDisplayName(), artists));
if (variousArtists && !artists.empty()) }
{
entry->setCondition("if-has-artists", true); auto trackNumber{ track->getTrackNumber() };
entry->bindWidget("artists", Utils::createArtistDisplayNameWithAnchors(track->getArtistDisplayName(), artists)); if (trackNumber)
entry->bindWidget("artists-md", Utils::createArtistDisplayNameWithAnchors(track->getArtistDisplayName(),artists)); {
} entry->setCondition("if-has-track-number", true);
entry->bindInt("track-number", *trackNumber);
auto trackNumber {track->getTrackNumber()}; }
if (trackNumber)
{ Wt::WPushButton* playBtn{ entry->bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.template.play-btn"), Wt::TextFormat::XHTML) };
entry->setCondition("if-has-track-number", true); playBtn->clicked().connect([=]
entry->bindInt("track-number", *trackNumber); {
} _playQueueController.playTrackInRelease(trackId);
});
Wt::WPushButton* playBtn {entry->bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.template.play-btn"), Wt::TextFormat::XHTML)};
playBtn->clicked().connect([=] {
{ entry->bindNew<Wt::WPushButton>("more-btn", Wt::WString::tr("Lms.template.more-btn"), Wt::TextFormat::XHTML);
_playQueueController.playTrackInRelease(trackId); entry->bindNew<Wt::WPushButton>("play", Wt::WString::tr("Lms.Explore.play"))
}); ->clicked().connect([=]
{
{ _playQueueController.playTrackInRelease(trackId);
entry->bindNew<Wt::WPushButton>("more-btn", Wt::WString::tr("Lms.template.more-btn"), Wt::TextFormat::XHTML); });
entry->bindNew<Wt::WPushButton>("play", Wt::WString::tr("Lms.Explore.play")) entry->bindNew<Wt::WPushButton>("play-next", Wt::WString::tr("Lms.Explore.play-next"))
->clicked().connect([=] ->clicked().connect([=]
{ {
_playQueueController.playTrackInRelease(trackId); _playQueueController.processCommand(PlayQueueController::Command::PlayNext, { trackId });
}); });
entry->bindNew<Wt::WPushButton>("play-next", Wt::WString::tr("Lms.Explore.play-next")) entry->bindNew<Wt::WPushButton>("play-last", Wt::WString::tr("Lms.Explore.play-last"))
->clicked().connect([=] ->clicked().connect([=]
{ {
_playQueueController.processCommand(PlayQueueController::Command::PlayNext, {trackId}); _playQueueController.processCommand(PlayQueueController::Command::PlayOrAddLast, { trackId });
}); });
entry->bindNew<Wt::WPushButton>("play-last", Wt::WString::tr("Lms.Explore.play-last"))
->clicked().connect([=] auto isStarred{ [=] { return Service<Feedback::IFeedbackService>::get()->isStarred(LmsApp->getUserId(), trackId); } };
{
_playQueueController.processCommand(PlayQueueController::Command::PlayOrAddLast, {trackId}); Wt::WPushButton* starBtn{ entry->bindNew<Wt::WPushButton>("star", Wt::WString::tr(isStarred() ? "Lms.Explore.unstar" : "Lms.Explore.star")) };
}); starBtn->clicked().connect([=]
{
auto isStarred {[=] { return Service<Feedback::IFeedbackService>::get()->isStarred(LmsApp->getUserId(), trackId); }}; auto transaction{ LmsApp->getDbSession().createUniqueTransaction() };
Wt::WPushButton* starBtn {entry->bindNew<Wt::WPushButton>("star", Wt::WString::tr(isStarred() ? "Lms.Explore.unstar" : "Lms.Explore.star"))}; if (isStarred())
starBtn->clicked().connect([=] {
{ Service<Feedback::IFeedbackService>::get()->unstar(LmsApp->getUserId(), trackId);
auto transaction {LmsApp->getDbSession().createUniqueTransaction()}; starBtn->setText(Wt::WString::tr("Lms.Explore.star"));
}
if (isStarred()) else
{ {
Service<Feedback::IFeedbackService>::get()->unstar(LmsApp->getUserId(), trackId); Service<Feedback::IFeedbackService>::get()->star(LmsApp->getUserId(), trackId);
starBtn->setText(Wt::WString::tr("Lms.Explore.star")); starBtn->setText(Wt::WString::tr("Lms.Explore.unstar"));
} }
else });
{
Service<Feedback::IFeedbackService>::get()->star(LmsApp->getUserId(), trackId); entry->bindNew<Wt::WPushButton>("download", Wt::WString::tr("Lms.Explore.download"))
starBtn->setText(Wt::WString::tr("Lms.Explore.unstar")); ->setLink(Wt::WLink{ std::make_unique<DownloadTrackResource>(trackId) });
}
}); entry->bindNew<Wt::WPushButton>("track-info", Wt::WString::tr("Lms.Explore.track-info"))
->clicked().connect([=] { TrackListHelpers::showTrackInfoModal(trackId, _filters); });
entry->bindNew<Wt::WPushButton>("download", Wt::WString::tr("Lms.Explore.download")) }
->setLink(Wt::WLink {std::make_unique<DownloadTrackResource>(trackId)});
entry->bindString("duration", Utils::durationToString(track->getDuration()), Wt::TextFormat::Plain);
entry->bindNew<Wt::WPushButton>("track-info", Wt::WString::tr("Lms.Explore.track-info"))
->clicked().connect([=] { TrackListHelpers::showTrackInfoModal(trackId, _filters); }); LmsApp->getMediaPlayer().trackLoaded.connect(entry, [=](TrackId loadedTrackId)
} {
entry->toggleStyleClass("Lms-entry-playing", loadedTrackId == trackId);
entry->bindString("duration", Utils::durationToString(track->getDuration()), Wt::TextFormat::Plain); });
LmsApp->getMediaPlayer().trackLoaded.connect(entry, [=] (TrackId loadedTrackId) if (auto trackIdLoaded{ LmsApp->getMediaPlayer().getTrackLoaded() })
{ {
entry->toggleStyleClass("Lms-entry-playing", loadedTrackId == trackId); entry->toggleStyleClass("Lms-entry-playing", *trackIdLoaded == trackId);
}); }
else
if (auto trackIdLoaded {LmsApp->getMediaPlayer().getTrackLoaded()}) entry->removeStyleClass("Lms-entry-playing");
{ }
entry->toggleStyleClass("Lms-entry-playing", *trackIdLoaded == trackId); }
}
else void Release::refreshReleaseArtists(const Database::Release::pointer& release)
entry->removeStyleClass("Lms-entry-playing"); {
} auto container{ Utils::createArtistsAnchorsForRelease(release) };
} if (container)
{
void setCondition("if-has-release-artists", true);
Release::refreshReleaseArtists(const Database::Release::pointer& release) bindWidget("artists", std::move(container));
{ }
auto container{ Utils::createArtistsAnchorsForRelease(release) }; }
if (container)
{ void Release::refreshCopyright(const Database::Release::pointer& release)
setCondition("if-has-release-artists", true); {
bindWidget("artists", std::move(container)); std::optional<std::string> copyright{ release->getCopyright() };
} std::optional<std::string> copyrightURL{ release->getCopyrightURL() };
}
if (!copyright && !copyrightURL)
void return;
Release::refreshCopyright(const Database::Release::pointer& release)
{ setCondition("if-has-copyright", true);
std::optional<std::string> copyright {release->getCopyright()};
std::optional<std::string> copyrightURL {release->getCopyrightURL()}; std::string copyrightText{ copyright ? *copyright : "" };
if (copyrightText.empty() && copyrightURL)
if (!copyright && !copyrightURL) copyrightText = *copyrightURL;
return;
if (copyrightURL)
setCondition("if-has-copyright", true); {
Wt::WLink link{ *copyrightURL };
std::string copyrightText {copyright ? *copyright : ""}; link.setTarget(Wt::LinkTarget::NewWindow);
if (copyrightText.empty() && copyrightURL)
copyrightText = *copyrightURL; Wt::WAnchor* anchor{ bindNew<Wt::WAnchor>("copyright", link) };
anchor->setTextFormat(Wt::TextFormat::Plain);
if (copyrightURL) anchor->setText(Wt::WString::fromUTF8(copyrightText));
{ }
Wt::WLink link {*copyrightURL}; else
link.setTarget(Wt::LinkTarget::NewWindow); bindString("copyright", Wt::WString::fromUTF8(*copyright), Wt::TextFormat::Plain);
}
Wt::WAnchor* anchor {bindNew<Wt::WAnchor>("copyright", link)};
anchor->setTextFormat(Wt::TextFormat::Plain); void Release::refreshLinks(const Database::Release::pointer& release)
anchor->setText(Wt::WString::fromUTF8(copyrightText)); {
} const auto mbid{ release->getMBID() };
else if (mbid)
bindString("copyright", Wt::WString::fromUTF8(*copyright), Wt::TextFormat::Plain); {
} setCondition("if-has-mbid", true);
bindString("mbid-link", std::string{ "https://musicbrainz.org/release/" } + std::string{ mbid->getAsString() });
void }
Release::refreshLinks(const Database::Release::pointer& release) }
{
const auto mbid {release->getMBID()}; void Release::refreshSimilarReleases(const std::vector<ReleaseId>& similarReleasesId)
if (mbid) {
{ if (similarReleasesId.empty())
setCondition("if-has-mbid", true); return;
bindString("mbid-link", std::string {"https://musicbrainz.org/release/"} + std::string {mbid->getAsString()});
} setCondition("if-has-similar-releases", true);
} auto* similarReleasesContainer{ bindNew<Wt::WContainerWidget>("similar-releases") };
void for (const ReleaseId id : similarReleasesId)
Release::refreshSimilarReleases(const std::vector<ReleaseId>& similarReleasesId) {
{ const Database::Release::pointer similarRelease{ Database::Release::find(LmsApp->getDbSession(), id) };
if (similarReleasesId.empty()) if (!similarRelease)
return; continue;
setCondition("if-has-similar-releases", true); similarReleasesContainer->addWidget(ReleaseListHelpers::createEntry(similarRelease));
auto* similarReleasesContainer {bindNew<Wt::WContainerWidget>("similar-releases")}; }
}
for (const ReleaseId id : similarReleasesId)
{
const Database::Release::pointer similarRelease {Database::Release::find(LmsApp->getDbSession(), id)};
if (!similarRelease)
continue;
similarReleasesContainer->addWidget(ReleaseListHelpers::createEntry(similarRelease));
}
}
} // namespace UserInterface } // namespace UserInterface
+79 -82
View File
@@ -33,105 +33,102 @@
namespace UserInterface namespace UserInterface
{ {
using namespace Database; using namespace Database;
RangeResults<TrackId> RangeResults<TrackId> TrackCollector::get(Range range)
TrackCollector::get(Range range) {
{ Feedback::IFeedbackService& feedbackService{ *Service<Feedback::IFeedbackService>::get() };
Feedback::IFeedbackService& feedbackService{*Service<Feedback::IFeedbackService>::get()}; Scrobbling::IScrobblingService& scrobblingService{ *Service<Scrobbling::IScrobblingService>::get() };
Scrobbling::IScrobblingService& scrobblingService {*Service<Scrobbling::IScrobblingService>::get()};
RangeResults<TrackId> tracks; RangeResults<TrackId> tracks;
range = getActualRange(range); range = getActualRange(range);
if (range.size == 0) if (range.size == 0)
return tracks; return tracks;
switch (getMode()) switch (getMode())
{ {
case Mode::Random: case Mode::Random:
tracks = getRandomTracks(range); tracks = getRandomTracks(range);
break; break;
case Mode::Starred: case Mode::Starred:
tracks = feedbackService.getStarredTracks(LmsApp->getUserId(), getFilters().getClusterIds(), range); tracks = feedbackService.getStarredTracks(LmsApp->getUserId(), getFilters().getClusterIds(), range);
break; break;
case TrackCollector::Mode::RecentlyPlayed: case TrackCollector::Mode::RecentlyPlayed:
tracks = scrobblingService.getRecentTracks(LmsApp->getUserId(), getFilters().getClusterIds(), range); tracks = scrobblingService.getRecentTracks(LmsApp->getUserId(), getFilters().getClusterIds(), range);
break; break;
case Mode::MostPlayed: case Mode::MostPlayed:
tracks = scrobblingService.getTopTracks(LmsApp->getUserId(), getFilters().getClusterIds(), range); tracks = scrobblingService.getTopTracks(LmsApp->getUserId(), getFilters().getClusterIds(), range);
break; break;
case Mode::RecentlyAdded: case Mode::RecentlyAdded:
{ {
Track::FindParameters params; Track::FindParameters params;
params.setClusters(getFilters().getClusterIds()); params.setClusters(getFilters().getClusterIds());
params.setSortMethod(TrackSortMethod::LastWritten); params.setSortMethod(TrackSortMethod::LastWritten);
params.setRange(range); params.setRange(range);
{ {
auto transaction {LmsApp->getDbSession().createSharedTransaction()}; auto transaction{ LmsApp->getDbSession().createSharedTransaction() };
tracks = Track::find(LmsApp->getDbSession(), params); tracks = Track::findIds(LmsApp->getDbSession(), params);
} }
break; break;
} }
case Mode::Search: case Mode::Search:
{ {
Track::FindParameters params; Track::FindParameters params;
params.setClusters(getFilters().getClusterIds()); params.setClusters(getFilters().getClusterIds());
params.setKeywords(getSearchKeywords()); params.setKeywords(getSearchKeywords());
params.setRange(range); params.setRange(range);
{ {
auto transaction {LmsApp->getDbSession().createSharedTransaction()}; auto transaction{ LmsApp->getDbSession().createSharedTransaction() };
tracks = Track::find(LmsApp->getDbSession(), params); tracks = Track::findIds(LmsApp->getDbSession(), params);
} }
break; break;
} }
case Mode::All: case Mode::All:
{ {
Track::FindParameters params; Track::FindParameters params;
params.setClusters(getFilters().getClusterIds()); params.setClusters(getFilters().getClusterIds());
params.setRange(range); params.setRange(range);
{ {
auto transaction {LmsApp->getDbSession().createSharedTransaction()}; auto transaction{ LmsApp->getDbSession().createSharedTransaction() };
tracks = Track::find(LmsApp->getDbSession(), params); tracks = Track::findIds(LmsApp->getDbSession(), params);
} }
break; break;
} }
} }
if (range.offset + range.size == getMaxCount()) if (range.offset + range.size == getMaxCount())
tracks.moreResults = false; tracks.moreResults = false;
return tracks; return tracks;
} }
RangeResults<TrackId> RangeResults<TrackId> TrackCollector::getRandomTracks(Range range)
TrackCollector::getRandomTracks(Range range) {
{ assert(getMode() == Mode::Random);
assert(getMode() == Mode::Random);
if (!_randomTracks) if (!_randomTracks)
{ {
Track::FindParameters params; Track::FindParameters params;
params.setClusters(getFilters().getClusterIds()); params.setClusters(getFilters().getClusterIds());
params.setSortMethod(TrackSortMethod::Random); params.setSortMethod(TrackSortMethod::Random);
params.setRange({0, getMaxCount()}); params.setRange({ 0, getMaxCount() });
{ {
auto transaction {LmsApp->getDbSession().createSharedTransaction()}; auto transaction{ LmsApp->getDbSession().createSharedTransaction() };
_randomTracks = Track::find(LmsApp->getDbSession(), params); _randomTracks = Track::findIds(LmsApp->getDbSession(), params);
} }
} }
return _randomTracks->getSubRange(range); return _randomTracks->getSubRange(range);
} }
} // ns UserInterface } // ns UserInterface
+124 -132
View File
@@ -39,166 +39,158 @@
#include "ModalManager.hpp" #include "ModalManager.hpp"
#include "Utils.hpp" #include "Utils.hpp"
using namespace Database;
namespace
{
static
std::optional<TrackListId>
extractTrackListIdFromInternalPath()
{
return StringUtils::readAs<TrackListId::ValueType>(wApp->internalPathNextPart("/tracklist/"));
}
}
namespace UserInterface namespace UserInterface
{ {
TrackList::TrackList(Filters& filters, PlayQueueController& playQueueController) using namespace Database;
: Template {Wt::WString::tr("Lms.Explore.TrackList.template")}
, _filters {filters}
, _playQueueController {playQueueController}
{
addFunction("tr", &Wt::WTemplate::Functions::tr);
addFunction("id", &Wt::WTemplate::Functions::id);
wApp->internalPathChanged().connect(this, [this] namespace
{ {
refreshView(); std::optional<TrackListId> extractTrackListIdFromInternalPath()
}); {
return StringUtils::readAs<TrackListId::ValueType>(wApp->internalPathNextPart("/tracklist/"));
}
}
_filters.updated().connect([this] TrackList::TrackList(Filters& filters, PlayQueueController& playQueueController)
{ : Template{ Wt::WString::tr("Lms.Explore.TrackList.template") }
refreshView(); , _filters{ filters }
}); , _playQueueController{ playQueueController }
{
addFunction("tr", &Wt::WTemplate::Functions::tr);
addFunction("id", &Wt::WTemplate::Functions::id);
refreshView(); wApp->internalPathChanged().connect(this, [this]
} {
refreshView();
});
void _filters.updated().connect([this]
TrackList::refreshView() {
{ refreshView();
if (!wApp->internalPathMatches("/tracklist/")) });
return;
const std::optional<TrackListId> trackListId {extractTrackListIdFromInternalPath()}; refreshView();
if (!trackListId) }
throw TrackListNotFoundException {};
auto transaction {LmsApp->getDbSession().createSharedTransaction()}; void TrackList::refreshView()
{
if (!wApp->internalPathMatches("/tracklist/"))
return;
const Database::TrackList::pointer trackList {Database::TrackList::find(LmsApp->getDbSession(), *trackListId)}; const std::optional<TrackListId> trackListId{ extractTrackListIdFromInternalPath() };
if (!trackList) if (!trackListId)
throw TrackListNotFoundException {}; throw TrackListNotFoundException{};
LmsApp->setTitle(std::string {trackList->getName()}); auto transaction{ LmsApp->getDbSession().createSharedTransaction() };
_trackListId = *trackListId;
clear(); const Database::TrackList::pointer trackList{ Database::TrackList::find(LmsApp->getDbSession(), *trackListId) };
if (!trackList)
throw TrackListNotFoundException{};
bindString("name", std::string {trackList->getName()}, Wt::TextFormat::Plain); LmsApp->setTitle(std::string{ trackList->getName() });
bindString("duration", Utils::durationToString(trackList->getDuration())); _trackListId = *trackListId;
const auto trackCount {trackList->getCount()};
bindString("track-count", Wt::WString::trn("Lms.track-count", trackCount).arg(trackCount));
Wt::WContainerWidget* clusterContainers {bindNew<Wt::WContainerWidget>("clusters")}; clear();
{
const auto clusterTypes {ScanSettings::get(LmsApp->getDbSession())->getClusterTypes()};
const auto clusterGroups {trackList->getClusterGroups(clusterTypes, 3)};
for (const auto& clusters : clusterGroups) bindString("name", std::string{ trackList->getName() }, Wt::TextFormat::Plain);
{ bindString("duration", Utils::durationToString(trackList->getDuration()));
for (const Database::Cluster::pointer& cluster : clusters) const auto trackCount{ trackList->getCount() };
{ bindString("track-count", Wt::WString::trn("Lms.track-count", trackCount).arg(trackCount));
const ClusterId clusterId {cluster->getId()};
Wt::WInteractWidget* entry {clusterContainers->addWidget(Utils::createCluster(clusterId))};
entry->clicked().connect([=]
{
_filters.add(clusterId);
});
}
}
}
bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.Explore.play"), Wt::TextFormat::XHTML) Wt::WContainerWidget* clusterContainers{ bindNew<Wt::WContainerWidget>("clusters") };
->clicked().connect([=] {
{ const auto clusterTypes{ ScanSettings::get(LmsApp->getDbSession())->getClusterTypes() };
_playQueueController.processCommand(PlayQueueController::Command::Play, *trackListId); const auto clusterGroups{ trackList->getClusterGroups(clusterTypes, 3) };
});
bindNew<Wt::WPushButton>("play-shuffled", Wt::WString::tr("Lms.Explore.play-shuffled"), Wt::TextFormat::Plain) for (const auto& clusters : clusterGroups)
->clicked().connect([=] {
{ for (const Database::Cluster::pointer& cluster : clusters)
_playQueueController.processCommand(PlayQueueController::Command::PlayShuffled, *trackListId); {
}); const ClusterId clusterId{ cluster->getId() };
Wt::WInteractWidget* entry{ clusterContainers->addWidget(Utils::createCluster(clusterId)) };
entry->clicked().connect([=]
{
_filters.add(clusterId);
});
}
}
}
bindNew<Wt::WPushButton>("play-last", Wt::WString::tr("Lms.Explore.play-last"), Wt::TextFormat::Plain) bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.Explore.play"), Wt::TextFormat::XHTML)
->clicked().connect([=] ->clicked().connect([=]
{ {
_playQueueController.processCommand(PlayQueueController::Command::PlayOrAddLast, *trackListId); _playQueueController.processCommand(PlayQueueController::Command::Play, *trackListId);
}); });
bindNew<Wt::WPushButton>("download", Wt::WString::tr("Lms.Explore.download")) bindNew<Wt::WPushButton>("play-shuffled", Wt::WString::tr("Lms.Explore.play-shuffled"), Wt::TextFormat::Plain)
->setLink(Wt::WLink {std::make_unique<DownloadTrackListResource>(*trackListId)}); ->clicked().connect([=]
{
_playQueueController.processCommand(PlayQueueController::Command::PlayShuffled, *trackListId);
});
bindNew<Wt::WPushButton>("delete", Wt::WString::tr("Lms.delete")) bindNew<Wt::WPushButton>("play-last", Wt::WString::tr("Lms.Explore.play-last"), Wt::TextFormat::Plain)
->clicked().connect([=] ->clicked().connect([=]
{ {
auto modal {std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.TrackList.template.delete-tracklist"))}; _playQueueController.processCommand(PlayQueueController::Command::PlayOrAddLast, *trackListId);
modal->addFunction("tr", &Wt::WTemplate::Functions::tr); });
Wt::WWidget* modalPtr {modal.get()};
auto* delBtn {modal->bindNew<Wt::WPushButton>("del-btn", Wt::WString::tr("Lms.delete"))}; bindNew<Wt::WPushButton>("download", Wt::WString::tr("Lms.Explore.download"))
delBtn->clicked().connect([=] ->setLink(Wt::WLink{ std::make_unique<DownloadTrackListResource>(*trackListId) });
{
{
auto transaction {LmsApp->getDbSession().createUniqueTransaction()};
Database::TrackList::pointer trackList {Database::TrackList::find(LmsApp->getDbSession(), *trackListId)}; bindNew<Wt::WPushButton>("delete", Wt::WString::tr("Lms.delete"))
if (trackList) ->clicked().connect([=]
trackList.remove(); {
} auto modal{ std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.TrackList.template.delete-tracklist")) };
modal->addFunction("tr", &Wt::WTemplate::Functions::tr);
Wt::WWidget* modalPtr{ modal.get() };
clear(); auto* delBtn{ modal->bindNew<Wt::WPushButton>("del-btn", Wt::WString::tr("Lms.delete")) };
trackListDeleted.emit(*trackListId); delBtn->clicked().connect([=]
LmsApp->setInternalPath("/tracklists", true); {
{
auto transaction{ LmsApp->getDbSession().createUniqueTransaction() };
LmsApp->getModalManager().dispose(modalPtr); Database::TrackList::pointer trackList{ Database::TrackList::find(LmsApp->getDbSession(), *trackListId) };
}); if (trackList)
trackList.remove();
}
auto* cancelBtn {modal->bindNew<Wt::WPushButton>("cancel-btn", Wt::WString::tr("Lms.cancel"))}; clear();
cancelBtn->clicked().connect([=] trackListDeleted.emit(*trackListId);
{ LmsApp->setInternalPath("/tracklists", true);
LmsApp->getModalManager().dispose(modalPtr);
});
LmsApp->getModalManager().show(std::move(modal)); LmsApp->getModalManager().dispose(modalPtr);
}); });
_container = bindNew<InfiniteScrollingContainer>("tracks", Wt::WString::tr("Lms.Explore.TrackList.template.entry-container")); auto* cancelBtn{ modal->bindNew<Wt::WPushButton>("cancel-btn", Wt::WString::tr("Lms.cancel")) };
_container->onRequestElements.connect([this] cancelBtn->clicked().connect([=]
{ {
addSome(); LmsApp->getModalManager().dispose(modalPtr);
}); });
}
void LmsApp->getModalManager().show(std::move(modal));
TrackList::addSome() });
{
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
Database::Track::FindParameters params; _container = bindNew<InfiniteScrollingContainer>("tracks", Wt::WString::tr("Lms.Explore.TrackList.template.entry-container"));
params.setClusters(_filters.getClusterIds()); _container->onRequestElements.connect([this]
params.setTrackList(_trackListId); {
params.setSortMethod(Database::TrackSortMethod::TrackList); addSome();
params.setRange({static_cast<std::size_t>(_container->getCount()), _batchSize}); });
params.setDistinct(false); }
const auto trackIds {Database::Track::find(LmsApp->getDbSession(), params)}; void TrackList::addSome()
for (const TrackId trackId : trackIds.results) {
{ auto transaction{ LmsApp->getDbSession().createSharedTransaction() };
if (const Track::pointer track {Track::find(LmsApp->getDbSession(), trackId)})
_container->add(TrackListHelpers::createEntry(track, _playQueueController, _filters)); Database::Track::FindParameters params;
} params.setClusters(_filters.getClusterIds());
} params.setTrackList(_trackListId);
params.setSortMethod(Database::TrackSortMethod::TrackList);
params.setRange({ static_cast<std::size_t>(_container->getCount()), _batchSize });
params.setDistinct(false);
for (const Track::pointer& track : Database::Track::find(LmsApp->getDbSession(), params).results)
_container->add(TrackListHelpers::createEntry(track, _playQueueController, _filters));
}
} // namespace UserInterface } // namespace UserInterface
+165 -198
View File
@@ -19,10 +19,6 @@
#include "DownloadResource.hpp" #include "DownloadResource.hpp"
#include <array>
#include <iostream>
#include <iomanip>
#include <Wt/Http/Response.h> #include <Wt/Http/Response.h>
#include <Wt/WDateTime.h> #include <Wt/WDateTime.h>
@@ -38,243 +34,214 @@
#define LOG(level) LMS_LOG(UI, level) << "Download resource: " #define LOG(level) LMS_LOG(UI, level) << "Download resource: "
namespace UserInterface { namespace UserInterface
DownloadResource::~DownloadResource()
{ {
beingDeleted();
}
void DownloadResource::~DownloadResource()
DownloadResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response) {
{ beingDeleted();
try }
{
std::shared_ptr<Zip::IZipper> zipper;
// First, see if this request is for a continuation void DownloadResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response)
if (Wt::Http::ResponseContinuation *continuation {request.continuation()}) {
zipper = Wt::cpp17::any_cast<std::shared_ptr<Zip::IZipper>>(continuation->data()); try
else {
{ std::shared_ptr<Zip::IZipper> zipper;
zipper = createZipper();
response.setMimeType("application/zip");
}
zipper->writeSome(response.out()); // First, see if this request is for a continuation
if (!zipper->isComplete()) if (Wt::Http::ResponseContinuation * continuation{ request.continuation() })
{ zipper = Wt::cpp17::any_cast<std::shared_ptr<Zip::IZipper>>(continuation->data());
auto* continuation {response.createContinuation()}; else
continuation->setData(zipper); {
} zipper = createZipper();
} response.setMimeType("application/zip");
catch (Zip::Exception& exception) }
{
LOG(ERROR) << "Zipper exception: " << exception.what();
}
}
static zipper->writeSome(response.out());
std::string if (!zipper->isComplete())
getArtistPathName(Database::Artist::pointer artist) {
{ auto* continuation{ response.createContinuation() };
return StringUtils::replaceInString(artist->getName(), "/", "_"); continuation->setData(zipper);
} }
}
catch (Zip::Exception& exception)
{
LOG(ERROR) << "Zipper exception: " << exception.what();
}
}
static namespace
std::string {
getReleaseArtistPathName(Database::Release::pointer release) std::string getArtistPathName(Database::Artist::pointer artist)
{ {
std::string releaseArtistName; return StringUtils::replaceInString(artist->getName(), "/", "_");
}
std::vector<Database::ObjectPtr<Database::Artist>> artists; std::string getReleaseArtistPathName(Database::Release::pointer release)
{
std::string releaseArtistName;
artists = release->getReleaseArtists(); std::vector<Database::ObjectPtr<Database::Artist>> artists;
if (artists.empty())
artists = release->getArtists();
if (artists.size() > 1) artists = release->getReleaseArtists();
releaseArtistName = Wt::WString::tr("Lms.Explore.various-artists").toUTF8(); if (artists.empty())
else if (artists.size() == 1) artists = release->getArtists();
releaseArtistName = artists.front()->getName();
releaseArtistName = StringUtils::replaceInString(releaseArtistName, "/", "_"); if (artists.size() > 1)
releaseArtistName = Wt::WString::tr("Lms.Explore.various-artists").toUTF8();
else if (artists.size() == 1)
releaseArtistName = artists.front()->getName();
return releaseArtistName; releaseArtistName = StringUtils::replaceInString(releaseArtistName, "/", "_");
}
static return releaseArtistName;
std::string }
getReleasePathName(Database::Release::pointer release)
{
std::string releaseName;
if (const Wt::WDate releaseDate {release->getReleaseDate()}; releaseDate.isValid()) std::string getReleasePathName(Database::Release::pointer release)
releaseName += std::to_string(releaseDate.year()) + " - "; {
releaseName += StringUtils::replaceInString(release->getName(), "/", "_"); std::string releaseName;
return releaseName; if (const Wt::WDate releaseDate{ release->getReleaseDate() }; releaseDate.isValid())
} releaseName += std::to_string(releaseDate.year()) + " - ";
releaseName += StringUtils::replaceInString(release->getName(), "/", "_");
namespace details return releaseName;
{ }
static }
std::string
getTrackPathName(Database::Track::pointer track)
{
std::ostringstream fileName;
if (auto discNumber {track->getDiscNumber()}) namespace details
fileName << *discNumber << "."; {
if (auto trackNumber {track->getTrackNumber()}) std::string getTrackPathName(Database::Track::pointer track)
fileName << std::setw(2) << std::setfill('0') << *trackNumber << " - "; {
std::ostringstream fileName;
fileName << StringUtils::replaceInString(track->getName(), "/", "_") << track->getPath().filename().extension().string(); if (auto discNumber{ track->getDiscNumber() })
fileName << *discNumber << ".";
if (auto trackNumber{ track->getTrackNumber() })
fileName << std::setw(2) << std::setfill('0') << *trackNumber << " - ";
return fileName.str(); fileName << StringUtils::replaceInString(track->getName(), "/", "_") << track->getPath().filename().extension().string();
}
static return fileName.str();
std::string }
getTrackListPathName(Database::TrackList::pointer trackList)
{
return StringUtils::replaceInString(trackList->getName(), "/", "_");
}
static std::string getTrackListPathName(Database::TrackList::pointer trackList)
std::unique_ptr<Zip::IZipper> {
createZipper(const std::vector<Database::Track::pointer>& tracks) return StringUtils::replaceInString(trackList->getName(), "/", "_");
{ }
if (tracks.empty())
return {};
Zip::EntryContainer files; std::unique_ptr<Zip::IZipper> createZipper(const std::vector<Database::Track::pointer>& tracks)
for (const Database::Track::pointer& track : tracks) {
{ if (tracks.empty())
std::string releaseName; return {};
std::string releaseArtistName;
if (auto release {track->getRelease()})
{
releaseName = getReleasePathName(release);
releaseArtistName = getReleaseArtistPathName(release);
}
std::string fileName; Zip::EntryContainer files;
if (!releaseArtistName.empty()) for (const Database::Track::pointer& track : tracks)
fileName += releaseArtistName + "/"; {
if (!releaseName.empty()) std::string releaseName;
fileName += releaseName + "/"; std::string releaseArtistName;
fileName += getTrackPathName(track); if (auto release{ track->getRelease() })
{
releaseName = getReleasePathName(release);
releaseArtistName = getReleaseArtistPathName(release);
}
files.emplace_back(Zip::Entry {fileName, track->getPath()}); std::string fileName;
} if (!releaseArtistName.empty())
fileName += releaseArtistName + "/";
if (!releaseName.empty())
fileName += releaseName + "/";
fileName += getTrackPathName(track);
return Zip::createArchiveZipper(files); files.emplace_back(Zip::Entry{ fileName, track->getPath() });
} }
}
DownloadArtistResource::DownloadArtistResource(Database::ArtistId artistId) return Zip::createArchiveZipper(files);
: _artistId {artistId} }
{ }
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
Database::Artist::pointer artist {Database::Artist::find(LmsApp->getDbSession(), artistId)}; DownloadArtistResource::DownloadArtistResource(Database::ArtistId artistId)
if (artist) : _artistId{ artistId }
suggestFileName(getArtistPathName(artist) + ".zip"); {
} auto transaction{ LmsApp->getDbSession().createSharedTransaction() };
std::unique_ptr<Zip::IZipper> Database::Artist::pointer artist{ Database::Artist::find(LmsApp->getDbSession(), artistId) };
DownloadArtistResource::createZipper() if (artist)
{ suggestFileName(getArtistPathName(artist) + ".zip");
auto transaction {LmsApp->getDbSession().createSharedTransaction()}; }
const auto trackResults {Database::Track::find(LmsApp->getDbSession(), Database::Track::FindParameters {}.setArtist(_artistId).setSortMethod(Database::TrackSortMethod::DateDescAndRelease))}; std::unique_ptr<Zip::IZipper> DownloadArtistResource::createZipper()
std::vector<Database::Track::pointer> tracks; {
tracks.reserve(trackResults.results.size()); auto transaction{ LmsApp->getDbSession().createSharedTransaction() };
for (const Database::TrackId trackId : trackResults.results) const auto trackResults{ Database::Track::find(LmsApp->getDbSession(), Database::Track::FindParameters {}.setArtist(_artistId).setSortMethod(Database::TrackSortMethod::DateDescAndRelease)) };
tracks.push_back(Database::Track::find(LmsApp->getDbSession(), trackId)); return details::createZipper(trackResults.results);
}
return details::createZipper(tracks); DownloadReleaseResource::DownloadReleaseResource(Database::ReleaseId releaseId)
} : _releaseId{ releaseId }
{
auto transaction{ LmsApp->getDbSession().createSharedTransaction() };
DownloadReleaseResource::DownloadReleaseResource(Database::ReleaseId releaseId) Database::Release::pointer release{ Database::Release::find(LmsApp->getDbSession(), releaseId) };
: _releaseId {releaseId} if (release)
{ suggestFileName(getReleasePathName(release) + ".zip");
auto transaction {LmsApp->getDbSession().createSharedTransaction()}; }
Database::Release::pointer release {Database::Release::find(LmsApp->getDbSession(), releaseId)};
if (release)
suggestFileName(getReleasePathName(release) + ".zip");
}
std::unique_ptr<Zip::IZipper> std::unique_ptr<Zip::IZipper> DownloadReleaseResource::createZipper()
DownloadReleaseResource::createZipper() {
{ using namespace Database;
using namespace Database;
auto transaction {LmsApp->getDbSession().createSharedTransaction()}; auto transaction{ LmsApp->getDbSession().createSharedTransaction() };
auto trackResults {Track::find(LmsApp->getDbSession(), Track::FindParameters {}.setRelease(_releaseId).setSortMethod(TrackSortMethod::Release))}; auto tracks{ Track::find(LmsApp->getDbSession(), Track::FindParameters {}.setRelease(_releaseId).setSortMethod(TrackSortMethod::Release)) };
return details::createZipper(tracks.results);
}
std::vector<Track::pointer> tracks; DownloadTrackResource::DownloadTrackResource(Database::TrackId trackId)
tracks.reserve(trackResults.results.size()); : _trackId{ trackId }
std::transform(std::cbegin(trackResults.results), std::cend(trackResults.results), std::back_inserter(tracks), [](TrackId trackId){ return Track::find(LmsApp->getDbSession(), trackId); }); {
auto transaction{ LmsApp->getDbSession().createSharedTransaction() };
return details::createZipper(tracks); Database::Track::pointer track{ Database::Track::find(LmsApp->getDbSession(), trackId) };
} if (track)
suggestFileName(details::getTrackPathName(track) + ".zip");
}
DownloadTrackResource::DownloadTrackResource(Database::TrackId trackId) std::unique_ptr<Zip::IZipper> DownloadTrackResource::createZipper()
: _trackId {trackId} {
{ auto transaction{ LmsApp->getDbSession().createSharedTransaction() };
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
Database::Track::pointer track {Database::Track::find(LmsApp->getDbSession(), trackId)}; const Database::Track::pointer track{ Database::Track::find(LmsApp->getDbSession(), _trackId) };
if (track) if (!track)
suggestFileName(details::getTrackPathName(track) + ".zip"); {
} LOG(DEBUG) << "Cannot find track";
return {};
}
std::unique_ptr<Zip::IZipper> return details::createZipper({ track });
DownloadTrackResource::createZipper() }
{
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
const Database::Track::pointer track {Database::Track::find(LmsApp->getDbSession(), _trackId)}; DownloadTrackListResource::DownloadTrackListResource(Database::TrackListId trackListId)
if (!track) : _trackListId{ trackListId }
{ {
LOG(DEBUG) << "Cannot find track"; auto transaction{ LmsApp->getDbSession().createSharedTransaction() };
return {};
}
return details::createZipper({track}); const Database::TrackList::pointer trackList{ Database::TrackList::find(LmsApp->getDbSession(), trackListId) };
} if (trackList)
suggestFileName(details::getTrackListPathName(trackList) + ".zip");
}
DownloadTrackListResource::DownloadTrackListResource(Database::TrackListId trackListId) std::unique_ptr<Zip::IZipper> DownloadTrackListResource::createZipper()
: _trackListId {trackListId} {
{ using namespace Database;
auto transaction {LmsApp->getDbSession().createSharedTransaction()}; auto transaction{ LmsApp->getDbSession().createSharedTransaction() };
const Database::TrackList::pointer trackList {Database::TrackList::find(LmsApp->getDbSession(), trackListId)}; Track::FindParameters params;
if (trackList) params.setTrackList(_trackListId);
suggestFileName(details::getTrackListPathName(trackList) + ".zip"); const auto tracks{ Track::find(LmsApp->getDbSession(), params) };
} return details::createZipper(tracks.results);
}
std::unique_ptr<Zip::IZipper>
DownloadTrackListResource::createZipper()
{
using namespace Database;
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
Track::FindParameters params;
params.setTrackList(_trackListId);
const auto trackResults {Track::find(LmsApp->getDbSession(), params)};
std::vector<Track::pointer> tracks;
tracks.reserve(trackResults.results.size());
std::transform(std::cbegin(trackResults.results), std::cend(trackResults.results), std::back_inserter(tracks), [](TrackId trackId){ return Track::find(LmsApp->getDbSession(), trackId); });
return details::createZipper(tracks);
}
} // namespace UserInterface } // namespace UserInterface
+1 -1
View File
@@ -43,7 +43,7 @@ dumpTrackCovers(Database::Session& session, Image::ImageSize width)
RangeResults<Database::TrackId> trackIds; RangeResults<Database::TrackId> trackIds;
{ {
auto transaction {session.createSharedTransaction()}; auto transaction {session.createSharedTransaction()};
trackIds = Database::Track::find(session, Database::Track::FindParameters {}); trackIds = Database::Track::findIds(session, Database::Track::FindParameters {});
} }
for (const Database::TrackId trackId : trackIds.results) for (const Database::TrackId trackId : trackIds.results)
+4 -11
View File
@@ -38,14 +38,12 @@
using namespace Database; using namespace Database;
static static void dumpTracksRecommendation(Session session, Recommendation::IRecommendationService& recommendationService, unsigned maxSimilarityCount)
void
dumpTracksRecommendation(Session session, Recommendation::IRecommendationService& recommendationService, unsigned maxSimilarityCount)
{ {
const RangeResults<TrackId> trackIds{ [&] const RangeResults<TrackId> trackIds{ [&]
{ {
auto transaction {session.createSharedTransaction()}; auto transaction {session.createSharedTransaction()};
return Track::find(session, Track::FindParameters {}); return Track::findIds(session, Track::FindParameters{});
}() }; }() };
std::cout << "*** Tracks (" << trackIds.results.size() << ") ***" << std::endl; std::cout << "*** Tracks (" << trackIds.results.size() << ") ***" << std::endl;
@@ -74,9 +72,7 @@ dumpTracksRecommendation(Session session, Recommendation::IRecommendationService
} }
} }
static static void dumpReleasesRecommendation(Session session, Recommendation::IRecommendationService& recommendationService, unsigned maxSimilarityCount)
void
dumpReleasesRecommendation(Session session, Recommendation::IRecommendationService& recommendationService, unsigned maxSimilarityCount)
{ {
const RangeResults<ReleaseId> releaseIds{ std::invoke([&] const RangeResults<ReleaseId> releaseIds{ std::invoke([&]
{ {
@@ -101,9 +97,7 @@ dumpReleasesRecommendation(Session session, Recommendation::IRecommendationServi
} }
} }
static static void dumpArtistsRecommendation(Session session, Recommendation::IRecommendationService& recommendationService, unsigned maxSimilarityCount)
void
dumpArtistsRecommendation(Session session, Recommendation::IRecommendationService& recommendationService, unsigned maxSimilarityCount)
{ {
const RangeResults<ArtistId> artistIds = std::invoke([&]() const RangeResults<ArtistId> artistIds = std::invoke([&]()
{ {
@@ -130,7 +124,6 @@ dumpArtistsRecommendation(Session session, Recommendation::IRecommendationServic
} }
} }
int main(int argc, char* argv[]) int main(int argc, char* argv[])
{ {
try try