diff --git a/approot/messages.xml b/approot/messages.xml index cdaba83b..c7c44486 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -85,6 +85,7 @@ Scanning: step {1}/{2} Checking for duplicate files... {1} files Checking files... {1}% +Computing stats... {1}% Discovering files: {1} files Fetching track features from AcousticBrainz: {1}/{2} tracks ({3}%)... Reloading similarity engine: {1}%... @@ -244,9 +245,10 @@ ListenBrainz ListenBrainz API token Change password -Default transcode bitrate -Default transcode format +Default transcoding output bitrate +Default transcoding output format Cannot save using a demo account! +Enable transcoding by default Feedback Settings Bad password @@ -266,18 +268,18 @@ Album artists Track artists Subsonic API -Transcoding -Transcode bitrate -Transcode format -Matroska/Opus -MP3 -Ogg/Opus -Ogg/Vorbis -WebM/Vorbis -Enable transcoding -Always -Only when format is not supported -Never +Transcoding +Transcoding output bitrate +Transcoding output format +Matroska/Opus +MP3 +Ogg/Opus +Ogg/Vorbis +WebM/Vorbis +Enable transcoding +Always +Only when format is not supported by the browser +Never New settings saved! diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml index c67ab668..769bc1e9 100644 --- a/approot/messages_fr.xml +++ b/approot/messages_fr.xml @@ -85,6 +85,7 @@ En cours de scan : étape {1}/{2} Vérification des fichiers dupliqués... {1} fichiers Vérification des fichiers... {1}% +Calcul des statistiques... {1}% Découverte des fichiers : {1} fichiers Récupération des métadonnées AcousticBrainz : {1}/{2} fichiers ({3}%)... Rechargement du moteur de recommandation : {1}%... @@ -244,9 +245,10 @@ ListenBrainz Jeton d'API ListenBrainz Changement de mot de passe -Bitrate par defaut du transcodage -Format par defaut du transcodage +Bitrate par défaut du transcodage +Format par défaut du transcodage Impossible de sauvegarder en utilisant un compte de démo ! +Activer par défaut le transcodage Feedback Paramètres Mauvais mot de passe @@ -266,18 +268,18 @@ Tous les artistes d'album Tous les artistes de piste API Subsonic -Transcodage -Bitrate du transcodage -Format du transcodage -Matroska/Opus -MP3 -Ogg/Opus -Ogg/Vorbis -WebM/Vorbis -Appliquer le transcodage -Toujours -Seulement lorsque le format n'est pas pris en charge -Jamais +Transcodage +Bitrate du transcodage +Format du transcodage +Matroska/Opus +MP3 +Ogg/Opus +Ogg/Vorbis +WebM/Vorbis +Appliquer le transcodage +Toujours +Seulement lorsque le format n'est pas pris en charge par votre navigateur +Jamais Paramètres sauvegardés ! diff --git a/approot/messages_it.xml b/approot/messages_it.xml index 518fc9b6..92d4bcfc 100644 --- a/approot/messages_it.xml +++ b/approot/messages_it.xml @@ -85,6 +85,7 @@ Scansione: passo {1}/{2} Controllo file... {1}% + File trovati: {1} files Recupero metadati da AcousticBrainz: {1}/{2} tracce ({3}%)... Ricarica motore di tracce simili: {1}%... @@ -248,6 +249,7 @@ Non posso salvare le impostazioni utilizzando l'account demo! + Impostazioni Password errata Devi inserire la password attuale @@ -266,18 +268,18 @@ Artisti album Artisti tracce Subsonic API -Transcodifica -Bitrate transcodifica -Formato di transcodifica -Matroska/Opus -MP3 -Ogg/Opus -Ogg/Vorbis -WebM/Vorbis -Abilita transcodifica -Sempre -Quando il formato non è supportato -Mai +Transcodifica +Bitrate transcodifica +Formato di transcodifica +Matroska/Opus +MP3 +Ogg/Opus +Ogg/Vorbis +WebM/Vorbis +Abilita transcodifica +Sempre +Quando il formato non è supportato +Mai Nuove impostazioni salvate! diff --git a/approot/messages_zh.xml b/approot/messages_zh.xml index 9442202c..17c96ec7 100644 --- a/approot/messages_zh.xml +++ b/approot/messages_zh.xml @@ -85,6 +85,7 @@ 扫描中: 阶段 {1}/{2} 检查文件中... {1}% + 检索文件中: {1} 文件 从 AcousticBrainz 获取音轨特征: {1}/{2} 音轨 ({3}%)... 重载相似引擎中 {1}%... @@ -248,6 +249,7 @@ 使用演示账号时不能保存! + 设置 无效密码 必须填写旧密码 @@ -266,18 +268,18 @@ 专辑歌手 音轨歌手 Subsonic 应用程序接口 -转码 -转码比特率 -转码格式 -Matroska/Opus -MP3 -Ogg/Opus -Ogg/Vorbis -WebM/Vorbis -启动转码 -总是 -仅当格式不支持时使用 -从不 +转码 +转码比特率 +转码格式 +Matroska/Opus +MP3 +Ogg/Opus +Ogg/Vorbis +WebM/Vorbis +启动转码 +总是 +仅当格式不支持时使用 +从不 新设置已保存! diff --git a/approot/settings.xml b/approot/settings.xml index 507ec686..dbf53bf5 100644 --- a/approot/settings.xml +++ b/approot/settings.xml @@ -10,33 +10,33 @@ ${tr:Lms.Settings.audio-settings-are-local}
-
-
-
@@ -74,25 +74,36 @@
${} ${tr:Lms.Settings.subsonic-api} -
- - ${subsonic-transcode-format class="form-control"} -
- ${subsonic-transcode-format-info class="help-block"} +
+
+ ${subsonic-enable-transcoding-by-default class="form-check-input"} + +
+ ${subsonic-enable-transcoding-by-default-info} +
-
+
+
- ${subsonic-transcode-bitrate class="form-control"} + ${subsonic-transcoding-output-bitrate class="form-control"} kbps
- ${subsonic-transcode-bitrate-info class="help-block"} + ${subsonic-transcoding-output-bitrate-info class="help-block"}
diff --git a/conf/lms.conf b/conf/lms.conf index 07e7943f..02985108 100644 --- a/conf/lms.conf +++ b/conf/lms.conf @@ -61,7 +61,10 @@ api-subsonic = true; # Use this list to make the reported server version to 1.12.0 depending on the client's name # Main usage is to make auto detections for the 'p' (password) parameter work -api-subsonic-report-old-server-protocol = ("DSub"); +api-subsonic-old-server-protocol-clients = ("DSub"); + +# List of clients for whom a default cover is served (as they do not have their own) +api-subsonic-default-cover-clients = ("DSub", "substreamer"); # List of clients for whom open subsonic extensions and extra fields are disabled api-open-subsonic-disabled-clients = ("DSub"); diff --git a/docroot/js/mediaplayer.js b/docroot/js/mediaplayer.js index 2a345e78..b5dfdeb1 100644 --- a/docroot/js/mediaplayer.js +++ b/docroot/js/mediaplayer.js @@ -2,15 +2,15 @@ var LMS = LMS || {}; -// Keep in sync with MediaPlayer::TranscodeMode cpp -const TranscodeMode = { +// Keep in sync with MediaPlayer::TranscodingMode cpp +const TranscodingMode = { Never: 0, Always: 1, IfFormatNotSupported: 2, } const Mode = { - Transcode: 1, + Transcoding: 1, File: 2, } Object.freeze(Mode); @@ -22,7 +22,7 @@ LMS.mediaplayer = function () { let _trackId = null; let _duration = 0; let _audioNativeSrc; - let _audioTranscodeSrc; + let _audioTranscodingSrc; let _settings = {}; let _playedDuration = 0; let _lastStartPlaying = null; @@ -236,10 +236,10 @@ LMS.mediaplayer = function () { let selectedOffset = parseInt(_elems.seek.value, 10); switch (mode) { - case Mode.Transcode: + case Mode.Transcoding: _offset = selectedOffset; _removeAudioSources(); - _addAudioSource(_audioTranscodeSrc + "&offset=" + _offset); + _addAudioSource(_audioTranscodingSrc + "&offset=" + _offset); _elems.audio.load(); _elems.audio.currentTime = 0; _playTrack(); @@ -270,7 +270,7 @@ LMS.mediaplayer = function () { }); _elems.audio.addEventListener("canplay", function() { - if (_getAudioMode() == Mode.Transcode) { + if (_getAudioMode() == Mode.Transcoding) { _elems.transcodingActive.style.display = "inline"; } else { @@ -337,7 +337,7 @@ LMS.mediaplayer = function () { let _getAudioMode = function() { if (_elems.audio.currentSrc) { if (_elems.audio.currentSrc.includes("format")) - return Mode.Transcode; + return Mode.Transcoding; else return Mode.File; } @@ -353,19 +353,19 @@ LMS.mediaplayer = function () { _offset = 0; _duration = params.duration; _audioNativeSrc = params.nativeResource; - _audioTranscodeSrc = params.transcodeResource + "&bitrate=" + _settings.transcode.bitrate + "&format=" + _settings.transcode.format; + _audioTranscodingSrc = params.transcodingResource + "&bitrate=" + _settings.transcoding.bitrate + "&format=" + _settings.transcoding.format; _elems.seek.max = _duration; _removeAudioSources(); // ! order is important - if (_settings.transcode.mode == TranscodeMode.Never || _settings.transcode.mode == TranscodeMode.IfFormatNotSupported) + if (_settings.transcoding.mode == TranscodingMode.Never || _settings.transcoding.mode == TranscodingMode.IfFormatNotSupported) { _addAudioSource(_audioNativeSrc); } - if (_settings.transcode.mode == TranscodeMode.Always || _settings.transcode.mode == TranscodeMode.IfFormatNotSupported) + if (_settings.transcoding.mode == TranscodingMode.Always || _settings.transcoding.mode == TranscodingMode.IfFormatNotSupported) { - _addAudioSource(_audioTranscodeSrc); + _addAudioSource(_audioTranscodingSrc); } _elems.audio.load(); diff --git a/src/libs/av/CMakeLists.txt b/src/libs/av/CMakeLists.txt index 4205e35c..fee1201e 100644 --- a/src/libs/av/CMakeLists.txt +++ b/src/libs/av/CMakeLists.txt @@ -1,9 +1,9 @@ add_library(lmsav SHARED impl/AudioFile.cpp + impl/RawResourceHandlerCreator.cpp impl/Transcoder.cpp - impl/TranscodeResourceHandler.cpp - impl/Types.cpp + impl/TranscodingResourceHandler.cpp ) target_include_directories(lmsav INTERFACE diff --git a/src/libs/av/impl/AudioFile.cpp b/src/libs/av/impl/AudioFile.cpp index fa730e72..346fe074 100644 --- a/src/libs/av/impl/AudioFile.cpp +++ b/src/libs/av/impl/AudioFile.cpp @@ -32,267 +32,295 @@ extern "C" #include "utils/Logger.hpp" #include "utils/String.hpp" -namespace Av { - -static std::string averror_to_string(int error) +namespace Av { - std::array buf = {0}; + namespace + { + std::string averror_to_string(int error) + { + std::array buf = { 0 }; - if (::av_strerror(error, buf.data(), buf.size()) == 0) - return &buf[0]; - else - return "Unknown error"; -} + if (::av_strerror(error, buf.data(), buf.size()) == 0) + return &buf[0]; + else + return "Unknown error"; + } -class AudioFileException : public Av::Exception -{ - public: - AudioFileException(int avError) - : Av::Exception {"AudioFileException: " + averror_to_string(avError)} - {} -}; + class AudioFileException : public Av::Exception + { + public: + AudioFileException(int avError) + : Av::Exception{ "AudioFileException: " + averror_to_string(avError) } + {} + }; -std::unique_ptr -parseAudioFile(const std::filesystem::path& p) -{ - return std::make_unique(p); -} + void getMetaDataFromDictionnary(AVDictionary* dictionnary, AudioFile::MetadataMap& res) + { + if (!dictionnary) + return; -AudioFile::AudioFile(const std::filesystem::path& p) -: _p {p} -{ - int error {avformat_open_input(&_context, _p.string().c_str(), nullptr, nullptr)}; - if (error < 0) - { - LMS_LOG(AV, ERROR) << "Cannot open " << _p.string() << ": " << averror_to_string(error); - throw AudioFileException {error}; - } + AVDictionaryEntry* tag = NULL; + while ((tag = ::av_dict_get(dictionnary, "", tag, AV_DICT_IGNORE_SUFFIX))) + { + res[StringUtils::stringToUpper(tag->key)] = tag->value; + } + } - error = avformat_find_stream_info(_context, nullptr); - if (error < 0) - { - LMS_LOG(AV, ERROR) << "Cannot find stream information on " << _p.string() << ": " << averror_to_string(error); - avformat_close_input(&_context); - throw AudioFileException {error}; - } -} + DecodingCodec avcodecToDecodingCodec(AVCodecID codec) + { + switch (codec) + { + case AV_CODEC_ID_MP3: return DecodingCodec::MP3; + case AV_CODEC_ID_AAC: return DecodingCodec::AAC; + case AV_CODEC_ID_AC3: return DecodingCodec::AC3; + case AV_CODEC_ID_VORBIS: return DecodingCodec::VORBIS; + case AV_CODEC_ID_WMAV1: return DecodingCodec::WMAV1; + case AV_CODEC_ID_WMAV2: return DecodingCodec::WMAV2; + case AV_CODEC_ID_FLAC: return DecodingCodec::FLAC; + case AV_CODEC_ID_ALAC: return DecodingCodec::ALAC; + case AV_CODEC_ID_WAVPACK: return DecodingCodec::WAVPACK; + case AV_CODEC_ID_MUSEPACK7: return DecodingCodec::MUSEPACK7; + case AV_CODEC_ID_MUSEPACK8: return DecodingCodec::MUSEPACK8; + case AV_CODEC_ID_APE: return DecodingCodec::APE; + case AV_CODEC_ID_EAC3: return DecodingCodec::EAC3; + case AV_CODEC_ID_MP4ALS: return DecodingCodec::MP4ALS; + case AV_CODEC_ID_OPUS: return DecodingCodec::OPUS; + case AV_CODEC_ID_SHORTEN: return DecodingCodec::SHORTEN; + default: + return DecodingCodec::UNKNOWN; + } + } + } -AudioFile::~AudioFile() -{ - avformat_close_input(&_context); -} + std::unique_ptr parseAudioFile(const std::filesystem::path& p) + { + return std::make_unique(p); + } -const std::filesystem::path& -AudioFile::getPath() const -{ - return _p; -} + AudioFile::AudioFile(const std::filesystem::path& p) + : _p{ p } + { + int error{ avformat_open_input(&_context, _p.string().c_str(), nullptr, nullptr) }; + if (error < 0) + { + LMS_LOG(AV, ERROR) << "Cannot open " << _p.string() << ": " << averror_to_string(error); + throw AudioFileException{ error }; + } -std::chrono::milliseconds -AudioFile::getDuration() const -{ - if (_context->duration == AV_NOPTS_VALUE) - return std::chrono::milliseconds {0}; // TODO estimate + error = avformat_find_stream_info(_context, nullptr); + if (error < 0) + { + LMS_LOG(AV, ERROR) << "Cannot find stream information on " << _p.string() << ": " << averror_to_string(error); + avformat_close_input(&_context); + throw AudioFileException{ error }; + } + } - return std::chrono::milliseconds {_context->duration / AV_TIME_BASE * 1000}; -} + AudioFile::~AudioFile() + { + avformat_close_input(&_context); + } -void -getMetaDataFromDictionnary(AVDictionary* dictionnary, AudioFile::MetadataMap& res) -{ - if (!dictionnary) - return; + const std::filesystem::path& AudioFile::getPath() const + { + return _p; + } - AVDictionaryEntry *tag = NULL; - while ((tag = ::av_dict_get(dictionnary, "", tag, AV_DICT_IGNORE_SUFFIX))) - { - res[StringUtils::stringToUpper(tag->key)] = tag->value; - } -} + ContainerInfo AudioFile::getContainerInfo() const + { + ContainerInfo info; + info.bitrate = _context->bit_rate; + info.duration = std::chrono::milliseconds{ _context->duration == AV_NOPTS_VALUE ? 0 : _context->duration / AV_TIME_BASE * 1000 }; + info.name = _context->iformat->name; -AudioFile::MetadataMap -AudioFile::getMetaData() const -{ - MetadataMap res; + return info; + } - getMetaDataFromDictionnary(_context->metadata, res); + AudioFile::MetadataMap AudioFile::getMetaData() const + { + MetadataMap res; - // HACK for OGG files - // If we did not find tags, search metadata in streams - if (res.empty()) - { - for (std::size_t i {}; i < _context->nb_streams; ++i) - { - getMetaDataFromDictionnary(_context->streams[i]->metadata, res); + getMetaDataFromDictionnary(_context->metadata, res); - if (!res.empty()) - break; - } - } + // HACK for OGG files + // If we did not find tags, search metadata in streams + if (res.empty()) + { + for (std::size_t i{}; i < _context->nb_streams; ++i) + { + getMetaDataFromDictionnary(_context->streams[i]->metadata, res); - return res; -} + if (!res.empty()) + break; + } + } -std::vector -AudioFile::getStreamInfo() const -{ - std::vector res; + return res; + } - for (std::size_t i {}; i < _context->nb_streams; ++i) - { - std::optional streamInfo {getStreamInfo(i)}; - if (streamInfo) - res.emplace_back(std::move(*streamInfo)); - } + std::vector AudioFile::getStreamInfo() const + { + std::vector res; - return res; -} + for (std::size_t i{}; i < _context->nb_streams; ++i) + { + std::optional streamInfo{ getStreamInfo(i) }; + if (streamInfo) + res.emplace_back(std::move(*streamInfo)); + } -std::optional -AudioFile::getBestStreamIndex() const -{ - int res = ::av_find_best_stream(_context, - AVMEDIA_TYPE_AUDIO, - -1, // Auto - -1, // Auto - NULL, - 0); + return res; + } - if (res < 0) - return std::nullopt; + std::optional AudioFile::getBestStreamIndex() const + { + int res = ::av_find_best_stream(_context, + AVMEDIA_TYPE_AUDIO, + -1, // Auto + -1, // Auto + NULL, + 0); - return res; -} + if (res < 0) + return std::nullopt; -std::optional -AudioFile::getBestStreamInfo() const -{ - std::optional res; + return res; + } - std::optional bestStreamIndex {getBestStreamIndex()}; - if (bestStreamIndex) - res = getStreamInfo(*bestStreamIndex); + std::optional AudioFile::getBestStreamInfo() const + { + std::optional res; - return res; -} + std::optional bestStreamIndex{ getBestStreamIndex() }; + if (bestStreamIndex) + res = getStreamInfo(*bestStreamIndex); -bool -AudioFile::hasAttachedPictures() const -{ - for (std::size_t i = 0; i < _context->nb_streams; ++i) - { - if (_context->streams[i]->disposition & AV_DISPOSITION_ATTACHED_PIC) - return true; - } + return res; + } - return false; -} + bool AudioFile::hasAttachedPictures() const + { + for (std::size_t i = 0; i < _context->nb_streams; ++i) + { + if (_context->streams[i]->disposition & AV_DISPOSITION_ATTACHED_PIC) + return true; + } -void -AudioFile::visitAttachedPictures(std::function func) const -{ - static const std::unordered_map codecMimeMap = - { - { AV_CODEC_ID_BMP, "image/x-bmp" }, - { AV_CODEC_ID_GIF, "image/gif" }, - { AV_CODEC_ID_MJPEG, "image/jpeg" }, - { AV_CODEC_ID_PNG, "image/png" }, - { AV_CODEC_ID_PNG, "image/x-png" }, - { AV_CODEC_ID_PPM, "image/x-portable-pixmap" }, - }; + return false; + } - for (std::size_t i = 0; i < _context->nb_streams; ++i) - { - AVStream *avstream = _context->streams[i]; + void AudioFile::visitAttachedPictures(std::function func) const + { + static const std::unordered_map codecMimeMap = + { + { AV_CODEC_ID_BMP, "image/x-bmp" }, + { AV_CODEC_ID_GIF, "image/gif" }, + { AV_CODEC_ID_MJPEG, "image/jpeg" }, + { AV_CODEC_ID_PNG, "image/png" }, + { AV_CODEC_ID_PNG, "image/x-png" }, + { AV_CODEC_ID_PPM, "image/x-portable-pixmap" }, + }; - // Skip attached pics - if (!(avstream->disposition & AV_DISPOSITION_ATTACHED_PIC)) - continue; + for (std::size_t i = 0; i < _context->nb_streams; ++i) + { + AVStream* avstream = _context->streams[i]; - if (avstream->codecpar == nullptr) - { - LMS_LOG(AV, ERROR) << "Skipping stream " << i << " since no codecpar is set"; - continue; - } + // Skip attached pics + if (!(avstream->disposition & AV_DISPOSITION_ATTACHED_PIC)) + continue; - Picture picture; + if (avstream->codecpar == nullptr) + { + LMS_LOG(AV, ERROR) << "Skipping stream " << i << " since no codecpar is set"; + continue; + } - auto itMime = codecMimeMap.find(avstream->codecpar->codec_id); - if (itMime != codecMimeMap.end()) - { - picture.mimeType = itMime->second; - } - else - { - picture.mimeType = "application/octet-stream"; - LMS_LOG(AV, ERROR) << "CODEC ID " << avstream->codecpar->codec_id << " not handled in mime type conversion"; - } + Picture picture; - const AVPacket& pkt {avstream->attached_pic}; + auto itMime = codecMimeMap.find(avstream->codecpar->codec_id); + if (itMime != codecMimeMap.end()) + { + picture.mimeType = itMime->second; + } + else + { + picture.mimeType = "application/octet-stream"; + LMS_LOG(AV, ERROR) << "CODEC ID " << avstream->codecpar->codec_id << " not handled in mime type conversion"; + } - picture.data = reinterpret_cast(pkt.data); - picture.dataSize = pkt.size; + const AVPacket& pkt{ avstream->attached_pic }; - func(picture); - } -} + picture.data = reinterpret_cast(pkt.data); + picture.dataSize = pkt.size; -std::optional -AudioFile::getStreamInfo(std::size_t streamIndex) const -{ - std::optional res; + func(picture); + } + } - AVStream* avstream { _context->streams[streamIndex]}; - assert(avstream); + std::optional AudioFile::getStreamInfo(std::size_t streamIndex) const + { + std::optional res; - if (avstream->disposition & AV_DISPOSITION_ATTACHED_PIC) - return res; + AVStream* avstream{ _context->streams[streamIndex] }; + assert(avstream); - if (!avstream->codecpar) - { - LMS_LOG(AV, ERROR) << "Skipping stream " << streamIndex << " since no codecpar is set"; - return res; - } + if (avstream->disposition & AV_DISPOSITION_ATTACHED_PIC) + return res; - if (avstream->codecpar->codec_type != AVMEDIA_TYPE_AUDIO) - return res; + if (!avstream->codecpar) + { + LMS_LOG(AV, ERROR) << "Skipping stream " << streamIndex << " since no codecpar is set"; + return res; + } - res.emplace(); - res->index = streamIndex; - res->bitrate = static_cast(avstream->codecpar->bit_rate); - res->codec = ::avcodec_get_name(avstream->codecpar->codec_id); - assert(!res->codec.empty()); + if (avstream->codecpar->codec_type != AVMEDIA_TYPE_AUDIO) + return res; - return res; -} + res.emplace(); + res->index = streamIndex; + res->bitrate = static_cast(avstream->codecpar->bit_rate); + res->codec = avcodecToDecodingCodec(avstream->codecpar->codec_id); + res->codecName = ::avcodec_get_name(avstream->codecpar->codec_id); + assert(!res->codecName.empty()); // doc says it is never NULL -std::optional -guessAudioFileFormat(const std::filesystem::path& file) -{ - const AVOutputFormat* format {::av_guess_format(NULL, file.string().c_str(), NULL)}; - if (!format || !format->name) - return {}; + return res; + } - LMS_LOG(AV, DEBUG) << "File '" << file.string() << "', formats = '" << format->name << "'"; + std::string_view getMimeType(const std::filesystem::path& fileExtension) + { + // List should be sync with the demuxers shipped in the lms's docker version + // + the _audioFileExtensions in ScanSettings + static const std::unordered_map entries + { + {".mp3", "audio/mpeg"}, + {".ogg", "audio/ogg"}, + {".oga", "audio/ogg"}, + {".opus", "audio/opus"}, + {".aac", "audio/aac"}, + {".alac", "audio/mp4"}, + {".m4a", "audio/mp4"}, + {".m4b", "audio/mp4"}, + {".flac", "audio/flac"}, + {".webm", "audio/webm"}, + {".wav", "audio/x-wav"}, + {".wma", "audio/x-ms-wma"}, + {".ape", "audio/x-monkeys-audio"}, + {".mpc", "audio/x-musepack"}, + {".shn", "audio/x-shn"}, + {".aif", "audio/x-aiff"}, + {".aiff", "audio/x-aiff"}, + {".m3u", "audio/x-mpegurl"}, + {".pls", "audio/x-scpls"}, + {".dsf", "audio/dsd"}, + {".wv", "audio/x-wavpack"}, + {".wvp", "audio/x-wavpack"}, + {".mka", "audio/x-matroska"}, + }; - auto formats {StringUtils::splitString(format->name, ",")}; - if (formats.size() > 1) - LMS_LOG(AV, INFO) << "File '" << file.string() << "' reported several formats: '" << format->name << "'"; - - std::vector mimeTypes; - if (format->mime_type) - mimeTypes = StringUtils::splitString(format->mime_type, ","); - - if (mimeTypes.empty()) - LMS_LOG(AV, INFO) << "File '" << file.string() << "', no mime type found!"; - else if (mimeTypes.size() > 1) - LMS_LOG(AV, INFO) << "File '" << file.string() << "' reported several mime types: '" << format->mime_type << "'"; - - AudioFileFormat res; - res.format = formats.front(); - res.mimeType = mimeTypes.empty() ? "application/octet-stream" : mimeTypes.front(); - - return res; -} + auto it{ entries.find(fileExtension) }; + if (it == std::cend(entries)) + return ""; + return it->second; + } } // namespace Av diff --git a/src/libs/av/impl/AudioFile.hpp b/src/libs/av/impl/AudioFile.hpp index 823b7e00..46e8f830 100644 --- a/src/libs/av/impl/AudioFile.hpp +++ b/src/libs/av/impl/AudioFile.hpp @@ -17,7 +17,7 @@ * along with LMS. If not, see . */ -/* This file contains some classes in order to get info from file using the libavconv */ + /* This file contains some classes in order to get info from file using the libavconv */ #pragma once @@ -28,32 +28,30 @@ struct AVFormatContext; namespace Av { - class AudioFile final : public IAudioFile - { - public: - AudioFile(const std::filesystem::path& p); - ~AudioFile(); + class AudioFile final : public IAudioFile + { + public: + AudioFile(const std::filesystem::path& p); + ~AudioFile(); - AudioFile(const AudioFile&) = delete; - AudioFile(AudioFile&&) = delete; - AudioFile& operator=(const AudioFile&) = delete; - AudioFile& operator=(AudioFile&&) = delete; + const std::filesystem::path& getPath() const override; + ContainerInfo getContainerInfo() const override; + MetadataMap getMetaData() const override; + std::vector getStreamInfo() const override; + std::optional getBestStreamInfo() const override; + std::optional getBestStreamIndex() const override; + bool hasAttachedPictures() const override; + void visitAttachedPictures(std::function func) const override; - const std::filesystem::path& getPath() const override; - std::chrono::milliseconds getDuration() const override; - MetadataMap getMetaData() const override; - std::vector getStreamInfo() const override; - std::optional getBestStreamInfo() const override; - std::optional getBestStreamIndex() const override; - bool hasAttachedPictures() const override; - void visitAttachedPictures(std::function func) const override; + private: + AudioFile(const AudioFile&) = delete; + AudioFile& operator=(const AudioFile&) = delete; - private: - std::optional getStreamInfo(std::size_t streamIndex) const; + std::optional getStreamInfo(std::size_t streamIndex) const; - const std::filesystem::path _p; - AVFormatContext* _context {}; - }; + const std::filesystem::path _p; + AVFormatContext* _context{}; + }; } // namespace Av diff --git a/src/libs/av/impl/RawResourceHandlerCreator.cpp b/src/libs/av/impl/RawResourceHandlerCreator.cpp new file mode 100644 index 00000000..04ea6f88 --- /dev/null +++ b/src/libs/av/impl/RawResourceHandlerCreator.cpp @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2023 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "av/RawResourceHandlerCreator.hpp" + +#include "av/IAudioFile.hpp" +#include "utils/FileResourceHandlerCreator.hpp" + +namespace Av +{ + std::unique_ptr createRawResourceHandler(const std::filesystem::path& path) + { + std::string_view mimeType{ Av::getMimeType(path.extension()) }; + return createFileResourceHandler(path, mimeType.empty() ? "application/octet-stream" : mimeType); + } +} diff --git a/src/libs/av/impl/TranscodeResourceHandler.cpp b/src/libs/av/impl/TranscodeResourceHandler.cpp deleted file mode 100644 index 753213c4..00000000 --- a/src/libs/av/impl/TranscodeResourceHandler.cpp +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright (C) 2020 Emeric Poupon - * - * This file is part of LMS. - * - * LMS is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LMS is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LMS. If not, see . - */ - -#include "TranscodeResourceHandler.hpp" -#include "utils/Logger.hpp" - -namespace Av -{ - namespace - { - std::size_t - doEstimateContentLength(const InputFileParameters& inputFileParameters, const TranscodeParameters& transcodeParameters) - { - const std::size_t estimatedContentLength {transcodeParameters.bitrate / 8 * static_cast(std::chrono::duration_cast(inputFileParameters.duration).count()) / 1000}; - return estimatedContentLength; - } - } - - std::unique_ptr - createTranscodeResourceHandler(const InputFileParameters& inputFileParameters, const TranscodeParameters& transcodeParameters, bool estimateContentLength) - { - return std::make_unique(inputFileParameters, transcodeParameters, estimateContentLength); - } - - // TODO set some nice HTTP return code - - TranscodeResourceHandler::TranscodeResourceHandler(const InputFileParameters& inputFileParameters, const TranscodeParameters& transcodeParameters, bool estimateContentLength) - : _estimatedContentLength {estimateContentLength ? std::make_optional(doEstimateContentLength(inputFileParameters, transcodeParameters)) : std::nullopt} - , _transcoder {inputFileParameters, transcodeParameters} - { - if (_estimatedContentLength) - LMS_LOG(TRANSCODE, DEBUG) << "Estimated content length = " << *_estimatedContentLength; - else - LMS_LOG(TRANSCODE, DEBUG) << "Not using estimated content length"; - } - - Wt::Http::ResponseContinuation* - TranscodeResourceHandler::processRequest(const Wt::Http::Request& /*request*/, Wt::Http::Response& response) - { - if (_estimatedContentLength) - response.setContentLength(*_estimatedContentLength); - response.setMimeType(_transcoder.getOutputMimeType()); - LMS_LOG(TRANSCODE, DEBUG) << "Transcoder finished = " << _transcoder.finished() << ", total served bytes = " << _totalServedByteCount << ", mime type = " << _transcoder.getOutputMimeType(); - - if (_bytesReadyCount > 0) - { - LMS_LOG(TRANSCODE, DEBUG) << "Writing " << _bytesReadyCount << " bytes back to client"; - - response.out().write(reinterpret_cast(&_buffer[0]), _bytesReadyCount); - _totalServedByteCount += _bytesReadyCount; - _bytesReadyCount = 0; - } - - if (!_transcoder.finished()) - { - Wt::Http::ResponseContinuation *continuation {response.createContinuation()}; - continuation->waitForMoreData(); - _transcoder.asyncRead(_buffer.data(), _buffer.size(), [=](std::size_t nbBytesRead) - { - LMS_LOG(TRANSCODE, DEBUG) << "Have " << nbBytesRead << " more bytes to send back"; - - assert(_bytesReadyCount == 0); - _bytesReadyCount = nbBytesRead; - continuation->haveMoreData(); - }); - - return continuation; - } - else - { - // pad with 0 if necessary as duration may not be accurate - if (_estimatedContentLength && *_estimatedContentLength > _totalServedByteCount) - { - const std::size_t padSize {*_estimatedContentLength - _totalServedByteCount}; - - LMS_LOG(TRANSCODE, DEBUG) << "Adding " << padSize << " padding bytes"; - - for (std::size_t i {}; i < padSize; ++i) - response.out().put(0); - - _totalServedByteCount += padSize; - } - - LMS_LOG(TRANSCODE, DEBUG) << "Transcoding finished. Total served byte count = " << _totalServedByteCount; - } - - return {}; - } -} - diff --git a/src/libs/av/impl/Transcoder.cpp b/src/libs/av/impl/Transcoder.cpp index 5519021d..c4c76902 100644 --- a/src/libs/av/impl/Transcoder.cpp +++ b/src/libs/av/impl/Transcoder.cpp @@ -28,181 +28,191 @@ #include "utils/Logger.hpp" #include "utils/Service.hpp" -namespace Av { - -#define LOG(sev) LMS_LOG(TRANSCODE, sev) << "[" << _debugId << "] - " - -static std::atomic globalId {}; -static std::filesystem::path ffmpegPath; - -void -Transcoder::init() +namespace Av::Transcoding { - ffmpegPath = Service::get()->getPath("ffmpeg-file", "/usr/bin/ffmpeg"); - if (!std::filesystem::exists(ffmpegPath)) - throw Exception {"File '" + ffmpegPath.string() + "' does not exist!"}; -} -Transcoder::Transcoder(const InputFileParameters& inputFileParameters, const TranscodeParameters& transcodeParameters) -: _debugId {globalId++} -, _inputFileParameters {inputFileParameters} -, _transcodeParameters {transcodeParameters} -{ - start(); -} +#define LOG(sev) LMS_LOG(TRANSCODING, sev) << "[" << _debugId << "] - " -Transcoder::~Transcoder() = default; + static std::atomic globalId{}; + static std::filesystem::path ffmpegPath; -void -Transcoder::start() -{ - if (ffmpegPath.empty()) - init(); + std::string_view formatToMimetype(OutputFormat format) + { + switch (format) + { + case OutputFormat::MP3: return "audio/mpeg"; + case OutputFormat::OGG_OPUS: return "audio/opus"; + case OutputFormat::MATROSKA_OPUS: return "audio/x-matroska"; + case OutputFormat::OGG_VORBIS: return "audio/ogg"; + case OutputFormat::WEBM_VORBIS: return "audio/webm"; + } - try - { - if (!std::filesystem::exists(_inputFileParameters.trackPath)) - throw Exception {"File '" + _inputFileParameters.trackPath.string() + "' does not exist!"}; - else if (!std::filesystem::is_regular_file( _inputFileParameters.trackPath) ) - throw Exception {"File '" + _inputFileParameters.trackPath.string() + "' is not regular!"}; - } - catch (const std::filesystem::filesystem_error& e) - { - throw Exception {"File error '" + _inputFileParameters.trackPath.string() + "': " + e.what()}; - } + throw Exception{ "Invalid encoding" }; + } - LOG(INFO) << "Transcoding file '" << _inputFileParameters.trackPath.string() << "'"; + void Transcoder::init() + { + ffmpegPath = Service::get()->getPath("ffmpeg-file", "/usr/bin/ffmpeg"); + if (!std::filesystem::exists(ffmpegPath)) + throw Exception{ "File '" + ffmpegPath.string() + "' does not exist!" }; + } - std::vector args; + Transcoder::Transcoder(const InputParameters& inputParameters, const OutputParameters& outputParameters) + : _debugId{ globalId++ } + , _inputParameters{ inputParameters } + , _outputParameters{ outputParameters } + { + start(); + } - args.emplace_back(ffmpegPath.string()); + Transcoder::~Transcoder() = default; - // Make sure: - // - we do not produce anything in the stderr output - // - we do not rely on input - // in order not to block the whole forked process - args.emplace_back("-loglevel"); - args.emplace_back("quiet"); - args.emplace_back("-nostdin"); + void Transcoder::start() + { + if (ffmpegPath.empty()) + init(); - // input Offset - { - args.emplace_back("-ss"); + try + { + if (!std::filesystem::exists(_inputParameters.trackPath)) + throw Exception{ "File '" + _inputParameters.trackPath.string() + "' does not exist!" }; + else if (!std::filesystem::is_regular_file(_inputParameters.trackPath)) + throw Exception{ "File '" + _inputParameters.trackPath.string() + "' is not regular!" }; + } + catch (const std::filesystem::filesystem_error& e) + { + throw Exception{ "File error '" + _inputParameters.trackPath.string() + "': " + e.what() }; + } - std::ostringstream oss; - oss << std::fixed << std::showpoint << std::setprecision(3) << (_transcodeParameters.offset.count() / float {1000}); - args.emplace_back(oss.str()); - } + LOG(INFO) << "Transcoding file '" << _inputParameters.trackPath.string() << "'"; - // Input file - args.emplace_back("-i"); - args.emplace_back(_inputFileParameters.trackPath.string()); + std::vector args; - // Stream mapping, if set - if (_transcodeParameters.stream) - { - args.emplace_back("-map"); - args.emplace_back("0:" + std::to_string(*_transcodeParameters.stream)); - } + args.emplace_back(ffmpegPath.string()); - if (_transcodeParameters.stripMetadata) - { - // Strip metadata - args.emplace_back("-map_metadata"); - args.emplace_back("-1"); - } + // Make sure: + // - we do not produce anything in the stderr output + // - we do not rely on input + // in order not to block the whole forked process + args.emplace_back("-loglevel"); + args.emplace_back("quiet"); + args.emplace_back("-nostdin"); - // Skip video flows (including covers) - args.emplace_back("-vn"); + // input Offset + { + args.emplace_back("-ss"); - // Output bitrates - args.emplace_back("-b:a"); - args.emplace_back(std::to_string(_transcodeParameters.bitrate)); + std::ostringstream oss; + oss << std::fixed << std::showpoint << std::setprecision(3) << (_outputParameters.offset.count() / float{ 1000 }); + args.emplace_back(oss.str()); + } - // Codecs and formats - switch (_transcodeParameters.format) - { - case Format::MP3: - args.emplace_back("-f"); - args.emplace_back("mp3"); - break; + // Input file + args.emplace_back("-i"); + args.emplace_back(_inputParameters.trackPath.string()); - case Format::OGG_OPUS: - args.emplace_back("-acodec"); - args.emplace_back("libopus"); - args.emplace_back("-f"); - args.emplace_back("ogg"); - break; + // Stream mapping, if set + if (_outputParameters.stream) + { + args.emplace_back("-map"); + args.emplace_back("0:" + std::to_string(*_outputParameters.stream)); + } - case Format::MATROSKA_OPUS: - args.emplace_back("-acodec"); - args.emplace_back("libopus"); - args.emplace_back("-f"); - args.emplace_back("matroska"); - break; + if (_outputParameters.stripMetadata) + { + // Strip metadata + args.emplace_back("-map_metadata"); + args.emplace_back("-1"); + } - case Format::OGG_VORBIS: - args.emplace_back("-acodec"); - args.emplace_back("libvorbis"); - args.emplace_back("-f"); - args.emplace_back("ogg"); - break; + // Skip video flows (including covers) + args.emplace_back("-vn"); - case Format::WEBM_VORBIS: - args.emplace_back("-acodec"); - args.emplace_back("libvorbis"); - args.emplace_back("-f"); - args.emplace_back("webm"); - break; + // Output bitrates + args.emplace_back("-b:a"); + args.emplace_back(std::to_string(_outputParameters.bitrate)); - default: - throw Exception {"Unhandled format (" + std::to_string(static_cast(_transcodeParameters.format)) + ")"}; - } + // Codecs and formats + switch (_outputParameters.format) + { + case OutputFormat::MP3: + args.emplace_back("-f"); + args.emplace_back("mp3"); + break; - _outputMimeType = formatToMimetype(_transcodeParameters.format); + case OutputFormat::OGG_OPUS: + args.emplace_back("-acodec"); + args.emplace_back("libopus"); + args.emplace_back("-f"); + args.emplace_back("ogg"); + break; - args.emplace_back("pipe:1"); + case OutputFormat::MATROSKA_OPUS: + args.emplace_back("-acodec"); + args.emplace_back("libopus"); + args.emplace_back("-f"); + args.emplace_back("matroska"); + break; - LOG(DEBUG) << "Dumping args (" << args.size() << ")"; - for (const std::string& arg : args) - LOG(DEBUG) << "Arg = '" << arg << "'"; + case OutputFormat::OGG_VORBIS: + args.emplace_back("-acodec"); + args.emplace_back("libvorbis"); + args.emplace_back("-f"); + args.emplace_back("ogg"); + break; - // Caution: stdin must have been closed before - try - { - _childProcess = Service::get()->spawnChildProcess(ffmpegPath, args); - } - catch (ChildProcessException& exception) - { - throw Exception {"Cannot execute '" + ffmpegPath.string() + "': " + exception.what()}; - } -} + case OutputFormat::WEBM_VORBIS: + args.emplace_back("-acodec"); + args.emplace_back("libvorbis"); + args.emplace_back("-f"); + args.emplace_back("webm"); + break; -void -Transcoder::asyncRead(std::byte* buffer, std::size_t bufferSize, ReadCallback readCallback) -{ - assert(_childProcess); + default: + throw Exception{ "Unhandled format (" + std::to_string(static_cast(_outputParameters.format)) + ")" }; + } - return _childProcess->asyncRead(buffer, bufferSize, [readCallback {std::move(readCallback)}](IChildProcess::ReadResult /*res*/, std::size_t nbBytesRead) - { - readCallback(nbBytesRead); - }); -} + _outputMimeType = formatToMimetype(_outputParameters.format); -std::size_t -Transcoder::readSome(std::byte* buffer, std::size_t bufferSize) -{ - assert(_childProcess); + args.emplace_back("pipe:1"); - return _childProcess->readSome(buffer, bufferSize); -} + LOG(DEBUG) << "Dumping args (" << args.size() << ")"; + for (const std::string& arg : args) + LOG(DEBUG) << "Arg = '" << arg << "'"; -bool -Transcoder::finished() const -{ - assert(_childProcess); + // Caution: stdin must have been closed before + try + { + _childProcess = Service::get()->spawnChildProcess(ffmpegPath, args); + } + catch (ChildProcessException& exception) + { + throw Exception{ "Cannot execute '" + ffmpegPath.string() + "': " + exception.what() }; + } + } - return _childProcess->finished(); -} + void Transcoder::asyncRead(std::byte* buffer, std::size_t bufferSize, ReadCallback readCallback) + { + assert(_childProcess); -} // namespace Transcode + return _childProcess->asyncRead(buffer, bufferSize, [readCallback{ std::move(readCallback) }](IChildProcess::ReadResult /*res*/, std::size_t nbBytesRead) + { + readCallback(nbBytesRead); + }); + } + + std::size_t Transcoder::readSome(std::byte* buffer, std::size_t bufferSize) + { + assert(_childProcess); + + return _childProcess->readSome(buffer, bufferSize); + } + + bool Transcoder::finished() const + { + assert(_childProcess); + + return _childProcess->finished(); + } + +} // namespace Av::Transcoding diff --git a/src/libs/av/impl/Transcoder.hpp b/src/libs/av/impl/Transcoder.hpp index 0f6cf4c7..0bd3e1d2 100644 --- a/src/libs/av/impl/Transcoder.hpp +++ b/src/libs/av/impl/Transcoder.hpp @@ -22,47 +22,45 @@ #include #include -#include "av/TranscodeParameters.hpp" +#include "av/TranscodingParameters.hpp" #include "av/Types.hpp" class IChildProcess; -namespace Av +namespace Av::Transcoding { - class Transcoder - { - public: - Transcoder(const InputFileParameters& inputFileParameters, const TranscodeParameters& transcodeParameters); - ~Transcoder(); + class Transcoder + { + public: + Transcoder(const InputParameters& inputParameters, const OutputParameters& outputParameters); + ~Transcoder(); - Transcoder(const Transcoder&) = delete; - Transcoder& operator=(const Transcoder&) = delete; - Transcoder(Transcoder&&) = delete; - Transcoder& operator=(Transcoder&&) = delete; + Transcoder(const Transcoder&) = delete; + Transcoder& operator=(const Transcoder&) = delete; + Transcoder(Transcoder&&) = delete; + Transcoder& operator=(Transcoder&&) = delete; - // non blocking calls - using ReadCallback = std::function; - void asyncRead(std::byte* buffer, std::size_t bufferSize, ReadCallback); - std::size_t readSome(std::byte* buffer, std::size_t bufferSize); + // non blocking calls + using ReadCallback = std::function; + void asyncRead(std::byte* buffer, std::size_t bufferSize, ReadCallback); + std::size_t readSome(std::byte* buffer, std::size_t bufferSize); - const std::string& getOutputMimeType() const { return _outputMimeType; } - const TranscodeParameters& getParameters() const { return _transcodeParameters; } + const std::string& getOutputMimeType() const { return _outputMimeType; } + const OutputParameters& getOutputParameters() const { return _outputParameters; } - bool finished() const; + bool finished() const; - private: - static void init(); + private: + static void init(); - void start(); + void start(); - const std::size_t _debugId {}; - const InputFileParameters _inputFileParameters; - const TranscodeParameters _transcodeParameters; + const std::size_t _debugId{}; + const InputParameters _inputParameters; + const OutputParameters _outputParameters; + std::string _outputMimeType; - std::unique_ptr _childProcess; - - std::string _outputMimeType; - }; - -} // namespace Av + std::unique_ptr _childProcess; + }; +} // namespace Av::Transcoding diff --git a/src/libs/av/impl/TranscodingResourceHandler.cpp b/src/libs/av/impl/TranscodingResourceHandler.cpp new file mode 100644 index 00000000..f5d2ca0f --- /dev/null +++ b/src/libs/av/impl/TranscodingResourceHandler.cpp @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2020 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "TranscodingResourceHandler.hpp" +#include "utils/Logger.hpp" + +namespace Av::Transcoding +{ + namespace + { + std::size_t doEstimateContentLength(const InputParameters& inputParameters, const OutputParameters& outputParameters) + { + const std::size_t estimatedContentLength{ outputParameters.bitrate / 8 * static_cast(std::chrono::duration_cast(inputParameters.duration).count()) / 1000 }; + return estimatedContentLength; + } + } + + std::unique_ptr createResourceHandler(const InputParameters& inputParameters, const OutputParameters& outputParameters, bool estimateContentLength) + { + return std::make_unique(inputParameters, outputParameters, estimateContentLength); + } + + // TODO set some nice HTTP return code + + TranscodingResourceHandler::TranscodingResourceHandler(const InputParameters& inputParameters, const OutputParameters& outputParameters, bool estimateContentLength) + : _estimatedContentLength{ estimateContentLength ? std::make_optional(doEstimateContentLength(inputParameters, outputParameters)) : std::nullopt } + , _transcoder{ inputParameters, outputParameters } + { + if (_estimatedContentLength) + LMS_LOG(TRANSCODING, DEBUG) << "Estimated content length = " << *_estimatedContentLength; + else + LMS_LOG(TRANSCODING, DEBUG) << "Not using estimated content length"; + } + + Wt::Http::ResponseContinuation* TranscodingResourceHandler::processRequest(const Wt::Http::Request& /*request*/, Wt::Http::Response& response) + { + if (_estimatedContentLength) + response.setContentLength(*_estimatedContentLength); + response.setMimeType(_transcoder.getOutputMimeType()); + LMS_LOG(TRANSCODING, DEBUG) << "Transcoder finished = " << _transcoder.finished() << ", total served bytes = " << _totalServedByteCount << ", mime type = " << _transcoder.getOutputMimeType(); + + if (_bytesReadyCount > 0) + { + LMS_LOG(TRANSCODING, DEBUG) << "Writing " << _bytesReadyCount << " bytes back to client"; + + response.out().write(reinterpret_cast(&_buffer[0]), _bytesReadyCount); + _totalServedByteCount += _bytesReadyCount; + _bytesReadyCount = 0; + } + + if (!_transcoder.finished()) + { + Wt::Http::ResponseContinuation* continuation{ response.createContinuation() }; + continuation->waitForMoreData(); + _transcoder.asyncRead(_buffer.data(), _buffer.size(), [=](std::size_t nbBytesRead) + { + LMS_LOG(TRANSCODING, DEBUG) << "Have " << nbBytesRead << " more bytes to send back"; + + assert(_bytesReadyCount == 0); + _bytesReadyCount = nbBytesRead; + continuation->haveMoreData(); + }); + + return continuation; + } + else + { + // pad with 0 if necessary as duration may not be accurate + if (_estimatedContentLength && *_estimatedContentLength > _totalServedByteCount) + { + const std::size_t padSize{ *_estimatedContentLength - _totalServedByteCount }; + + LMS_LOG(TRANSCODING, DEBUG) << "Adding " << padSize << " padding bytes"; + + for (std::size_t i{}; i < padSize; ++i) + response.out().put(0); + + _totalServedByteCount += padSize; + } + + LMS_LOG(TRANSCODING, DEBUG) << "Transcoding finished. Total served byte count = " << _totalServedByteCount; + } + + return {}; + } +} + diff --git a/src/libs/av/impl/TranscodeResourceHandler.hpp b/src/libs/av/impl/TranscodingResourceHandler.hpp similarity index 52% rename from src/libs/av/impl/TranscodeResourceHandler.hpp rename to src/libs/av/impl/TranscodingResourceHandler.hpp index ff254b63..6821d42a 100644 --- a/src/libs/av/impl/TranscodeResourceHandler.hpp +++ b/src/libs/av/impl/TranscodingResourceHandler.hpp @@ -23,27 +23,27 @@ #include #include -#include "av/TranscodeParameters.hpp" +#include "av/TranscodingParameters.hpp" #include "utils/IResourceHandler.hpp" #include "Transcoder.hpp" -namespace Av +namespace Av::Transcoding { - class TranscodeResourceHandler final : public IResourceHandler - { - public: - TranscodeResourceHandler(const InputFileParameters& inputFileParameters, const TranscodeParameters& parameters, bool estimateContentLength); + class TranscodingResourceHandler final : public IResourceHandler + { + public: + TranscodingResourceHandler(const InputParameters& inputParameters, const OutputParameters& outputParameters, bool estimateContentLength); - private: - Wt::Http::ResponseContinuation* processRequest(const Wt::Http::Request& request, Wt::Http::Response& reponse) override; - void abort() override {}; + private: + Wt::Http::ResponseContinuation* processRequest(const Wt::Http::Request& request, Wt::Http::Response& reponse) override; + void abort() override {}; - static constexpr std::size_t _chunkSize {32768}; - std::optional _estimatedContentLength; - std::array _buffer; - std::size_t _bytesReadyCount {}; - std::size_t _totalServedByteCount {}; - Transcoder _transcoder; - }; + static constexpr std::size_t _chunkSize{ 262'144 }; + std::optional _estimatedContentLength; + std::array _buffer; + std::size_t _bytesReadyCount{}; + std::size_t _totalServedByteCount{}; + Transcoder _transcoder; + }; } diff --git a/src/libs/av/include/av/IAudioFile.hpp b/src/libs/av/include/av/IAudioFile.hpp index 3083056a..1ce66e57 100644 --- a/src/libs/av/include/av/IAudioFile.hpp +++ b/src/libs/av/include/av/IAudioFile.hpp @@ -17,7 +17,7 @@ * along with LMS. If not, see . */ -/* This file contains some classes in order to get info from file using the libavconv */ + /* This file contains some classes in order to get info from file using the libavconv */ #pragma once @@ -27,52 +27,84 @@ #include #include #include +#include #include #include "Types.hpp" namespace Av { - struct Picture - { - std::string mimeType; - const std::byte* data {}; - std::size_t dataSize {}; - }; + // List should be sync with the codecs shipped in the lms's docker version + enum class DecodingCodec + { + UNKNOWN, + MP3, + AAC, + AC3, + VORBIS, + WMAV1, + WMAV2, + FLAC, // Flac + ALAC, // Apple Lossless Audio Codec (ALAC) + WAVPACK, // WavPack + MUSEPACK7, // Musepack + MUSEPACK8, + APE, // // Monkey's Audio + EAC3, // Enhanced AC-3 + MP4ALS, // MPEG-4 Audio Lossless Coding + OPUS, // Opus + SHORTEN, // Shorten (shn) + // TODO add PCM codecs + }; - struct StreamInfo - { - size_t index {}; - std::size_t bitrate {}; - std::string codec; - }; + struct Picture + { + std::string mimeType; + const std::byte* data{}; + std::size_t dataSize{}; + }; - class IAudioFile - { - public: - virtual ~IAudioFile() = default; + struct ContainerInfo + { + std::size_t bitrate{}; + std::string name{}; + std::chrono::milliseconds duration{}; + }; - using MetadataMap = std::unordered_map; + struct StreamInfo + { + size_t index{}; + std::size_t bitrate{}; + DecodingCodec codec; + std::string codecName; + }; - virtual const std::filesystem::path& getPath() const = 0; - virtual std::chrono::milliseconds getDuration() const = 0; - virtual MetadataMap getMetaData() const = 0; - virtual std::vector getStreamInfo() const = 0; - virtual std::optional getBestStreamInfo() const = 0; // none if failure/unknown - virtual std::optional getBestStreamIndex() const = 0; // none if failure/unknown - virtual bool hasAttachedPictures() const = 0; - virtual void visitAttachedPictures(std::function func) const = 0; - }; + class IAudioFile + { + public: + virtual ~IAudioFile() = default; - std::unique_ptr parseAudioFile(const std::filesystem::path& p); + using MetadataMap = std::unordered_map; - struct AudioFileFormat - { - std::string mimeType; - std::string format; - }; + virtual const std::filesystem::path& getPath() const = 0; + virtual ContainerInfo getContainerInfo() const = 0; + virtual MetadataMap getMetaData() const = 0; + virtual std::vector getStreamInfo() const = 0; + virtual std::optional getBestStreamInfo() const = 0; // none if failure/unknown + virtual std::optional getBestStreamIndex() const = 0; // none if failure/unknown + virtual bool hasAttachedPictures() const = 0; + virtual void visitAttachedPictures(std::function func) const = 0; + }; - std::optional guessAudioFileFormat(const std::filesystem::path& file); + std::unique_ptr parseAudioFile(const std::filesystem::path& p); + + struct AudioFileFormat + { + std::string mimeType; + std::string format; + }; + + std::string_view getMimeType(const std::filesystem::path& fileExtension); } // namespace Av diff --git a/src/libs/av/impl/Types.cpp b/src/libs/av/include/av/RawResourceHandlerCreator.hpp similarity index 60% rename from src/libs/av/impl/Types.cpp rename to src/libs/av/include/av/RawResourceHandlerCreator.hpp index 713491bc..ed7b0e14 100644 --- a/src/libs/av/impl/Types.cpp +++ b/src/libs/av/include/av/RawResourceHandlerCreator.hpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Emeric Poupon + * Copyright (C) 2023 Emeric Poupon * * This file is part of LMS. * @@ -17,25 +17,14 @@ * along with LMS. If not, see . */ -#include "av/Types.hpp" +#pragma once + +#include +#include + +#include "utils/IResourceHandler.hpp" namespace Av { - - std::string_view - formatToMimetype(Format format) - { - switch (format) - { - case Format::MP3: return "audio/mpeg"; - case Format::OGG_OPUS: return "audio/opus"; - case Format::MATROSKA_OPUS: return "audio/x-matroska"; - case Format::OGG_VORBIS: return "audio/ogg"; - case Format::WEBM_VORBIS: return "audio/webm"; - } - - throw Exception {"Invalid encoding"}; - } - -} - + std::unique_ptr createRawResourceHandler(const std::filesystem::path& path); +} \ No newline at end of file diff --git a/src/libs/av/include/av/TranscodingParameters.hpp b/src/libs/av/include/av/TranscodingParameters.hpp new file mode 100644 index 00000000..13d8f961 --- /dev/null +++ b/src/libs/av/include/av/TranscodingParameters.hpp @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2015 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include +#include +#include + +#include "Types.hpp" + +namespace Av::Transcoding +{ + struct InputParameters + { + std::filesystem::path trackPath; + std::chrono::milliseconds duration; // used to estimate content length + }; + + enum class OutputFormat + { + MP3, + OGG_OPUS, + MATROSKA_OPUS, + OGG_VORBIS, + WEBM_VORBIS, + }; + + std::string_view toMimetype(OutputFormat format); + + struct OutputParameters + { + OutputFormat format; + std::size_t bitrate{ 128000 }; + std::optional stream; // Id of the stream to be transcoded (auto detect by default) + std::chrono::milliseconds offset{ 0 }; + bool stripMetadata{ true }; + }; +} // namespace Av::Transcoding + diff --git a/src/libs/av/include/av/TranscodeResourceHandlerCreator.hpp b/src/libs/av/include/av/TranscodingResourceHandlerCreator.hpp similarity index 75% rename from src/libs/av/include/av/TranscodeResourceHandlerCreator.hpp rename to src/libs/av/include/av/TranscodingResourceHandlerCreator.hpp index 8b232d8a..16e0fcbf 100644 --- a/src/libs/av/include/av/TranscodeResourceHandlerCreator.hpp +++ b/src/libs/av/include/av/TranscodingResourceHandlerCreator.hpp @@ -23,11 +23,10 @@ #include "utils/IResourceHandler.hpp" -namespace Av +namespace Av::Transcoding { - struct InputFileParameters; - struct TranscodeParameters; + struct InputParameters; + struct OutputParameters; - std::unique_ptr createTranscodeResourceHandler(const InputFileParameters& inputFileParameters, const TranscodeParameters& parameters, bool estimateContentLength); + std::unique_ptr createResourceHandler(const InputParameters& inputParameters, const OutputParameters& outputParameters, bool estimateContentLength); } - diff --git a/src/libs/av/include/av/Types.hpp b/src/libs/av/include/av/Types.hpp index 166a19da..249529e3 100644 --- a/src/libs/av/include/av/Types.hpp +++ b/src/libs/av/include/av/Types.hpp @@ -19,27 +19,13 @@ #pragma once -#include - #include "utils/Exception.hpp" -namespace Av { - - class Exception : public LmsException - { - public: - using LmsException::LmsException; - }; - - enum class Format - { - MP3, - OGG_OPUS, - MATROSKA_OPUS, - OGG_VORBIS, - WEBM_VORBIS, - }; - - std::string_view formatToMimetype(Format format); +namespace Av +{ + class Exception : public LmsException + { + public: + using LmsException::LmsException; + }; } - diff --git a/src/libs/metadata/impl/AvFormatParser.cpp b/src/libs/metadata/impl/AvFormatParser.cpp index 8b3938f6..63347cc5 100644 --- a/src/libs/metadata/impl/AvFormatParser.cpp +++ b/src/libs/metadata/impl/AvFormatParser.cpp @@ -186,18 +186,9 @@ AvFormatParser::parse(const std::filesystem::path& p, bool debug) { const auto mediaFile {Av::parseAudioFile(p)}; - // Stream info - { - std::vector audioStreams; - - for (auto stream : mediaFile->getStreamInfo()) - { - MetaData::AudioStream audioStream {static_cast(stream.bitrate)}; - track.audioStreams.emplace_back(audioStream); - } - } - - track.duration = mediaFile->getDuration(); + Av::ContainerInfo info{ mediaFile->getContainerInfo() }; + track.duration = info.duration; + track.bitrate = info.bitrate; track.hasCover = mediaFile->hasAttachedPictures(); MetaData::Tags tags; diff --git a/src/libs/metadata/impl/TagLibParser.cpp b/src/libs/metadata/impl/TagLibParser.cpp index cfcaaeb7..1540142f 100644 --- a/src/libs/metadata/impl/TagLibParser.cpp +++ b/src/libs/metadata/impl/TagLibParser.cpp @@ -44,503 +44,479 @@ namespace MetaData { - -// TODO use string_views here for values -using TagMap = std::map>; - -template -std::vector -getPropertyValuesFirstMatchAs(const TagMap& tags, std::initializer_list keys) -{ - std::vector res; - - for (std::string_view key : keys) - { - const auto itValues {tags.find(std::string {key})}; - if (itValues == std::cend(tags)) - continue; - - const std::vector& values {itValues->second}; - if (values.empty()) - continue; - - res.reserve(values.size()); - - for (const auto& value : values) - { - std::optional val {StringUtils::readAs(value)}; - if (!val) - continue; - - res.emplace_back(std::move(*val)); - } - - break; - } - - return res; -} - -template -std::optional -getPropertyValueFirstMatchAs(const TagMap& tags, std::initializer_list keys) -{ - std::optional res; - std::vector values {getPropertyValuesFirstMatchAs(tags, keys)}; - if (!values.empty()) - res = std::move(values.front()); - - return res; -} - -template -std::vector -getPropertyValuesAs(const TagMap& tags, std::string_view key) -{ - return getPropertyValuesFirstMatchAs(tags, {key}); -} - -template -std::optional -getPropertyValueAs(const TagMap& tags, std::string_view key) -{ - return getPropertyValueFirstMatchAs(tags, {key}); -} - -static -std::vector -splitAndTrimString(std::string_view str, std::string_view delimiters) -{ - std::vector strings {StringUtils::splitString(str, delimiters)}; - for (std::string_view& s : strings) - s = StringUtils::stringTrim(s); - - return strings; -} - -static -std::vector -getArtists(const TagMap& tags, - std::initializer_list artistTagNames, - std::initializer_list artistSortTagNames, - std::initializer_list artistMBIDTagNames - ) -{ - const std::vector artistNames {getPropertyValuesFirstMatchAs(tags, artistTagNames)}; - if (artistNames.empty()) - return {}; - - std::vector artists; - artists.reserve(artistNames.size()); - std::transform(std::cbegin(artistNames), std::cend(artistNames), std::back_inserter(artists), - [&](std::string_view name) { return Artist {name}; }); - - { - const std::vector artistSortNames {getPropertyValuesFirstMatchAs(tags, artistSortTagNames)}; - if (artistSortNames.size() == artists.size()) - { - for (std::size_t i {}; i < artistSortNames.size(); ++i) - artists[i].sortName = artistSortNames[i]; - } - } - - { - const std::vector artistsMBID {getPropertyValuesFirstMatchAs(tags, artistMBIDTagNames)}; - - if (artistNames.size() == artistsMBID.size()) - { - for (std::size_t i {}; i < artistsMBID.size(); ++i) - artists[i].mbid = artistsMBID[i]; - } - } - - - return artists; -} - -static -PerformerContainer -getPerformerArtists(const TagMap& tags, - std::initializer_list artistTagNames) -{ - PerformerContainer performers; - - // picard stores like this: (see https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html#performer) - // We may hit both styles for the same track - // PERFORMER: artist (role) - if (const std::vector artistNames {getPropertyValuesFirstMatchAs(tags, artistTagNames)}; !artistNames.empty()) - { - for (std::string_view entry : artistNames) - { - Utils::PerformerArtist performer {Utils::extractPerformerAndRole(entry)}; - StringUtils::capitalize(performer.role); - performers[performer.role].push_back(std::move(performer.artist)); - } - } - // PERFORMER:role (MP3) - for (const auto& [key, values] : tags) - { - if (key.find("PERFORMER:") == 0) - { - std::string performerStr {key}; - std::string role; - if (const std::size_t rolePos {performerStr.find(':')}; rolePos != std::string::npos) - { - role = StringUtils::stringToLower(performerStr.substr(rolePos + 1, performerStr.size() - rolePos + 1)); - StringUtils::capitalize(role); - } - - for (const auto& value : values) - performers[role].push_back(Artist {value}); - } - } - - return performers; -} - -static -std::optional -getRelease(const TagMap& tags) -{ - std::optional release; - - auto releaseName {getPropertyValueAs(tags, "ALBUM")}; - if (!releaseName) - return release; - - release.emplace(); - release->name = std::move(*releaseName); - release->artistDisplayName = getPropertyValueAs(tags, "ALBUMARTIST").value_or(""); - release->mbid = getPropertyValueFirstMatchAs(tags, {"MUSICBRAINZ_ALBUMID", "MUSICBRAINZ ALBUM ID", "MUSICBRAINZ/ALBUM ID"}); - release->artists = getArtists(tags, {"ALBUMARTISTS", "ALBUMARTIST"}, {"ALBUMARTISTSSORT", "ALBUMARTISTSORT"}, {"MUSICBRAINZ_ALBUMARTISTID", "MUSICBRAINZ ALBUM ARTIST ID", "MUSICBRAINZ/ALBUM ARTIST ID"}); - release->mediumCount = getPropertyValueAs(tags, "DISCTOTAL"); - if (!release->mediumCount) - { - // mediumCount may be encoded as "position/count" - if (const auto value {getPropertyValueAs(tags, "DISCNUMBER")}) - { - // Expecting 'Number/Total' - const std::vector strings {StringUtils::splitString(*value, "/") }; - if (strings.size() == 2) - release->mediumCount = StringUtils::readAs(strings[1]); - } - } - - release->primaryType = getPropertyValueFirstMatchAs(tags, {"MUSICBRAINZ_ALBUMTYPE", "RELEASETYPE", "MUSICBRAINZ ALBUM TYPE", "MUSICBRAINZ/ALBUM TYPE"}); - if (release->primaryType) - { - const auto secondaryTypes {getPropertyValuesFirstMatchAs(tags, {"MUSICBRAINZ_ALBUMTYPE", "RELEASETYPE", "MUSICBRAINZ ALBUM TYPE", "MUSICBRAINZ/ALBUM TYPE"})}; - release->secondaryTypes.assign(std::cbegin(secondaryTypes), std::cend(secondaryTypes)); - } - - return release; -} - -static -std::optional -getMedium(const TagMap& tags) -{ - std::optional medium; - medium.emplace(); - - medium->type = getPropertyValueAs(tags, "MEDIA").value_or(""); - medium->name = getPropertyValueFirstMatchAs(tags, {"DISCSUBTITLE", "SETSUBTITLE"}).value_or(""); - medium->trackCount = getPropertyValueAs(tags, "TRACKTOTAL"); - if (!medium->trackCount) - { - // totalTracks may be encoded as "position/count" - if (const auto value {getPropertyValueAs(tags, "TRACKNUMBER")}) - { - // Expecting 'Number/Total' - const std::vector strings {StringUtils::splitString(*value, "/") }; - if (strings.size() == 2) - medium->trackCount = StringUtils::readAs(strings[1]); - } - } - // Expecting 'Number[/Total]' - medium->position = getPropertyValueAs(tags, "DISCNUMBER"); - medium->release = getRelease(tags); - medium->replayGain = getPropertyValueAs(tags, "REPLAYGAIN_ALBUM_GAIN"); - - if (medium->type.empty() - && medium->name.empty() - && !medium->trackCount - && !medium->position - && !medium->release - && !medium->replayGain) - { - medium.reset(); - } - - return medium; -} - -static -TagLib::AudioProperties::ReadStyle -readStyleToTagLibReadStyle(ParserReadStyle readStyle) -{ - switch (readStyle) - { - case ParserReadStyle::Fast: return TagLib::AudioProperties::ReadStyle::Fast; - case ParserReadStyle::Average: return TagLib::AudioProperties::ReadStyle::Average; - case ParserReadStyle::Accurate: return TagLib::AudioProperties::ReadStyle::Accurate; - } - - throw LmsException {"Cannot convert read style"}; -} - -TagLibParser::TagLibParser(ParserReadStyle readStyle) - : _readStyle {readStyleToTagLibReadStyle(readStyle)} -{ -} - -void -TagLibParser::processTag(Track& track, const std::string& tag, const std::vector& values, bool debug) -{ - if (debug) - std::cout << "[" << tag << "] = " << StringUtils::joinStrings(values, "*SEP*") << std::endl; - - if (tag.empty() || values.empty()) - return; - - std::string_view value {values.front()}; - - if (tag == "TITLE") - track.title = value; - else if (tag == "MUSICBRAINZ_RELEASETRACKID" - || tag == "MUSICBRAINZ RELEASE TRACK ID" - || tag == "MUSICBRAINZ/RELEASE TRACK ID") - { - track.mbid = UUID::fromString(value); - } - else if (tag == "MUSICBRAINZ_TRACKID" - || tag == "MUSICBRAINZ TRACK ID" - || tag == "MUSICBRAINZ/TRACK ID") - track.recordingMBID = UUID::fromString(value); - else if (tag == "ACOUSTID_ID") - track.acoustID = UUID::fromString(value); - else if (tag == "TRACKNUMBER") - { - // Expecting 'Number/Total' - track.position = StringUtils::readAs(value); - } - else if (tag == "DATE") - { - // Higher priority than YEAR - if (const Wt::WDate date {Utils::parseDate(value)}; date.isValid()) - track.date = date; - } - else if (tag == "YEAR" && !track.date.isValid()) - { - // lower priority than DATE - track.date = Utils::parseDate(value); - } - else if (tag == "ORIGINALDATE") - { - // Higher priority than ORIGINALYEAR - if (const Wt::WDate date {Utils::parseDate(value)}; date.isValid()) - track.originalDate = date; - } - else if (tag == "ORIGINALYEAR" && !track.originalDate.isValid()) - { - // Lower priority than ORIGINALDATE - track.originalDate = Utils::parseDate(value); - } - else if (tag == "METADATA_BLOCK_PICTURE") - track.hasCover = true; - else if (tag == "COPYRIGHT") - track.copyright = value; - else if (tag == "COPYRIGHTURL") - track.copyrightURL = value; - else if (tag == "REPLAYGAIN_TRACK_GAIN") - track.replayGain = StringUtils::readAs(value); - else if (tag == "ARTIST") - track.artistDisplayName = value; - else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end()) - { - std::set clusterNames; - for (std::string_view valueList : values) - { - const std::vector splittedValues {splitAndTrimString(valueList, "/,;")}; - for (std::string_view value : splittedValues) - clusterNames.insert(std::string {value}); - } - - if (!clusterNames.empty()) - track.tags[tag] = std::move(clusterNames); - } -} - -static -TagMap -constructTagMap(const TagLib::PropertyMap& properties) -{ - TagMap tagMap; - - for (const auto& [propertyName, propertyValues] : properties) - { - std::vector& values {tagMap[propertyName.upper().to8Bit(true)]}; - for (const TagLib::String& propertyValue : propertyValues) - { - std::string trimedValue {StringUtils::stringTrim(propertyValue.to8Bit(true))}; - if (!trimedValue.empty()) - values.emplace_back(std::move(trimedValue)); - } - } - - return tagMap; -} - -static -void -mergeTagMaps(TagMap& dst, TagMap&& src) -{ - for (auto&& [tag, values] : src) - { - if (dst.find(tag) == std::cend(dst)) - dst[tag] = std::move(values); - } -} - -std::optional -TagLibParser::parse(const std::filesystem::path& p, bool debug) -{ - TagLib::FileRef f {p.string().c_str(), - true, // read audio properties - _readStyle}; - - if (f.isNull()) - { - LMS_LOG(METADATA, ERROR) << "File '" << p.string() << "': parsing failed"; - return std::nullopt; - } - - if (!f.audioProperties()) - { - LMS_LOG(METADATA, INFO) << "File '" << p.string() << "': no audio properties"; - return std::nullopt; - } - - Track track; - - { - const TagLib::AudioProperties *properties {f.audioProperties() }; - - track.duration = std::chrono::milliseconds {properties->lengthInMilliseconds()}; - - MetaData::AudioStream audioStream {static_cast(properties->bitrate() * 1000)}; - track.audioStreams = {audioStream}; - } - - TagMap tags {constructTagMap(f.file()->properties())}; - - auto getAPETags = [&](const TagLib::APE::Tag* apeTag) - { - if (!apeTag) - return; - - mergeTagMaps(tags, constructTagMap(apeTag->properties())); - }; - - // Not that good embedded pictures handling - - // WMA - if (TagLib::ASF::File* asfFile {dynamic_cast(f.file())}) - { - const TagLib::ASF::Tag* tag {asfFile->tag()}; - if (tag) - { - if (tag->attributeListMap().contains("WM/Picture")) - track.hasCover = true; - - for (const auto& [name, attributeList] : tag->attributeListMap()) - { - std::string strName {StringUtils::stringToUpper(name.to8Bit(true))}; - if (strName.find("WM/") == 0 || tags.find(strName) != std::cend(tags)) - continue; - - std::vector attributes; - for (const auto& attribute : attributeList) - { - if (attribute.type() == TagLib::ASF::Attribute::AttributeTypes::UnicodeType) - attributes.emplace_back(attribute.toString().to8Bit(true)); - } - - if (!attributes.empty()) - { - if (debug) - std::cout << "ASF property: '" << name << "'" << std::endl; - - tags.emplace(strName, std::move(attributes)); - } - } - } - } - // MP3 - else if (TagLib::MPEG::File* mp3File {dynamic_cast(f.file())}) - { - if (mp3File->ID3v2Tag()) - { - const auto& frameListMap {mp3File->ID3v2Tag()->frameListMap()}; - - if (!frameListMap["APIC"].isEmpty()) - track.hasCover = true; - if (!frameListMap["TSST"].isEmpty()) - tags["DISCSUBTITLE"] = {frameListMap["TSST"].front()->toString().to8Bit(true)}; - } - - getAPETags(mp3File->APETag()); - } - //MP4 - else if (TagLib::MP4::File* mp4File {dynamic_cast(f.file())}) - { - TagLib::MP4::Item coverItem {mp4File->tag()->item("covr")}; - TagLib::MP4::CoverArtList coverArtList {coverItem.toCoverArtList()}; - if (!coverArtList.isEmpty()) - track.hasCover = true; - } - // MPC - else if (TagLib::MPC::File* mpcFile {dynamic_cast(f.file())}) - { - getAPETags(mpcFile->APETag()); - } - // WavPack - else if (TagLib::WavPack::File* wavPackFile {dynamic_cast(f.file())}) - { - getAPETags(wavPackFile->APETag()); - } - // FLAC - else if (TagLib::FLAC::File* flacFile {dynamic_cast(f.file())}) - { - if (!flacFile->pictureList().isEmpty()) - track.hasCover = true; - } - else if (TagLib::Ogg::Vorbis::File* vorbisFile {dynamic_cast(f.file())}) - { - if (!vorbisFile->tag()->pictureList().isEmpty()) - track.hasCover = true; - } - else if (TagLib::Ogg::Opus::File* opusFile {dynamic_cast(f.file())}) - { - if (!opusFile->tag()->pictureList().isEmpty()) - track.hasCover = true; - } - - track.medium = getMedium(tags); - track.artists = getArtists(tags, {"ARTISTS", "ARTIST"}, {"ARTISTSORT"}, {"MUSICBRAINZ_ARTISTID", "MUSICBRAINZ ARTIST ID", "MUSICBRAINZ/ARTIST ID"}); - track.conductorArtists = getArtists(tags, {"CONDUCTORS", "CONDUCTOR"}, {"CONDUCTORSSORT", "CONDUCTORSORT"}, {}); - track.composerArtists = getArtists(tags, {"COMPOSERS", "COMPOSER"}, {"COMPOSERSSORT", "COMPOSERSORT"}, {}); - track.lyricistArtists = getArtists(tags, {"LYRICISTS", "LYRICIST"}, {"LYRICISTSSORT", "LYRICISTSORT"}, {}); - track.mixerArtists = getArtists(tags, {"MIXERS", "MIXER"}, {"MIXERSSORT", "MIXERSORT"}, {}); - track.producerArtists = getArtists(tags, {"PRODUCERS", "PRODUCER"}, {"PRODUCERSSORT", "PRODUCERSORT"}, {}); - track.remixerArtists = getArtists(tags, {"REMIXERS", "REMIXER", "ModifiedBy"}, {"REMIXERSSORT", "REMIXERSORT"}, {}); - track.performerArtists = getPerformerArtists(tags, {"PERFORMERS", "PERFORMER"}); - - for (const auto& [tag, values] : tags) - processTag(track, tag, values, debug); - - return track; -} + namespace + { + // TODO use string_views here for values + using TagMap = std::map>; + + template + std::vector getPropertyValuesFirstMatchAs(const TagMap& tags, std::initializer_list keys) + { + std::vector res; + + for (std::string_view key : keys) + { + const auto itValues{ tags.find(std::string {key}) }; + if (itValues == std::cend(tags)) + continue; + + const std::vector& values{ itValues->second }; + if (values.empty()) + continue; + + res.reserve(values.size()); + + for (const auto& value : values) + { + std::optional val{ StringUtils::readAs(value) }; + if (!val) + continue; + + res.emplace_back(std::move(*val)); + } + + break; + } + + return res; + } + + template + std::optional getPropertyValueFirstMatchAs(const TagMap& tags, std::initializer_list keys) + { + std::optional res; + std::vector values{ getPropertyValuesFirstMatchAs(tags, keys) }; + if (!values.empty()) + res = std::move(values.front()); + + return res; + } + + template + std::vector getPropertyValuesAs(const TagMap& tags, std::string_view key) + { + return getPropertyValuesFirstMatchAs(tags, { key }); + } + + template + std::optional getPropertyValueAs(const TagMap& tags, std::string_view key) + { + return getPropertyValueFirstMatchAs(tags, { key }); + } + + std::vector splitAndTrimString(std::string_view str, std::string_view delimiters) + { + std::vector strings{ StringUtils::splitString(str, delimiters) }; + for (std::string_view& s : strings) + s = StringUtils::stringTrim(s); + + return strings; + } + + std::vector getArtists(const TagMap& tags, + std::initializer_list artistTagNames, + std::initializer_list artistSortTagNames, + std::initializer_list artistMBIDTagNames + ) + { + const std::vector artistNames{ getPropertyValuesFirstMatchAs(tags, artistTagNames) }; + if (artistNames.empty()) + return {}; + + std::vector artists; + artists.reserve(artistNames.size()); + std::transform(std::cbegin(artistNames), std::cend(artistNames), std::back_inserter(artists), + [&](std::string_view name) { return Artist{ name }; }); + + { + const std::vector artistSortNames{ getPropertyValuesFirstMatchAs(tags, artistSortTagNames) }; + if (artistSortNames.size() == artists.size()) + { + for (std::size_t i{}; i < artistSortNames.size(); ++i) + artists[i].sortName = artistSortNames[i]; + } + } + + { + const std::vector artistsMBID{ getPropertyValuesFirstMatchAs(tags, artistMBIDTagNames) }; + + if (artistNames.size() == artistsMBID.size()) + { + for (std::size_t i{}; i < artistsMBID.size(); ++i) + artists[i].mbid = artistsMBID[i]; + } + } + + + return artists; + } + + PerformerContainer getPerformerArtists(const TagMap& tags, std::initializer_list artistTagNames) + { + PerformerContainer performers; + + // picard stores like this: (see https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html#performer) + // We may hit both styles for the same track + // PERFORMER: artist (role) + if (const std::vector artistNames{ getPropertyValuesFirstMatchAs(tags, artistTagNames) }; !artistNames.empty()) + { + for (std::string_view entry : artistNames) + { + Utils::PerformerArtist performer{ Utils::extractPerformerAndRole(entry) }; + StringUtils::capitalize(performer.role); + performers[performer.role].push_back(std::move(performer.artist)); + } + } + // PERFORMER:role (MP3) + for (const auto& [key, values] : tags) + { + if (key.find("PERFORMER:") == 0) + { + std::string performerStr{ key }; + std::string role; + if (const std::size_t rolePos{ performerStr.find(':') }; rolePos != std::string::npos) + { + role = StringUtils::stringToLower(performerStr.substr(rolePos + 1, performerStr.size() - rolePos + 1)); + StringUtils::capitalize(role); + } + + for (const auto& value : values) + performers[role].push_back(Artist{ value }); + } + } + + return performers; + } + + std::optional getRelease(const TagMap& tags) + { + std::optional release; + + auto releaseName{ getPropertyValueAs(tags, "ALBUM") }; + if (!releaseName) + return release; + + release.emplace(); + release->name = std::move(*releaseName); + release->artistDisplayName = getPropertyValueAs(tags, "ALBUMARTIST").value_or(""); + release->mbid = getPropertyValueFirstMatchAs(tags, { "MUSICBRAINZ_ALBUMID", "MUSICBRAINZ ALBUM ID", "MUSICBRAINZ/ALBUM ID" }); + release->artists = getArtists(tags, { "ALBUMARTISTS", "ALBUMARTIST" }, { "ALBUMARTISTSSORT", "ALBUMARTISTSORT" }, { "MUSICBRAINZ_ALBUMARTISTID", "MUSICBRAINZ ALBUM ARTIST ID", "MUSICBRAINZ/ALBUM ARTIST ID" }); + release->mediumCount = getPropertyValueAs(tags, "DISCTOTAL"); + if (!release->mediumCount) + { + // mediumCount may be encoded as "position/count" + if (const auto value{ getPropertyValueAs(tags, "DISCNUMBER") }) + { + // Expecting 'Number/Total' + const std::vector strings{ StringUtils::splitString(*value, "/") }; + if (strings.size() == 2) + release->mediumCount = StringUtils::readAs(strings[1]); + } + } + + release->primaryType = getPropertyValueFirstMatchAs(tags, { "MUSICBRAINZ_ALBUMTYPE", "RELEASETYPE", "MUSICBRAINZ ALBUM TYPE", "MUSICBRAINZ/ALBUM TYPE" }); + if (release->primaryType) + { + const auto secondaryTypes{ getPropertyValuesFirstMatchAs(tags, {"MUSICBRAINZ_ALBUMTYPE", "RELEASETYPE", "MUSICBRAINZ ALBUM TYPE", "MUSICBRAINZ/ALBUM TYPE"}) }; + release->secondaryTypes.assign(std::cbegin(secondaryTypes), std::cend(secondaryTypes)); + } + + return release; + } + + std::optional getMedium(const TagMap& tags) + { + std::optional medium; + medium.emplace(); + + medium->type = getPropertyValueAs(tags, "MEDIA").value_or(""); + medium->name = getPropertyValueFirstMatchAs(tags, { "DISCSUBTITLE", "SETSUBTITLE" }).value_or(""); + medium->trackCount = getPropertyValueAs(tags, "TRACKTOTAL"); + if (!medium->trackCount) + { + // totalTracks may be encoded as "position/count" + if (const auto value{ getPropertyValueAs(tags, "TRACKNUMBER") }) + { + // Expecting 'Number/Total' + const std::vector strings{ StringUtils::splitString(*value, "/") }; + if (strings.size() == 2) + medium->trackCount = StringUtils::readAs(strings[1]); + } + } + // Expecting 'Number[/Total]' + medium->position = getPropertyValueAs(tags, "DISCNUMBER"); + medium->release = getRelease(tags); + medium->replayGain = getPropertyValueAs(tags, "REPLAYGAIN_ALBUM_GAIN"); + + if (medium->type.empty() + && medium->name.empty() + && !medium->trackCount + && !medium->position + && !medium->release + && !medium->replayGain) + { + medium.reset(); + } + + return medium; + } + + TagLib::AudioProperties::ReadStyle readStyleToTagLibReadStyle(ParserReadStyle readStyle) + { + switch (readStyle) + { + case ParserReadStyle::Fast: return TagLib::AudioProperties::ReadStyle::Fast; + case ParserReadStyle::Average: return TagLib::AudioProperties::ReadStyle::Average; + case ParserReadStyle::Accurate: return TagLib::AudioProperties::ReadStyle::Accurate; + } + + throw LmsException{ "Cannot convert read style" }; + } + + TagMap constructTagMap(const TagLib::PropertyMap& properties) + { + TagMap tagMap; + + for (const auto& [propertyName, propertyValues] : properties) + { + std::vector& values{ tagMap[propertyName.upper().to8Bit(true)] }; + for (const TagLib::String& propertyValue : propertyValues) + { + std::string trimedValue{ StringUtils::stringTrim(propertyValue.to8Bit(true)) }; + if (!trimedValue.empty()) + values.emplace_back(std::move(trimedValue)); + } + } + + return tagMap; + } + + void mergeTagMaps(TagMap& dst, TagMap&& src) + { + for (auto&& [tag, values] : src) + { + if (dst.find(tag) == std::cend(dst)) + dst[tag] = std::move(values); + } + } + + } + + TagLibParser::TagLibParser(ParserReadStyle readStyle) + : _readStyle{ readStyleToTagLibReadStyle(readStyle) } + { + } + + void TagLibParser::processTag(Track& track, const std::string& tag, const std::vector& values, bool debug) + { + if (debug) + std::cout << "[" << tag << "] = " << StringUtils::joinStrings(values, "*SEP*") << std::endl; + + if (tag.empty() || values.empty()) + return; + + std::string_view value{ values.front() }; + + if (tag == "TITLE") + track.title = value; + else if (tag == "MUSICBRAINZ_RELEASETRACKID" + || tag == "MUSICBRAINZ RELEASE TRACK ID" + || tag == "MUSICBRAINZ/RELEASE TRACK ID") + { + track.mbid = UUID::fromString(value); + } + else if (tag == "MUSICBRAINZ_TRACKID" + || tag == "MUSICBRAINZ TRACK ID" + || tag == "MUSICBRAINZ/TRACK ID") + track.recordingMBID = UUID::fromString(value); + else if (tag == "ACOUSTID_ID") + track.acoustID = UUID::fromString(value); + else if (tag == "TRACKNUMBER") + { + // Expecting 'Number/Total' + track.position = StringUtils::readAs(value); + } + else if (tag == "DATE") + { + // Higher priority than YEAR + if (const Wt::WDate date{ Utils::parseDate(value) }; date.isValid()) + track.date = date; + } + else if (tag == "YEAR" && !track.date.isValid()) + { + // lower priority than DATE + track.date = Utils::parseDate(value); + } + else if (tag == "ORIGINALDATE") + { + // Higher priority than ORIGINALYEAR + if (const Wt::WDate date{ Utils::parseDate(value) }; date.isValid()) + track.originalDate = date; + } + else if (tag == "ORIGINALYEAR" && !track.originalDate.isValid()) + { + // Lower priority than ORIGINALDATE + track.originalDate = Utils::parseDate(value); + } + else if (tag == "METADATA_BLOCK_PICTURE") + track.hasCover = true; + else if (tag == "COPYRIGHT") + track.copyright = value; + else if (tag == "COPYRIGHTURL") + track.copyrightURL = value; + else if (tag == "REPLAYGAIN_TRACK_GAIN") + track.replayGain = StringUtils::readAs(value); + else if (tag == "ARTIST") + track.artistDisplayName = value; + else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end()) + { + std::set clusterNames; + for (std::string_view valueList : values) + { + const std::vector splittedValues{ splitAndTrimString(valueList, "/,;") }; + for (std::string_view value : splittedValues) + clusterNames.insert(std::string{ value }); + } + + if (!clusterNames.empty()) + track.tags[tag] = std::move(clusterNames); + } + } + + std::optional TagLibParser::parse(const std::filesystem::path& p, bool debug) + { + TagLib::FileRef f{ p.string().c_str(), + true, // read audio properties + _readStyle }; + + if (f.isNull()) + { + LMS_LOG(METADATA, ERROR) << "File '" << p.string() << "': parsing failed"; + return std::nullopt; + } + + Track track; + + if (const TagLib::AudioProperties* properties{ f.audioProperties() }) + { + track.duration = std::chrono::milliseconds{ properties->lengthInMilliseconds() }; + track.bitrate = static_cast(properties->bitrate() * 1000); + } + else + { + LMS_LOG(METADATA, INFO) << "File '" << p.string() << "': no audio properties"; + return std::nullopt; + } + + TagMap tags{ constructTagMap(f.file()->properties()) }; + + auto getAPETags = [&](const TagLib::APE::Tag* apeTag) + { + if (!apeTag) + return; + + mergeTagMaps(tags, constructTagMap(apeTag->properties())); + }; + + // Not that good embedded pictures handling + + // WMA + if (TagLib::ASF::File * asfFile{ dynamic_cast(f.file()) }) + { + const TagLib::ASF::Tag* tag{ asfFile->tag() }; + if (tag) + { + if (tag->attributeListMap().contains("WM/Picture")) + track.hasCover = true; + + for (const auto& [name, attributeList] : tag->attributeListMap()) + { + std::string strName{ StringUtils::stringToUpper(name.to8Bit(true)) }; + if (strName.find("WM/") == 0 || tags.find(strName) != std::cend(tags)) + continue; + + std::vector attributes; + for (const auto& attribute : attributeList) + { + if (attribute.type() == TagLib::ASF::Attribute::AttributeTypes::UnicodeType) + attributes.emplace_back(attribute.toString().to8Bit(true)); + } + + if (!attributes.empty()) + { + if (debug) + std::cout << "ASF property: '" << name << "'" << std::endl; + + tags.emplace(strName, std::move(attributes)); + } + } + } + } + // MP3 + else if (TagLib::MPEG::File * mp3File{ dynamic_cast(f.file()) }) + { + if (mp3File->ID3v2Tag()) + { + const auto& frameListMap{ mp3File->ID3v2Tag()->frameListMap() }; + + if (!frameListMap["APIC"].isEmpty()) + track.hasCover = true; + if (!frameListMap["TSST"].isEmpty()) + tags["DISCSUBTITLE"] = { frameListMap["TSST"].front()->toString().to8Bit(true) }; + } + + getAPETags(mp3File->APETag()); + } + //MP4 + else if (TagLib::MP4::File * mp4File{ dynamic_cast(f.file()) }) + { + TagLib::MP4::Item coverItem{ mp4File->tag()->item("covr") }; + TagLib::MP4::CoverArtList coverArtList{ coverItem.toCoverArtList() }; + if (!coverArtList.isEmpty()) + track.hasCover = true; + } + // MPC + else if (TagLib::MPC::File * mpcFile{ dynamic_cast(f.file()) }) + { + getAPETags(mpcFile->APETag()); + } + // WavPack + else if (TagLib::WavPack::File * wavPackFile{ dynamic_cast(f.file()) }) + { + getAPETags(wavPackFile->APETag()); + } + // FLAC + else if (TagLib::FLAC::File * flacFile{ dynamic_cast(f.file()) }) + { + if (!flacFile->pictureList().isEmpty()) + track.hasCover = true; + } + else if (TagLib::Ogg::Vorbis::File * vorbisFile{ dynamic_cast(f.file()) }) + { + if (!vorbisFile->tag()->pictureList().isEmpty()) + track.hasCover = true; + } + else if (TagLib::Ogg::Opus::File * opusFile{ dynamic_cast(f.file()) }) + { + if (!opusFile->tag()->pictureList().isEmpty()) + track.hasCover = true; + } + + track.medium = getMedium(tags); + track.artists = getArtists(tags, { "ARTISTS", "ARTIST" }, { "ARTISTSORT" }, { "MUSICBRAINZ_ARTISTID", "MUSICBRAINZ ARTIST ID", "MUSICBRAINZ/ARTIST ID" }); + track.conductorArtists = getArtists(tags, { "CONDUCTORS", "CONDUCTOR" }, { "CONDUCTORSSORT", "CONDUCTORSORT" }, {}); + track.composerArtists = getArtists(tags, { "COMPOSERS", "COMPOSER" }, { "COMPOSERSSORT", "COMPOSERSORT" }, {}); + track.lyricistArtists = getArtists(tags, { "LYRICISTS", "LYRICIST" }, { "LYRICISTSSORT", "LYRICISTSORT" }, {}); + track.mixerArtists = getArtists(tags, { "MIXERS", "MIXER" }, { "MIXERSSORT", "MIXERSORT" }, {}); + track.producerArtists = getArtists(tags, { "PRODUCERS", "PRODUCER" }, { "PRODUCERSSORT", "PRODUCERSORT" }, {}); + track.remixerArtists = getArtists(tags, { "REMIXERS", "REMIXER", "ModifiedBy" }, { "REMIXERSSORT", "REMIXERSORT" }, {}); + track.performerArtists = getPerformerArtists(tags, { "PERFORMERS", "PERFORMER" }); + + for (const auto& [tag, values] : tags) + processTag(track, tag, values, debug); + + return track; + } } // namespace MetaData diff --git a/src/libs/metadata/include/metadata/IParser.hpp b/src/libs/metadata/include/metadata/IParser.hpp index bb2b0274..6db82619 100644 --- a/src/libs/metadata/include/metadata/IParser.hpp +++ b/src/libs/metadata/include/metadata/IParser.hpp @@ -34,126 +34,121 @@ namespace MetaData { - using Tags = std::map /* names */>; + using Tags = std::map /* names */>; - // Very simplified version of https://musicbrainz.org/doc/MusicBrainz_Database/Schema + // Very simplified version of https://musicbrainz.org/doc/MusicBrainz_Database/Schema - struct Artist - { - std::optional mbid; - std::string name; - std::optional sortName; + struct Artist + { + std::optional mbid; + std::string name; + std::optional sortName; - Artist(std::string_view _name) : name {_name} {} - Artist(std::optional _mbid, std::string_view _name, std::optional _sortName) : mbid {std::move(_mbid)}, name {_name}, sortName {std::move(_sortName)} {} - }; + Artist(std::string_view _name) : name{ _name } {} + Artist(std::optional _mbid, std::string_view _name, std::optional _sortName) : mbid{ std::move(_mbid) }, name{ _name }, sortName{ std::move(_sortName) } {} + }; - using PerformerContainer = std::map>; + using PerformerContainer = std::map>; - struct Release - { - // see https://musicbrainz.org/doc/Release_Group/Type - enum class PrimaryType - { - Album, - Single, - EP, - Broadcast, - Other - }; + struct Release + { + // see https://musicbrainz.org/doc/Release_Group/Type + enum class PrimaryType + { + Album, + Single, + EP, + Broadcast, + Other + }; - enum class SecondaryType - { - Compilation, - Soundtrack, - Spokenword, - Interview, - Audiobook, - AudioDrama, - Live, - Remix, - DJMix, - Mixtape_Street, - Demo, - }; + enum class SecondaryType + { + Compilation, + Soundtrack, + Spokenword, + Interview, + Audiobook, + AudioDrama, + Live, + Remix, + DJMix, + Mixtape_Street, + Demo, + }; - std::optional mbid; - std::string name; - std::string artistDisplayName; - std::vector artists; - std::optional mediumCount; - std::optional primaryType; - EnumSet secondaryTypes; - }; + std::optional mbid; + std::string name; + std::string artistDisplayName; + std::vector artists; + std::optional mediumCount; + std::optional primaryType; + EnumSet secondaryTypes; + }; - struct Medium - { - std::string type; - std::string name; - std::optional release; - std::optional position; // in release - std::optional trackCount; - std::optional replayGain; - }; + struct Medium + { + std::string type; + std::string name; + std::optional release; + std::optional position; // in release + std::optional trackCount; + std::optional replayGain; + }; - struct AudioStream - { - unsigned bitRate; - }; + struct Track + { + std::optional mbid; + std::optional recordingMBID; + std::string title; + std::optional medium; + std::optional position; // in medium + Tags tags; + std::chrono::milliseconds duration{}; + std::size_t bitrate{}; + Wt::WDate date; + Wt::WDate originalDate; + bool hasCover{}; + std::optional acoustID; + std::string copyright; + std::string copyrightURL; + std::optional replayGain; + std::string artistDisplayName; + std::vector artists; + std::vector conductorArtists; + std::vector composerArtists; + std::vector lyricistArtists; + std::vector mixerArtists; + PerformerContainer performerArtists; + std::vector producerArtists; + std::vector remixerArtists; + }; - struct Track - { - std::optional mbid; - std::optional recordingMBID; - std::string title; - std::optional medium; - std::optional position; // in medium - Tags tags; - std::chrono::milliseconds duration; - Wt::WDate date; - Wt::WDate originalDate; - bool hasCover {}; - std::vector audioStreams; - std::optional acoustID; - std::string copyright; - std::string copyrightURL; - std::optional replayGain; - std::string artistDisplayName; - std::vector artists; - std::vector conductorArtists; - std::vector composerArtists; - std::vector lyricistArtists; - std::vector mixerArtists; - PerformerContainer performerArtists; - std::vector producerArtists; - std::vector remixerArtists; - }; + class IParser + { + public: + virtual ~IParser() = default; - class IParser - { - public: - virtual ~IParser() = default; + virtual std::optional parse(const std::filesystem::path& p, bool debug = false) = 0; - virtual std::optional parse(const std::filesystem::path& p, bool debug = false) = 0; + void setClusterTypeNames(const std::set& clusterTypeNames) { _clusterTypeNames = clusterTypeNames; } - void setClusterTypeNames(const std::set& clusterTypeNames) { _clusterTypeNames = clusterTypeNames; } + protected: + std::set _clusterTypeNames; + }; - protected: - std::set _clusterTypeNames; - }; + enum class ParserType + { + TagLib, + AvFormat, + }; - enum class ParserType - { - TagLib, - AvFormat, - }; - - enum class ParserReadStyle - { - Fast, - Average, - Accurate, - }; - std::unique_ptr createParser(ParserType parserType, ParserReadStyle parserReadStyle); + enum class ParserReadStyle + { + Fast, + Average, + Accurate, + }; + std::unique_ptr createParser(ParserType parserType, ParserReadStyle parserReadStyle); } // namespace MetaData diff --git a/src/libs/services/cover/impl/CoverService.cpp b/src/libs/services/cover/impl/CoverService.cpp index 0912828e..773e41f9 100644 --- a/src/libs/services/cover/impl/CoverService.cpp +++ b/src/libs/services/cover/impl/CoverService.cpp @@ -345,9 +345,6 @@ namespace Cover } } - if (!cover) - cover = getDefault(width); - if (cover) saveToCache(cacheEntryDesc, cover); @@ -377,8 +374,8 @@ namespace Cover auto transaction{ session.createSharedTransaction() }; - const auto tracks{ Track::find(session, Track::FindParameters {}.setRelease(releaseId).setRange({0, 1}).setSortMethod(TrackSortMethod::Release)) }; - + // get a track in this release, consider the release is in a single directory + const auto tracks{ Track::find(session, Track::FindParameters {}.setRelease(releaseId).setRange(Range{ 0, 1 }).setSortMethod(TrackSortMethod::Release)) }; if (!tracks.results.empty()) { const Track::pointer& track{ tracks.results.front() }; @@ -397,9 +394,6 @@ namespace Cover cover = getFromTrack(session, releaseInfo->firstTrackId, width, false /* no release fallback */); } - if (!cover) - cover = getDefault(width); - if (cover) saveToCache(cacheEntryDesc, cover); diff --git a/src/libs/services/cover/impl/CoverService.hpp b/src/libs/services/cover/impl/CoverService.hpp index f23d437a..dd5b82c5 100644 --- a/src/libs/services/cover/impl/CoverService.hpp +++ b/src/libs/services/cover/impl/CoverService.hpp @@ -35,103 +35,97 @@ namespace Database { - class Session; + class Session; } namespace Av { - class IAudioFile; + class IAudioFile; } namespace Cover { - struct CacheEntryDesc - { - std::variant id; - std::size_t size; - - bool operator==(const CacheEntryDesc& other) const - { - return id == other.id - && size == other.size; - } - }; + struct CacheEntryDesc + { + std::variant id; + std::size_t size; + bool operator==(const CacheEntryDesc& other) const + { + return id == other.id + && size == other.size; + } + }; } // ns Cover namespace std { - - template<> - class hash - { - public: - size_t operator()(const Cover::CacheEntryDesc& e) const - { - size_t h {}; - std::visit([&](auto id) - { - using IdType = std::decay_t; - h ^= std::hash()(id); - }, e.id); - h ^= std::hash()(e.size) << 1; - return h; - } - }; + template<> + class hash + { + public: + size_t operator()(const Cover::CacheEntryDesc& e) const + { + size_t h{}; + std::visit([&](auto id) + { + using IdType = std::decay_t; + h ^= std::hash()(id); + }, e.id); + h ^= std::hash()(e.size) << 1; + return h; + } + }; } // ns std namespace Cover { - class CoverService : public ICoverService - { - public: - CoverService(Database::Db& db, - const std::filesystem::path& execPath, - const std::filesystem::path& defaultCoverPath); + class CoverService : public ICoverService + { + public: + CoverService(Database::Db& db, const std::filesystem::path& execPath, const std::filesystem::path& defaultCoverPath); - CoverService(const CoverService&) = delete; - CoverService& operator=(const CoverService&) = delete; - CoverService(CoverService&&) = delete; - CoverService& operator=(CoverService&&) = delete; + CoverService(const CoverService&) = delete; + CoverService& operator=(const CoverService&) = delete; - private: - std::shared_ptr getFromTrack(Database::TrackId trackId, Image::ImageSize width) override; - std::shared_ptr getFromRelease(Database::ReleaseId releaseId, Image::ImageSize width) override; - void flushCache() override; - void setJpegQuality(unsigned quality) override; + private: + std::shared_ptr getFromTrack(Database::TrackId trackId, Image::ImageSize width) override; + std::shared_ptr getFromRelease(Database::ReleaseId releaseId, Image::ImageSize width) override; + std::shared_ptr getDefault(Image::ImageSize width) override; + void flushCache() override; + void setJpegQuality(unsigned quality) override; - std::shared_ptr getFromTrack(Database::Session& dbSession, Database::TrackId trackId, Image::ImageSize width, bool allowReleaseFallback); - std::unique_ptr getFromAvMediaFile(const Av::IAudioFile& input, Image::ImageSize width) const; - std::unique_ptr getFromCoverFile(const std::filesystem::path& p, Image::ImageSize width) const; + std::shared_ptr getFromTrack(Database::Session& dbSession, Database::TrackId trackId, Image::ImageSize width, bool allowReleaseFallback); + std::unique_ptr getFromAvMediaFile(const Av::IAudioFile& input, Image::ImageSize width) const; + std::unique_ptr getFromCoverFile(const std::filesystem::path& p, Image::ImageSize width) const; - std::unique_ptr getFromTrack(const std::filesystem::path& path, Image::ImageSize width) const; - std::multimap getCoverPaths(const std::filesystem::path& directoryPath) const; - std::unique_ptr getFromDirectory(const std::filesystem::path& directory, Image::ImageSize width) const; - std::unique_ptr getFromSameNamedFile(const std::filesystem::path& filePath, Image::ImageSize width) const; - std::shared_ptr getDefault(Image::ImageSize width); + std::unique_ptr getFromTrack(const std::filesystem::path& path, Image::ImageSize width) const; + std::multimap getCoverPaths(const std::filesystem::path& directoryPath) const; + std::unique_ptr getFromDirectory(const std::filesystem::path& directory, Image::ImageSize width) const; + std::unique_ptr getFromSameNamedFile(const std::filesystem::path& filePath, Image::ImageSize width) const; - bool checkCoverFile(const std::filesystem::path& directoryPath) const; + bool checkCoverFile(const std::filesystem::path& directoryPath) const; - Database::Db& _db; + Database::Db& _db; - std::shared_mutex _cacheMutex; - std::unordered_map> _cache; - std::unordered_map> _defaultCoverCache; - std::atomic _cacheMisses {}; - std::atomic _cacheHits {}; - std::size_t _cacheSize {}; + std::shared_mutex _cacheMutex; + std::unordered_map> _cache; + std::unordered_map> _defaultCoverCache; + std::atomic _cacheMisses{}; + std::atomic _cacheHits{}; + std::size_t _cacheSize{}; - void saveToCache(const CacheEntryDesc& entryDesc, std::shared_ptr image); - std::shared_ptr loadFromCache(const CacheEntryDesc& entryDesc); + void saveToCache(const CacheEntryDesc& entryDesc, std::shared_ptr image); + std::shared_ptr loadFromCache(const CacheEntryDesc& entryDesc); - const std::filesystem::path _defaultCoverPath; - const std::size_t _maxCacheSize; - static inline const std::vector _fileExtensions {".jpg", ".jpeg", ".png", ".bmp"}; // TODO parametrize - const std::size_t _maxFileSize; - const std::vector _preferredFileNames; - unsigned _jpegQuality; - }; + const std::filesystem::path _defaultCoverPath; + const std::size_t _maxCacheSize; + static inline const std::vector _fileExtensions{ ".jpg", ".jpeg", ".png", ".bmp" }; // TODO parametrize + const std::size_t _maxFileSize; + const std::vector _preferredFileNames; + unsigned _jpegQuality; + }; } // namespace Cover diff --git a/src/libs/services/cover/include/services/cover/ICoverService.hpp b/src/libs/services/cover/include/services/cover/ICoverService.hpp index 0421f589..7ce4a2df 100644 --- a/src/libs/services/cover/include/services/cover/ICoverService.hpp +++ b/src/libs/services/cover/include/services/cover/ICoverService.hpp @@ -28,27 +28,27 @@ namespace Database { - class Db; + class Db; } namespace Cover { - class ICoverService - { - public: - virtual ~ICoverService() = default; + class ICoverService + { + public: + virtual ~ICoverService() = default; - virtual std::shared_ptr getFromTrack(Database::TrackId trackId, Image::ImageSize width) = 0; - virtual std::shared_ptr getFromRelease(Database::ReleaseId releaseId, Image::ImageSize width) = 0; + virtual std::shared_ptr getFromTrack(Database::TrackId trackId, Image::ImageSize width) = 0; + virtual std::shared_ptr getFromRelease(Database::ReleaseId releaseId, Image::ImageSize width) = 0; - virtual void flushCache() = 0; + virtual std::shared_ptr getDefault(Image::ImageSize width) = 0; - virtual void setJpegQuality(unsigned quality) = 0; // from 1 to 100 - }; + virtual void flushCache() = 0; - std::unique_ptr createCoverService(Database::Db& db, - const std::filesystem::path& execPath, - const std::filesystem::path& defaultCoverPath); + virtual void setJpegQuality(unsigned quality) = 0; // from 1 to 100 + }; + + std::unique_ptr createCoverService(Database::Db& db, const std::filesystem::path& execPath, const std::filesystem::path& defaultCoverPath); } // namespace CoverArt diff --git a/src/libs/services/database/impl/Artist.cpp b/src/libs/services/database/impl/Artist.cpp index c9ca60a4..5f8db6eb 100644 --- a/src/libs/services/database/impl/Artist.cpp +++ b/src/libs/services/database/impl/Artist.cpp @@ -210,12 +210,11 @@ namespace Database } - RangeResults Artist::findOrphanIds(Session& session, Range range) + RangeResults Artist::findOrphanIds(Session& session, std::optional range) { session.checkSharedLocked(); auto query{ session.getDboSession().query("SELECT DISTINCT a.id FROM artist a WHERE NOT EXISTS(SELECT 1 FROM track t INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id WHERE t.id = t_a_l.track_id)") }; - - return Utils::execQuery(query, range); + return Utils::execQuery(query, range); } RangeResults Artist::findIds(Session& session, const FindParameters& params) @@ -223,7 +222,7 @@ namespace Database session.checkSharedLocked(); auto query{ createQuery(session, params) }; - return Utils::execQuery(query, params.range); + return Utils::execQuery(query, params.range); } RangeResults Artist::find(Session& session, const FindParameters& params) @@ -231,10 +230,18 @@ namespace Database session.checkSharedLocked(); auto query{ createQuery>(session, params) }; - return Utils::execQuery(query, params.range); + return Utils::execQuery(query, params.range); } - RangeResults Artist::findSimilarArtistIds(EnumSet artistLinkTypes, Range range) const + void Artist::find(Session& session, const FindParameters& params, std::function func) + { + session.checkSharedLocked(); + + auto query{ createQuery>(session, params) }; + Utils::execQuery(query, params.range, func); + } + + RangeResults Artist::findSimilarArtistIds(EnumSet artistLinkTypes, std::optional range) const { assert(session()); @@ -278,7 +285,7 @@ namespace Database for (TrackArtistLinkType type : artistLinkTypes) query.bind(type); - return Utils::execQuery(query, range); + return Utils::execQuery(query, range); } std::vector> Artist::getClusterGroups(std::vector clusterTypes, std::size_t size) const diff --git a/src/libs/services/database/impl/Cluster.cpp b/src/libs/services/database/impl/Cluster.cpp index 1dcbf1b7..f93e8de2 100644 --- a/src/libs/services/database/impl/Cluster.cpp +++ b/src/libs/services/database/impl/Cluster.cpp @@ -32,11 +32,12 @@ namespace Database { namespace { - Wt::Dbo::Query createQuery(Session& session, const Cluster::FindParameters& params) + template + Wt::Dbo::Query createQuery(Session& session, std::string_view itemToSelect, const Cluster::FindParameters& params) { session.checkSharedLocked(); - auto query{ session.getDboSession().query("SELECT DISTINCT c.id,c.name FROM cluster c") }; + auto query{ session.getDboSession().query("SELECT DISTINCT " + std::string{ itemToSelect } + " FROM cluster c") }; if (params.track.isValid() || params.release.isValid()) { @@ -54,6 +55,21 @@ namespace Database return query; } + + template + Wt::Dbo::Query createQuery(Session& session, const Cluster::FindParameters& params) + { + std::string_view itemToSelect; + + if constexpr (std::is_same_v) + itemToSelect = "c.id"; + else if constexpr (std::is_same_v>) + itemToSelect = "c"; + else + static_assert("Unhandled type"); + + return createQuery(session, itemToSelect, params); + } } Cluster::Cluster(ObjectPtr type, std::string_view name) @@ -74,20 +90,28 @@ namespace Database return session.getDboSession().query("SELECT COUNT(*) FROM cluster"); } - RangeResults Cluster::find(Session& session, const FindParameters& params) + RangeResults Cluster::findIds(Session& session, const FindParameters& params) { session.checkSharedLocked(); - auto query{ createQuery(session, params) }; + auto query{ createQuery(session, params) }; - return Utils::execQuery(query, params.range); + return Utils::execQuery(query, params.range); } - RangeResults Cluster::findOrphans(Session& session, Range range) + RangeResults Cluster::find(Session& session, const FindParameters& params) + { + session.checkSharedLocked(); + auto query{ createQuery>(session, params) }; + + return Utils::execQuery(query, params.range); + } + + RangeResults Cluster::findOrphans(Session& session, std::optional range) { session.checkSharedLocked(); auto query{ session.getDboSession().query("SELECT DISTINCT c.id FROM cluster c WHERE NOT EXISTS(SELECT 1 FROM track_cluster t_c WHERE t_c.cluster_id = c.id)") }; - return Utils::execQuery(query, range); + return Utils::execQuery(query, range); } Cluster::pointer Cluster::find(Session& session, ClusterId id) @@ -97,30 +121,37 @@ namespace Database return session.getDboSession().find().where("id = ?").bind(id).resultValue(); } + std::size_t Cluster::computeTrackCount(Session& session, ClusterId id) + { + session.checkSharedLocked(); + + return session.getDboSession().query("SELECT COUNT(t.id) FROM track t INNER JOIN track_cluster t_c ON t_c.track_id = t.id") + .where("t_c.cluster_id = ?").bind(id).resultValue(); + } + + std::size_t Cluster::computeReleaseCount(Session& session, ClusterId id) + { + session.checkSharedLocked(); + + return session.getDboSession().query("SELECT COUNT(DISTINCT r.id) FROM release r INNER JOIN track t on t.release_id = r.id INNER JOIN track_cluster t_c ON t_c.track_id = t.id") + .where("t_c.cluster_id = ?").bind(id).resultValue(); + } + void Cluster::addTrack(ObjectPtr track) { _tracks.insert(getDboPtr(track)); } - RangeResults Cluster::getTracks(Range range) const + RangeResults Cluster::getTracks(std::optional range) const { assert(session()); auto query{ session()->query("SELECT t.id FROM track t INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id") .where("c.id = ?").bind(getId()) }; - return Utils::execQuery(query, range); + return Utils::execQuery(query, range); } - std::size_t Cluster::getReleasesCount() const - { - assert(session()); - - return session()->query("SELECT COUNT(DISTINCT r.id) FROM release r INNER JOIN track t on t.release_id = r.id INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id") - .where("c.id = ?").bind(getId()); - } - - ClusterType::ClusterType(std::string_view name) : _name{ name } { @@ -139,7 +170,7 @@ namespace Database } - RangeResults ClusterType::findOrphans(Session& session, Range range) + RangeResults ClusterType::findOrphans(Session& session, std::optional range) { session.checkSharedLocked(); @@ -148,10 +179,10 @@ namespace Database " LEFT OUTER JOIN cluster c ON c_t.id = c.cluster_type_id") .where("c.id IS NULL") }; - return Utils::execQuery(query, range); + return Utils::execQuery(query, range); } - RangeResults ClusterType::findUsed(Session& session, Range range) + RangeResults ClusterType::findUsed(Session& session, std::optional range) { session.checkSharedLocked(); @@ -159,7 +190,7 @@ namespace Database "SELECT DISTINCT c_t.id from cluster_type c_t") .join("cluster c ON c_t.id = c.cluster_type_id") }; - return Utils::execQuery(query, range); + return Utils::execQuery(query, range); } ClusterType::pointer ClusterType::find(Session& session, std::string_view name) @@ -176,13 +207,13 @@ namespace Database return session.getDboSession().find().where("id = ?").bind(id).resultValue(); } - RangeResults ClusterType::find(Session& session, Range range) + RangeResults ClusterType::find(Session& session, std::optional range) { session.checkSharedLocked(); auto query{ session.getDboSession().query("SELECT id from cluster_type") }; - return Utils::execQuery(query, range); + return Utils::execQuery(query, range); } Cluster::pointer ClusterType::getCluster(const std::string& name) const diff --git a/src/libs/services/database/impl/Listen.cpp b/src/libs/services/database/impl/Listen.cpp index d7af57d0..4f645a04 100644 --- a/src/libs/services/database/impl/Listen.cpp +++ b/src/libs/services/database/impl/Listen.cpp @@ -172,7 +172,7 @@ namespace Database if (parameters.syncState) query.where("sync_state = ?").bind(*parameters.syncState); - return Utils::execQuery(query, parameters.range); + return Utils::execQuery(query, parameters.range); } Listen::pointer Listen::find(Session& session, UserId userId, TrackId trackId, ScrobblingBackend backend, const Wt::WDateTime& dateTime) @@ -187,7 +187,7 @@ namespace Database .resultValue(); } - RangeResults Listen::getTopArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, std::optional linkType, Range range) + RangeResults Listen::getTopArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, std::optional linkType, std::optional range) { session.checkSharedLocked(); auto query{ createArtistsQuery(session.getDboSession(), userId, backend, clusterIds, linkType) }; @@ -196,57 +196,57 @@ namespace Database .orderBy("COUNT(a.id) DESC") .groupBy("a.id") }; - return Utils::execQuery(query, range); + return Utils::execQuery(query, range); } - RangeResults Listen::getTopReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, Range range) + RangeResults Listen::getTopReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, std::optional range) { session.checkSharedLocked(); auto query{ createReleasesQuery(session.getDboSession(), userId, backend, clusterIds) .orderBy("COUNT(r.id) DESC") .groupBy("r.id") }; - return Utils::execQuery(query, range); + return Utils::execQuery(query, range); } - RangeResults Listen::getTopTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, Range range) + RangeResults Listen::getTopTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, std::optional range) { session.checkSharedLocked(); auto query{ createTracksQuery(session.getDboSession(), userId, backend, clusterIds) .orderBy("COUNT(t.id) DESC") .groupBy("t.id") }; - return Utils::execQuery(query, range); + return Utils::execQuery(query, range); } - RangeResults Listen::getRecentArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, std::optional linkType, Range range) + RangeResults Listen::getRecentArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, std::optional linkType, std::optional range) { session.checkSharedLocked(); auto query{ createArtistsQuery(session.getDboSession(), userId, backend, clusterIds, linkType) .groupBy("a.id").having("l.date_time = MAX(l.date_time)") .orderBy("l.date_time DESC") }; - return Utils::execQuery(query, range); + return Utils::execQuery(query, range); } - RangeResults Listen::getRecentReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, Range range) + RangeResults Listen::getRecentReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, std::optional range) { session.checkSharedLocked(); auto query{ createReleasesQuery(session.getDboSession(), userId, backend, clusterIds) .groupBy("r.id").having("l.date_time = MAX(l.date_time)") .orderBy("l.date_time DESC") }; - return Utils::execQuery(query, range); + return Utils::execQuery(query, range); } - RangeResults Listen::getRecentTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, Range range) + RangeResults Listen::getRecentTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, std::optional range) { session.checkSharedLocked(); auto query{ createTracksQuery(session.getDboSession(), userId, backend, clusterIds) .groupBy("t.id").having("l.date_time = MAX(l.date_time)") .orderBy("l.date_time DESC") }; - return Utils::execQuery(query, range); + return Utils::execQuery(query, range); } std::size_t Listen::getCount(Session& session, UserId userId, ScrobblingBackend backend, TrackId trackId) diff --git a/src/libs/services/database/impl/Migration.cpp b/src/libs/services/database/impl/Migration.cpp index da002ba0..93fb283a 100644 --- a/src/libs/services/database/impl/Migration.cpp +++ b/src/libs/services/database/impl/Migration.cpp @@ -224,6 +224,31 @@ CREATE TABLE IF NOT EXISTS "track_backup" ( session.getDboSession().execute("UPDATE user SET feedback_backend = scrobbling_backend"); } + static void migrateFromV43(Session& session) + { + // add counts in genre table + session.getDboSession().execute("ALTER TABLE cluster ADD track_count INTEGER"); + session.getDboSession().execute("ALTER TABLE cluster ADD release_count INTEGER"); + + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + ScanSettings::get(session).modify()->incScanVersion(); + } + + static void migrateFromV44(Session& session) + { + // add bitrate + session.getDboSession().execute("ALTER TABLE track ADD bitrate INTEGER"); + + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + ScanSettings::get(session).modify()->incScanVersion(); + } + + void migrateFromV45(Session& session) + { + // add subsonic_enable_transcoding_by_default, default is disabled + session.getDboSession().execute("ALTER TABLE user ADD subsonic_enable_transcoding_by_default INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(/*User::defaultSubsonicEnableTranscodingByDefault*/0)) + ")"); + } + void doDbMigration(Session& session) { static const std::string outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" }; @@ -245,6 +270,9 @@ CREATE TABLE IF NOT EXISTS "track_backup" ( {40, migrateFromV40}, {41, migrateFromV41}, {42, migrateFromV42}, + {43, migrateFromV43}, + {44, migrateFromV44}, + {45, migrateFromV45}, }; { diff --git a/src/libs/services/database/impl/Migration.hpp b/src/libs/services/database/impl/Migration.hpp index 55b1e679..d431713d 100644 --- a/src/libs/services/database/impl/Migration.hpp +++ b/src/libs/services/database/impl/Migration.hpp @@ -26,7 +26,7 @@ namespace Database class Session; using Version = std::size_t; - static constexpr Version LMS_DATABASE_VERSION{ 43 }; + static constexpr Version LMS_DATABASE_VERSION{ 46 }; class VersionInfo { public: diff --git a/src/libs/services/database/impl/Release.cpp b/src/libs/services/database/impl/Release.cpp index 8021dc98..f3091eb6 100644 --- a/src/libs/services/database/impl/Release.cpp +++ b/src/libs/services/database/impl/Release.cpp @@ -188,7 +188,7 @@ namespace Database Wt::Dbo::Query createQuery(Session& session, const Release::FindParameters& params) { std::string_view itemToSelect; - + if constexpr (std::is_same_v) itemToSelect = "r.id"; else if constexpr (std::is_same_v>) @@ -256,7 +256,7 @@ namespace Database return session.getDboSession().query("SELECT COUNT(*) FROM release"); } - RangeResults Release::findIdsOrderedByArtist(Session& session, Range range) + RangeResults Release::findIdsOrderedByArtist(Session& session, std::optional range) { session.checkSharedLocked(); @@ -268,15 +268,15 @@ namespace Database " INNER JOIN artist a ON t_a_l.artist_id = a.id") .orderBy("a.name COLLATE NOCASE, r.name COLLATE NOCASE") }; - return Utils::execQuery(query, range); + return Utils::execQuery(query, range); } - RangeResults Release::findOrphanIds(Session& session, Range range) + RangeResults Release::findOrphanIds(Session& session, std::optional range) { session.checkSharedLocked(); auto query{ session.getDboSession().query("select r.id from release r LEFT OUTER JOIN Track t ON r.id = t.release_id WHERE t.id IS NULL") }; - return Utils::execQuery(query, range); + return Utils::execQuery(query, range); } RangeResults Release::find(Session& session, const FindParameters& params) @@ -284,7 +284,15 @@ namespace Database session.checkSharedLocked(); auto query{ createQuery>(session, params) }; - return Utils::execQuery(query, params.range); + return Utils::execQuery(query, params.range); + } + + void Release::find(Session& session, const FindParameters& params, std::function func) + { + session.checkSharedLocked(); + + auto query{ createQuery>(session, params) }; + Utils::execQuery(query, params.range, func); } RangeResults Release::findIds(Session& session, const FindParameters& params) @@ -292,7 +300,7 @@ namespace Database session.checkSharedLocked(); auto query{ createQuery(session, params) }; - return Utils::execQuery(query, params.range); + return Utils::execQuery(query, params.range); } std::size_t Release::getDiscCount() const @@ -379,9 +387,8 @@ namespace Database Wt::Dbo::collection copyrights = session()->query ("SELECT copyright_url FROM track t INNER JOIN release r ON r.id = t.release_id") - .where("r.id = ?") - .groupBy("copyright_url") - .bind(getId()); + .where("r.id = ?").bind(getId()) + .groupBy("copyright_url"); std::vector values(copyrights.begin(), copyrights.end()); @@ -392,6 +399,16 @@ namespace Database return values.front(); } + std::size_t Release::getMeanBitrate() const + { + assert(session()); + + return session()->query("SELECT COALESCE(AVG(t.bitrate), 0) FROM track t") + .where("release_id = ?").bind(getId()) + .where("bitrate > 0") + .resultValue(); + } + std::vector Release::getArtists(TrackArtistLinkType linkType) const { assert(session()); diff --git a/src/libs/services/database/impl/StarredTrack.cpp b/src/libs/services/database/impl/StarredTrack.cpp index 2f03fbe6..78112ded 100644 --- a/src/libs/services/database/impl/StarredTrack.cpp +++ b/src/libs/services/database/impl/StarredTrack.cpp @@ -76,7 +76,7 @@ namespace Database if (params.user.isValid()) query.where("s_t.user_id = ?").bind(params.user); - return Utils::execQuery(query, params.range); + return Utils::execQuery(query, params.range); } void StarredTrack::setDateTime(const Wt::WDateTime& dateTime) diff --git a/src/libs/services/database/impl/Track.cpp b/src/libs/services/database/impl/Track.cpp index be1f0a16..0be23942 100644 --- a/src/libs/services/database/impl/Track.cpp +++ b/src/libs/services/database/impl/Track.cpp @@ -44,7 +44,8 @@ namespace Database { session.checkSharedLocked(); - auto query{ session.getDboSession().query(params.distinct ? "SELECT DISTINCT " + std::string{ itemToSelect } + " FROM track t" : "SELECT t.id FROM track t") }; + std::string selectStatement{ params.distinct ? "SELECT DISTINCT" : "SELECT" }; + auto query{ session.getDboSession().query(selectStatement + " " + std::string{ itemToSelect } + " FROM track t") }; assert(params.keywords.empty() || params.name.empty()); for (std::string_view keyword : params.keywords) @@ -129,7 +130,7 @@ namespace Database query.where("r.name = ?").bind(params.releaseName); } - if (params.trackList.isValid()) + if (params.trackList.isValid() || params.sortMethod == TrackSortMethod::TrackList) { query.join("tracklist t_l ON t_l_e.tracklist_id = t_l.id"); query.join("tracklist_entry t_l_e ON t.id = t_l_e.track_id"); @@ -249,7 +250,7 @@ namespace Database return std::vector(res.begin(), res.end()); } - RangeResults Track::findPaths(Session& session, Range range) + RangeResults Track::findPaths(Session& session, std::optional range) { using QueryResultType = std::tuple; session.checkSharedLocked(); @@ -257,7 +258,7 @@ namespace Database // TODO Dbo traits on filesystem auto query{ session.getDboSession().query("SELECT id, file_path FROM track") }; - RangeResults queryResults{ Utils::execQuery(query, range) }; + RangeResults queryResults{ Utils::execQuery(query, range) }; RangeResults res; res.range = queryResults.range; @@ -267,23 +268,23 @@ namespace Database std::transform(std::cbegin(queryResults.results), std::cend(queryResults.results), std::back_inserter(res.results), [](const QueryResultType& queryResult) { - return PathResult{ std::get<0>(queryResult), std::get<1>(queryResult) }; + return PathResult{ std::get(queryResult), std::move(std::get(queryResult)) }; }); return res; } - RangeResults Track::findIdsTrackMBIDDuplicates(Session& session, Range range) + RangeResults Track::findIdsTrackMBIDDuplicates(Session& session, std::optional range) { session.checkSharedLocked(); auto query{ session.getDboSession().query("SELECT track.id FROM track WHERE mbid in (SELECT mbid FROM track WHERE mbid <> '' GROUP BY mbid HAVING COUNT (*) > 1)") .orderBy("track.release_id,track.disc_number,track.track_number,track.mbid") }; - return Utils::execQuery(query, range); + return Utils::execQuery(query, range); } - RangeResults Track::findIdsWithRecordingMBIDAndMissingFeatures(Session& session, Range range) + RangeResults Track::findIdsWithRecordingMBIDAndMissingFeatures(Session& session, std::optional range) { session.checkSharedLocked(); @@ -291,7 +292,7 @@ namespace Database .where("LENGTH(t.recording_mbid) > 0") .where("NOT EXISTS (SELECT * FROM track_features t_f WHERE t_f.track_id = t.id)") }; - return Utils::execQuery(query, range); + return Utils::execQuery(query, range); } std::vector Track::getClusters() const @@ -316,7 +317,7 @@ namespace Database session.checkSharedLocked(); auto query{ createQuery(session, parameters) }; - return Utils::execQuery(query, parameters.range); + return Utils::execQuery(query, parameters.range); } RangeResults Track::find(Session& session, const FindParameters& parameters) @@ -324,10 +325,18 @@ namespace Database session.checkSharedLocked(); auto query{ createQuery>(session, parameters) }; - return Utils::execQuery(query, parameters.range); + return Utils::execQuery(query, parameters.range); } - RangeResults Track::findSimilarTrackIds(Session& session, const std::vector& tracks, Range range) + void Track::find(Session& session, const FindParameters& params, std::function func) + { + session.checkSharedLocked(); + + auto query{ createQuery>(session, params)}; + Utils::execQuery(query, params.range, func); + } + + RangeResults Track::findSimilarTrackIds(Session& session, const std::vector& tracks, std::optional range) { assert(!tracks.empty()); session.checkSharedLocked(); @@ -354,7 +363,7 @@ namespace Database for (TrackId trackId : tracks) query.bind(trackId); - return Utils::execQuery(query, range); + return Utils::execQuery(query, range); } void Track::clearArtistLinks() diff --git a/src/libs/services/database/impl/TrackArtistLink.cpp b/src/libs/services/database/impl/TrackArtistLink.cpp index ab64672a..97fda619 100644 --- a/src/libs/services/database/impl/TrackArtistLink.cpp +++ b/src/libs/services/database/impl/TrackArtistLink.cpp @@ -84,7 +84,7 @@ namespace Database session.checkSharedLocked(); auto query{ createQuery(session, params) }; - return Utils::execQuery(query, params.range); + return Utils::execQuery(query, params.range); } EnumSet TrackArtistLink::findUsedTypes(Session& session) diff --git a/src/libs/services/database/impl/TrackBookmark.cpp b/src/libs/services/database/impl/TrackBookmark.cpp index 71b09708..48dae983 100644 --- a/src/libs/services/database/impl/TrackBookmark.cpp +++ b/src/libs/services/database/impl/TrackBookmark.cpp @@ -27,57 +27,52 @@ namespace Database { -TrackBookmark::TrackBookmark(ObjectPtr user, ObjectPtr track) -: _user {getDboPtr(user)}, -_track {getDboPtr(track)} -{ -} + TrackBookmark::TrackBookmark(ObjectPtr user, ObjectPtr track) + : _user{ getDboPtr(user) }, + _track{ getDboPtr(track) } + { + } -TrackBookmark::pointer -TrackBookmark::create(Session& session, ObjectPtr user, ObjectPtr track) -{ - return session.getDboSession().add(std::unique_ptr {new TrackBookmark {user, track}}); -} + TrackBookmark::pointer TrackBookmark::create(Session& session, ObjectPtr user, ObjectPtr track) + { + return session.getDboSession().add(std::unique_ptr {new TrackBookmark{ user, track }}); + } -std::size_t -TrackBookmark::getCount(Session& session) -{ - session.checkSharedLocked(); + std::size_t TrackBookmark::getCount(Session& session) + { + session.checkSharedLocked(); - return session.getDboSession().query("SELECT COUNT(*) FROM track_bookmark"); -} + return session.getDboSession().query("SELECT COUNT(*) FROM track_bookmark"); + } -RangeResults -TrackBookmark::find(Session& session, UserId userId, Range range) -{ - session.checkSharedLocked(); + RangeResults TrackBookmark::find(Session& session, UserId userId, std::optional range) + { + session.checkSharedLocked(); - auto query {session.getDboSession().query("SELECT id from track_bookmark") - .where("user_id = ?").bind(userId)}; + auto query{ session.getDboSession().query("SELECT id from track_bookmark") + .where("user_id = ?").bind(userId) }; - return Utils::execQuery(query, range); -} + return Utils::execQuery(query, range); + } -TrackBookmark::pointer -TrackBookmark::find(Session& session, UserId userId, TrackId trackId) -{ - session.checkSharedLocked(); + TrackBookmark::pointer TrackBookmark::find(Session& session, UserId userId, TrackId trackId) + { + session.checkSharedLocked(); - return session.getDboSession().find() - .where("user_id = ?").bind(userId) - .where("track_id = ?").bind(trackId) - .resultValue(); -} + return session.getDboSession().find() + .where("user_id = ?").bind(userId) + .where("track_id = ?").bind(trackId) + .resultValue(); + } -TrackBookmark::pointer -TrackBookmark::find(Session& session, TrackBookmarkId id) -{ - session.checkSharedLocked(); + TrackBookmark::pointer TrackBookmark::find(Session& session, TrackBookmarkId id) + { + session.checkSharedLocked(); - return session.getDboSession().find() - .where("id = ?").bind(id) - .resultValue(); -} + return session.getDboSession().find() + .where("id = ?").bind(id) + .resultValue(); + } } // namespace Database diff --git a/src/libs/services/database/impl/TrackFeatures.cpp b/src/libs/services/database/impl/TrackFeatures.cpp index 35f92b27..c7a0293e 100644 --- a/src/libs/services/database/impl/TrackFeatures.cpp +++ b/src/libs/services/database/impl/TrackFeatures.cpp @@ -30,99 +30,92 @@ namespace Database { -TrackFeatures::TrackFeatures(ObjectPtr track, const std::string& jsonEncodedFeatures) -: _data {jsonEncodedFeatures}, -_track {getDboPtr(track)} -{ -} + TrackFeatures::TrackFeatures(ObjectPtr track, const std::string& jsonEncodedFeatures) + : _data{ jsonEncodedFeatures }, + _track{ getDboPtr(track) } + { + } -TrackFeatures::pointer -TrackFeatures::create(Session& session, ObjectPtr track, const std::string& jsonEncodedFeatures) -{ - return session.getDboSession().add(std::unique_ptr {new TrackFeatures {track, jsonEncodedFeatures}}); -} + TrackFeatures::pointer TrackFeatures::create(Session& session, ObjectPtr track, const std::string& jsonEncodedFeatures) + { + return session.getDboSession().add(std::unique_ptr {new TrackFeatures{ track, jsonEncodedFeatures }}); + } -std::size_t -TrackFeatures::getCount(Session& session) -{ - session.checkSharedLocked(); + std::size_t TrackFeatures::getCount(Session& session) + { + session.checkSharedLocked(); - return session.getDboSession().query("SELECT COUNT(*) FROM track_features"); -} + return session.getDboSession().query("SELECT COUNT(*) FROM track_features"); + } -TrackFeatures::pointer -TrackFeatures::find(Session& session, TrackFeaturesId id) -{ - session.checkSharedLocked(); + TrackFeatures::pointer TrackFeatures::find(Session& session, TrackFeaturesId id) + { + session.checkSharedLocked(); - return session.getDboSession().find() - .where("id = ?").bind(id) - .resultValue(); -} + return session.getDboSession().find() + .where("id = ?").bind(id) + .resultValue(); + } -TrackFeatures::pointer -TrackFeatures::find(Session& session, TrackId trackId) -{ - session.checkSharedLocked(); + TrackFeatures::pointer TrackFeatures::find(Session& session, TrackId trackId) + { + session.checkSharedLocked(); - return session.getDboSession().find() - .where("track_id = ?").bind(trackId) - .resultValue(); -} + return session.getDboSession().find() + .where("track_id = ?").bind(trackId) + .resultValue(); + } -RangeResults -TrackFeatures::find(Session& session, Range range) -{ - session.checkSharedLocked(); + RangeResults TrackFeatures::find(Session& session, std::optional range) + { + session.checkSharedLocked(); - auto query {session.getDboSession().query("SELECT id from track_features")}; + auto query{ session.getDboSession().query("SELECT id from track_features") }; - return Utils::execQuery(query, range); -} + return Utils::execQuery(query, range); + } -FeatureValues -TrackFeatures::getFeatureValues(const FeatureName& featureNode) const -{ - FeatureValuesMap featuresValuesMap {getFeatureValuesMap({featureNode})}; - return std::move(featuresValuesMap[featureNode]); -} + FeatureValues TrackFeatures::getFeatureValues(const FeatureName& featureNode) const + { + FeatureValuesMap featuresValuesMap{ getFeatureValuesMap({featureNode}) }; + return std::move(featuresValuesMap[featureNode]); + } -FeatureValuesMap -TrackFeatures::getFeatureValuesMap(const std::unordered_set& featureNames) const -{ - FeatureValuesMap res; + FeatureValuesMap TrackFeatures::getFeatureValuesMap(const std::unordered_set& featureNames) const + { + FeatureValuesMap res; - try - { - std::istringstream iss {_data}; - boost::property_tree::ptree root; + try + { + std::istringstream iss{ _data }; + boost::property_tree::ptree root; - boost::property_tree::read_json(iss, root); + boost::property_tree::read_json(iss, root); - for (const FeatureName& featureName : featureNames) - { - FeatureValues& featureValues {res[featureName]}; + for (const FeatureName& featureName : featureNames) + { + FeatureValues& featureValues{ res[featureName] }; - auto node {root.get_child(featureName)}; + auto node{ root.get_child(featureName) }; - bool hasChildren = false; - for (const auto& child : node.get_child("")) - { - hasChildren = true; - featureValues.push_back(child.second.get_value()); - } + bool hasChildren = false; + for (const auto& child : node.get_child("")) + { + hasChildren = true; + featureValues.push_back(child.second.get_value()); + } - if (!hasChildren) - featureValues.push_back(node.get_value()); - } - } - catch (boost::property_tree::ptree_error& error) - { - LMS_LOG(DB, ERROR) << "Track " << _track.id() << ": ptree exception: " << error.what(); - res.clear(); - } + if (!hasChildren) + featureValues.push_back(node.get_value()); + } + } + catch (boost::property_tree::ptree_error& error) + { + LMS_LOG(DB, ERROR) << "Track " << _track.id() << ": ptree exception: " << error.what(); + res.clear(); + } - return res; -} + return res; + } } // namespace Database diff --git a/src/libs/services/database/impl/TrackList.cpp b/src/libs/services/database/impl/TrackList.cpp index 490fc06a..1e98fd6e 100644 --- a/src/libs/services/database/impl/TrackList.cpp +++ b/src/libs/services/database/impl/TrackList.cpp @@ -33,665 +33,299 @@ #include "IdTypeTraits.hpp" #include "Utils.hpp" -namespace Database { - -TrackList::TrackList(std::string_view name, TrackListType type, bool isPublic, ObjectPtr user) - : _name {name} -, _type {type} -, _isPublic {isPublic} -, _creationDateTime {Utils::normalizeDateTime(Wt::WDateTime::currentDateTime())} -, _lastModifiedDateTime {Utils::normalizeDateTime(Wt::WDateTime::currentDateTime())} -, _user {getDboPtr(user)} +namespace Database { - assert(user); -} - -TrackList::pointer -TrackList::create(Session& session, std::string_view name, TrackListType type, bool isPublic, ObjectPtr user) -{ - return session.getDboSession().add(std::unique_ptr {new TrackList {name, type, isPublic, user}}); -} - -std::size_t -TrackList::getCount(Session& session) -{ - session.checkSharedLocked(); - - return session.getDboSession().query("SELECT COUNT(*) FROM tracklist"); -} - - -TrackList::pointer -TrackList::find(Session& session, std::string_view name, TrackListType type, UserId userId) -{ - session.checkSharedLocked(); - assert(userId.isValid()); - - return session.getDboSession().find() - .where("name = ?").bind(name) - .where("type = ?").bind(type) - .where("user_id = ?").bind(userId).resultValue(); -} - -RangeResults -TrackList::find(Session& session, const FindParameters& params) -{ - session.checkSharedLocked(); - - auto query {session.getDboSession().query("SELECT DISTINCT t_l.id FROM tracklist t_l")}; - - if (params.user.isValid()) - query.where("t_l.user_id = ?").bind(params.user); - - if (params.type) - query.where("t_l.type = ?").bind(*params.type); - - if (!params.clusters.empty()) - { - query.join("tracklist_entry t_l_e ON t_l_e.tracklist_id = t_l.id"); - query.join("track t ON t.id = t_l_e.track_id"); - - std::ostringstream oss; - oss << "t.id IN (SELECT DISTINCT t.id FROM track t" - " INNER JOIN track_cluster t_c ON t_c.track_id = t.id" - " INNER JOIN cluster c ON c.id = t_c.cluster_id"; - - WhereClause clusterClause; - for (const ClusterId clusterId : params.clusters) - { - clusterClause.Or(WhereClause("c.id = ?")); - query.bind(clusterId); - } - - oss << " " << clusterClause.get(); - oss << " GROUP BY t.id HAVING COUNT(*) = " << params.clusters.size() << ")"; - - query.where(oss.str()); - } - - switch (params.sortMethod) - { - case TrackListSortMethod::None: - break; - case TrackListSortMethod::Name: - query.orderBy("t_l.name COLLATE NOCASE"); - break; - case TrackListSortMethod::LastModifiedDesc: - query.orderBy("t_l.last_modified_date_time DESC"); - break; - } - - return Utils::execQuery(query, params.range); -} - -TrackList::pointer -TrackList::find(Session& session, TrackListId id) -{ - session.checkSharedLocked(); - - return session.getDboSession().find().where("id = ?").bind(id).resultValue(); -} - -bool -TrackList::isEmpty() const -{ - return _entries.empty(); -} - -std::size_t -TrackList::getCount() const -{ - return _entries.size(); -} - -TrackListEntry::pointer -TrackList::getEntry(std::size_t pos) const -{ - TrackListEntry::pointer res; - - auto entries = getEntries(Range {pos, 1}); - if (!entries.empty()) - res = entries.front(); - - return res; -} - -std::vector -TrackList::getEntries(std::optional range) const -{ - assert(session()); - - auto entries { - session()->find() - .where("tracklist_id = ?").bind(getId()) - .orderBy("id") - .limit(range ? static_cast(range->size) + 1 : -1) - .offset(range ? static_cast(range->offset) : -1) - .resultList()}; - - return std::vector(entries.begin(), entries.end()); -} - -TrackListEntry::pointer -TrackList::getEntryByTrackAndDateTime(ObjectPtr track, const Wt::WDateTime& dateTime) const -{ - assert(session()); - - return session()->find() - .where("tracklist_id = ?").bind(getId()) - .where("track_id = ?").bind(track->getId()) - .where("date_time = ?").bind(Utils::normalizeDateTime(dateTime)) - .resultValue(); -} - -static -Wt::Dbo::Query> -createArtistsQuery(Wt::Dbo::Session& session, const std::string& queryStr, TrackListId tracklistId, const std::vector& clusterIds, std::optional linkType) -{ - auto query {session.query>(queryStr)}; - query.join("track t ON t.id = t_a_l.track_id"); - query.join("track_artist_link t_a_l ON t_a_l.artist_id = a.id"); - query.join("tracklist_entry p_e ON p_e.track_id = t.id"); - query.join("tracklist p ON p.id = p_e.tracklist_id"); - - query.where("p.id = ?").bind(tracklistId); - - if (linkType) - query.where("t_a_l.type = ?").bind(*linkType); - - if (!clusterIds.empty()) - { - std::ostringstream oss; - oss << "a.id IN (SELECT DISTINCT a.id FROM artist a" - " INNER JOIN track t ON t.id = t_a_l.track_id" - " INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id" - " INNER JOIN cluster c ON c.id = t_c.cluster_id" - " INNER JOIN track_cluster t_c ON t_c.track_id = t.id"; - - WhereClause clusterClause; - for (auto id : clusterIds) - { - clusterClause.Or(WhereClause("c.id = ?")); - query.bind(id); - } - - oss << " " << clusterClause.get(); - oss << " GROUP BY t.id,a.id HAVING COUNT(DISTINCT c.id) = " << clusterIds.size() << ")"; - - query.where(oss.str()); - } - - return query; -} - -static -Wt::Dbo::Query> -createReleasesQuery(Wt::Dbo::Session& session, const std::string& queryStr, TrackListId tracklistId, const std::vector& clusterIds) -{ - auto query {session.query>(queryStr)}; - query.join("track t ON t.release_id = r.id"); - query.join("tracklist_entry p_e ON p_e.track_id = t.id"); - query.join("tracklist p ON p.id = p_e.tracklist_id"); - - query.where("p.id = ?").bind(tracklistId); - - if (!clusterIds.empty()) - { - std::ostringstream oss; - oss << "r.id IN (SELECT DISTINCT r.id FROM release r" - " INNER JOIN track t ON t.release_id = r.id" - " INNER JOIN cluster c ON c.id = t_c.cluster_id" - " INNER JOIN track_cluster t_c ON t_c.track_id = t.id"; - - WhereClause clusterClause; - for (ClusterId id : clusterIds) - { - clusterClause.Or(WhereClause("c.id = ?")); - query.bind(id); - } - - oss << " " << clusterClause.get(); - oss << " GROUP BY t.id HAVING COUNT(DISTINCT c.id) = " << clusterIds.size() << ")"; - - query.where(oss.str()); - } - - return query; -} - -static -Wt::Dbo::Query> -createTracksQuery(Wt::Dbo::Session& session, TrackListId tracklistId, const std::vector& clusterIds) -{ - auto query {session.query>("SELECT t from track t INNER JOIN tracklist_entry p_e ON p_e.track_id = t.id INNER JOIN tracklist p ON p.id = p_e.tracklist_id")}; - - query.where("p.id = ?").bind(tracklistId); - - if (!clusterIds.empty()) - { - std::ostringstream oss; - oss << "t.id IN (SELECT DISTINCT t.id FROM track t" - " INNER JOIN track_cluster t_c ON t_c.track_id = t.id" - " INNER JOIN cluster c ON c.id = t_c.cluster_id"; - - WhereClause clusterClause; - for (auto id : clusterIds) - { - clusterClause.Or(WhereClause("c.id = ?")).bind(id.toString()); - query.bind(id); - } - - oss << " " << clusterClause.get(); - oss << " GROUP BY t.id HAVING COUNT(*) = " << clusterIds.size() << ")"; - - query.where(oss.str()); - } - - return query; -} - -std::vector -TrackList::getArtists(const std::vector& clusterIds, std::optional linkType, ArtistSortMethod sortMethod, std::optional range, bool& moreResults) const -{ - assert(session()); - - auto query {createArtistsQuery(*session(), "SELECT a from artist a", getId(), clusterIds, linkType) - .groupBy("a.id").having("p_e.date_time = MAX(p_e.date_time)") - .limit(range ? static_cast(range->size) + 1 : -1) - .offset(range ? static_cast(range->offset) : -1)}; - - switch (sortMethod) - { - case ArtistSortMethod::None: - break; - case ArtistSortMethod::ByName: - query.orderBy("a.name COLLATE NOCASE"); - break; - case ArtistSortMethod::BySortName: - query.orderBy("a.sort_name COLLATE NOCASE"); - break; - case ArtistSortMethod::Random: - query.orderBy("RANDOM()"); - break; - case ArtistSortMethod::LastWritten: - case ArtistSortMethod::StarredDateDesc: - assert(false); // Not implemented! - break; - } - - Wt::Dbo::collection> collection {query.resultList()}; - - auto res {std::vector(collection.begin(), collection.end())}; - if (range && res.size() == static_cast(range->size) + 1) - { - moreResults = true; - res.pop_back(); - } - else - moreResults = false; - - return res; -} - - -std::vector> -TrackList::getReleases(const std::vector& clusterIds, std::optional range, bool& moreResults) const -{ - assert(session()); - - auto collection {createReleasesQuery(*session(), "SELECT r from release r", getId(), clusterIds) - .groupBy("r.id").having("p_e.date_time = MAX(p_e.date_time)") - .limit(range ? static_cast(range->size) + 1 : -1) - .offset(range ? static_cast(range->offset) : -1) - .resultList()}; - - std::vector res(collection.begin(), collection.end()); - if (range && res.size() == static_cast(range->size) + 1) - { - moreResults = true; - res.pop_back(); - } - else - moreResults = false; - - return res; -} - -std::vector> -TrackList::getTracks(const std::vector& clusterIds, std::optional range, bool& moreResults) const -{ - assert(session()); - - auto collection {createTracksQuery(*session(), getId(), clusterIds) - .groupBy("t.id").having("p_e.date_time = MAX(p_e.date_time)") - .limit(range ? static_cast(range->size) + 1 : -1) - .offset(range ? static_cast(range->offset) : -1) - .resultList()}; - - std::vector res(collection.begin(), collection.end()); - if (range && res.size() == static_cast(range->size) + 1) - { - moreResults = true; - res.pop_back(); - } - else - moreResults = false; - - return res; -} - -std::vector -TrackList::getArtistsOrderedByRecentFirst(const std::vector& clusterIds, std::optional linkType, std::optional range, bool& moreResults) const -{ - assert(session()); - - auto collection {createArtistsQuery(*session(), "SELECT a from artist a", getId(), clusterIds, linkType) - .groupBy("a.id").having("p_e.date_time = MAX(p_e.date_time)") - .orderBy("p_e.date_time DESC, p_e.id DESC") - .limit(range ? static_cast(range->size) + 1 : -1) - .offset(range ? static_cast(range->offset) : -1) - .resultList()}; - - auto res {std::vector(collection.begin(), collection.end())}; - if (range && res.size() == static_cast(range->size) + 1) - { - moreResults = true; - res.pop_back(); - } - else - moreResults = false; - - return res; -} - -std::vector -TrackList::getReleasesOrderedByRecentFirst(const std::vector& clusterIds, std::optional range, bool& moreResults) const -{ - assert(session()); - - auto collection {createReleasesQuery(*session(), "SELECT r from release r", getId(), clusterIds) - .groupBy("r.id").having("p_e.date_time = MAX(p_e.date_time)") - .orderBy("p_e.date_time DESC, p_e.id DESC") - .limit(range ? static_cast(range->size) + 1 : -1) - .offset(range ? static_cast(range->offset) : -1) - .resultList()}; - - std::vector res(collection.begin(), collection.end()); - if (range && res.size() == static_cast(range->size) + 1) - { - moreResults = true; - res.pop_back(); - } - else - moreResults = false; - - return res; -} - -std::vector -TrackList::getTracksOrderedByRecentFirst(const std::vector& clusterIds, std::optional range, bool& moreResults) const -{ - assert(session()); - - auto collection {createTracksQuery(*session(), getId(), clusterIds) - .groupBy("t.id").having("p_e.date_time = MAX(p_e.date_time)") - .orderBy("p_e.date_time DESC, p_e.id DESC") - .limit(range ? static_cast(range->size) + 1 : -1) - .offset(range ? static_cast(range->offset) : -1) - .resultList()}; - - std::vector res(collection.begin(), collection.end()); - if (range && res.size() == static_cast(range->size) + 1) - { - moreResults = true; - res.pop_back(); - } - else - moreResults = false; - - return res; -} - -std::vector -TrackList::getClusters() const -{ - assert(session()); - - auto res {session()->query>("SELECT c from cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN tracklist_entry p_e ON p_e.track_id = t.id INNER JOIN tracklist p ON p.id = p_e.tracklist_id") - .where("p.id = ?").bind(getId()) - .groupBy("c.id") - .orderBy("COUNT(c.id) DESC") - .resultList()}; - - return std::vector(res.begin(), res.end()); -} - -std::vector> -TrackList::getClusterGroups(const std::vector& clusterTypes, std::size_t size) const -{ - assert(session()); - std::vector> res; - - if (clusterTypes.empty()) - return res; - - auto query {session()->query>("SELECT c from cluster c")}; - - query.join("track t ON c.id = t_c.cluster_id") - .join("track_cluster t_c ON t_c.track_id = t.id") - .join("cluster_type c_type ON c.cluster_type_id = c_type.id") - .join("tracklist_entry t_l_e ON t_l_e.track_id = t.id") - .join("tracklist t_l ON t_l.id = t_l_e.tracklist_id") - .where("t_l.id = ?").bind(getId()); - - { - std::ostringstream oss; - oss << "c_type.id IN ("; - bool first {true}; - for (auto clusterType : clusterTypes) - { - if (!first) - oss << ", "; - oss << "?"; - query.bind(clusterType ->getId()); - first = false; - } - oss << ")"; - query.where(oss.str()); - } - query.groupBy("c.id"); - query.orderBy("COUNT(c.id) DESC"); - - auto queryRes {query.resultList()}; - - std::map> clustersByType; - for (const Wt::Dbo::ptr& cluster : queryRes) - { - if (clustersByType[cluster->getType()->getId()].size() < size) - clustersByType[cluster->getType()->getId()].push_back(cluster); - } - - for (const auto& [clusterTypeId, clusters] : clustersByType) - res.push_back(clusters); - - return res; -} - -bool -TrackList::hasTrack(TrackId trackId) const -{ - assert(session()); - - Wt::Dbo::collection res = session()->query("SELECT p_e from tracklist_entry p_e INNER JOIN tracklist p ON p_e.tracklist_id = p.id") - .where("p_e.track_id = ?").bind(trackId) - .where("p.id = ?").bind(getId()); - - return res.size() > 0; -} - -std::vector -TrackList::getSimilarTracks(std::optional offset, std::optional size) const -{ - assert(session()); - - auto res {session()->query>( - "SELECT t FROM track t" - " INNER JOIN track_cluster t_c ON t_c.track_id = t.id" - " WHERE " - " (t_c.cluster_id IN (SELECT DISTINCT c.id from cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN tracklist_entry p_e ON p_e.track_id = t.id INNER JOIN tracklist p ON p.id = p_e.tracklist_id WHERE p.id = ?)" - " AND t.id NOT IN (SELECT tracklist_t.id FROM track tracklist_t INNER JOIN tracklist_entry t_e ON t_e.track_id = tracklist_t.id WHERE t_e.tracklist_id = ?))" - ) - .bind(getId()) - .bind(getId()) - .groupBy("t.id") - .orderBy("COUNT(*) DESC, RANDOM()") - .limit(size ? static_cast(*size) : -1) - .offset(offset ? static_cast(*offset) : -1) - .resultList()}; - - return std::vector(res.begin(), res.end()); -} - -std::vector -TrackList::getTrackIds() const -{ - assert(session()); - - Wt::Dbo::collection res = session()->query("SELECT p_e.track_id from tracklist_entry p_e INNER JOIN tracklist p ON p_e.tracklist_id = p.id") - .where("p.id = ?").bind(getId()); - - return std::vector(res.begin(), res.end()); -} - -std::chrono::milliseconds -TrackList::getDuration() const -{ - assert(session()); - - using milli = std::chrono::duration; - - Wt::Dbo::Query query {session()->query("SELECT COALESCE(SUM(duration), 0) FROM track t INNER JOIN tracklist_entry p_e ON t.id = p_e.track_id") - .where("p_e.tracklist_id = ?").bind(getId())}; - - return query.resultValue(); -} - -void -TrackList::setLastModifiedDateTime(const Wt::WDateTime& dateTime) -{ - _lastModifiedDateTime = Utils::normalizeDateTime(dateTime); -} - -std::vector -TrackList::getTopArtists(const std::vector& clusterIds, std::optional linkType, std::optional range, bool& moreResults) const -{ - assert(session()); - - auto query {createArtistsQuery(*session(), "SELECT a from artist a", getId(), clusterIds, linkType)}; - - auto collection {query - .orderBy("COUNT(a.id) DESC") - .groupBy("a.id") - .limit(range ? static_cast(range->size) + 1 : -1) - .offset(range ? static_cast(range->offset) : -1) - .resultList()}; - - std::vector res(collection.begin(), collection.end()); - - if (range && res.size() == static_cast(range->size) + 1) - { - moreResults = true; - res.pop_back(); - } - else - moreResults = false; - - return res; -} - -std::vector -TrackList::getTopReleases(const std::vector& clusterIds, std::optional range, bool& moreResults) const -{ - assert(session()); - - auto query {createReleasesQuery(*session(), "SELECT r from release r", getId(), clusterIds)}; - auto collection {query - .orderBy("COUNT(r.id) DESC") - .groupBy("r.id") - .limit(range ? static_cast(range->size) + 1 : -1) - .offset(range ? static_cast(range->offset) : -1) - .resultList()}; - - std::vector res(collection.begin(), collection.end()); - if (range && res.size() == static_cast(range->size) + 1) - { - moreResults = true; - res.pop_back(); - } - else - moreResults = false; - - return res; -} - -std::vector -TrackList::getTopTracks(const std::vector& clusterIds, std::optional range, bool& moreResults) const -{ - assert(session()); - - auto query {createTracksQuery(*session(), getId(), clusterIds)}; - auto collection {query - .orderBy("COUNT(t.id) DESC") - .groupBy("t.id") - .limit(range ? static_cast(range->size) + 1 : -1) - .offset(range ? static_cast(range->offset) : -1) - .resultList()}; - - std::vector res(collection.begin(), collection.end()); - if (range && res.size() == static_cast(range->size) + 1) - { - moreResults = true; - res.pop_back(); - } - else - moreResults = false; - - return res; -} - -TrackListEntry::TrackListEntry(ObjectPtr track, ObjectPtr tracklist, const Wt::WDateTime& dateTime) -: _dateTime {Utils::normalizeDateTime(dateTime)} -, _track {getDboPtr(track)} -, _tracklist {getDboPtr(tracklist)} -{ - assert(track); - assert(tracklist); -} - -TrackListEntry::pointer -TrackListEntry::create(Session& session, ObjectPtr track, ObjectPtr tracklist, const Wt::WDateTime& dateTime) -{ - return session.getDboSession().add(std::unique_ptr {new TrackListEntry {track, tracklist, dateTime}}); -} - -void -TrackListEntry::onPostCreated() -{ - _tracklist.modify()->setLastModifiedDateTime(Utils::normalizeDateTime(Wt::WDateTime::currentDateTime())); -} - -void -TrackListEntry::onPreRemove() -{ - _tracklist.modify()->setLastModifiedDateTime(Utils::normalizeDateTime(Wt::WDateTime::currentDateTime())); -} - -TrackListEntry::pointer -TrackListEntry::getById(Session& session, TrackListEntryId id) -{ - session.checkSharedLocked(); - - return session.getDboSession().find().where("id = ?").bind(id).resultValue(); -} + TrackList::TrackList(std::string_view name, TrackListType type, bool isPublic, ObjectPtr user) + : _name{ name } + , _type{ type } + , _isPublic{ isPublic } + , _creationDateTime{ Utils::normalizeDateTime(Wt::WDateTime::currentDateTime()) } + , _lastModifiedDateTime{ Utils::normalizeDateTime(Wt::WDateTime::currentDateTime()) } + , _user{ getDboPtr(user) } + { + assert(user); + } + + TrackList::pointer TrackList::create(Session& session, std::string_view name, TrackListType type, bool isPublic, ObjectPtr user) + { + return session.getDboSession().add(std::unique_ptr {new TrackList{ name, type, isPublic, user }}); + } + + std::size_t TrackList::getCount(Session& session) + { + session.checkSharedLocked(); + + return session.getDboSession().query("SELECT COUNT(*) FROM tracklist"); + } + + + TrackList::pointer TrackList::find(Session& session, std::string_view name, TrackListType type, UserId userId) + { + session.checkSharedLocked(); + assert(userId.isValid()); + + return session.getDboSession().find() + .where("name = ?").bind(name) + .where("type = ?").bind(type) + .where("user_id = ?").bind(userId).resultValue(); + } + + RangeResults TrackList::find(Session& session, const FindParameters& params) + { + session.checkSharedLocked(); + + auto query{ session.getDboSession().query("SELECT DISTINCT t_l.id FROM tracklist t_l") }; + + if (params.user.isValid()) + query.where("t_l.user_id = ?").bind(params.user); + + if (params.type) + query.where("t_l.type = ?").bind(*params.type); + + if (!params.clusters.empty()) + { + query.join("tracklist_entry t_l_e ON t_l_e.tracklist_id = t_l.id"); + query.join("track t ON t.id = t_l_e.track_id"); + + std::ostringstream oss; + oss << "t.id IN (SELECT DISTINCT t.id FROM track t" + " INNER JOIN track_cluster t_c ON t_c.track_id = t.id" + " INNER JOIN cluster c ON c.id = t_c.cluster_id"; + + WhereClause clusterClause; + for (const ClusterId clusterId : params.clusters) + { + clusterClause.Or(WhereClause("c.id = ?")); + query.bind(clusterId); + } + + oss << " " << clusterClause.get(); + oss << " GROUP BY t.id HAVING COUNT(*) = " << params.clusters.size() << ")"; + + query.where(oss.str()); + } + + switch (params.sortMethod) + { + case TrackListSortMethod::None: + break; + case TrackListSortMethod::Name: + query.orderBy("t_l.name COLLATE NOCASE"); + break; + case TrackListSortMethod::LastModifiedDesc: + query.orderBy("t_l.last_modified_date_time DESC"); + break; + } + + return Utils::execQuery(query, params.range); + } + + TrackList::pointer TrackList::find(Session& session, TrackListId id) + { + session.checkSharedLocked(); + + return session.getDboSession().find().where("id = ?").bind(id).resultValue(); + } + + bool TrackList::isEmpty() const + { + return _entries.empty(); + } + + std::size_t TrackList::getCount() const + { + return _entries.size(); + } + + TrackListEntry::pointer TrackList::getEntry(std::size_t pos) const + { + TrackListEntry::pointer res; + + auto entries = getEntries(Range{ pos, 1 }); + if (!entries.empty()) + res = entries.front(); + + return res; + } + + std::vector TrackList::getEntries(std::optional range) const + { + assert(session()); + + auto entries{ + session()->find() + .where("tracklist_id = ?").bind(getId()) + .orderBy("id") + .limit(range ? static_cast(range->size) + 1 : -1) + .offset(range ? static_cast(range->offset) : -1) + .resultList() }; + + return std::vector(entries.begin(), entries.end()); + } + + TrackListEntry::pointer TrackList::getEntryByTrackAndDateTime(ObjectPtr track, const Wt::WDateTime& dateTime) const + { + assert(session()); + + return session()->find() + .where("tracklist_id = ?").bind(getId()) + .where("track_id = ?").bind(track->getId()) + .where("date_time = ?").bind(Utils::normalizeDateTime(dateTime)) + .resultValue(); + } + + std::vector TrackList::getClusters() const + { + assert(session()); + + auto res{ session()->query>("SELECT c from cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN tracklist_entry p_e ON p_e.track_id = t.id INNER JOIN tracklist p ON p.id = p_e.tracklist_id") + .where("p.id = ?").bind(getId()) + .groupBy("c.id") + .orderBy("COUNT(c.id) DESC") + .resultList() }; + + return std::vector(res.begin(), res.end()); + } + + std::vector> TrackList::getClusterGroups(const std::vector& clusterTypes, std::size_t size) const + { + assert(session()); + std::vector> res; + + if (clusterTypes.empty()) + return res; + + auto query{ session()->query>("SELECT c from cluster c") }; + + query.join("track t ON c.id = t_c.cluster_id") + .join("track_cluster t_c ON t_c.track_id = t.id") + .join("cluster_type c_type ON c.cluster_type_id = c_type.id") + .join("tracklist_entry t_l_e ON t_l_e.track_id = t.id") + .join("tracklist t_l ON t_l.id = t_l_e.tracklist_id") + .where("t_l.id = ?").bind(getId()); + + { + std::ostringstream oss; + oss << "c_type.id IN ("; + bool first{ true }; + for (auto clusterType : clusterTypes) + { + if (!first) + oss << ", "; + oss << "?"; + query.bind(clusterType->getId()); + first = false; + } + oss << ")"; + query.where(oss.str()); + } + query.groupBy("c.id"); + query.orderBy("COUNT(c.id) DESC"); + + auto queryRes{ query.resultList() }; + + std::map> clustersByType; + for (const Wt::Dbo::ptr& cluster : queryRes) + { + if (clustersByType[cluster->getType()->getId()].size() < size) + clustersByType[cluster->getType()->getId()].push_back(cluster); + } + + for (const auto& [clusterTypeId, clusters] : clustersByType) + res.push_back(clusters); + + return res; + } + + bool TrackList::hasTrack(TrackId trackId) const + { + assert(session()); + + Wt::Dbo::collection res = session()->query("SELECT p_e from tracklist_entry p_e INNER JOIN tracklist p ON p_e.tracklist_id = p.id") + .where("p_e.track_id = ?").bind(trackId) + .where("p.id = ?").bind(getId()); + + return res.size() > 0; + } + + std::vector TrackList::getSimilarTracks(std::optional offset, std::optional size) const + { + assert(session()); + + auto res{ session()->query>( + "SELECT t FROM track t" + " INNER JOIN track_cluster t_c ON t_c.track_id = t.id" + " WHERE " + " (t_c.cluster_id IN (SELECT DISTINCT c.id from cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN tracklist_entry p_e ON p_e.track_id = t.id INNER JOIN tracklist p ON p.id = p_e.tracklist_id WHERE p.id = ?)" + " AND t.id NOT IN (SELECT tracklist_t.id FROM track tracklist_t INNER JOIN tracklist_entry t_e ON t_e.track_id = tracklist_t.id WHERE t_e.tracklist_id = ?))" + ) + .bind(getId()) + .bind(getId()) + .groupBy("t.id") + .orderBy("COUNT(*) DESC, RANDOM()") + .limit(size ? static_cast(*size) : -1) + .offset(offset ? static_cast(*offset) : -1) + .resultList() }; + + return std::vector(res.begin(), res.end()); + } + + std::vector TrackList::getTrackIds() const + { + assert(session()); + + Wt::Dbo::collection res = session()->query("SELECT p_e.track_id from tracklist_entry p_e INNER JOIN tracklist p ON p_e.tracklist_id = p.id") + .where("p.id = ?").bind(getId()); + + return std::vector(res.begin(), res.end()); + } + + std::chrono::milliseconds TrackList::getDuration() const + { + assert(session()); + + using milli = std::chrono::duration; + + Wt::Dbo::Query query{ session()->query("SELECT COALESCE(SUM(duration), 0) FROM track t INNER JOIN tracklist_entry p_e ON t.id = p_e.track_id") + .where("p_e.tracklist_id = ?").bind(getId()) }; + + return query.resultValue(); + } + + void TrackList::setLastModifiedDateTime(const Wt::WDateTime& dateTime) + { + _lastModifiedDateTime = Utils::normalizeDateTime(dateTime); + } + + TrackListEntry::TrackListEntry(ObjectPtr track, ObjectPtr tracklist, const Wt::WDateTime& dateTime) + : _dateTime{ Utils::normalizeDateTime(dateTime) } + , _track{ getDboPtr(track) } + , _tracklist{ getDboPtr(tracklist) } + { + assert(track); + assert(tracklist); + } + + TrackListEntry::pointer TrackListEntry::create(Session& session, ObjectPtr track, ObjectPtr tracklist, const Wt::WDateTime& dateTime) + { + return session.getDboSession().add(std::unique_ptr {new TrackListEntry{ track, tracklist, dateTime }}); + } + + void TrackListEntry::onPostCreated() + { + _tracklist.modify()->setLastModifiedDateTime(Utils::normalizeDateTime(Wt::WDateTime::currentDateTime())); + } + + void TrackListEntry::onPreRemove() + { + _tracklist.modify()->setLastModifiedDateTime(Utils::normalizeDateTime(Wt::WDateTime::currentDateTime())); + } + + TrackListEntry::pointer TrackListEntry::getById(Session& session, TrackListEntryId id) + { + session.checkSharedLocked(); + + return session.getDboSession().find().where("id = ?").bind(id).resultValue(); + } } // namespace Database diff --git a/src/libs/services/database/impl/User.cpp b/src/libs/services/database/impl/User.cpp index 926b4ac3..5831b86b 100644 --- a/src/libs/services/database/impl/User.cpp +++ b/src/libs/services/database/impl/User.cpp @@ -58,7 +58,7 @@ namespace Database { if (params.feedbackBackend) query.where("feedback_backend = ?").bind(*params.feedbackBackend); - return Utils::execQuery(query, params.range); + return Utils::execQuery(query, params.range); } User::pointer User::findDemoUser(Session& session) @@ -80,10 +80,10 @@ namespace Database { .resultValue(); } - void User::setSubsonicDefaultTranscodeBitrate(Bitrate bitrate) + void User::setSubsonicDefaultTranscodingOutputBitrate(Bitrate bitrate) { assert(isAudioBitrateAllowed(bitrate)); - _subsonicDefaultTranscodeBitrate = bitrate; + _subsonicDefaultTranscodingOutputBitrate = bitrate; } void User::clearAuthTokens() diff --git a/src/libs/services/database/impl/Utils.hpp b/src/libs/services/database/impl/Utils.hpp index 213bce59..85623814 100644 --- a/src/libs/services/database/impl/Utils.hpp +++ b/src/libs/services/database/impl/Utils.hpp @@ -19,6 +19,7 @@ #pragma once +#include #include #include @@ -30,57 +31,54 @@ namespace Database::Utils { #define ESCAPE_CHAR_STR "\\" - static inline constexpr char escapeChar {'\\'}; - std::string escapeLikeKeyword(std::string_view keywords); + static inline constexpr char escapeChar{ '\\' }; + std::string escapeLikeKeyword(std::string_view keywords); - template - RangeResults - execQuery(Wt::Dbo::Query& query, Range range) - { - RangeResults res; + template + void applyRange(Query& query, std::optional range) + { + if (range) + { + query.limit(static_cast(range->size)); + query.offset(static_cast(range->offset)); + } + } - auto collection {query.limit(range.size ? static_cast(range.size) + 1 : -1) - .offset(range.offset ? static_cast(range.offset) : -1) - .resultList()}; + template + RangeResults execQuery(Query& query, std::optional range) + { + RangeResults res; - res.results.assign(collection.begin(), collection.end()); - if (range.size && res.results.size() == static_cast(range.size) + 1) - { - res.moreResults = true; - res.results.pop_back(); - } - else - res.moreResults = false; + if (range) + applyRange(query, Range{ range->offset, range->size + 1 }); - res.range.offset = range.offset; - res.range.size = res.results.size(); - return res; - } + auto collection{ query.resultList() }; + res.results.assign(collection.begin(), collection.end()); + if (range && res.results.size() == static_cast(range->size) + 1) + { + // TODO may optim by not actually requesting the last one + res.moreResults = true; + res.results.pop_back(); + } + else + res.moreResults = false; - template - RangeResults - execQuery(Wt::Dbo::Query>& query, Range range) - { - RangeResults res; + res.range.offset = range->offset; + res.range.size = res.results.size(); - auto collection {query.limit(range.size ? static_cast(range.size) + 1 : -1) - .offset(range.offset ? static_cast(range.offset) : -1) - .resultList()}; + return res; + } - res.results.assign(collection.begin(), collection.end()); - if (range.size && res.results.size() == static_cast(range.size) + 1) - { - res.moreResults = true; - res.results.pop_back(); - } - else - res.moreResults = false; + template + void execQuery(Query& query, std::optional range, std::function func) + { + if (range) + applyRange(query, range); - res.range.offset = range.offset; - res.range.size = res.results.size(); - return res; - } + for (const auto& res : query.resultList()) + func(res); + } - Wt::WDateTime normalizeDateTime(const Wt::WDateTime& dateTime); + Wt::WDateTime normalizeDateTime(const Wt::WDateTime& dateTime); } // namespace Database::Utils diff --git a/src/libs/services/database/include/services/database/Artist.hpp b/src/libs/services/database/include/services/database/Artist.hpp index 7fcaadac..e561df6b 100644 --- a/src/libs/services/database/include/services/database/Artist.hpp +++ b/src/libs/services/database/include/services/database/Artist.hpp @@ -58,7 +58,7 @@ namespace Database std::vector keywords; // if non empty, name must match all of these keywords (on either name field OR sort name field) std::optional linkType; // if set, only artists that have produced at least one track with this link type ArtistSortMethod sortMethod{ ArtistSortMethod::None }; - Range range; + std::optional range; Wt::WDateTime writtenAfter; UserId starringUser; // only artists starred by this user std::optional feedbackBackend; // and for this feedback backend @@ -69,7 +69,7 @@ namespace Database FindParameters& setKeywords(const std::vector& _keywords) { keywords = _keywords; return *this; } FindParameters& setLinkType(std::optional _linkType) { linkType = _linkType; return *this; } FindParameters& setSortMethod(ArtistSortMethod _sortMethod) { sortMethod = _sortMethod; return *this; } - FindParameters& setRange(Range _range) { range = _range; return *this; } + FindParameters& setRange(std::optional _range) { range = _range; return *this; } FindParameters& setWrittenAfter(const Wt::WDateTime& _after) { writtenAfter = _after; return *this; } FindParameters& setStarringUser(UserId _user, FeedbackBackend _feedbackBackend) { starringUser = _user; feedbackBackend = _feedbackBackend; return *this; } FindParameters& setTrack(TrackId _track) { track = _track; return *this; } @@ -84,8 +84,9 @@ namespace Database static pointer find(Session& session, ArtistId id); static std::vector find(Session& session, const std::string& name); // exact match on name field static RangeResults find(Session& session, const FindParameters& parameters); + static void find(Session& session, const FindParameters& parameters, std::function func); static RangeResults findIds(Session& session, const FindParameters& parameters); - static RangeResults findOrphanIds(Session& session, Range range); // No track related + static RangeResults findOrphanIds(Session& session, std::optional range = std::nullopt); // No track related static bool exists(Session& session, ArtistId id); // Accessors @@ -94,7 +95,7 @@ namespace Database std::optional getMBID() const { return UUID::fromString(_MBID); } // No artistLinkTypes means get them all - RangeResults findSimilarArtistIds(EnumSet artistLinkTypes = {}, Range range = {}) const; + RangeResults findSimilarArtistIds(EnumSet artistLinkTypes = {}, std::optional range = std::nullopt) const; // Get the cluster of the tracks made by this artist // Each clusters are grouped by cluster type, sorted by the number of occurence diff --git a/src/libs/services/database/include/services/database/Cluster.hpp b/src/libs/services/database/include/services/database/Cluster.hpp index 521727e6..7220d647 100644 --- a/src/libs/services/database/include/services/database/Cluster.hpp +++ b/src/libs/services/database/include/services/database/Cluster.hpp @@ -19,6 +19,7 @@ #pragma once +#include #include #include #include @@ -45,12 +46,12 @@ namespace Database { public: struct FindParameters { - Range range; - ClusterTypeId clusterType; // if non empty, clusters that belong to this cluster type - TrackId track; // if set, clusters involved in this track - ReleaseId release; // if set, clusters involved in this release + std::optional range; + ClusterTypeId clusterType; // if non empty, clusters that belong to this cluster type + TrackId track; // if set, clusters involved in this track + ReleaseId release; // if set, clusters involved in this release - FindParameters& setRange(Range _range) { range = _range; return *this; } + FindParameters& setRange(std::optional _range) { range = _range; return *this; } FindParameters& setClusterType(ClusterTypeId _clusterType) { clusterType = _clusterType; return *this; } FindParameters& setTrack(TrackId _track) { track = _track; return *this; } FindParameters& setRelease(ReleaseId _release) { release = _release; return *this; } @@ -59,26 +60,35 @@ namespace Database { Cluster() = default; // Find utility - // As clusters only have a name, this is an optim to directly get the cluster names - using ClusterFindResult = std::tuple; static std::size_t getCount(Session& session); - static RangeResults find(Session& session, const FindParameters& range); - static pointer find(Session& session, ClusterId id); - static RangeResults findOrphans(Session& session, Range range); + static RangeResults findIds(Session& session, const FindParameters& params); + static RangeResults find(Session& session, const FindParameters& params); + static void find(Session& session, const FindParameters& params, std::function _func); + static pointer find(Session& session, ClusterId id); + static RangeResults findOrphans(Session& session, std::optional range = std::nullopt); + + // May be very slow + static std::size_t computeTrackCount(Session& session, ClusterId id); + static std::size_t computeReleaseCount(Session& session, ClusterId id); // Accessors - const std::string& getName() const { return _name; } - ObjectPtr getType() const { return _clusterType; } - std::size_t getTracksCount() const { return _tracks.size(); } - RangeResults getTracks(Range range) const; - std::size_t getReleasesCount() const; + std::string_view getName() const { return _name; } + ObjectPtr getType() const { return _clusterType; } + std::size_t getTracksCount() const { return _trackCount; } + RangeResults getTracks(std::optional range = std::nullopt) const; + std::size_t getReleasesCount() const { return _releaseCount; }; + void setReleaseCount(std::size_t releaseCount) { _releaseCount = releaseCount; } + void setTrackCount(std::size_t trackCount) { _trackCount = trackCount; } void addTrack(ObjectPtr track); template void persist(Action& a) { Wt::Dbo::field(a, _name, "name"); + // cached field since queries are too long + Wt::Dbo::field(a, _trackCount, "track_count"); + Wt::Dbo::field(a, _releaseCount, "release_count"); Wt::Dbo::belongsTo(a, _clusterType, "cluster_type", Wt::Dbo::OnDeleteCascade); Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade); @@ -92,6 +102,8 @@ namespace Database { static const std::size_t _maxNameLength = 128; std::string _name; + int _trackCount{}; + int _releaseCount{}; Wt::Dbo::ptr _clusterType; Wt::Dbo::collection< Wt::Dbo::ptr > _tracks; @@ -105,11 +117,11 @@ namespace Database { // Getters static std::size_t getCount(Session& session); - static RangeResults find(Session& session, Range range); + static RangeResults find(Session& session, std::optional range = std::nullopt); static pointer find(Session& session, std::string_view name); static pointer find(Session& session, ClusterTypeId id); - static RangeResults findOrphans(Session& session, Range range); - static RangeResults findUsed(Session& session, Range range); + static RangeResults findOrphans(Session& session, std::optional range = std::nullopt); + static RangeResults findUsed(Session& session, std::optional range = std::nullopt); static void remove(Session& session, const std::string& name); diff --git a/src/libs/services/database/include/services/database/Listen.hpp b/src/libs/services/database/include/services/database/Listen.hpp index 20930bc8..66eadcf3 100644 --- a/src/libs/services/database/include/services/database/Listen.hpp +++ b/src/libs/services/database/include/services/database/Listen.hpp @@ -19,6 +19,8 @@ #pragma once +#include + #include #include @@ -45,10 +47,10 @@ namespace Database struct FindParameters { - UserId user; - std::optional backend; - std::optional syncState; - Range range; + UserId user; + std::optional backend; + std::optional syncState; + std::optional range; FindParameters& setUser(UserId _user) { user = _user; return *this; } FindParameters& setScrobblingBackend(ScrobblingBackend _backend) { backend = _backend; return *this; } @@ -63,13 +65,13 @@ namespace Database static RangeResults find(Session& session, const FindParameters& parameters); // Stats - static RangeResults getTopArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, std::optional linkType, Range range = {}); - static RangeResults getTopReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, Range range = {}); - static RangeResults getTopTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, Range range = {}); + static RangeResults getTopArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, std::optional linkType, std::optional range = std::nullopt); + static RangeResults getTopReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, std::optional range = std::nullopt); + static RangeResults getTopTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, std::optional range = std::nullopt); - static RangeResults getRecentArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, std::optional linkType, Range range = {}); - static RangeResults getRecentReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, Range range = {}); - static RangeResults getRecentTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, Range range = {}); + static RangeResults getRecentArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, std::optional linkType, std::optional range = std::nullopt); + static RangeResults getRecentReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, std::optional range = std::nullopt); + static RangeResults getRecentTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, std::optional range = std::nullopt); static std::size_t getCount(Session& session, UserId userId, ScrobblingBackend backend, TrackId trackId); static std::size_t getCount(Session& session, UserId userId, ScrobblingBackend backend, ReleaseId trackId); diff --git a/src/libs/services/database/include/services/database/Release.hpp b/src/libs/services/database/include/services/database/Release.hpp index 28035a38..92bf6ffd 100644 --- a/src/libs/services/database/include/services/database/Release.hpp +++ b/src/libs/services/database/include/services/database/Release.hpp @@ -53,7 +53,7 @@ namespace Database std::vector clusters; // if non empty, releases that belong to these clusters std::vector keywords; // if non empty, name must match all of these keywords ReleaseSortMethod sortMethod{ ReleaseSortMethod::None }; - Range range; + std::optional range; Wt::WDateTime writtenAfter; std::optional dateRange; UserId starringUser; // only releases starred by this user @@ -67,7 +67,7 @@ namespace Database FindParameters& setClusters(const std::vector& _clusters) { clusters = _clusters; return *this; } FindParameters& setKeywords(const std::vector& _keywords) { keywords = _keywords; return *this; } FindParameters& setSortMethod(ReleaseSortMethod _sortMethod) { sortMethod = _sortMethod; return *this; } - FindParameters& setRange(Range _range) { range = _range; return *this; } + FindParameters& setRange(std::optional _range) { range = _range; return *this; } FindParameters& setWrittenAfter(const Wt::WDateTime& _after) { writtenAfter = _after; return *this; } FindParameters& setDateRange(const std::optional& _dateRange) { dateRange = _dateRange; return *this; } FindParameters& setStarringUser(UserId _user, FeedbackBackend _feedbackBackend) { starringUser = _user; feedbackBackend = _feedbackBackend; return *this; } @@ -89,9 +89,10 @@ namespace Database static std::vector find(Session& session, const std::string& name); static pointer find(Session& session, ReleaseId id); static RangeResults find(Session& session, const FindParameters& parameters); + static void find(Session& session, const FindParameters& parameters, std::function func); static RangeResults findIds(Session& session, const FindParameters& parameters); - static RangeResults findOrphanIds(Session& session, Range range); // not track related - static RangeResults findIdsOrderedByArtist(Session& session, Range range); + static RangeResults findOrphanIds(Session& session, std::optional range = std::nullopt); // not track related + static RangeResults findIdsOrderedByArtist(Session& session, std::optional range = std::nullopt); // Get the cluster of the tracks that belong to this release // Each clusters are grouped by cluster type, sorted by the number of occurence (max to min) @@ -103,6 +104,7 @@ namespace Database Wt::WDate getOriginalReleaseDate() const; std::optional getCopyright() const; std::optional getCopyrightURL() const; + std::size_t getMeanBitrate() const; // Accessors const std::string& getName() const { return _name; } diff --git a/src/libs/services/database/include/services/database/StarredTrack.hpp b/src/libs/services/database/include/services/database/StarredTrack.hpp index 7b3e50f7..5db11aaa 100644 --- a/src/libs/services/database/include/services/database/StarredTrack.hpp +++ b/src/libs/services/database/include/services/database/StarredTrack.hpp @@ -45,11 +45,11 @@ namespace Database std::optional backend; // for this backend std::optional syncState; // and these states UserId user; // and this user - Range range; + std::optional range; FindParameters& setFeedbackBackend(FeedbackBackend _backend, SyncState _syncState) { backend = _backend; syncState = _syncState; return *this; } FindParameters& setUser(UserId _user) { user = _user; return *this; } - FindParameters& setRange(Range _range) { range = _range; return *this; } + FindParameters& setRange(std::optional _range) { range = _range; return *this; } }; // Search utility diff --git a/src/libs/services/database/include/services/database/Track.hpp b/src/libs/services/database/include/services/database/Track.hpp index e15aaf1e..3828672e 100644 --- a/src/libs/services/database/include/services/database/Track.hpp +++ b/src/libs/services/database/include/services/database/Track.hpp @@ -46,207 +46,212 @@ namespace Database { -class Artist; -class Cluster; -class ClusterType; -class Release; -class Session; -class TrackArtistLink; -class TrackStats; -class User; + class Artist; + class Cluster; + class ClusterType; + class Release; + class Session; + class TrackArtistLink; + class TrackStats; + class User; -class Track final : public Object -{ - public: - struct FindParameters - { - std::vector clusters; // if non empty, tracks that belong to these clusters - std::vector keywords; // if non empty, name must match all of these keywords - std::string name; // if non empty, must match this name - TrackSortMethod sortMethod {TrackSortMethod::None}; - Range range; - Wt::WDateTime writtenAfter; - UserId starringUser; // only tracks starred by this user - std::optional feedbackBackend; // and for this feedback backend - ArtistId artist; // only tracks that involve this artist - std::string artistName; // only tracks that involve this artist name - EnumSet trackArtistLinkTypes; // and for these link types - bool nonRelease {}; // only tracks that do not belong to a release - ReleaseId release; // matching this release - std::string releaseName; // matching this release name - TrackListId trackList; // matching this trackList - std::optional trackNumber; // matching this track number - bool distinct {true}; + class Track final : public Object + { + public: + struct FindParameters + { + std::vector clusters; // if non empty, tracks that belong to these clusters + std::vector keywords; // if non empty, name must match all of these keywords + std::string name; // if non empty, must match this name + TrackSortMethod sortMethod{ TrackSortMethod::None }; + std::optional range; + Wt::WDateTime writtenAfter; + UserId starringUser; // only tracks starred by this user + std::optional feedbackBackend; // and for this feedback backend + ArtistId artist; // only tracks that involve this artist + std::string artistName; // only tracks that involve this artist name + EnumSet trackArtistLinkTypes; // and for these link types + bool nonRelease{}; // only tracks that do not belong to a release + ReleaseId release; // matching this release + std::string releaseName; // matching this release name + TrackListId trackList; // matching this trackList + std::optional trackNumber; // matching this track number + bool distinct{ true }; - FindParameters& setClusters(const std::vector& _clusters) { clusters = _clusters; return *this; } - FindParameters& setKeywords(const std::vector& _keywords) { keywords = _keywords; return *this; } - FindParameters& setName(std::string_view _name) { name = _name; return *this; } - FindParameters& setSortMethod(TrackSortMethod _method) { sortMethod = _method; return *this; } - FindParameters& setRange(Range _range) { range = _range; return *this; } - FindParameters& setWrittenAfter(const Wt::WDateTime& _after) { writtenAfter = _after; return *this; } - FindParameters& setStarringUser(UserId _user, FeedbackBackend _feedbackBackend) { starringUser = _user; feedbackBackend = _feedbackBackend; return *this; } - FindParameters& setArtist(ArtistId _artist, EnumSet _trackArtistLinkTypes = {}) { artist = _artist; trackArtistLinkTypes = _trackArtistLinkTypes; return *this; } - FindParameters& setArtistName(std::string_view _artistName, EnumSet _trackArtistLinkTypes = {}) { artistName = _artistName; trackArtistLinkTypes = _trackArtistLinkTypes; return *this; } - FindParameters& setNonRelease(bool _nonRelease) { nonRelease = _nonRelease; return *this; } - FindParameters& setRelease(ReleaseId _release) { release = _release; return *this; } - FindParameters& setReleaseName(std::string_view _releaseName) { releaseName = _releaseName; return *this; } - FindParameters& setTrackList(TrackListId _trackList) { trackList = _trackList; return *this; } - FindParameters& setTrackNumber(int _trackNumber) { trackNumber = _trackNumber; return *this; } - FindParameters& setDistinct(bool _distinct) { distinct = _distinct; return *this; } - }; + FindParameters& setClusters(const std::vector& _clusters) { clusters = _clusters; return *this; } + FindParameters& setKeywords(const std::vector& _keywords) { keywords = _keywords; return *this; } + FindParameters& setName(std::string_view _name) { name = _name; return *this; } + FindParameters& setSortMethod(TrackSortMethod _method) { sortMethod = _method; return *this; } + FindParameters& setRange(std::optional _range) { range = _range; return *this; } + FindParameters& setWrittenAfter(const Wt::WDateTime& _after) { writtenAfter = _after; return *this; } + FindParameters& setStarringUser(UserId _user, FeedbackBackend _feedbackBackend) { starringUser = _user; feedbackBackend = _feedbackBackend; return *this; } + FindParameters& setArtist(ArtistId _artist, EnumSet _trackArtistLinkTypes = {}) { artist = _artist; trackArtistLinkTypes = _trackArtistLinkTypes; return *this; } + FindParameters& setArtistName(std::string_view _artistName, EnumSet _trackArtistLinkTypes = {}) { artistName = _artistName; trackArtistLinkTypes = _trackArtistLinkTypes; return *this; } + FindParameters& setNonRelease(bool _nonRelease) { nonRelease = _nonRelease; return *this; } + FindParameters& setRelease(ReleaseId _release) { release = _release; return *this; } + FindParameters& setReleaseName(std::string_view _releaseName) { releaseName = _releaseName; return *this; } + FindParameters& setTrackList(TrackListId _trackList) { trackList = _trackList; return *this; } + FindParameters& setTrackNumber(int _trackNumber) { trackNumber = _trackNumber; return *this; } + FindParameters& setDistinct(bool _distinct) { distinct = _distinct; return *this; } + }; - struct PathResult - { - TrackId trackId; - std::filesystem::path path; - }; + struct PathResult + { + TrackId trackId; + std::filesystem::path path; + }; - Track() = default; + Track() = default; - // Find utility functions - static std::size_t getCount(Session& session); - static pointer findByPath(Session& session, const std::filesystem::path& p); - static pointer find(Session& session, TrackId id); - static bool exists(Session& session, TrackId id); - static std::vector findByRecordingMBID(Session& session, const UUID& MBID); - static std::vector findByMBID(Session& session, const UUID& MBID); - static RangeResults findSimilarTrackIds(Session& session, const std::vector& trackIds, Range range); + // Find utility functions + static std::size_t getCount(Session& session); + static pointer findByPath(Session& session, const std::filesystem::path& p); + static pointer find(Session& session, TrackId id); + static bool exists(Session& session, TrackId id); + static std::vector findByRecordingMBID(Session& session, const UUID& MBID); + static std::vector findByMBID(Session& session, const UUID& MBID); + static RangeResults findSimilarTrackIds(Session& session, const std::vector& trackIds, std::optional range = std::nullopt); - static RangeResults findIds(Session& session, const FindParameters& parameters); - static RangeResults find(Session& session, const FindParameters& parameters); - static RangeResults findPaths(Session& session, Range range); - static RangeResults findIdsTrackMBIDDuplicates(Session& session, Range range); - static RangeResults findIdsWithRecordingMBIDAndMissingFeatures(Session& session, Range range); + static RangeResults findIds(Session& session, const FindParameters& parameters); + static RangeResults find(Session& session, const FindParameters& parameters); + static void find(Session& session, const FindParameters& parameters, std::function func); + static RangeResults findPaths(Session& session, std::optional range = std::nullopt); + static RangeResults findIdsTrackMBIDDuplicates(Session& session, std::optional range = std::nullopt); + static RangeResults findIdsWithRecordingMBIDAndMissingFeatures(Session& session, std::optional range = std::nullopt); - // Accessors - void setScanVersion(std::size_t version) { _scanVersion = version; } - void setTrackNumber(std::optional num) { _trackNumber = num; } - void setDiscNumber(std::optional num) { _discNumber = num; } - void setTotalTrack(std::optional totalTrack) { _totalTrack = totalTrack; } - void setDiscSubtitle(const std::string& name) { _discSubtitle = name; } - void setName(const std::string& name) { _name = std::string(name, 0, _maxNameLength); } - void setPath(const std::filesystem::path& filePath) { _filePath = filePath; } - void setDuration(std::chrono::milliseconds duration) { _duration = duration; } - void setLastWriteTime(Wt::WDateTime time) { _fileLastWrite = time; } - void setAddedTime(Wt::WDateTime time) { _fileAdded = time; } - void setDate(const Wt::WDate& date) { _date = date; } - void setOriginalDate(const Wt::WDate& date) { _originalDate = date; } - void setHasCover(bool hasCover) { _hasCover = hasCover; } - void setTrackMBID(const std::optional& MBID) { _trackMBID = MBID ? MBID->getAsString() : ""; } - void setRecordingMBID(const std::optional& MBID) { _recordingMBID = MBID ? MBID->getAsString() : ""; } - void setCopyright(const std::string& copyright) { _copyright = std::string(copyright, 0, _maxCopyrightLength); } - void setCopyrightURL(const std::string& copyrightURL) { _copyrightURL = std::string(copyrightURL, 0, _maxCopyrightURLLength); } - void setTrackReplayGain(std::optional replayGain) { _trackReplayGain = replayGain; } - void setReleaseReplayGain(std::optional replayGain) { _releaseReplayGain = replayGain; } // may be by disc! - void setArtistDisplayName(std::string_view name) { _artistDisplayName = name; } - void clearArtistLinks(); - void addArtistLink(const ObjectPtr& artistLink); - void setRelease(ObjectPtr release) { _release = getDboPtr(release); } - void setClusters(const std::vector>& clusters ); + // Accessors + void setScanVersion(std::size_t version) { _scanVersion = version; } + void setTrackNumber(std::optional num) { _trackNumber = num; } + void setDiscNumber(std::optional num) { _discNumber = num; } + void setTotalTrack(std::optional totalTrack) { _totalTrack = totalTrack; } + void setDiscSubtitle(const std::string& name) { _discSubtitle = name; } + void setName(const std::string& name) { _name = std::string(name, 0, _maxNameLength); } + void setPath(const std::filesystem::path& filePath) { _filePath = filePath; } + void setDuration(std::chrono::milliseconds duration) { _duration = duration; } + void setBitrate(std::size_t bitrate) { _bitrate = bitrate; } + void setLastWriteTime(Wt::WDateTime time) { _fileLastWrite = time; } + void setAddedTime(Wt::WDateTime time) { _fileAdded = time; } + void setDate(const Wt::WDate& date) { _date = date; } + void setOriginalDate(const Wt::WDate& date) { _originalDate = date; } + void setHasCover(bool hasCover) { _hasCover = hasCover; } + void setTrackMBID(const std::optional& MBID) { _trackMBID = MBID ? MBID->getAsString() : ""; } + void setRecordingMBID(const std::optional& MBID) { _recordingMBID = MBID ? MBID->getAsString() : ""; } + void setCopyright(const std::string& copyright) { _copyright = std::string(copyright, 0, _maxCopyrightLength); } + void setCopyrightURL(const std::string& copyrightURL) { _copyrightURL = std::string(copyrightURL, 0, _maxCopyrightURLLength); } + void setTrackReplayGain(std::optional replayGain) { _trackReplayGain = replayGain; } + void setReleaseReplayGain(std::optional replayGain) { _releaseReplayGain = replayGain; } // may be by disc! + void setArtistDisplayName(std::string_view name) { _artistDisplayName = name; } + void clearArtistLinks(); + void addArtistLink(const ObjectPtr& artistLink); + void setRelease(ObjectPtr release) { _release = getDboPtr(release); } + void setClusters(const std::vector>& clusters); - std::size_t getScanVersion() const { return _scanVersion; } - std::optional getTrackNumber() const { return _trackNumber; } - std::optional getTotalTrack() const { return _totalTrack; } - std::optional getDiscNumber() const { return _discNumber; } - const std::string& getDiscSubtitle() const { return _discSubtitle; } - std::string getName() const { return _name; } - std::filesystem::path getPath() const { return _filePath; } - std::chrono::milliseconds getDuration() const { return _duration; } - const Wt::WDateTime& getLastWritten() const { return _fileLastWrite; } - std::optional getYear() const; - std::optional getOriginalYear() const; - Wt::WDateTime getLastWriteTime() const { return _fileLastWrite; } - Wt::WDateTime getAddedTime() const { return _fileAdded; } - bool hasCover() const { return _hasCover; } - std::optional getTrackMBID() const { return UUID::fromString(_trackMBID); } - std::optional getRecordingMBID() const { return UUID::fromString(_recordingMBID); } - std::optional getCopyright() const; - std::optional getCopyrightURL() const; - std::optional getTrackReplayGain() const { return _trackReplayGain; } - std::optional getReleaseReplayGain() const { return _releaseReplayGain; } - std::string_view getArtistDisplayName() const { return _artistDisplayName; } - // no artistLinkTypes means get all - std::vector> getArtists(EnumSet artistLinkTypes) const; // no type means all - std::vector getArtistIds(EnumSet artistLinkTypes) const; // no type means all - std::vector> getArtistLinks() const; - ObjectPtr getRelease() const { return _release; } - std::vector> getClusters() const; - std::vector getClusterIds() const; + std::size_t getScanVersion() const { return _scanVersion; } + std::optional getTrackNumber() const { return _trackNumber; } + std::optional getTotalTrack() const { return _totalTrack; } + std::optional getDiscNumber() const { return _discNumber; } + const std::string& getDiscSubtitle() const { return _discSubtitle; } + std::string getName() const { return _name; } + std::filesystem::path getPath() const { return _filePath; } + std::chrono::milliseconds getDuration() const { return _duration; } + std::size_t getBitrate() const { return _bitrate; } + const Wt::WDateTime& getLastWritten() const { return _fileLastWrite; } + std::optional getYear() const; + std::optional getOriginalYear() const; + Wt::WDateTime getLastWriteTime() const { return _fileLastWrite; } + Wt::WDateTime getAddedTime() const { return _fileAdded; } + bool hasCover() const { return _hasCover; } + std::optional getTrackMBID() const { return UUID::fromString(_trackMBID); } + std::optional getRecordingMBID() const { return UUID::fromString(_recordingMBID); } + std::optional getCopyright() const; + std::optional getCopyrightURL() const; + std::optional getTrackReplayGain() const { return _trackReplayGain; } + std::optional getReleaseReplayGain() const { return _releaseReplayGain; } + std::string_view getArtistDisplayName() const { return _artistDisplayName; } + // no artistLinkTypes means get all + std::vector> getArtists(EnumSet artistLinkTypes) const; // no type means all + std::vector getArtistIds(EnumSet artistLinkTypes) const; // no type means all + std::vector> getArtistLinks() const; + ObjectPtr getRelease() const { return _release; } + std::vector> getClusters() const; + std::vector getClusterIds() const; - std::vector>> getClusterGroups(const std::vector>& clusterTypes, std::size_t size) const; + std::vector>> getClusterGroups(const std::vector>& clusterTypes, std::size_t size) const; - template - void persist(Action& a) - { - Wt::Dbo::field(a, _scanVersion, "scan_version"); - Wt::Dbo::field(a, _trackNumber, "track_number"); - Wt::Dbo::field(a, _discNumber, "disc_number"); - Wt::Dbo::field(a, _totalTrack, "total_track"); // here in Track since Release does not have concept of "disc" (yet?) - Wt::Dbo::field(a, _discSubtitle, "disc_subtitle"); // here in Track since Release does not have concept of "disc" (yet?) - Wt::Dbo::field(a, _name, "name"); - Wt::Dbo::field(a, _duration, "duration"); - Wt::Dbo::field(a, _date, "date"); - Wt::Dbo::field(a, _originalDate, "original_date"); - Wt::Dbo::field(a, _filePath, "file_path"); - Wt::Dbo::field(a, _fileLastWrite, "file_last_write"); - Wt::Dbo::field(a, _fileAdded, "file_added"); - Wt::Dbo::field(a, _hasCover, "has_cover"); - Wt::Dbo::field(a, _trackMBID, "mbid"); - Wt::Dbo::field(a, _recordingMBID, "recording_mbid"); - Wt::Dbo::field(a, _copyright, "copyright"); - Wt::Dbo::field(a, _copyrightURL, "copyright_url"); - Wt::Dbo::field(a, _trackReplayGain, "track_replay_gain"); - Wt::Dbo::field(a, _releaseReplayGain, "release_replay_gain"); // here in Track since Release does not have concept of "disc" (yet?) - Wt::Dbo::field(a, _artistDisplayName, "artist_display_name"); - Wt::Dbo::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade); - Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "track"); - Wt::Dbo::hasMany(a, _clusters, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade); - } + template + void persist(Action& a) + { + Wt::Dbo::field(a, _scanVersion, "scan_version"); + Wt::Dbo::field(a, _trackNumber, "track_number"); + Wt::Dbo::field(a, _discNumber, "disc_number"); + Wt::Dbo::field(a, _totalTrack, "total_track"); // here in Track since Release does not have concept of "disc" (yet?) + Wt::Dbo::field(a, _discSubtitle, "disc_subtitle"); // here in Track since Release does not have concept of "disc" (yet?) + Wt::Dbo::field(a, _name, "name"); + Wt::Dbo::field(a, _duration, "duration"); + Wt::Dbo::field(a, _bitrate, "bitrate"); + Wt::Dbo::field(a, _date, "date"); + Wt::Dbo::field(a, _originalDate, "original_date"); + Wt::Dbo::field(a, _filePath, "file_path"); + Wt::Dbo::field(a, _fileLastWrite, "file_last_write"); + Wt::Dbo::field(a, _fileAdded, "file_added"); + Wt::Dbo::field(a, _hasCover, "has_cover"); + Wt::Dbo::field(a, _trackMBID, "mbid"); + Wt::Dbo::field(a, _recordingMBID, "recording_mbid"); + Wt::Dbo::field(a, _copyright, "copyright"); + Wt::Dbo::field(a, _copyrightURL, "copyright_url"); + Wt::Dbo::field(a, _trackReplayGain, "track_replay_gain"); + Wt::Dbo::field(a, _releaseReplayGain, "release_replay_gain"); // here in Track since Release does not have concept of "disc" (yet?) + Wt::Dbo::field(a, _artistDisplayName, "artist_display_name"); + Wt::Dbo::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade); + Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "track"); + Wt::Dbo::hasMany(a, _clusters, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade); + } - private: - friend class ::Database::Session; - Track(const std::filesystem::path& p); - static pointer create(Session& session, const std::filesystem::path& p); + private: + friend class ::Database::Session; + Track(const std::filesystem::path& p); + static pointer create(Session& session, const std::filesystem::path& p); - static constexpr std::size_t _maxNameLength {128}; - static constexpr std::size_t _maxCopyrightLength {128}; - static constexpr std::size_t _maxCopyrightURLLength {128}; + static constexpr std::size_t _maxNameLength{ 128 }; + static constexpr std::size_t _maxCopyrightLength{ 128 }; + static constexpr std::size_t _maxCopyrightURLLength{ 128 }; - int _scanVersion {}; - std::optional _trackNumber {}; - std::optional _discNumber {}; - std::optional _totalTrack {}; - std::string _discSubtitle; - std::string _name; - std::chrono::duration _duration {}; - Wt::WDate _date; - Wt::WDate _originalDate; - std::string _filePath; - Wt::WDateTime _fileLastWrite; - Wt::WDateTime _fileAdded; - bool _hasCover {}; - std::string _trackMBID; - std::string _recordingMBID; - std::string _copyright; - std::string _copyrightURL; - std::optional _trackReplayGain; - std::optional _releaseReplayGain; - std::string _artistDisplayName; + int _scanVersion{}; + std::optional _trackNumber{}; + std::optional _discNumber{}; + std::optional _totalTrack{}; + std::string _discSubtitle; + std::string _name; + std::chrono::duration _duration{}; + int _bitrate; // in bps + Wt::WDate _date; + Wt::WDate _originalDate; + std::string _filePath; + Wt::WDateTime _fileLastWrite; + Wt::WDateTime _fileAdded; + bool _hasCover{}; + std::string _trackMBID; + std::string _recordingMBID; + std::string _copyright; + std::string _copyrightURL; + std::optional _trackReplayGain; + std::optional _releaseReplayGain; + std::string _artistDisplayName; - Wt::Dbo::ptr _release; - Wt::Dbo::collection> _trackArtistLinks; - Wt::Dbo::collection> _clusters; -}; + Wt::Dbo::ptr _release; + Wt::Dbo::collection> _trackArtistLinks; + Wt::Dbo::collection> _clusters; + }; -namespace Debug -{ - struct TrackInfo - { - Session& session; - TrackId trackId; - }; - std::ostream& operator<<(std::ostream& os, const TrackInfo& trackInfo); -} + namespace Debug + { + struct TrackInfo + { + Session& session; + TrackId trackId; + }; + std::ostream& operator<<(std::ostream& os, const TrackInfo& trackInfo); + } } // namespace database diff --git a/src/libs/services/database/include/services/database/TrackArtistLink.hpp b/src/libs/services/database/include/services/database/TrackArtistLink.hpp index a1dec682..657d6ae3 100644 --- a/src/libs/services/database/include/services/database/TrackArtistLink.hpp +++ b/src/libs/services/database/include/services/database/TrackArtistLink.hpp @@ -19,6 +19,7 @@ #pragma once +#include #include #include @@ -45,13 +46,13 @@ namespace Database public: struct FindParameters { - Range range; + std::optional range; std::optional linkType; // if set, only artists that have produced at least one track with this link type ArtistId artist; // if set, links involved with this artist ReleaseId release; // if set, artists involved in this release TrackId track; // if set, artists involved in this track - FindParameters& setRange(Range _range) { range = _range; return *this; } + FindParameters& setRange(std::optional _range) { range = _range; return *this; } FindParameters& setLinkType(std::optional _linkType) { linkType = _linkType; return *this; } FindParameters& setArtist(ArtistId _artist) { artist = _artist; return *this; } FindParameters& setRelease(ReleaseId _release) { release = _release; return *this; } diff --git a/src/libs/services/database/include/services/database/TrackBookmark.hpp b/src/libs/services/database/include/services/database/TrackBookmark.hpp index fd8ae8df..15aaaf07 100644 --- a/src/libs/services/database/include/services/database/TrackBookmark.hpp +++ b/src/libs/services/database/include/services/database/TrackBookmark.hpp @@ -20,6 +20,7 @@ #pragma once #include +#include #include @@ -45,7 +46,7 @@ class TrackBookmark final : public Object // Find utility functions static std::size_t getCount(Session& session); static pointer find(Session& session, TrackBookmarkId id); - static RangeResults find(Session& session, UserId userId, Range range); + static RangeResults find(Session& session, UserId userId, std::optional range = std::nullopt); static pointer find(Session& session, UserId userId, TrackId trackId); // Setters diff --git a/src/libs/services/database/include/services/database/TrackFeatures.hpp b/src/libs/services/database/include/services/database/TrackFeatures.hpp index 93dcbcd6..5c7715ec 100644 --- a/src/libs/services/database/include/services/database/TrackFeatures.hpp +++ b/src/libs/services/database/include/services/database/TrackFeatures.hpp @@ -19,6 +19,7 @@ #pragma once +#include #include #include #include @@ -51,7 +52,7 @@ class TrackFeatures final : public Object static std::size_t getCount(Session& session); static pointer find(Session& session, TrackFeaturesId id); static pointer find(Session& session, TrackId trackId); - static RangeResults find(Session& session, Range range); + static RangeResults find(Session& session, std::optional range = std::nullopt); FeatureValues getFeatureValues(const FeatureName& feature) const; FeatureValuesMap getFeatureValuesMap(const std::unordered_set& featureNames) const; diff --git a/src/libs/services/database/include/services/database/TrackList.hpp b/src/libs/services/database/include/services/database/TrackList.hpp index ce84c1a4..dc29ef33 100644 --- a/src/libs/services/database/include/services/database/TrackList.hpp +++ b/src/libs/services/database/include/services/database/TrackList.hpp @@ -36,151 +36,141 @@ namespace Database { -class Artist; -class Cluster; -class ClusterType; -class Release; -class Session; -class Track; -class TrackListEntry; -class User; + class Artist; + class Cluster; + class ClusterType; + class Release; + class Session; + class Track; + class TrackListEntry; + class User; -class TrackList final : public Object -{ - public: - TrackList() = default; + class TrackList final : public Object + { + public: + TrackList() = default; - // Stats utility - std::vector> getTopArtists(const std::vector& clusterIds, std::optional linkType, std::optional range, bool& moreResults) const; - std::vector> getTopReleases(const std::vector& clusterIds, std::optional range, bool& moreResults) const; - std::vector> getTopTracks(const std::vector& clusterIds, std::optional range, bool& moreResults) const; + // Search utility + struct FindParameters + { + std::vector clusters; // if non empty, tracklists that have tracks that belong to these clusters + std::optional range; + std::optional type; + UserId user; // only tracklists owned by this user + TrackListSortMethod sortMethod{ TrackListSortMethod::None }; - // Search utility - struct FindParameters - { - std::vector clusters; // if non empty, tracklists that have tracks that belong to these clusters - Range range; - std::optional type; - UserId user; // only tracklists owned by this user - TrackListSortMethod sortMethod {TrackListSortMethod::None}; + FindParameters& setClusters(const std::vector& _clusters) { clusters = _clusters; return *this; } + FindParameters& setRange(std::optional _range) { range = _range; return *this; } + FindParameters& setType(TrackListType _type) { type = _type; return *this; } + FindParameters& setUser(UserId _user) { user = _user; return *this; } + FindParameters& setSortMethod(TrackListSortMethod _sortMethod) { sortMethod = _sortMethod; return *this; } + }; + static std::size_t getCount(Session& session); + static pointer find(Session& session, std::string_view name, TrackListType type, UserId userId); + static pointer find(Session& session, TrackListId tracklistId); + static RangeResults find(Session& session, const FindParameters& params); - FindParameters& setClusters(const std::vector& _clusters) { clusters = _clusters; return *this; } - FindParameters& setRange(Range _range) { range = _range; return *this; } - FindParameters& setType(TrackListType _type) { type = _type; return *this; } - FindParameters& setUser(UserId _user) { user = _user; return *this; } - FindParameters& setSortMethod(TrackListSortMethod _sortMethod) {sortMethod = _sortMethod; return *this; } - }; - static std::size_t getCount(Session& session); - static pointer find(Session& session, std::string_view name, TrackListType type, UserId userId); - static pointer find(Session& session, TrackListId tracklistId); - static RangeResults find(Session& session, const FindParameters& params); + // Accessors + std::string_view getName() const { return _name; } + bool isPublic() const { return _isPublic; } + TrackListType getType() const { return _type; } + ObjectPtr getUser() const { return _user; } - // Accessors - std::string_view getName() const { return _name; } - bool isPublic() const { return _isPublic; } - TrackListType getType() const { return _type; } - ObjectPtr getUser() const { return _user; } + // Modifiers + void setName(const std::string& name) { _name = name; } + void setIsPublic(bool isPublic) { _isPublic = isPublic; } + void clear() { _entries.clear(); } - // Modifiers - void setName(const std::string& name) { _name = name; } - void setIsPublic(bool isPublic) { _isPublic = isPublic; } - void clear() { _entries.clear(); } + // Get tracks, ordered by position + bool isEmpty() const; + std::size_t getCount() const; + ObjectPtr getEntry(std::size_t pos) const; + std::vector> getEntries(std::optional range = {}) const; + ObjectPtr getEntryByTrackAndDateTime(ObjectPtr track, const Wt::WDateTime& dateTime) const; - // Get tracks, ordered by position - bool isEmpty() const; - std::size_t getCount() const; - ObjectPtr getEntry(std::size_t pos) const; - std::vector> getEntries(std::optional range = {}) const; - ObjectPtr getEntryByTrackAndDateTime(ObjectPtr track, const Wt::WDateTime& dateTime) const; + RangeResults> getArtists(const std::vector& clusters, std::optional linkType, ArtistSortMethod sortMethod, std::optional range, bool& moreResults) const; + RangeResults> getReleases(const std::vector& clusters, std::optional range, bool& moreResults) const; + RangeResults> getTracks(const std::vector& clusters, std::optional range, bool& moreResults) const; - std::vector> getArtists(const std::vector& clusters, std::optional linkType, ArtistSortMethod sortMethod, std::optional range, bool& moreResults) const; - std::vector> getReleases(const std::vector& clusters, std::optional range, bool& moreResults) const; - std::vector> getTracks(const std::vector& clusters, std::optional range, bool& moreResults) const; + std::vector getTrackIds() const; + std::chrono::milliseconds getDuration() const; - // Sorted by date time - std::vector> getArtistsOrderedByRecentFirst(const std::vector& clusterIds, std::optional linkType, std::optional range, bool& moreResults) const; - std::vector> getReleasesOrderedByRecentFirst(const std::vector& clusterIds, std::optional range, bool& moreResults) const; - std::vector> getTracksOrderedByRecentFirst(const std::vector& clusterIds, std::optional range, bool& moreResults) const; + void setLastModifiedDateTime(const Wt::WDateTime& dateTime); - std::vector getTrackIds() const; - std::chrono::milliseconds getDuration() const; + // Get clusters, order by occurence + std::vector> getClusters() const; + std::vector>> getClusterGroups(const std::vector>& clusterTypes, std::size_t size) const; - void setLastModifiedDateTime(const Wt::WDateTime& dateTime); + bool hasTrack(TrackId trackId) const; - // Get clusters, order by occurence - std::vector> getClusters() const; - std::vector>> getClusterGroups(const std::vector>& clusterTypes, std::size_t size) const; + // Ordered from most clusters in common + std::vector> getSimilarTracks(std::optional offset = {}, std::optional size = {}) const; - bool hasTrack(TrackId trackId) const; + template + void persist(Action& a) + { + Wt::Dbo::field(a, _name, "name"); + Wt::Dbo::field(a, _type, "type"); + Wt::Dbo::field(a, _isPublic, "public"); + Wt::Dbo::field(a, _creationDateTime, "creation_date_time"); + Wt::Dbo::field(a, _lastModifiedDateTime, "last_modified_date_time"); - // Ordered from most clusters in common - std::vector> getSimilarTracks(std::optional offset = {}, std::optional size = {}) const; + Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade); + Wt::Dbo::hasMany(a, _entries, Wt::Dbo::ManyToOne, "tracklist"); + } - template - void persist(Action& a) - { - Wt::Dbo::field(a, _name, "name"); - Wt::Dbo::field(a, _type, "type"); - Wt::Dbo::field(a, _isPublic, "public"); - Wt::Dbo::field(a, _creationDateTime, "creation_date_time"); - Wt::Dbo::field(a, _lastModifiedDateTime, "last_modified_date_time"); + private: + friend class Session; + TrackList(std::string_view name, TrackListType type, bool isPublic, ObjectPtr user); + static pointer create(Session& session, std::string_view name, TrackListType type, bool isPublic, ObjectPtr user); - Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade); - Wt::Dbo::hasMany(a, _entries, Wt::Dbo::ManyToOne, "tracklist"); - } + std::string _name; + TrackListType _type{ TrackListType::Playlist }; + bool _isPublic{ false }; + Wt::WDateTime _creationDateTime; + Wt::WDateTime _lastModifiedDateTime; - private: - friend class Session; - TrackList(std::string_view name, TrackListType type, bool isPublic, ObjectPtr user); - static pointer create(Session& session, std::string_view name, TrackListType type, bool isPublic, ObjectPtr user); + Wt::Dbo::ptr _user; + Wt::Dbo::collection> _entries; + }; - std::string _name; - TrackListType _type {TrackListType::Playlist}; - bool _isPublic {false}; - Wt::WDateTime _creationDateTime; - Wt::WDateTime _lastModifiedDateTime; + class TrackListEntry final : public Object + { + public: + TrackListEntry() = default; - Wt::Dbo::ptr _user; - Wt::Dbo::collection> _entries; -}; + bool hasOnPostCreated() const override { return true; } + void onPostCreated() override; -class TrackListEntry final : public Object -{ - public: - TrackListEntry() = default; + bool hasOnPreRemove() const override { return true; } + void onPreRemove() override; - bool hasOnPostCreated() const override { return true; } - void onPostCreated() override; + // find utility + static pointer getById(Session& session, TrackListEntryId id); - bool hasOnPreRemove() const override { return true; } - void onPreRemove() override; + // Accessors + ObjectPtr getTrack() const { return _track; } + const Wt::WDateTime& getDateTime() const { return _dateTime; } - // find utility - static pointer getById(Session& session, TrackListEntryId id); + template + void persist(Action& a) + { + Wt::Dbo::field(a, _dateTime, "date_time"); - // Accessors - ObjectPtr getTrack() const { return _track; } - const Wt::WDateTime& getDateTime() const { return _dateTime; } + Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade); + Wt::Dbo::belongsTo(a, _tracklist, "tracklist", Wt::Dbo::OnDeleteCascade); + } - template - void persist(Action& a) - { - Wt::Dbo::field(a, _dateTime, "date_time"); + private: + friend class Session; + TrackListEntry(ObjectPtr track, ObjectPtr tracklist, const Wt::WDateTime& dateTime); + TrackListEntry(ObjectPtr track, ObjectPtr tracklist); + static pointer create(Session& session, ObjectPtr track, ObjectPtr tracklist, const Wt::WDateTime& dateTime = {}); - Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade); - Wt::Dbo::belongsTo(a, _tracklist, "tracklist", Wt::Dbo::OnDeleteCascade); - } - - private: - friend class Session; - TrackListEntry(ObjectPtr track, ObjectPtr tracklist, const Wt::WDateTime& dateTime); - TrackListEntry(ObjectPtr track, ObjectPtr tracklist); - static pointer create(Session& session, ObjectPtr track, ObjectPtr tracklist, const Wt::WDateTime& dateTime = {}); - - Wt::WDateTime _dateTime; // optional date time - Wt::Dbo::ptr _track; - Wt::Dbo::ptr _tracklist; -}; + Wt::WDateTime _dateTime; // optional date time + Wt::Dbo::ptr _track; + Wt::Dbo::ptr _tracklist; + }; } // namespace Database diff --git a/src/libs/services/database/include/services/database/Types.hpp b/src/libs/services/database/include/services/database/Types.hpp index a07ec274..fc261058 100644 --- a/src/libs/services/database/include/services/database/Types.hpp +++ b/src/libs/services/database/include/services/database/Types.hpp @@ -26,208 +26,224 @@ namespace Database { - // Caution: do not change enum values if they are set! + // Caution: do not change enum values if they are set! - // Request: - // size = 0 => no size limit! - // Response (via RangeResults) - // size => results size - struct Range - { - std::size_t offset {}; - std::size_t size {}; + // Request: + // size = 0 => means we don't want data + // Response (via RangeResults) + // size => results size + struct Range + { + std::size_t offset{}; + std::size_t size{}; - // TODO remove this - operator bool() const { return size != 0; } - }; + bool operator==(const Range& rhs) const { return offset == rhs.offset && size == rhs.size; } + }; - template - struct RangeResults - { - Range range; - std::vector results; - bool moreResults{}; + // Func must return true to continue iterating + template + void foreachSubRange(Range range, std::size_t subRangeSize, Func&& func) + { + assert(subRangeSize > 0); - RangeResults getSubRange(Range subRange) - { - assert(subRange.offset >= range.offset); + Range subRange{ range.offset, std::min(range.size, subRangeSize) }; + while (subRange.size > 0) + { + if (!func(subRange)) + break; - if (!subRange.size) - subRange.size = range.size - (subRange.offset - range.offset); + subRange.offset += subRange.size; + subRange.size = std::min(subRangeSize, range.size - (subRange.offset - range.offset)); + } + } - subRange.offset = std::min(subRange.offset, range.offset + range.size); - subRange.size = std::min(subRange.size, range.offset + range.size - subRange.offset); + template + struct RangeResults + { + Range range; + std::vector results; + bool moreResults{}; - RangeResults subResults; + RangeResults getSubRange(Range subRange) + { + assert(subRange.offset >= range.offset); - auto itBegin {std::cbegin(results) + subRange.offset - range.offset}; - auto itEnd {itBegin + subRange.size}; - subResults.results.reserve(std::distance(itBegin, itEnd)); - std::copy(itBegin, itEnd, std::back_inserter(subResults.results)); + if (!subRange.size) + subRange.size = range.size - (subRange.offset - range.offset); - subResults.range = subRange; - if (subRange.offset + subRange.size == range.offset + range.size) - subResults.moreResults = moreResults; - else - subResults.moreResults = true; + subRange.offset = std::min(subRange.offset, range.offset + range.size); + subRange.size = std::min(subRange.size, range.offset + range.size - subRange.offset); - return subResults; - } - }; + RangeResults subResults; - struct DateRange - { - Wt::WDate begin; - Wt::WDate end; + auto itBegin{ std::cbegin(results) + subRange.offset - range.offset }; + auto itEnd{ itBegin + subRange.size }; + subResults.results.reserve(std::distance(itBegin, itEnd)); + std::copy(itBegin, itEnd, std::back_inserter(subResults.results)); - static DateRange fromYearRange(int from, int to); - }; + subResults.range = subRange; + if (subRange.offset + subRange.size == range.offset + range.size) + subResults.moreResults = moreResults; + else + subResults.moreResults = true; - struct DiscInfo - { - std::size_t position; - std::string name; - }; + return subResults; + } + }; - enum class ArtistSortMethod - { - None, - ByName, - BySortName, - Random, - LastWritten, - StarredDateDesc, - }; + struct DateRange + { + Wt::WDate begin; + Wt::WDate end; - enum class ReleaseSortMethod - { - None, - Name, - Date, - OriginalDate, - OriginalDateDesc, - Random, - LastWritten, - StarredDateDesc, - }; + static DateRange fromYearRange(int from, int to); + }; - enum class TrackListSortMethod - { - None, - Name, - LastModifiedDesc, - }; + struct DiscInfo + { + std::size_t position; + std::string name; + }; - enum class TrackSortMethod - { - None, - Random, - LastWritten, - StarredDateDesc, - Name, - DateDescAndRelease, - Release, // order by disc/track number - TrackList, // order by asc order in tracklist - }; + enum class ArtistSortMethod + { + None, + ByName, + BySortName, + Random, + LastWritten, + StarredDateDesc, + }; - enum class TrackArtistLinkType - { - Artist = 0, // regular track artist - Arranger = 1, - Composer = 2, - Conductor = 3, - Lyricist = 4, - Mixer = 5, - Performer = 6, - Producer = 7, - ReleaseArtist = 8, - Remixer = 9, - Writer = 10, - }; + enum class ReleaseSortMethod + { + None, + Name, + Date, + OriginalDate, + OriginalDateDesc, + Random, + LastWritten, + StarredDateDesc, + }; - // User selectable audio file formats - enum class AudioFormat - { - MP3 = 1, - OGG_OPUS = 2, - OGG_VORBIS = 3, - WEBM_VORBIS = 4, - MATROSKA_OPUS = 5, - }; + enum class TrackListSortMethod + { + None, + Name, + LastModifiedDesc, + }; - using Bitrate = std::uint32_t; - // Do not remove values! - void visitAllowedAudioBitrates(std::function); - bool isAudioBitrateAllowed(Bitrate bitrate); + enum class TrackSortMethod + { + None, + Random, + LastWritten, + StarredDateDesc, + Name, + DateDescAndRelease, + Release, // order by disc/track number + TrackList, // order by asc order in tracklist + }; - enum class ScrobblingBackend - { - Internal = 0, - ListenBrainz = 1, - }; + enum class TrackArtistLinkType + { + Artist = 0, // regular track artist + Arranger = 1, + Composer = 2, + Conductor = 3, + Lyricist = 4, + Mixer = 5, + Performer = 6, + Producer = 7, + ReleaseArtist = 8, + Remixer = 9, + Writer = 10, + }; - enum class FeedbackBackend - { - Internal = 0, - ListenBrainz = 1, - }; + // User selectable transcoding output formats + enum class TranscodingOutputFormat + { + MP3 = 1, + OGG_OPUS = 2, + OGG_VORBIS = 3, + WEBM_VORBIS = 4, + MATROSKA_OPUS = 5, + }; - enum class SyncState - { - PendingAdd = 0, - Synchronized = 1, - PendingRemove = 2, - }; + using Bitrate = std::uint32_t; + // Do not remove values! + void visitAllowedAudioBitrates(std::function); + bool isAudioBitrateAllowed(Bitrate bitrate); - enum class UserType - { - REGULAR = 0, - ADMIN = 1, - DEMO = 2, - }; + enum class ScrobblingBackend + { + Internal = 0, + ListenBrainz = 1, + }; - enum class UITheme - { - Light = 0, - Dark = 1, - }; + enum class FeedbackBackend + { + Internal = 0, + ListenBrainz = 1, + }; - enum class SubsonicArtistListMode - { - AllArtists = 0, - ReleaseArtists = 1, - TrackArtists = 2, - }; + enum class SyncState + { + PendingAdd = 0, + Synchronized = 1, + PendingRemove = 2, + }; - enum class TrackListType - { - Playlist, // user controlled playlists - Internal, // internal usage (current playqueue, history, ...) - }; + enum class UserType + { + REGULAR = 0, + ADMIN = 1, + DEMO = 2, + }; - // as defined in https://musicbrainz.org/doc/Release_Group/Type - enum class ReleaseTypePrimary - { - Album, - Single, - EP, - Broadcast, - Other, - }; + enum class UITheme + { + Light = 0, + Dark = 1, + }; - enum class ReleaseTypeSecondary - { - Compilation, - Soundtrack, - Spokenword, - Interview, - Audiobook, - AudioDrama, - Live, - Remix, - DJMix, - Mixtape_Street, - Demo, - }; + enum class SubsonicArtistListMode + { + AllArtists = 0, + ReleaseArtists = 1, + TrackArtists = 2, + }; + + enum class TrackListType + { + Playlist, // user controlled playlists + Internal, // internal usage (current playqueue, history, ...) + }; + + // as defined in https://musicbrainz.org/doc/Release_Group/Type + enum class ReleaseTypePrimary + { + Album, + Single, + EP, + Broadcast, + Other, + }; + + enum class ReleaseTypeSecondary + { + Compilation, + Soundtrack, + Spokenword, + Interview, + Audiobook, + AudioDrama, + Live, + Remix, + DJMix, + Mixtape_Street, + Demo, + }; } diff --git a/src/libs/services/database/include/services/database/User.hpp b/src/libs/services/database/include/services/database/User.hpp index f177120f..c88269f6 100644 --- a/src/libs/services/database/include/services/database/User.hpp +++ b/src/libs/services/database/include/services/database/User.hpp @@ -49,17 +49,18 @@ namespace Database { { std::optional scrobblingBackend; std::optional feedbackBackend; - Range range; + std::optional range; FindParameters& setFeedbackBackend(FeedbackBackend _feedbackBackend) { feedbackBackend = _feedbackBackend; return *this; } FindParameters& setScrobblingBackend(ScrobblingBackend _scrobblingBackend) { scrobblingBackend = _scrobblingBackend; return *this; } - FindParameters& setRange(Range _range) { range = _range; return *this; } + FindParameters& setRange(std::optional _range) { range = _range; return *this; } }; static inline constexpr std::size_t MinNameLength{ 3 }; static inline constexpr std::size_t MaxNameLength{ 15 }; - static inline constexpr AudioFormat defaultSubsonicTranscodeFormat{ AudioFormat::OGG_OPUS }; - static inline constexpr Bitrate defaultSubsonicTranscodeBitrate{ 128000 }; + static inline constexpr bool defaultSubsonicEnableTranscodingByDefault{ false }; + static inline constexpr TranscodingOutputFormat defaultSubsonicTranscodingOutputFormat{ TranscodingOutputFormat::OGG_OPUS }; + static inline constexpr Bitrate defaultSubsonicTranscodingOutputBitrate{ 128000 }; static inline constexpr UITheme defaultUITheme{ UITheme::Dark }; static inline constexpr SubsonicArtistListMode defaultSubsonicArtistListMode{ SubsonicArtistListMode::AllArtists }; static inline constexpr ScrobblingBackend defaultScrobblingBackend{ ScrobblingBackend::Internal }; @@ -83,8 +84,9 @@ namespace Database { void setLastLogin(const Wt::WDateTime& dateTime) { _lastLogin = dateTime; } void setPasswordHash(const PasswordHash& passwordHash) { _passwordSalt = passwordHash.salt; _passwordHash = passwordHash.hash; } void setType(UserType type) { _type = type; } - void setSubsonicDefaultTranscodeFormat(AudioFormat encoding) { _subsonicDefaultTranscodeFormat = encoding; } - void setSubsonicDefaultTranscodeBitrate(Bitrate bitrate); + void setSubsonicEnableTranscodingByDefault(bool value) { _subsonicEnableTranscodingByDefault = value; } + void setSubsonicDefaultTranscodintOutputFormat(TranscodingOutputFormat encoding) { _subsonicDefaultTranscodingOutputFormat = encoding; } + void setSubsonicDefaultTranscodingOutputBitrate(Bitrate bitrate); void setCurPlayingTrackPos(std::size_t pos) { _curPlayingTrackPos = pos; } void setRadio(bool val) { _radio = val; } void setRepeatAll(bool val) { _repeatAll = val; } @@ -99,8 +101,9 @@ namespace Database { bool isAdmin() const { return _type == UserType::ADMIN; } bool isDemo() const { return _type == UserType::DEMO; } UserType getType() const { return _type; } - AudioFormat getSubsonicDefaultTranscodeFormat() const { return _subsonicDefaultTranscodeFormat; } - Bitrate getSubsonicDefaultTranscodeBitrate() const { return _subsonicDefaultTranscodeBitrate; } + bool getSubsonicEnableTranscodingByDefault() const { return _subsonicEnableTranscodingByDefault; } + TranscodingOutputFormat getSubsonicDefaultTranscodingOutputFormat() const { return _subsonicDefaultTranscodingOutputFormat; } + Bitrate getSubsonicDefaultTranscodingOutputBitrate() const { return _subsonicDefaultTranscodingOutputBitrate; } std::size_t getCurPlayingTrackPos() const { return _curPlayingTrackPos; } bool isRepeatAllSet() const { return _repeatAll; } bool isRadioSet() const { return _radio; } @@ -118,8 +121,9 @@ namespace Database { Wt::Dbo::field(a, _passwordSalt, "password_salt"); Wt::Dbo::field(a, _passwordHash, "password_hash"); Wt::Dbo::field(a, _lastLogin, "last_login"); - Wt::Dbo::field(a, _subsonicDefaultTranscodeFormat, "subsonic_default_transcode_format"); - Wt::Dbo::field(a, _subsonicDefaultTranscodeBitrate, "subsonic_default_transcode_bitrate"); + Wt::Dbo::field(a, _subsonicEnableTranscodingByDefault, "subsonic_enable_transcoding_by_default"); + Wt::Dbo::field(a, _subsonicDefaultTranscodingOutputFormat, "subsonic_default_transcode_format"); + Wt::Dbo::field(a, _subsonicDefaultTranscodingOutputBitrate, "subsonic_default_transcode_bitrate"); Wt::Dbo::field(a, _subsonicArtistListMode, "subsonic_artist_list_mode"); Wt::Dbo::field(a, _uiTheme, "ui_theme"); Wt::Dbo::field(a, _feedbackBackend, "feedback_backend"); @@ -153,8 +157,9 @@ namespace Database { // User defined settings SubsonicArtistListMode _subsonicArtistListMode{ defaultSubsonicArtistListMode }; - AudioFormat _subsonicDefaultTranscodeFormat{ defaultSubsonicTranscodeFormat }; - int _subsonicDefaultTranscodeBitrate{ defaultSubsonicTranscodeBitrate }; + bool _subsonicEnableTranscodingByDefault{ defaultSubsonicEnableTranscodingByDefault }; + TranscodingOutputFormat _subsonicDefaultTranscodingOutputFormat{ defaultSubsonicTranscodingOutputFormat }; + int _subsonicDefaultTranscodingOutputBitrate{ defaultSubsonicTranscodingOutputBitrate }; // User's dynamic data (UI) int _curPlayingTrackPos{}; // Current track position in queue diff --git a/src/libs/services/database/test/Artist.cpp b/src/libs/services/database/test/Artist.cpp index 6e9020fd..b3127c04 100644 --- a/src/libs/services/database/test/Artist.cpp +++ b/src/libs/services/database/test/Artist.cpp @@ -51,7 +51,7 @@ TEST_F(DatabaseFixture, Artist) ASSERT_EQ(artists.results.size(), 1); EXPECT_EQ(artists.results.front(), artist.getId()); - artists = Artist::findOrphanIds(session, Range{}); + artists = Artist::findOrphanIds(session); ASSERT_EQ(artists.results.size(), 1); EXPECT_EQ(artists.results.front(), artist.getId()); } @@ -64,6 +64,18 @@ TEST_F(DatabaseFixture, Artist) ASSERT_EQ(artists.results.size(), 1); EXPECT_EQ(artists.results.front()->getId(), artist.getId()); } + + { + auto transaction{ session.createSharedTransaction() }; + + bool visited{}; + Artist::find(session, Artist::FindParameters{}, [&](const Artist::pointer& a) + { + visited = true; + EXPECT_EQ(a->getId(), artist.getId()); + }); + EXPECT_TRUE(visited); + } } TEST_F(DatabaseFixture, Artist_singleTrack) @@ -80,7 +92,7 @@ TEST_F(DatabaseFixture, Artist_singleTrack) { auto transaction{ session.createSharedTransaction() }; - EXPECT_TRUE(Artist::findOrphanIds(session, Range{}).results.empty()); + EXPECT_TRUE(Artist::findOrphanIds(session).results.empty()); } { @@ -215,7 +227,7 @@ TEST_F(DatabaseFixture, Artist_singleTrackMultiArtists) { auto transaction{ session.createSharedTransaction() }; - EXPECT_TRUE(Artist::findOrphanIds(session, Range{}).results.empty()); + EXPECT_TRUE(Artist::findOrphanIds(session).results.empty()); } { diff --git a/src/libs/services/database/test/Cluster.cpp b/src/libs/services/database/test/Cluster.cpp index 8e0186b6..aeea9be6 100644 --- a/src/libs/services/database/test/Cluster.cpp +++ b/src/libs/services/database/test/Cluster.cpp @@ -49,26 +49,26 @@ TEST_F(DatabaseFixture, Cluster) EXPECT_EQ(cluster->getType()->getId(), clusterType.getId()); { - const auto clusters{ Cluster::find(session, Cluster::FindParameters {}) }; - ASSERT_EQ(clusters.results.size(), 1); - EXPECT_EQ(std::get(clusters.results.front()), cluster.getId()); - } - - { - const auto clusters{ Cluster::findOrphans(session, Range{}) }; + const auto clusters{ Cluster::findIds(session, Cluster::FindParameters {}) }; ASSERT_EQ(clusters.results.size(), 1); EXPECT_EQ(clusters.results.front(), cluster.getId()); } - auto clusterTypes{ ClusterType::find(session, Range {}) }; + { + const auto clusters{ Cluster::findOrphans(session) }; + ASSERT_EQ(clusters.results.size(), 1); + EXPECT_EQ(clusters.results.front(), cluster.getId()); + } + + auto clusterTypes{ ClusterType::find(session) }; ASSERT_EQ(clusterTypes.results.size(), 1); EXPECT_EQ(clusterTypes.results.front(), clusterType.getId()); - clusterTypes = ClusterType::findUsed(session, Range{}); + clusterTypes = ClusterType::findUsed(session); ASSERT_EQ(clusterTypes.results.size(), 1); EXPECT_EQ(clusterTypes.results.front(), clusterType.getId()); - clusterTypes = ClusterType::findOrphans(session, Range{}); + clusterTypes = ClusterType::findOrphans(session); EXPECT_TRUE(clusterTypes.results.empty()); } } @@ -76,11 +76,11 @@ TEST_F(DatabaseFixture, Cluster) { auto transaction{ session.createUniqueTransaction() }; - auto clusterTypes{ ClusterType::findOrphans(session, Range {}) }; + auto clusterTypes{ ClusterType::findOrphans(session) }; ASSERT_EQ(clusterTypes.results.size(), 1); EXPECT_EQ(clusterTypes.results.front(), clusterType.getId()); - ASSERT_TRUE(ClusterType::findUsed(session, Range{}).results.empty()); + ASSERT_TRUE(ClusterType::findUsed(session).results.empty()); } } @@ -91,8 +91,8 @@ TEST_F(DatabaseFixture, Cluster_singleTrack) { auto transaction{ session.createSharedTransaction() }; - EXPECT_TRUE(Cluster::findOrphans(session, Range{}).results.empty()); - auto clusterTypes{ ClusterType::findOrphans(session, Range {}) }; + EXPECT_TRUE(Cluster::findOrphans(session).results.empty()); + auto clusterTypes{ ClusterType::findOrphans(session) }; ASSERT_EQ(clusterTypes.results.size(), 1); EXPECT_EQ(clusterTypes.results.front(), clusterType.getId()); } @@ -102,10 +102,12 @@ TEST_F(DatabaseFixture, Cluster_singleTrack) { auto transaction{ session.createSharedTransaction() }; - auto clusters{ Cluster::findOrphans(session, Range {}) }; + auto clusters{ Cluster::findOrphans(session) }; EXPECT_EQ(clusters.results.size(), 2); EXPECT_TRUE(track->getClusters().empty()); EXPECT_TRUE(track->getClusterIds().empty()); + EXPECT_EQ(Cluster::computeTrackCount(session, cluster1.getId()), 0); + EXPECT_EQ(Cluster::computeTrackCount(session, cluster2.getId()), 0); } { @@ -116,18 +118,20 @@ TEST_F(DatabaseFixture, Cluster_singleTrack) { auto transaction{ session.createSharedTransaction() }; - auto clusters{ Cluster::find(session, Cluster::FindParameters {}.setTrack(track.getId())) }; + auto clusters{ Cluster::findIds(session, Cluster::FindParameters {}.setTrack(track.getId())) }; ASSERT_EQ(clusters.results.size(), 1); - EXPECT_EQ(std::get(clusters.results.front()), cluster1.getId()); + EXPECT_EQ(clusters.results.front(), cluster1.getId()); + EXPECT_EQ(Cluster::computeTrackCount(session, cluster1.getId()), 1); + EXPECT_EQ(Cluster::computeTrackCount(session, cluster2.getId()), 0); } { auto transaction{ session.createSharedTransaction() }; - auto clusters{ Cluster::findOrphans(session, Range {}) }; + auto clusters{ Cluster::findOrphans(session) }; ASSERT_EQ(clusters.results.size(), 1); EXPECT_EQ(clusters.results.front(), cluster2.getId()); - EXPECT_TRUE(ClusterType::findOrphans(session, Range{}).results.empty()); + EXPECT_TRUE(ClusterType::findOrphans(session).results.empty()); } { @@ -182,6 +186,8 @@ TEST_F(DatabaseFixture, Cluster_singleTrackWithSeveralClusters) const auto tracks{ Track::findIds(session, Track::FindParameters{}.setClusters(clusterIds)) }; EXPECT_TRUE(tracks.results.empty()); + EXPECT_EQ(Cluster::computeTrackCount(session, cluster1.getId()), 1); + EXPECT_EQ(Cluster::computeTrackCount(session, cluster2.getId()), 0); } { @@ -196,6 +202,8 @@ TEST_F(DatabaseFixture, Cluster_singleTrackWithSeveralClusters) const auto tracks{ Track::findIds(session, Track::FindParameters{}.setClusters(clusterIds)) }; ASSERT_FALSE(tracks.results.empty()); EXPECT_EQ(tracks.results.front(), track.getId()); + EXPECT_EQ(Cluster::computeTrackCount(session, cluster1.getId()), 1); + EXPECT_EQ(Cluster::computeTrackCount(session, cluster2.getId()), 1); } } @@ -217,11 +225,11 @@ TEST_F(DatabaseFixture, Cluster_multiTracks) { auto transaction{ session.createSharedTransaction() }; - EXPECT_TRUE(Cluster::findOrphans(session, Range{}).results.empty()); + EXPECT_TRUE(Cluster::findOrphans(session).results.empty()); - EXPECT_EQ(cluster->getTracksCount(), tracks.size()); + EXPECT_EQ(Cluster::computeTrackCount(session, cluster.getId()), tracks.size()); - for (TrackId trackId : cluster->getTracks(Range{}).results) + for (TrackId trackId : cluster->getTracks().results) { auto it{ std::find_if(std::cbegin(tracks), std::cend(tracks), [&](const ScopedTrack& track) { return trackId == track.getId(); }) }; EXPECT_TRUE(it != std::cend(tracks)); @@ -229,102 +237,6 @@ TEST_F(DatabaseFixture, Cluster_multiTracks) } } -TEST_F(DatabaseFixture, Cluster_multiTracksMultipleClustersTopRelease) -{ - ScopedClusterType clusterType{ session, "ClusterType" }; - ScopedCluster cluster1{ session, clusterType.lockAndGet(), "Cluster1" }; - ScopedCluster cluster2{ session, clusterType.lockAndGet(), "Cluster2" }; - ScopedCluster cluster3{ session, clusterType.lockAndGet(), "Cluster3" }; - ScopedTrack trackA{ session, "TrackA" }; - ScopedTrack trackB{ session, "TrackB" }; - ScopedTrack trackC{ session, "TrackC" }; - ScopedRelease releaseA{ session, "ReleaseA" }; - ScopedRelease releaseB{ session, "ReleaseB" }; - ScopedRelease releaseC{ session, "ReleaseC" }; - - ScopedUser user{ session, "MyUser" }; - ScopedTrackList trackList{ session, "TrackList", TrackListType::Playlist, false, user.lockAndGet() }; - - { - auto transaction{ session.createSharedTransaction() }; - - EXPECT_EQ(trackList->getDuration(), std::chrono::seconds{ 0 }); - } - - { - auto transaction{ session.createUniqueTransaction() }; - - cluster1.get().modify()->addTrack(trackA.get()); - cluster2.get().modify()->addTrack(trackB.get()); - cluster2.get().modify()->addTrack(trackC.get()); - cluster3.get().modify()->addTrack(trackC.get()); - - trackA.get().modify()->setRelease(releaseA.get()); - trackB.get().modify()->setRelease(releaseB.get()); - trackC.get().modify()->setRelease(releaseC.get()); - } - - { - auto transaction{ session.createUniqueTransaction() }; - - session.create(trackA.get(), trackList.get()); - session.create(trackB.get(), trackList.get()); - session.create(trackB.get(), trackList.get()); - } - - { - auto transaction{ session.createSharedTransaction() }; - - bool hasMore; - const auto releases{ trackList->getTopReleases({}, std::nullopt, hasMore) }; - ASSERT_EQ(releases.size(), 2); - EXPECT_EQ(releases[0]->getId(), releaseB.getId()); - EXPECT_EQ(releases[1]->getId(), releaseA.getId()); - } - - { - auto transaction{ session.createSharedTransaction() }; - - bool hasMore; - auto releases{ trackList->getTopReleases({cluster1.getId()}, std::nullopt, hasMore) }; - ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases[0]->getId(), releaseA.getId()); - - releases = trackList->getTopReleases({ cluster2.getId() }, std::nullopt, hasMore); - ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases[0]->getId(), releaseB.getId()); - - releases = trackList->getTopReleases({ cluster2.getId(), cluster1.getId() }, std::nullopt, hasMore); - EXPECT_TRUE(releases.empty()); - - releases = trackList->getTopReleases({ cluster2.getId(), cluster3.getId() }, std::nullopt, hasMore); - EXPECT_TRUE(releases.empty()); - } - - - { - auto transaction{ session.createUniqueTransaction() }; - - session.create(trackC.get(), trackList.get()); - session.create(trackC.get(), trackList.get()); - session.create(trackC.get(), trackList.get()); - } - - { - auto transaction{ session.createSharedTransaction() }; - - bool hasMore; - auto releases{ trackList->getTopReleases({cluster2.getId(), cluster3.getId()}, std::nullopt, hasMore) }; - ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases[0]->getId(), releaseC.getId()); - - releases = trackList->getTopReleases({ cluster2.getId() }, std::nullopt, hasMore); - ASSERT_EQ(releases.size(), 2); - EXPECT_EQ(releases[0]->getId(), releaseC.getId()); - EXPECT_EQ(releases[1]->getId(), releaseB.getId()); - } -} - TEST_F(DatabaseFixture, Cluster_singleTrackSingleReleaseSingleCluster) { ScopedTrack track{ session, "MyTrackFile" }; @@ -332,7 +244,7 @@ TEST_F(DatabaseFixture, Cluster_singleTrackSingleReleaseSingleCluster) { auto transaction{ session.createSharedTransaction() }; - EXPECT_TRUE(Cluster::findOrphans(session, Range{}).results.empty()); + EXPECT_TRUE(Cluster::findOrphans(session).results.empty()); } ScopedClusterType clusterType{ session, "MyClusterType" }; @@ -341,9 +253,11 @@ TEST_F(DatabaseFixture, Cluster_singleTrackSingleReleaseSingleCluster) { auto transaction{ session.createSharedTransaction() }; - ASSERT_EQ(Cluster::findOrphans(session, Range{}).results.size(), 2); + ASSERT_EQ(Cluster::findOrphans(session).results.size(), 2); EXPECT_TRUE(Release::find(session, Release::FindParameters{}.setClusters({ unusedCluster.getId() })).results.empty()); EXPECT_EQ(Release::find(session, Release::FindParameters{}).results.size(), 1); + EXPECT_EQ(Cluster::computeReleaseCount(session, cluster.getId()), 0); + EXPECT_EQ(Cluster::computeReleaseCount(session, unusedCluster.getId()), 0); } { @@ -357,18 +271,20 @@ TEST_F(DatabaseFixture, Cluster_singleTrackSingleReleaseSingleCluster) auto transaction{ session.createSharedTransaction() }; { - auto clusters{ Cluster::findOrphans(session, Range {}) }; + auto clusters{ Cluster::findOrphans(session) }; ASSERT_EQ(clusters.results.size(), 1); EXPECT_EQ(clusters.results.front(), unusedCluster.getId()); } + EXPECT_EQ(Cluster::computeReleaseCount(session, cluster.getId()), 1); + EXPECT_EQ(Cluster::computeReleaseCount(session, unusedCluster.getId()), 0); } { auto transaction{ session.createSharedTransaction() }; - const auto clusters{ Cluster::find(session, Cluster::FindParameters{}.setRelease(release.getId())) }; + const auto clusters{ Cluster::findIds(session, Cluster::FindParameters{}.setRelease(release.getId())) }; ASSERT_EQ(clusters.results.size(), 1); - EXPECT_EQ(std::get(clusters.results.front()), cluster.getId()); + EXPECT_EQ(clusters.results.front(), cluster.getId()); } { @@ -389,10 +305,10 @@ TEST_F(DatabaseFixture, Cluster_singleTrackSingleReleaseSingleCluster) { auto transaction{ session.createSharedTransaction() }; - EXPECT_EQ(cluster->getReleasesCount(), 1); - EXPECT_EQ(cluster->getTracksCount(), 1); - EXPECT_EQ(unusedCluster->getReleasesCount(), 0); - EXPECT_EQ(unusedCluster->getTracksCount(), 0); + EXPECT_EQ(Cluster::computeReleaseCount(session, cluster.getId()), 1); + EXPECT_EQ(Cluster::computeTrackCount(session, cluster.getId()), 1); + EXPECT_EQ(Cluster::computeReleaseCount(session, unusedCluster.getId()), 0); + EXPECT_EQ(Cluster::computeTrackCount(session, unusedCluster.getId()), 0); } } @@ -413,10 +329,10 @@ TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiClusters) { auto transaction{ session.createSharedTransaction() }; - EXPECT_TRUE(ClusterType::findOrphans(session, Range{}).results.empty()); - EXPECT_EQ(Cluster::findOrphans(session, Range{}).results.size(), 2); - EXPECT_TRUE(Release::findOrphanIds(session, Range{}).results.empty()); - EXPECT_TRUE(Artist::findOrphanIds(session, Range{}).results.empty()); + EXPECT_TRUE(ClusterType::findOrphans(session).results.empty()); + EXPECT_EQ(Cluster::findOrphans(session).results.size(), 2); + EXPECT_TRUE(Release::findOrphanIds(session).results.empty()); + EXPECT_TRUE(Artist::findOrphanIds(session).results.empty()); } { @@ -474,9 +390,9 @@ TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiRolesMultiClusters) { auto transaction{ session.createSharedTransaction() }; - EXPECT_TRUE(Cluster::findOrphans(session, Range{}).results.empty()); - EXPECT_TRUE(Release::findOrphanIds(session, Range{}).results.empty()); - EXPECT_TRUE(Artist::findOrphanIds(session, Range{}).results.empty()); + EXPECT_TRUE(Cluster::findOrphans(session).results.empty()); + EXPECT_TRUE(Release::findOrphanIds(session).results.empty()); + EXPECT_TRUE(Artist::findOrphanIds(session).results.empty()); } { @@ -514,8 +430,8 @@ TEST_F(DatabaseFixture, MultiTracksSingleArtistMultiClusters) { auto transaction{ session.createSharedTransaction() }; - EXPECT_TRUE(Cluster::findOrphans(session, Range{}).results.empty()); - EXPECT_TRUE(Artist::findOrphanIds(session, Range{}).results.empty()); + EXPECT_TRUE(Cluster::findOrphans(session).results.empty()); + EXPECT_TRUE(Artist::findOrphanIds(session).results.empty()); } { @@ -550,7 +466,7 @@ TEST_F(DatabaseFixture, MultipleTracksSingleClusterSimilarity) { auto transaction{ session.createSharedTransaction() }; - const auto similarTracks{ Track::findSimilarTrackIds(session, {tracks.front().getId()}, Range {}) }; + const auto similarTracks{ Track::findSimilarTrackIds(session, {tracks.front().getId()}) }; EXPECT_EQ(similarTracks.results.size(), tracks.size() - 1); for (const TrackId similarTrackId : similarTracks.results) { @@ -598,7 +514,7 @@ TEST_F(DatabaseFixture, MultipleTracksMultipleClustersSimilarity) } { - auto similarTracks{ Track::findSimilarTrackIds(session, {tracks.front().getId()}, Range {}) }; + auto similarTracks{ Track::findSimilarTrackIds(session, {tracks.front().getId()}) }; EXPECT_EQ(similarTracks.results.size(), tracks.size() - 1); for (const TrackId similarTrackId : similarTracks.results) EXPECT_TRUE(std::find_if(std::next(std::cbegin(tracks), 1), std::cend(tracks), [&](const auto& track) { return similarTrackId == track.getId(); }) != std::cend(tracks)); @@ -625,10 +541,10 @@ TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtistSingleCluster) { auto transaction{ session.createSharedTransaction() }; - EXPECT_TRUE(Cluster::findOrphans(session, Range{}).results.empty()); - EXPECT_TRUE(ClusterType::findOrphans(session, Range{}).results.empty()); - EXPECT_TRUE(Artist::findOrphanIds(session, Range{}).results.empty()); - EXPECT_TRUE(Release::findOrphanIds(session, Range{}).results.empty()); + EXPECT_TRUE(Cluster::findOrphans(session).results.empty()); + EXPECT_TRUE(ClusterType::findOrphans(session).results.empty()); + EXPECT_TRUE(Artist::findOrphanIds(session).results.empty()); + EXPECT_TRUE(Release::findOrphanIds(session).results.empty()); } { @@ -763,259 +679,6 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClusters) } } -TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed) -{ - ScopedUser user{ session, "MyUser" }; - ScopedTrackList trackList{ session, "MyTrackList", TrackListType::Playlist, false, user.lockAndGet() }; - ScopedClusterType clusterType{ session, "MyClusterType" }; - ScopedCluster cluster1{ session, clusterType.lockAndGet(), "MyCluster1" }; - ScopedCluster cluster2{ session, clusterType.lockAndGet(), "MyCluster2" }; - ScopedCluster cluster3{ session, clusterType.lockAndGet(), "MyCluster3" }; - ScopedTrack track1{ session, "MyTrack1" }; - ScopedTrack track2{ session, "MyTrack1" }; - ScopedArtist artist1{ session, "MyArtist1" }; - ScopedArtist artist2{ session, "MyArtist2" }; - ScopedRelease release1{ session, "MyRelease1" }; - ScopedRelease release2{ session, "MyRelease2" }; - - const Wt::WDateTime now{ Wt::WDateTime::currentDateTime() }; - - { - auto transaction{ session.createUniqueTransaction() }; - - track1.get().modify()->setRelease(release1.get()); - track2.get().modify()->setRelease(release2.get()); - TrackArtistLink::create(session, track1.get(), artist1.get(), TrackArtistLinkType::Artist); - TrackArtistLink::create(session, track2.get(), artist2.get(), TrackArtistLinkType::Artist); - - cluster1.get().modify()->addTrack(track1.get()); - cluster2.get().modify()->addTrack(track2.get()); - cluster3.get().modify()->addTrack(track1.get()); - cluster3.get().modify()->addTrack(track2.get()); - } - - { - auto transaction{ session.createSharedTransaction() }; - - bool moreResults{}; - EXPECT_TRUE(trackList->getArtistsOrderedByRecentFirst({}, std::nullopt, std::nullopt, moreResults).empty()); - EXPECT_TRUE(trackList->getReleasesOrderedByRecentFirst({}, std::nullopt, moreResults).empty()); - EXPECT_TRUE(trackList->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults).empty()); - } - - { - auto transaction{ session.createUniqueTransaction() }; - - session.create(track1.get(), trackList.get(), now); - } - - { - auto transaction{ session.createSharedTransaction() }; - - bool moreResults{}; - const auto artists{ trackList->getArtistsOrderedByRecentFirst({}, std::nullopt, std::nullopt, moreResults) }; - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front()->getId(), artist1.getId()); - - const auto releases{ trackList->getReleasesOrderedByRecentFirst({}, std::nullopt, moreResults) }; - ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases.front()->getId(), release1.getId()); - - const auto tracks{ trackList->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults) }; - ASSERT_EQ(tracks.size(), 1); - EXPECT_EQ(tracks.front()->getId(), track1.getId()); - } - - { - auto transaction{ session.createSharedTransaction() }; - - bool moreResults{}; - const auto artists{ trackList->getArtistsOrderedByRecentFirst({cluster1.getId()}, std::nullopt, std::nullopt, moreResults) }; - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front()->getId(), artist1.getId()); - - const auto releases{ trackList->getReleasesOrderedByRecentFirst({cluster1.getId()}, std::nullopt, moreResults) }; - ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases.front()->getId(), release1.getId()); - - const auto tracks{ trackList->getTracksOrderedByRecentFirst({cluster1.getId()}, std::nullopt, moreResults) }; - ASSERT_EQ(tracks.size(), 1); - EXPECT_EQ(tracks.front()->getId(), track1.getId()); - } - - { - auto transaction{ session.createSharedTransaction() }; - - bool moreResults{}; - const auto artists{ trackList->getArtistsOrderedByRecentFirst({cluster3.getId()}, std::nullopt, std::nullopt, moreResults) }; - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front()->getId(), artist1.getId()); - - const auto releases{ trackList->getReleasesOrderedByRecentFirst({cluster3.getId()}, std::nullopt, moreResults) }; - ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases.front()->getId(), release1.getId()); - - const auto tracks{ trackList->getTracksOrderedByRecentFirst({cluster3.getId()}, std::nullopt, moreResults) }; - ASSERT_EQ(tracks.size(), 1); - EXPECT_EQ(tracks.front()->getId(), track1.getId()); - } - - { - auto transaction{ session.createSharedTransaction() }; - - bool moreResults{}; - const auto artists{ trackList->getArtistsOrderedByRecentFirst({cluster1.getId()}, TrackArtistLinkType::Artist, std::nullopt, moreResults) }; - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front()->getId(), artist1.getId()); - } - - { - auto transaction{ session.createSharedTransaction() }; - - bool moreResults{}; - const auto artists{ trackList->getArtistsOrderedByRecentFirst({}, TrackArtistLinkType::Artist, std::nullopt, moreResults) }; - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front()->getId(), artist1.getId()); - } - - { - auto transaction{ session.createSharedTransaction() }; - - bool moreResults{}; - EXPECT_TRUE(trackList->getArtistsOrderedByRecentFirst({ cluster2.getId() }, std::nullopt, std::nullopt, moreResults).empty()); - EXPECT_TRUE(trackList->getReleasesOrderedByRecentFirst({ cluster2.getId() }, std::nullopt, moreResults).empty()); - EXPECT_TRUE(trackList->getTracksOrderedByRecentFirst({ cluster2.getId() }, std::nullopt, moreResults).empty()); - - EXPECT_TRUE(trackList->getArtistsOrderedByRecentFirst({}, TrackArtistLinkType::ReleaseArtist, std::nullopt, moreResults).empty()); - } - - { - auto transaction{ session.createUniqueTransaction() }; - - session.create(track2.get(), trackList.get(), now.addSecs(1)); - } - - { - auto transaction{ session.createSharedTransaction() }; - - bool moreResults{}; - const auto artists{ trackList->getArtistsOrderedByRecentFirst({}, std::nullopt, std::nullopt, moreResults) }; - ASSERT_EQ(artists.size(), 2); - EXPECT_EQ(artists[0]->getId(), artist2.getId()); - EXPECT_EQ(artists[1]->getId(), artist1.getId()); - - const auto releases{ trackList->getReleasesOrderedByRecentFirst({}, std::nullopt, moreResults) }; - ASSERT_EQ(releases.size(), 2); - EXPECT_EQ(releases[0]->getId(), release2.getId()); - EXPECT_EQ(releases[1]->getId(), release1.getId()); - - const auto tracks{ trackList->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults) }; - ASSERT_EQ(tracks.size(), 2); - EXPECT_EQ(tracks[0]->getId(), track2.getId()); - EXPECT_EQ(tracks[1]->getId(), track1.getId()); - } - - { - auto transaction{ session.createSharedTransaction() }; - - bool moreResults{}; - const auto artists{ trackList->getArtistsOrderedByRecentFirst({cluster3.getId()}, std::nullopt, std::nullopt, moreResults) }; - ASSERT_EQ(artists.size(), 2); - EXPECT_EQ(artists[0]->getId(), artist2.getId()); - EXPECT_EQ(artists[1]->getId(), artist1.getId()); - - const auto releases{ trackList->getReleasesOrderedByRecentFirst({cluster3.getId()}, std::nullopt, moreResults) }; - ASSERT_EQ(releases.size(), 2); - EXPECT_EQ(releases[0]->getId(), release2.getId()); - EXPECT_EQ(releases[1]->getId(), release1.getId()); - - const auto tracks{ trackList->getTracksOrderedByRecentFirst({cluster3.getId()}, std::nullopt, moreResults) }; - ASSERT_EQ(tracks.size(), 2); - EXPECT_EQ(tracks[0]->getId(), track2.getId()); - EXPECT_EQ(tracks[1]->getId(), track1.getId()); - } - - { - auto transaction{ session.createSharedTransaction() }; - - bool moreResults{}; - const auto artists{ trackList->getArtistsOrderedByRecentFirst({cluster1.getId()}, std::nullopt, std::nullopt, moreResults) }; - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front()->getId(), artist1.getId()); - - const auto releases{ trackList->getReleasesOrderedByRecentFirst({cluster1.getId()}, std::nullopt, moreResults) }; - ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases.front()->getId(), release1.getId()); - - const auto tracks{ trackList->getTracksOrderedByRecentFirst({cluster1.getId()}, std::nullopt, moreResults) }; - ASSERT_EQ(tracks.size(), 1); - EXPECT_EQ(tracks.front()->getId(), track1.getId()); - } - - { - auto transaction{ session.createSharedTransaction() }; - - bool moreResults{}; - const auto artists{ trackList->getArtistsOrderedByRecentFirst({cluster2.getId()}, std::nullopt, std::nullopt, moreResults) }; - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front()->getId(), artist2.getId()); - - const auto releases{ trackList->getReleasesOrderedByRecentFirst({cluster2.getId()}, std::nullopt, moreResults) }; - ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases.front()->getId(), release2.getId()); - - const auto tracks{ trackList->getTracksOrderedByRecentFirst({cluster2.getId()}, std::nullopt, moreResults) }; - ASSERT_EQ(tracks.size(), 1); - EXPECT_EQ(tracks.front()->getId(), track2.getId()); - } - - { - auto transaction{ session.createUniqueTransaction() }; - - session.create(track1.get(), trackList.get(), now.addSecs(2)); - } - - { - auto transaction{ session.createSharedTransaction() }; - - bool moreResults{}; - const auto artists{ trackList->getArtistsOrderedByRecentFirst({}, std::nullopt, std::nullopt, moreResults) }; - ASSERT_EQ(artists.size(), 2); - EXPECT_EQ(artists[0]->getId(), artist1.getId()); - EXPECT_EQ(artists[1]->getId(), artist2.getId()); - - const auto releases{ trackList->getReleasesOrderedByRecentFirst({}, std::nullopt, moreResults) }; - ASSERT_EQ(releases.size(), 2); - EXPECT_EQ(releases[0]->getId(), release1.getId()); - EXPECT_EQ(releases[1]->getId(), release2.getId()); - - const auto tracks{ trackList->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults) }; - ASSERT_EQ(tracks.size(), 2); - EXPECT_EQ(tracks[0]->getId(), track1.getId()); - EXPECT_EQ(tracks[1]->getId(), track2.getId()); - } - - { - auto transaction{ session.createSharedTransaction() }; - - bool moreResults{}; - const auto artists{ trackList->getArtistsOrderedByRecentFirst({cluster3.getId()}, std::nullopt, std::nullopt, moreResults) }; - ASSERT_EQ(artists.size(), 2); - EXPECT_EQ(artists[0]->getId(), artist1.getId()); - EXPECT_EQ(artists[1]->getId(), artist2.getId()); - - const auto releases{ trackList->getReleasesOrderedByRecentFirst({cluster3.getId()}, std::nullopt, moreResults) }; - ASSERT_EQ(releases.size(), 2); - EXPECT_EQ(releases[0]->getId(), release1.getId()); - EXPECT_EQ(releases[1]->getId(), release2.getId()); - - const auto tracks{ trackList->getTracksOrderedByRecentFirst({cluster3.getId()}, std::nullopt, moreResults) }; - ASSERT_EQ(tracks.size(), 2); - EXPECT_EQ(tracks[0]->getId(), track1.getId()); - EXPECT_EQ(tracks[1]->getId(), track2.getId()); - } -} - TEST_F(DatabaseFixture, MultipleTracksMultipleArtistsMultiClusters) { ScopedArtist artist1{ session, "MyArtist1" }; diff --git a/src/libs/services/database/test/Common.cpp b/src/libs/services/database/test/Common.cpp index c32d8932..d63ce4e4 100644 --- a/src/libs/services/database/test/Common.cpp +++ b/src/libs/services/database/test/Common.cpp @@ -36,174 +36,208 @@ #include "services/database/User.hpp" TmpDatabase::TmpDatabase() -: _tmpFile {std::tmpnam(nullptr)} -, _fileDeleter {_tmpFile} -, _db {_tmpFile} + : _tmpFile{ std::tmpnam(nullptr) } + , _fileDeleter{ _tmpFile } + , _db{ _tmpFile } { } -Database::Db& -TmpDatabase::getDb() +Database::Db& TmpDatabase::getDb() { - return _db; + return _db; } DatabaseFixture::~DatabaseFixture() { - testDatabaseEmpty(); + testDatabaseEmpty(); } -void -DatabaseFixture::SetUpTestCase() +void DatabaseFixture::SetUpTestCase() { - _tmpDb = std::make_unique(); - { - Database::Session s {_tmpDb->getDb()}; - s.prepareTables(); - s.analyze(); + _tmpDb = std::make_unique(); + { + Database::Session s{ _tmpDb->getDb() }; + s.prepareTables(); + s.analyze(); - // remove default created entries - { - auto transaction {s.createUniqueTransaction()}; - auto clusterTypes {Database::ClusterType::find(s, Database::Range {})}; - for (auto clusterTypeId : clusterTypes.results) - { - auto clusterType {Database::ClusterType::find(s, clusterTypeId)}; - clusterType.remove(); - } - } - } + // remove default created entries + { + auto transaction{ s.createUniqueTransaction() }; + + for (const Database::ClusterTypeId clusterTypeId : Database::ClusterType::find(s).results) + { + auto clusterType{ Database::ClusterType::find(s, clusterTypeId) }; + clusterType.remove(); + } + } + } } -void -DatabaseFixture::TearDownTestCase() +void DatabaseFixture::TearDownTestCase() { - _tmpDb.reset(); + _tmpDb.reset(); } -void -DatabaseFixture::testDatabaseEmpty() +void DatabaseFixture::testDatabaseEmpty() { - using namespace Database; + using namespace Database; - auto uniqueTransaction {session.createUniqueTransaction()}; + auto uniqueTransaction{ session.createUniqueTransaction() }; - EXPECT_EQ(Artist::getCount(session), 0); - EXPECT_EQ(Cluster::getCount(session), 0); - EXPECT_EQ(ClusterType::getCount(session), 0); - EXPECT_EQ(Listen::getCount(session), 0); - EXPECT_EQ(Release::getCount(session), 0); - EXPECT_EQ(StarredArtist::getCount(session), 0); - EXPECT_EQ(StarredRelease::getCount(session), 0); - EXPECT_EQ(StarredTrack::getCount(session), 0); - EXPECT_EQ(Track::getCount(session), 0); - EXPECT_EQ(TrackBookmark::getCount(session), 0); - EXPECT_EQ(TrackList::getCount(session), 0); - EXPECT_EQ(User::getCount(session), 0); + EXPECT_EQ(Artist::getCount(session), 0); + EXPECT_EQ(Cluster::getCount(session), 0); + EXPECT_EQ(ClusterType::getCount(session), 0); + EXPECT_EQ(Listen::getCount(session), 0); + EXPECT_EQ(Release::getCount(session), 0); + EXPECT_EQ(StarredArtist::getCount(session), 0); + EXPECT_EQ(StarredRelease::getCount(session), 0); + EXPECT_EQ(StarredTrack::getCount(session), 0); + EXPECT_EQ(Track::getCount(session), 0); + EXPECT_EQ(TrackBookmark::getCount(session), 0); + EXPECT_EQ(TrackList::getCount(session), 0); + EXPECT_EQ(User::getCount(session), 0); } TEST_F(DatabaseFixture, Common_subRangeEmpty) { - using namespace Database; + using namespace Database; - RangeResults results; - results.range = Range {0, 0}; - results.results = {}; - results.moreResults = false; + RangeResults results; + results.range = Range{ 0, 0 }; + results.results = {}; + results.moreResults = false; - { - auto subRange {results.getSubRange(Range {0, 0})}; - EXPECT_FALSE(subRange.moreResults); - ASSERT_EQ(subRange.results.size(), 0); - EXPECT_EQ(subRange.range, Range {}); - } - { - auto subRange {results.getSubRange(Range {0, 1})}; - EXPECT_FALSE(subRange.moreResults); - ASSERT_EQ(subRange.results.size(), 0); - } + { + auto subRange{ results.getSubRange(Range {0, 0}) }; + EXPECT_FALSE(subRange.moreResults); + ASSERT_EQ(subRange.results.size(), 0); + EXPECT_EQ(subRange.range, Range{}); + } + { + auto subRange{ results.getSubRange(Range {0, 1}) }; + EXPECT_FALSE(subRange.moreResults); + ASSERT_EQ(subRange.results.size(), 0); + } +} + +TEST_F(DatabaseFixture, Common_subRangeForeach) +{ + using namespace Database; + + struct TestCase + { + Range range; + std::size_t subRangeSize; + std::vector expectedSubRanges; + }; + + TestCase testCases[] + { + {Range{0, 0}, 1, {}}, + {Range{1, 0}, 1, {}}, + {Range{1, 1}, 1, { Range{ 1,1 } }}, + {Range{1, 3}, 1, { Range{ 1,1 }, Range {2,1}, Range{3,1} }}, + {Range{0, 100}, 100, { Range{0,100} }}, + {Range{0, 50}, 100, { Range{0,50} }}, + {Range{100, 200}, 100, { Range{100,100}, Range{200,100} }}, + {Range{100, 101}, 100, { Range{100,100}, Range{200,1}}}, + {Range{1000, 10}, 100, { Range{1000,10} }}, + {Range{1, 100}, 50, { Range{1,50}, Range{51, 50} }}, + }; + + for (const TestCase& test : testCases) + { + std::vector subRanges; + foreachSubRange(test.range, test.subRangeSize, [&](Range subRange) + { + subRanges.push_back(subRange); + return true; + }); + + EXPECT_EQ(subRanges, test.expectedSubRanges) << ", test index = " << std::distance(std::cbegin(testCases), &test); + } } TEST_F(DatabaseFixture, Common_IdType) { - using namespace Database; + using namespace Database; - { - const IdType id{}; - EXPECT_FALSE(id.isValid()); - } + { + const IdType id{}; + EXPECT_FALSE(id.isValid()); + } - { - const IdType id{0}; - EXPECT_TRUE(id.isValid()); - } + { + const IdType id{ 0 }; + EXPECT_TRUE(id.isValid()); + } - { - const IdType id1{0}; - const IdType id2{0}; - EXPECT_EQ(id1, id2); - } + { + const IdType id1{ 0 }; + const IdType id2{ 0 }; + EXPECT_EQ(id1, id2); + } - { - const IdType id1{0}; - const IdType id2{1}; - EXPECT_NE(id1, id2); - EXPECT_LT(id1, id2); - EXPECT_GT(id2, id1); - } + { + const IdType id1{ 0 }; + const IdType id2{ 1 }; + EXPECT_NE(id1, id2); + EXPECT_LT(id1, id2); + EXPECT_GT(id2, id1); + } } TEST_F(DatabaseFixture, Common_subRange) { - using namespace Database; + using namespace Database; - RangeResults results; - results.range = Range {0, 2}; - results.results = {5, 6}; - results.moreResults = false; + RangeResults results; + results.range = Range{ 0, 2 }; + results.results = { 5, 6 }; + results.moreResults = false; - { - auto subRange {results.getSubRange(Range {0, 1})}; - EXPECT_TRUE(subRange.moreResults); - ASSERT_EQ(subRange.results.size(), 1); - EXPECT_EQ(subRange.results.front(), 5); - } - { - auto subRange {results.getSubRange(Range {1, 1})}; - EXPECT_FALSE(subRange.moreResults); - ASSERT_EQ(subRange.results.size(), 1); - EXPECT_EQ(subRange.results.front(), 6); - } - { - auto subRange {results.getSubRange(Range {0, 2})}; - EXPECT_FALSE(subRange.moreResults); - ASSERT_EQ(subRange.results.size(), 2); - EXPECT_EQ(subRange.results.front(), 5); - EXPECT_EQ(subRange.results.back(), 6); - } - { - auto subRange {results.getSubRange(Range {})}; - EXPECT_FALSE(subRange.moreResults); - ASSERT_EQ(subRange.results.size(), 2); - EXPECT_EQ(subRange.results.front(), 5); - EXPECT_EQ(subRange.results.back(), 6); - EXPECT_EQ(subRange.range, results.range); - } + { + auto subRange{ results.getSubRange(Range {0, 1}) }; + EXPECT_TRUE(subRange.moreResults); + ASSERT_EQ(subRange.results.size(), 1); + EXPECT_EQ(subRange.results.front(), 5); + } + { + auto subRange{ results.getSubRange(Range {1, 1}) }; + EXPECT_FALSE(subRange.moreResults); + ASSERT_EQ(subRange.results.size(), 1); + EXPECT_EQ(subRange.results.front(), 6); + } + { + auto subRange{ results.getSubRange(Range {0, 2}) }; + EXPECT_FALSE(subRange.moreResults); + ASSERT_EQ(subRange.results.size(), 2); + EXPECT_EQ(subRange.results.front(), 5); + EXPECT_EQ(subRange.results.back(), 6); + } + { + auto subRange{ results.getSubRange(Range {}) }; + EXPECT_FALSE(subRange.moreResults); + ASSERT_EQ(subRange.results.size(), 2); + EXPECT_EQ(subRange.results.front(), 5); + EXPECT_EQ(subRange.results.back(), 6); + EXPECT_EQ(subRange.range, results.range); + } - { - auto subRange {results.getSubRange(Range {1, 0})}; - EXPECT_FALSE(subRange.moreResults); - ASSERT_EQ(subRange.results.size(), 1); - EXPECT_EQ(subRange.results.front(), 6); - const Range expectedRange {1, 1}; - EXPECT_EQ(subRange.range, expectedRange); - } - { - auto subRange {results.getSubRange(Range {3, 2})}; - EXPECT_FALSE(subRange.moreResults); - ASSERT_EQ(subRange.results.size(), 0); - const Range expectedRange {2, 0}; - EXPECT_EQ(subRange.range, expectedRange); - } + { + auto subRange{ results.getSubRange(Range {1, 0}) }; + EXPECT_FALSE(subRange.moreResults); + ASSERT_EQ(subRange.results.size(), 1); + EXPECT_EQ(subRange.results.front(), 6); + const Range expectedRange{ 1, 1 }; + EXPECT_EQ(subRange.range, expectedRange); + } + { + auto subRange{ results.getSubRange(Range {3, 2}) }; + EXPECT_FALSE(subRange.moreResults); + ASSERT_EQ(subRange.results.size(), 0); + const Range expectedRange{ 2, 0 }; + EXPECT_EQ(subRange.range, expectedRange); + } } diff --git a/src/libs/services/database/test/DatabaseTest.cpp b/src/libs/services/database/test/DatabaseTest.cpp index 2b35929f..bb1926dd 100644 --- a/src/libs/services/database/test/DatabaseTest.cpp +++ b/src/libs/services/database/test/DatabaseTest.cpp @@ -42,8 +42,8 @@ TEST_F(DatabaseFixture, MultiTracksSingleArtistSingleRelease) { auto transaction{ session.createSharedTransaction() }; - EXPECT_TRUE(Release::findOrphanIds(session, Range{}).results.empty()); - EXPECT_TRUE(Artist::findOrphanIds(session, Range{}).results.empty()); + EXPECT_TRUE(Release::findOrphanIds(session).results.empty()); + EXPECT_TRUE(Artist::findOrphanIds(session).results.empty()); } { diff --git a/src/libs/services/database/test/Release.cpp b/src/libs/services/database/test/Release.cpp index cb3a35b2..520c5530 100644 --- a/src/libs/services/database/test/Release.cpp +++ b/src/libs/services/database/test/Release.cpp @@ -40,7 +40,7 @@ TEST_F(DatabaseFixture, Release) EXPECT_TRUE(Release::exists(session, release.getId())); { - const auto releases{ Release::findOrphanIds(session, Range {}) }; + const auto releases{ Release::findOrphanIds(session) }; ASSERT_EQ(releases.results.size(), 1); EXPECT_EQ(releases.results.front(), release.getId()); } @@ -57,6 +57,16 @@ TEST_F(DatabaseFixture, Release) ASSERT_EQ(releases.results.size(), 1); EXPECT_EQ(releases.results.front()->getId(), release.getId()); } + + { + bool visited{}; + Release::find(session, Release::FindParameters{}, [&](const Release::pointer& r) + { + visited = true; + EXPECT_EQ(r->getId(), release.getId()); + }); + EXPECT_TRUE(visited); + } } } @@ -76,7 +86,7 @@ TEST_F(DatabaseFixture, Release_singleTrack) { auto transaction{ session.createSharedTransaction() }; - EXPECT_TRUE(Release::findOrphanIds(session, Range{}).results.empty()); + EXPECT_TRUE(Release::findOrphanIds(session).results.empty()); const auto tracks{ Track::findIds(session, Track::FindParameters {}.setRelease(release.getId())) }; ASSERT_EQ(tracks.results.size(), 1); @@ -114,7 +124,7 @@ TEST_F(DatabaseFixture, Release_singleTrack) const auto tracks{ Track::findIds(session, Track::FindParameters {}.setRelease(release.getId())) }; EXPECT_TRUE(tracks.results.empty()); - auto releases{ Release::findOrphanIds(session, Range {}) }; + auto releases{ Release::findOrphanIds(session) }; ASSERT_EQ(releases.results.size(), 1); EXPECT_EQ(releases.results.front(), release.getId()); } @@ -537,7 +547,7 @@ TEST_F(DatabaseFixture, Release_releaseType) } } -TEST_F(DatabaseFixture, ReleaseSortOrder) +TEST_F(DatabaseFixture, Release_sortMethod) { ScopedRelease release1{ session, "MyRelease1" }; const Wt::WDate release1Date{ Wt::WDate {2000, 2, 3} }; @@ -606,3 +616,41 @@ TEST_F(DatabaseFixture, ReleaseSortOrder) } } + +TEST_F(DatabaseFixture, Release_meanBitrate) +{ + ScopedRelease release1{ session, "MyRelease1" }; + ScopedTrack track1{ session, "MyTrack1" }; + ScopedTrack track2{ session, "MyTrack2" }; + ScopedTrack track3{ session, "MyTrack3" }; + + auto checkExpectedBitrate = [&](std::size_t bitrate) + { + auto transaction{ session.createSharedTransaction() }; + EXPECT_EQ(release1->getMeanBitrate(), bitrate); + }; + + checkExpectedBitrate(0); + + { + auto transaction{ session.createUniqueTransaction() }; + track1.get().modify()->setBitrate(128); + track1.get().modify()->setRelease(release1.get()); + } + + checkExpectedBitrate(128); + + { + auto transaction{ session.createUniqueTransaction() }; + track2.get().modify()->setBitrate(256); + track2.get().modify()->setRelease(release1.get()); + } + checkExpectedBitrate(192); + + { + auto transaction{ session.createUniqueTransaction() }; + track3.get().modify()->setBitrate(0); + track3.get().modify()->setRelease(release1.get()); + } + checkExpectedBitrate(192); // 0 should not be taken into account +} \ No newline at end of file diff --git a/src/libs/services/database/test/Track.cpp b/src/libs/services/database/test/Track.cpp index 0711edb6..c88e9b69 100644 --- a/src/libs/services/database/test/Track.cpp +++ b/src/libs/services/database/test/Track.cpp @@ -31,6 +31,12 @@ TEST_F(DatabaseFixture, Track) EXPECT_EQ(Track::findIds(session, Track::FindParameters{}).results.size(), 0); EXPECT_EQ(Track::getCount(session), 0); EXPECT_FALSE(Track::exists(session, 0)); + + { + bool visited{}; + Track::find(session, Track::FindParameters{}, [&](const Track::pointer&) {visited = true;}); + EXPECT_FALSE(visited); + } } ScopedTrack track{ session, "MyTrackFile" }; @@ -44,6 +50,16 @@ TEST_F(DatabaseFixture, Track) auto myTrack{ Track::find(session, track.getId()) }; ASSERT_TRUE(myTrack); EXPECT_EQ(myTrack->getId(), track.getId()); + + { + bool visited{}; + Track::find(session, Track::FindParameters{}, [&](const Track::pointer& t) + { + visited = true; + EXPECT_EQ(t->getId(), track.getId()); + }); + EXPECT_TRUE(visited); + } } } diff --git a/src/libs/services/database/test/TrackBookmark.cpp b/src/libs/services/database/test/TrackBookmark.cpp index d99b0bd5..d8720beb 100644 --- a/src/libs/services/database/test/TrackBookmark.cpp +++ b/src/libs/services/database/test/TrackBookmark.cpp @@ -49,7 +49,7 @@ TEST_F(DatabaseFixture, TrackBookmark) EXPECT_EQ(TrackBookmark::getCount(session), 1); - const auto bookmarks {TrackBookmark::find(session, user.getId(), Range {})}; + const auto bookmarks {TrackBookmark::find(session, user.getId())}; ASSERT_EQ(bookmarks.results.size(), 1); EXPECT_EQ(bookmarks.results.front(), bookmark.getId()); } diff --git a/src/libs/services/database/test/TrackFeatures.cpp b/src/libs/services/database/test/TrackFeatures.cpp index 5d919a40..0f824124 100644 --- a/src/libs/services/database/test/TrackFeatures.cpp +++ b/src/libs/services/database/test/TrackFeatures.cpp @@ -41,7 +41,7 @@ TEST_F(DatabaseFixture, TrackFeatures) auto transaction {session.createUniqueTransaction()}; EXPECT_EQ(TrackFeatures::getCount(session), 1); - auto allTrackFeatures {TrackFeatures::find(session, Range {})}; + auto allTrackFeatures {TrackFeatures::find(session)}; ASSERT_EQ(allTrackFeatures.results.size(), 1); EXPECT_EQ(allTrackFeatures.results.front(), trackFeatures.getId()); } diff --git a/src/libs/services/database/test/TrackList.cpp b/src/libs/services/database/test/TrackList.cpp index f05cb48a..70467128 100644 --- a/src/libs/services/database/test/TrackList.cpp +++ b/src/libs/services/database/test/TrackList.cpp @@ -25,497 +25,200 @@ using namespace Database; TEST_F(DatabaseFixture, SingleTrackList) { - ScopedUser user {session, "MyUser"}; - { - auto transaction {session.createSharedTransaction()}; - EXPECT_EQ(TrackList::getCount(session), 0); - } + ScopedUser user{ session, "MyUser" }; + { + auto transaction{ session.createSharedTransaction() }; + EXPECT_EQ(TrackList::getCount(session), 0); + } - ScopedTrackList trackList {session, "MytrackList", TrackListType::Playlist, false, user.lockAndGet()}; + ScopedTrackList trackList{ session, "MytrackList", TrackListType::Playlist, false, user.lockAndGet() }; - { - auto transaction {session.createSharedTransaction()}; - EXPECT_EQ(TrackList::getCount(session), 1); - } + { + auto transaction{ session.createSharedTransaction() }; + EXPECT_EQ(TrackList::getCount(session), 1); + } } TEST_F(DatabaseFixture, SingleTrackListSingleTrack) { - ScopedUser user {session, "MyUser"}; - ScopedTrackList trackList1 {session, "MyTrackList1", TrackListType::Playlist, false, user.lockAndGet()}; - ScopedTrackList trackList2 {session, "MyTrackList2", TrackListType::Playlist, false, user.lockAndGet()}; - ScopedTrack track {session, "MyTrack"}; + ScopedUser user{ session, "MyUser" }; + ScopedTrackList trackList1{ session, "MyTrackList1", TrackListType::Playlist, false, user.lockAndGet() }; + ScopedTrackList trackList2{ session, "MyTrackList2", TrackListType::Playlist, false, user.lockAndGet() }; + ScopedTrack track{ session, "MyTrack" }; - { - auto transaction {session.createSharedTransaction()}; + { + auto transaction{ session.createSharedTransaction() }; - auto tracks {Track::findIds(session, Track::FindParameters {}.setTrackList(trackList1.getId()))}; - EXPECT_EQ(tracks.results.size(), 0); + auto tracks{ Track::findIds(session, Track::FindParameters {}.setTrackList(trackList1.getId())) }; + EXPECT_EQ(tracks.results.size(), 0); - tracks = Track::findIds(session, Track::FindParameters {}.setTrackList(trackList2.getId())); - EXPECT_EQ(tracks.results.size(), 0); - } + tracks = Track::findIds(session, Track::FindParameters{}.setTrackList(trackList2.getId())); + EXPECT_EQ(tracks.results.size(), 0); + } - { - auto transaction {session.createUniqueTransaction()}; + { + auto transaction{ session.createUniqueTransaction() }; - session.create(track.get(), trackList1.get()); - } + session.create(track.get(), trackList1.get()); + } - { - auto transaction {session.createSharedTransaction()}; + { + auto transaction{ session.createSharedTransaction() }; - auto tracks {Track::findIds(session, Track::FindParameters {}.setTrackList(trackList1.getId()))}; - ASSERT_EQ(tracks.results.size(), 1); - EXPECT_EQ(tracks.results.front(), track.getId()); + auto tracks{ Track::findIds(session, Track::FindParameters {}.setTrackList(trackList1.getId())) }; + ASSERT_EQ(tracks.results.size(), 1); + EXPECT_EQ(tracks.results.front(), track.getId()); - tracks = Track::findIds(session, Track::FindParameters {}.setTrackList(trackList2.getId())); - EXPECT_EQ(tracks.results.size(), 0); - } + tracks = Track::findIds(session, Track::FindParameters{}.setTrackList(trackList2.getId())); + EXPECT_EQ(tracks.results.size(), 0); + } } TEST_F(DatabaseFixture, TrackList_SortMethod) { - ScopedUser user {session, "MyUser"}; - ScopedTrackList trackList2 {session, "MyTrackList2", TrackListType::Playlist, false, user.lockAndGet()}; - ScopedTrackList trackList1 {session, "MyTrackList1", TrackListType::Playlist, false, user.lockAndGet()}; - ScopedTrack track {session, "MyTrack"}; + ScopedUser user{ session, "MyUser" }; + ScopedTrackList trackList2{ session, "MyTrackList2", TrackListType::Playlist, false, user.lockAndGet() }; + ScopedTrackList trackList1{ session, "MyTrackList1", TrackListType::Playlist, false, user.lockAndGet() }; + ScopedTrack track{ session, "MyTrack" }; - { - auto transaction {session.createSharedTransaction()}; + { + auto transaction{ session.createSharedTransaction() }; - const auto trackLists {TrackList::find(session, TrackList::FindParameters {}.setSortMethod(TrackListSortMethod::Name))}; - ASSERT_EQ(trackLists.results.size(), 2); - EXPECT_EQ(trackLists.results[0], trackList1.getId()); - EXPECT_EQ(trackLists.results[1], trackList2.getId()); - } + const auto trackLists{ TrackList::find(session, TrackList::FindParameters {}.setSortMethod(TrackListSortMethod::Name)) }; + ASSERT_EQ(trackLists.results.size(), 2); + EXPECT_EQ(trackLists.results[0], trackList1.getId()); + EXPECT_EQ(trackLists.results[1], trackList2.getId()); + } - { - auto transaction {session.createUniqueTransaction()}; + { + auto transaction{ session.createUniqueTransaction() }; - trackList1.get().modify()->setLastModifiedDateTime(Wt::WDateTime {Wt::WDate {1900,1,1}}); - trackList2.get().modify()->setLastModifiedDateTime(Wt::WDateTime {Wt::WDate {1900,1,2}}); - } + trackList1.get().modify()->setLastModifiedDateTime(Wt::WDateTime{ Wt::WDate {1900,1,1} }); + trackList2.get().modify()->setLastModifiedDateTime(Wt::WDateTime{ Wt::WDate {1900,1,2} }); + } - { - auto transaction {session.createSharedTransaction()}; + { + auto transaction{ session.createSharedTransaction() }; - const auto trackLists {TrackList::find(session, TrackList::FindParameters {}.setSortMethod(TrackListSortMethod::LastModifiedDesc))}; - ASSERT_EQ(trackLists.results.size(), 2); - EXPECT_EQ(trackLists.results[0], trackList2.getId()); - EXPECT_EQ(trackLists.results[1], trackList1.getId()); - } + const auto trackLists{ TrackList::find(session, TrackList::FindParameters {}.setSortMethod(TrackListSortMethod::LastModifiedDesc)) }; + ASSERT_EQ(trackLists.results.size(), 2); + EXPECT_EQ(trackLists.results[0], trackList2.getId()); + EXPECT_EQ(trackLists.results[1], trackList1.getId()); + } - { - auto transaction {session.createUniqueTransaction()}; + { + auto transaction{ session.createUniqueTransaction() }; - trackList1.get().modify()->setLastModifiedDateTime(Wt::WDateTime {Wt::WDate {1900,1,2}}); - trackList2.get().modify()->setLastModifiedDateTime(Wt::WDateTime {Wt::WDate {1900,1,1}}); - } + trackList1.get().modify()->setLastModifiedDateTime(Wt::WDateTime{ Wt::WDate {1900,1,2} }); + trackList2.get().modify()->setLastModifiedDateTime(Wt::WDateTime{ Wt::WDate {1900,1,1} }); + } - { - auto transaction {session.createSharedTransaction()}; + { + auto transaction{ session.createSharedTransaction() }; - const auto trackLists {TrackList::find(session, TrackList::FindParameters {}.setSortMethod(TrackListSortMethod::LastModifiedDesc))}; - ASSERT_EQ(trackLists.results.size(), 2); - EXPECT_EQ(trackLists.results[0], trackList1.getId()); - EXPECT_EQ(trackLists.results[1], trackList2.getId()); - } + const auto trackLists{ TrackList::find(session, TrackList::FindParameters {}.setSortMethod(TrackListSortMethod::LastModifiedDesc)) }; + ASSERT_EQ(trackLists.results.size(), 2); + EXPECT_EQ(trackLists.results[0], trackList1.getId()); + EXPECT_EQ(trackLists.results[1], trackList2.getId()); + } } TEST_F(DatabaseFixture, SingleTrackListMultipleTrack) { - ScopedUser user {session, "MyUser"}; - ScopedTrackList trackList {session, "MytrackList", TrackListType::Playlist, false, user.lockAndGet()}; - std::list tracks; + ScopedUser user{ session, "MyUser" }; + ScopedTrackList trackList{ session, "MytrackList", TrackListType::Playlist, false, user.lockAndGet() }; + std::list tracks; - for (std::size_t i {}; i < 10; ++i) - { - tracks.emplace_back(session, "MyTrack" + std::to_string(i)); + for (std::size_t i{}; i < 10; ++i) + { + tracks.emplace_back(session, "MyTrack" + std::to_string(i)); - auto transaction {session.createUniqueTransaction()}; - session.create(tracks.back().get(), trackList.get()); - } + auto transaction{ session.createUniqueTransaction() }; + session.create(tracks.back().get(), trackList.get()); + } - { - auto transaction {session.createSharedTransaction()}; + { + auto transaction{ session.createSharedTransaction() }; - ASSERT_EQ(trackList->getCount(), tracks.size()); - const auto trackIds {trackList->getTrackIds()}; - ASSERT_EQ(trackIds.size(), tracks.size()); + ASSERT_EQ(trackList->getCount(), tracks.size()); + const auto trackIds{ trackList->getTrackIds() }; + ASSERT_EQ(trackIds.size(), tracks.size()); - // Same order - std::size_t i {}; - for (const ScopedTrack& track : tracks) - EXPECT_EQ(track.getId(), trackIds[i++]); - } - - { - auto transaction {session.createSharedTransaction()}; - - bool moreResults {}; - const auto reverseTracks {trackList->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults)}; - ASSERT_EQ(reverseTracks.size(), tracks.size()); - - // Reverse order - ASSERT_TRUE(tracks.size() > 0); - std::size_t i {tracks.size() - 1}; - for (const ScopedTrack& track : tracks) - EXPECT_EQ(track.getId(), reverseTracks[i--]->getId()); - } -} - -TEST_F(DatabaseFixture, SingleTrackListMultipleTrackDateTime) -{ - ScopedUser user {session, "MyUser"}; - ScopedTrackList trackList {session, "MytrackList", TrackListType::Playlist, false, user.lockAndGet()}; - ScopedTrack track1 {session, "MyTrack1"}; - ScopedTrack track2 {session, "MyTrack2"}; - ScopedTrack track3 {session, "MyTrack3"}; - - { - Wt::WDateTime now {Wt::WDateTime::currentDateTime()}; - auto transaction {session.createUniqueTransaction()}; - session.create(track1.get(), trackList.get(), now); - session.create(track2.get(), trackList.get(), now.addSecs(-1)); - session.create(track3.get(), trackList.get(), now.addSecs(1)); - } - - { - auto transaction {session.createSharedTransaction()}; - - bool moreResults; - const auto tracks {trackList.get()->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults)}; - ASSERT_EQ(tracks.size(), 3); - EXPECT_EQ(tracks.front()->getId(), track3.getId()); - EXPECT_EQ(tracks.back()->getId(), track2.getId()); - } -} - -TEST_F(DatabaseFixture, SingleTrackListMultipleTrackRecentlyPlayed) -{ - ScopedUser user {session, "MyUser"}; - ScopedTrackList trackList {session, "MyTrackList", TrackListType::Playlist, false, user.lockAndGet()}; - ScopedTrack track1 {session, "MyTrack1"}; - ScopedTrack track2 {session, "MyTrack1"}; - ScopedArtist artist1 {session, "MyArtist1"}; - ScopedArtist artist2 {session, "MyArtist2"}; - ScopedRelease release1 {session, "MyRelease1"}; - ScopedRelease release2 {session, "MyRelease2"}; - - const Wt::WDateTime now {Wt::WDateTime::currentDateTime()}; - - { - auto transaction {session.createUniqueTransaction()}; - - track1.get().modify()->setRelease(release1.get()); - track2.get().modify()->setRelease(release2.get()); - TrackArtistLink::create(session, track1.get(), artist1.get(), TrackArtistLinkType::Artist); - TrackArtistLink::create(session, track2.get(), artist2.get(), TrackArtistLinkType::Artist); - } - { - auto transaction {session.createSharedTransaction()}; - - bool moreResults {}; - EXPECT_TRUE(trackList->getArtistsOrderedByRecentFirst({}, std::nullopt, std::nullopt, moreResults).empty()); - EXPECT_TRUE(trackList->getReleasesOrderedByRecentFirst({}, std::nullopt, moreResults).empty()); - EXPECT_TRUE(trackList->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults).empty()); - } - - { - auto transaction {session.createUniqueTransaction()}; - - session.create(track1.get(), trackList.get(), now); - } - - { - auto transaction {session.createSharedTransaction()}; - - bool moreResults {}; - const auto artists {trackList->getArtistsOrderedByRecentFirst({}, std::nullopt, std::nullopt, moreResults)}; - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front()->getId(), artist1.getId()); - - const auto releases {trackList->getReleasesOrderedByRecentFirst({}, std::nullopt, moreResults)}; - ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases.front()->getId(), release1.getId()); - - const auto tracks {trackList->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults)}; - EXPECT_EQ(tracks.size(), 1); - } - - { - auto transaction {session.createUniqueTransaction()}; - - session.create(track2.get(), trackList.get(), now.addSecs(1)); - } - - { - auto transaction {session.createSharedTransaction()}; - - bool moreResults {}; - const auto artists {trackList->getArtistsOrderedByRecentFirst({}, std::nullopt, std::nullopt, moreResults)}; - ASSERT_EQ(artists.size(), 2); - EXPECT_EQ(artists[0]->getId(), artist2.getId()); - EXPECT_EQ(artists[1]->getId(), artist1.getId()); - - const auto releases {trackList->getReleasesOrderedByRecentFirst({}, std::nullopt, moreResults)}; - ASSERT_EQ(releases.size(), 2); - EXPECT_EQ(releases[0]->getId(), release2.getId()); - EXPECT_EQ(releases[1]->getId(), release1.getId()); - - const auto tracks {trackList->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults)}; - ASSERT_EQ(tracks.size(), 2); - EXPECT_EQ(tracks[0]->getId(), track2.getId()); - EXPECT_EQ(tracks[1]->getId(), track1.getId()); - } - - { - auto transaction {session.createUniqueTransaction()}; - - session.create(track1.get(), trackList.get(), now.addSecs(2)); - } - - { - auto transaction {session.createSharedTransaction()}; - - bool moreResults {}; - const auto artists {trackList->getArtistsOrderedByRecentFirst({}, std::nullopt, std::nullopt, moreResults)}; - ASSERT_EQ(artists.size(), 2); - EXPECT_EQ(artists[0]->getId(), artist1.getId()); - EXPECT_EQ(artists[1]->getId(), artist2.getId()); - - const auto releases {trackList->getReleasesOrderedByRecentFirst({}, std::nullopt, moreResults)}; - ASSERT_EQ(releases.size(), 2); - EXPECT_EQ(releases[0]->getId(), release1.getId()); - EXPECT_EQ(releases[1]->getId(), release2.getId()); - - const auto tracks {trackList->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults)}; - ASSERT_EQ(tracks.size(), 2); - EXPECT_EQ(tracks[0]->getId(), track1.getId()); - EXPECT_EQ(tracks[1]->getId(), track2.getId()); - } -} - -TEST_F(DatabaseFixture, SingleTrackList_getArtists) -{ - ScopedUser user {session, "MyUser"}; - ScopedTrackList trackList {session, "MytrackList", TrackListType::Playlist, false, user.lockAndGet()}; - ScopedTrack track1 {session, "MyTrack1"}; - ScopedTrack track2 {session, "MyTrack2"}; - ScopedRelease release {session, "MyRelease"}; - ScopedArtist artist1 {session, "MyArtist1"}; - ScopedArtist artist2 {session, "MyArtist2"}; - - { - auto transaction {session.createSharedTransaction()}; - - EXPECT_EQ(trackList->getCount(), 0); - bool moreResults {}; - const auto artists {trackList->getArtists({} /*clusters*/, std::nullopt /* linkType */, ArtistSortMethod::ByName, std::nullopt /* range */, moreResults)}; - ASSERT_TRUE(artists.empty()); - } - - { - auto transaction {session.createUniqueTransaction()}; - TrackArtistLink::create(session, track1.get(), artist1.get(), TrackArtistLinkType::Artist); - session.create(track1.get(), trackList.get()); - } - - { - auto transaction {session.createSharedTransaction()}; - - EXPECT_EQ(trackList->getCount(), 1); - bool moreResults {}; - const auto artists {trackList->getArtists({} /*clusters*/, std::nullopt /* linkType */, ArtistSortMethod::ByName, std::nullopt /* range */, moreResults)}; - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front()->getId(), artist1.getId()); - } - - { - auto transaction {session.createSharedTransaction()}; - - bool moreResults {}; - const auto artists {trackList->getArtists({} /*clusters*/, TrackArtistLinkType::ReleaseArtist, ArtistSortMethod::ByName, std::nullopt /* range */, moreResults)}; - EXPECT_TRUE(artists.empty()); - } - - { - auto transaction {session.createSharedTransaction()}; - - bool moreResults {}; - const auto artists {trackList->getArtists({} /*clusters*/, TrackArtistLinkType::Artist, ArtistSortMethod::ByName, std::nullopt /* range */, moreResults)}; - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front()->getId(), artist1.getId()); - } - - { - auto transaction {session.createUniqueTransaction()}; - TrackArtistLink::create(session, track2.get(), artist2.get(), TrackArtistLinkType::Artist); - session.create(track2.get(), trackList.get()); - artist1.get().modify()->setSortName("ZZZ"); - artist2.get().modify()->setSortName("AAA"); - } - - { - auto transaction {session.createSharedTransaction()}; - - bool moreResults {}; - const auto artists {trackList->getArtists({} /*clusters*/, TrackArtistLinkType::Artist, ArtistSortMethod::ByName, std::nullopt /* range */, moreResults)}; - ASSERT_EQ(artists.size(), 2); - EXPECT_EQ(artists.front()->getId(), artist1.getId()); - EXPECT_EQ(artists.back()->getId(), artist2.getId()); - } - - { - auto transaction {session.createSharedTransaction()}; - - bool moreResults {}; - const auto artists {trackList->getArtists({} /*clusters*/, TrackArtistLinkType::Artist, ArtistSortMethod::BySortName, std::nullopt /* range */, moreResults)}; - ASSERT_EQ(artists.size(), 2); - EXPECT_EQ(artists.front()->getId(), artist2.getId()); - EXPECT_EQ(artists.back()->getId(), artist1.getId()); - } -} - -TEST_F(DatabaseFixture, SingleTrackList_getReleases) -{ - ScopedUser user {session, "MyUser"}; - ScopedTrackList trackList {session, "MytrackList", TrackListType::Playlist, false, user.lockAndGet()}; - ScopedTrack track {session, "MyTrack"}; - ScopedRelease release {session, "MyRelease"}; - - { - auto transaction {session.createSharedTransaction()}; - - EXPECT_EQ(trackList->getCount(), 0); - bool moreResults {}; - const auto releases {trackList->getReleases({} /*clusters*/, std::nullopt /* range */, moreResults)}; - ASSERT_TRUE(releases.empty()); - } - - { - auto transaction {session.createUniqueTransaction()}; - - track.get().modify()->setRelease(release.get()); - - session.create(track.get(), trackList.get()); - } - - { - auto transaction {session.createSharedTransaction()}; - - ASSERT_EQ(trackList->getCount(), 1); - bool moreResults {}; - const auto releases {trackList->getReleases({} /*clusters*/, std::nullopt /* range */, moreResults)}; - ASSERT_EQ(releases.size(), 1); - ASSERT_EQ(releases.front()->getId(), release->getId()); - } -} - -TEST_F(DatabaseFixture, SingleTrackList_getTracks) -{ - ScopedUser user {session, "MyUser"}; - ScopedTrackList trackList {session, "MytrackList", TrackListType::Playlist, false, user.lockAndGet()}; - ScopedTrack track1 {session, "MyTrack1"}; - ScopedTrack track2 {session, "MyTrack1"}; - - { - auto transaction {session.createSharedTransaction()}; - - EXPECT_EQ(trackList->getCount(), 0); - bool moreResults {}; - const auto tracks {trackList->getTracks({} /*clusters*/, std::nullopt /* range */, moreResults)}; - ASSERT_TRUE(tracks.empty()); - } - - { - auto transaction {session.createUniqueTransaction()}; - session.create(track1.get(), trackList.get()); - session.create(track2.get(), trackList.get()); - session.create(track1.get(), trackList.get()); - } - - { - auto transaction {session.createSharedTransaction()}; - - ASSERT_EQ(trackList->getCount(), 3); - bool moreResults {}; - const auto tracks {trackList->getTracks({} /*clusters*/, std::nullopt /* range */, moreResults)}; - ASSERT_EQ(tracks.size(), 2); - ASSERT_EQ(tracks[0]->getId(), track1->getId()); - ASSERT_EQ(tracks[1]->getId(), track2->getId()); - } + // Same order + std::size_t i{}; + for (const ScopedTrack& track : tracks) + EXPECT_EQ(track.getId(), trackIds[i++]); + } } TEST_F(DatabaseFixture, SingleTrackListSingleTrackWithCluster) { - ScopedUser user {session, "MyUser"}; - ScopedTrackList trackList1 {session, "MyTrackList1", TrackListType::Playlist, false, user.lockAndGet()}; - ScopedTrackList trackList2 {session, "MyTrackList2", TrackListType::Playlist, false, user.lockAndGet()}; - ScopedClusterType clusterType {session, "MyClusterType"}; - ScopedCluster cluster {session, clusterType.lockAndGet(), "MyCluster"}; - ScopedTrack track {session, "MyTrack"}; + ScopedUser user{ session, "MyUser" }; + ScopedTrackList trackList1{ session, "MyTrackList1", TrackListType::Playlist, false, user.lockAndGet() }; + ScopedTrackList trackList2{ session, "MyTrackList2", TrackListType::Playlist, false, user.lockAndGet() }; + ScopedClusterType clusterType{ session, "MyClusterType" }; + ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" }; + ScopedTrack track{ session, "MyTrack" }; - { - auto transaction {session.createSharedTransaction()}; + { + auto transaction{ session.createSharedTransaction() }; - auto trackLists {TrackList::find(session, TrackList::FindParameters {}.setClusters({cluster.getId()}))}; - EXPECT_EQ(trackLists.results.size(), 0); - } + auto trackLists{ TrackList::find(session, TrackList::FindParameters {}.setClusters({cluster.getId()})) }; + EXPECT_EQ(trackLists.results.size(), 0); + } - { - auto transaction {session.createUniqueTransaction()}; + { + auto transaction{ session.createUniqueTransaction() }; - session.create(track.get(), trackList1.get()); - cluster.get().modify()->addTrack(track.get()); - } + session.create(track.get(), trackList1.get()); + cluster.get().modify()->addTrack(track.get()); + } - { - auto transaction {session.createSharedTransaction()}; + { + auto transaction{ session.createSharedTransaction() }; - auto trackLists {TrackList::find(session, TrackList::FindParameters {}.setClusters({cluster.getId()}))}; - ASSERT_EQ(trackLists.results.size(), 1); - EXPECT_EQ(trackLists.results.front(), trackList1.getId()); - } + auto trackLists{ TrackList::find(session, TrackList::FindParameters {}.setClusters({cluster.getId()})) }; + ASSERT_EQ(trackLists.results.size(), 1); + EXPECT_EQ(trackLists.results.front(), trackList1.getId()); + } } TEST_F(DatabaseFixture, SingleTrackList_getEntries) { - ScopedUser user {session, "MyUser"}; - ScopedTrackList trackList {session, "MyTrackList", TrackListType::Playlist, false, user.lockAndGet()}; - ScopedTrack track1 {session, "MyTrack"}; - ScopedTrack track2 {session, "MyTrack"}; + ScopedUser user{ session, "MyUser" }; + ScopedTrackList trackList{ session, "MyTrackList", TrackListType::Playlist, false, user.lockAndGet() }; + ScopedTrack track1{ session, "MyTrack" }; + ScopedTrack track2{ session, "MyTrack" }; - { - auto transaction {session.createUniqueTransaction()}; - session.create(track1.get(), trackList.get()); - } + { + auto transaction{ session.createUniqueTransaction() }; + session.create(track1.get(), trackList.get()); + } - { - auto transaction {session.createSharedTransaction()}; - auto entries {trackList.get()->getEntries()}; - ASSERT_EQ(entries.size(), 1); - EXPECT_EQ(entries.front()->getTrack()->getId(), track1.getId()); - } + { + auto transaction{ session.createSharedTransaction() }; + auto entries{ trackList.get()->getEntries() }; + ASSERT_EQ(entries.size(), 1); + EXPECT_EQ(entries.front()->getTrack()->getId(), track1.getId()); + } - { - auto transaction {session.createUniqueTransaction()}; - session.create(track2.get(), trackList.get()); - } + { + auto transaction{ session.createUniqueTransaction() }; + session.create(track2.get(), trackList.get()); + } - { - auto transaction {session.createSharedTransaction()}; - auto entries {trackList.get()->getEntries()}; - ASSERT_EQ(entries.size(), 2); - EXPECT_EQ(entries[0]->getTrack()->getId(), track1.getId()); - EXPECT_EQ(entries[1]->getTrack()->getId(), track2.getId()); - } + { + auto transaction{ session.createSharedTransaction() }; + auto entries{ trackList.get()->getEntries() }; + ASSERT_EQ(entries.size(), 2); + EXPECT_EQ(entries[0]->getTrack()->getId(), track1.getId()); + EXPECT_EQ(entries[1]->getTrack()->getId(), track2.getId()); + } - { - auto transaction {session.createSharedTransaction()}; - auto entries {trackList.get()->getEntries(Range {1, 1})}; - ASSERT_EQ(entries.size(), 1); - EXPECT_EQ(entries[0]->getTrack()->getId(), track2.getId()); - } + { + auto transaction{ session.createSharedTransaction() }; + auto entries{ trackList.get()->getEntries(Range {1, 1}) }; + ASSERT_EQ(entries.size(), 1); + EXPECT_EQ(entries[0]->getTrack()->getId(), track2.getId()); + } } diff --git a/src/libs/services/feedback/impl/FeedbackService.cpp b/src/libs/services/feedback/impl/FeedbackService.cpp index 2a9a60a9..fb66c740 100644 --- a/src/libs/services/feedback/impl/FeedbackService.cpp +++ b/src/libs/services/feedback/impl/FeedbackService.cpp @@ -87,23 +87,23 @@ namespace Feedback return getStarredDateTime(userId, artistId); } - FeedbackService::ArtistContainer FeedbackService::getStarredArtists(UserId userId, const std::vector& clusterIds, std::optional linkType, ArtistSortMethod sortMethod, Range range) + FeedbackService::ArtistContainer FeedbackService::findStarredArtists(const ArtistFindParameters& params) { - auto backend{ getUserFeedbackBackend(userId) }; + auto backend{ getUserFeedbackBackend(params.user) }; if (!backend) return {}; - Artist::FindParameters params; - params.setStarringUser(userId, *backend); - params.setClusters(clusterIds); - params.setLinkType(linkType); - params.setSortMethod(sortMethod); - params.setRange(range); + Artist::FindParameters searchParams; + searchParams.setStarringUser(params.user, *backend); + searchParams.setClusters(params.clusters); + searchParams.setLinkType(params.linkType); + searchParams.setSortMethod(params.sortMethod); + searchParams.setRange(params.range); Session& session{ _db.getTLSSession() }; auto transaction{ session.createSharedTransaction() }; - return Artist::findIds(session, params); + return Artist::findIds(session, searchParams); } void FeedbackService::star(UserId userId, ReleaseId releaseId) @@ -126,22 +126,22 @@ namespace Feedback return getStarredDateTime(userId, releaseId); } - FeedbackService::ReleaseContainer FeedbackService::getStarredReleases(UserId userId, const std::vector& clusterIds, Range range) + FeedbackService::ReleaseContainer FeedbackService::findStarredReleases(const FindParameters& params) { - auto backend{ getUserFeedbackBackend(userId) }; + auto backend{ getUserFeedbackBackend(params.user) }; if (!backend) return {}; - Release::FindParameters params; - params.setStarringUser(userId, *backend); - params.setClusters(clusterIds); - params.setSortMethod(ReleaseSortMethod::StarredDateDesc); - params.setRange(range); + Release::FindParameters searchParams; + searchParams.setStarringUser(params.user, *backend); + searchParams.setClusters(params.clusters); + searchParams.setSortMethod(ReleaseSortMethod::StarredDateDesc); + searchParams.setRange(params.range); Session& session{ _db.getTLSSession() }; auto transaction{ session.createSharedTransaction() }; - return Release::findIds(session, params); + return Release::findIds(session, searchParams); } void FeedbackService::star(UserId userId, TrackId trackId) @@ -164,22 +164,22 @@ namespace Feedback return getStarredDateTime(userId, trackId); } - FeedbackService::TrackContainer FeedbackService::getStarredTracks(UserId userId, const std::vector& clusterIds, Range range) + FeedbackService::TrackContainer FeedbackService::findStarredTracks(const FindParameters& params) { - auto backend{ getUserFeedbackBackend(userId) }; + auto backend{ getUserFeedbackBackend(params.user) }; if (!backend) return {}; - Track::FindParameters params; - params.setStarringUser(userId, *backend); - params.setClusters(clusterIds); - params.setSortMethod(TrackSortMethod::StarredDateDesc); - params.setRange(range); + Track::FindParameters searchParams; + searchParams.setStarringUser(params.user, *backend); + searchParams.setClusters(params.clusters); + searchParams.setSortMethod(TrackSortMethod::StarredDateDesc); + searchParams.setRange(params.range); Session& session{ _db.getTLSSession() }; auto transaction{ session.createSharedTransaction() }; - return Track::findIds(session, params); + return Track::findIds(session, searchParams); } } // ns Feedback diff --git a/src/libs/services/feedback/impl/FeedbackService.hpp b/src/libs/services/feedback/impl/FeedbackService.hpp index 279a8fa7..3b28d469 100644 --- a/src/libs/services/feedback/impl/FeedbackService.hpp +++ b/src/libs/services/feedback/impl/FeedbackService.hpp @@ -46,19 +46,19 @@ namespace Feedback void unstar(Database::UserId userId, Database::ArtistId artistId) override; bool isStarred(Database::UserId userId, Database::ArtistId artistId) override; Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::ArtistId artistId) override; - ArtistContainer getStarredArtists(Database::UserId userId, const std::vector& clusterIds, std::optional linkType, Database::ArtistSortMethod sortMethod, Database::Range range) override; + ArtistContainer findStarredArtists(const ArtistFindParameters& params) override; void star(Database::UserId userId, Database::ReleaseId releaseId) override; void unstar(Database::UserId userId, Database::ReleaseId releaseId) override; bool isStarred(Database::UserId userId, Database::ReleaseId releasedId) override; Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::ReleaseId releasedId) override; - ReleaseContainer getStarredReleases(Database::UserId userId, const std::vector& clusterIds, Database::Range range) override; + ReleaseContainer findStarredReleases(const FindParameters& params) override; void star(Database::UserId userId, Database::TrackId trackId) override; void unstar(Database::UserId userId, Database::TrackId trackId) override; bool isStarred(Database::UserId userId, Database::TrackId trackId) override; Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::TrackId trackId) override; - TrackContainer getStarredTracks(Database::UserId userId, const std::vector& clusterIds, Database::Range range) override; + TrackContainer findStarredTracks(const FindParameters& params) override; std::optional getUserFeedbackBackend(Database::UserId userId); diff --git a/src/libs/services/feedback/include/services/feedback/IFeedbackService.hpp b/src/libs/services/feedback/include/services/feedback/IFeedbackService.hpp index dbd8085f..05ed487f 100644 --- a/src/libs/services/feedback/include/services/feedback/IFeedbackService.hpp +++ b/src/libs/services/feedback/include/services/feedback/IFeedbackService.hpp @@ -48,23 +48,46 @@ namespace Feedback using ReleaseContainer = Database::RangeResults; using TrackContainer = Database::RangeResults; - virtual void star(Database::UserId userId, Database::ArtistId artistId) = 0; - virtual void unstar(Database::UserId userId, Database::ArtistId artistId) = 0; - virtual bool isStarred(Database::UserId userId, Database::ArtistId artistId) = 0; - virtual Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::ArtistId artistId) = 0; - virtual ArtistContainer getStarredArtists(Database::UserId userId, const std::vector& clusterIds, std::optional linkType, Database::ArtistSortMethod sortMethod, Database::Range range) = 0; + struct FindParameters + { + Database::UserId user; + std::vector clusters; // if non empty, at least one artist that belongs to these clusters + std::optional range; + FindParameters& setUser(const Database::UserId _user) { user = _user; return *this; } + FindParameters& setClusters(const std::vector& _clusters) { clusters = _clusters; return *this; } + FindParameters& setRange(std::optional _range) { range = _range; return *this; } + }; + + // Artists + struct ArtistFindParameters : public FindParameters + { + std::optional linkType; // if set, only artists that have produced at least one track with this link type + Database::ArtistSortMethod sortMethod{ Database::ArtistSortMethod::None }; + + ArtistFindParameters& setLinkType(std::optional _linkType) { linkType = _linkType; return *this; } + ArtistFindParameters& setSortMethod(Database::ArtistSortMethod _sortMethod) { sortMethod = _sortMethod; return *this; } + }; + + virtual void star(Database::UserId userId, Database::ArtistId artistId) = 0; + virtual void unstar(Database::UserId userId, Database::ArtistId artistId) = 0; + virtual bool isStarred(Database::UserId userId, Database::ArtistId artistId) = 0; + virtual Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::ArtistId artistId) = 0; + virtual ArtistContainer findStarredArtists(const ArtistFindParameters& params) = 0; + + // Releases virtual void star(Database::UserId userId, Database::ReleaseId releaseId) = 0; virtual void unstar(Database::UserId userId, Database::ReleaseId releaseId) = 0; virtual bool isStarred(Database::UserId userId, Database::ReleaseId artistId) = 0; virtual Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::ReleaseId artistId) = 0; - virtual ReleaseContainer getStarredReleases(Database::UserId userId, const std::vector& clusterIds, Database::Range range) = 0; + virtual ReleaseContainer findStarredReleases(const FindParameters& params) = 0; + // Tracks virtual void star(Database::UserId userId, Database::TrackId trackId) = 0; virtual void unstar(Database::UserId userId, Database::TrackId trackId) = 0; virtual bool isStarred(Database::UserId userId, Database::TrackId artistId) = 0; virtual Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::TrackId artistId) = 0; - virtual TrackContainer getStarredTracks(Database::UserId userId, const std::vector& clusterIds, Database::Range range) = 0; + virtual TrackContainer findStarredTracks(const FindParameters& params) = 0; }; std::unique_ptr createFeedbackService(boost::asio::io_service& ioService, Database::Db& db); diff --git a/src/libs/services/recommendation/impl/RecommendationService.cpp b/src/libs/services/recommendation/impl/RecommendationService.cpp index 4ec0ac75..02e77870 100644 --- a/src/libs/services/recommendation/impl/RecommendationService.cpp +++ b/src/libs/services/recommendation/impl/RecommendationService.cpp @@ -33,240 +33,91 @@ namespace Recommendation { + namespace + { + Database::ScanSettings::SimilarityEngineType getSimilarityEngineType(Database::Session& session) + { + auto transaction{ session.createSharedTransaction() }; - static - std::string_view - engineTypeToString(EngineType engineType) - { - switch (engineType) - { - case EngineType::Clusters: return "clusters"; - case EngineType::Features: return "features"; - } + return Database::ScanSettings::get(session)->getSimilarityEngineType(); + } + } - throw LmsException {"Internal error"}; - } + std::unique_ptr createRecommendationService(Database::Db& db) + { + return std::make_unique(db); + } - std::unique_ptr - createRecommendationService(Database::Db& db) - { - return std::make_unique(db); - } + RecommendationService::RecommendationService(Database::Db& db) + : _db{ db } + { + load(); + } - RecommendationService::RecommendationService(Database::Db& db) - : _db {db} - { - } + TrackContainer RecommendationService::findSimilarTracks(Database::TrackListId trackListId, std::size_t maxCount) const + { + TrackContainer res; - TrackContainer - RecommendationService::findSimilarTracks(Database::TrackListId trackListId, std::size_t maxCount) const - { - TrackContainer res; + if (!_engine) + return res; - std::shared_lock lock {_enginesMutex}; - for (const auto& engineType : _enginePriorities) - { - auto itEngine {_engines.find(engineType)}; - if (itEngine == std::cend(_engines)) - continue; + return _engine->findSimilarTracksFromTrackList(trackListId, maxCount); + } - res = itEngine->second->findSimilarTracksFromTrackList(trackListId, maxCount); - if (!res.empty()) - break; - } + TrackContainer RecommendationService::findSimilarTracks(const std::vector& trackIds, std::size_t maxCount) const + { + TrackContainer res; - return res; - } + if (!_engine) + return res; - TrackContainer - RecommendationService::findSimilarTracks(const std::vector& trackIds, std::size_t maxCount) const - { - TrackContainer res; + return _engine->findSimilarTracks(trackIds, maxCount); + } - std::shared_lock lock {_enginesMutex}; - for (EngineType engineType : _enginePriorities) - { - auto itEngine {_engines.find(engineType)}; - if (itEngine == std::cend(_engines)) - continue; + ReleaseContainer RecommendationService::getSimilarReleases(Database::ReleaseId releaseId, std::size_t maxCount) const + { + ReleaseContainer res; - LMS_LOG(RECOMMENDATION, DEBUG) << "Trying engine '" << engineTypeToString(engineType) << "' to get similar tracks"; + if (!_engine) + return res; - const IEngine& engine {*itEngine->second}; - res = engine.findSimilarTracks(trackIds, maxCount); - if (!res.empty()) - { - LMS_LOG(RECOMMENDATION, DEBUG) << "Got " << res.size() << " similar tracks using engine '" << engineTypeToString(engineType) << "'"; - break; - } - } + return _engine->getSimilarReleases(releaseId, maxCount);; + } - return res; - } + ArtistContainer RecommendationService::getSimilarArtists(Database::ArtistId artistId, EnumSet linkTypes, std::size_t maxCount) const + { + ArtistContainer res; - ReleaseContainer - RecommendationService::getSimilarReleases(Database::ReleaseId releaseId, std::size_t maxCount) const - { - ReleaseContainer res; + if (!_engine) + return res; - std::shared_lock lock {_enginesMutex}; - for (EngineType engineType : _enginePriorities) - { - auto itEngine {_engines.find(engineType)}; - if (itEngine == std::cend(_engines)) - continue; + return _engine->getSimilarArtists(artistId, linkTypes, maxCount); - LMS_LOG(RECOMMENDATION, DEBUG) << "Trying engine '" << engineTypeToString(engineType) << "' to get similar releases"; + return res; + } - const IEngine& engine {*itEngine->second}; - res = engine.getSimilarReleases(releaseId, maxCount); - if (!res.empty()) - { - LMS_LOG(RECOMMENDATION, DEBUG) << "Got " << res.size() << " similar releases using engine '" << engineTypeToString(engineType) << "'"; - break; - } + void RecommendationService::load() + { + using namespace Database; - LMS_LOG(RECOMMENDATION, DEBUG) << "No result using engine '" << engineTypeToString(engineType) << "'"; - } + switch (getSimilarityEngineType(_db.getTLSSession())) + { + case ScanSettings::SimilarityEngineType::Clusters: + if (_engineType != EngineType::Clusters) + { + _engineType = EngineType::Clusters; + _engine = createClustersEngine(_db); + } + break; - return res; - } - - ArtistContainer - RecommendationService::getSimilarArtists(Database::ArtistId artistId, EnumSet linkTypes, std::size_t maxCount) const - { - ArtistContainer res; - - std::shared_lock lock {_enginesMutex}; - for (EngineType engineType : _enginePriorities) - { - auto itEngine {_engines.find(engineType)}; - if (itEngine == std::cend(_engines)) - continue; - - LMS_LOG(RECOMMENDATION, DEBUG) << "Trying engine '" << engineTypeToString(engineType) << "' to get similar artists"; - - const IEngine& engine {*itEngine->second}; - res = engine.getSimilarArtists(artistId, linkTypes, maxCount); - if (!res.empty()) - { - LMS_LOG(RECOMMENDATION, DEBUG) << "Got " << res.size() << " similar artists using engine '" << engineTypeToString(engineType) << "'"; - return res; - } - } - - return res; - } - - static - Database::ScanSettings::SimilarityEngineType - getSimilarityEngineType(Database::Session& session) - { - auto transaction {session.createSharedTransaction()}; - - return Database::ScanSettings::get(session)->getSimilarityEngineType(); - } - - void - RecommendationService::load(bool forceReload, const ProgressCallback& progressCallback) - { - using namespace Database; - - LMS_LOG(RECOMMENDATION, INFO) << "Reloading recommendation engines..."; - - EngineContainer enginesToLoad; - - { - std::unique_lock controlLock {_controlMutex}; - - { - std::unique_lock lock {_enginesMutex}; - _engines.clear(); - } - - switch (getSimilarityEngineType(_db.getTLSSession())) - { - case ScanSettings::SimilarityEngineType::Clusters: - _enginePriorities = {EngineType::Clusters}; - enginesToLoad.try_emplace(EngineType::Clusters, createClustersEngine(_db)); - break; - - case ScanSettings::SimilarityEngineType::Features: - _enginePriorities = {EngineType::Features, EngineType::Clusters}; - - // not same order since clusters is faster to load - enginesToLoad.try_emplace(EngineType::Clusters, createClustersEngine(_db)); - enginesToLoad.try_emplace(EngineType::Features, createFeaturesEngine(_db)); - break; - - case ScanSettings::SimilarityEngineType::None: - _enginePriorities.clear(); - break; - } - - assert(_pendingEngines.empty()); - for (auto& [engineType, engine] : enginesToLoad) - _pendingEngines.push_back(engine.get()); - } - - for (auto& [engineType, engine] : enginesToLoad) - loadPendingEngine(engineType, std::move(engine), forceReload, progressCallback); - - _pendingEnginesCondvar.notify_all(); - - LMS_LOG(RECOMMENDATION, INFO) << "Recommendation engines loaded!"; - } - - void - RecommendationService::loadPendingEngine(EngineType engineType, std::unique_ptr engine, bool forceReload, const ProgressCallback& progressCallback) - { - if (!_loadCancelled) - { - LMS_LOG(RECOMMENDATION, INFO) << "Initializing engine '" << engineTypeToString(engineType) << "'..."; - - auto progress {[&](const Progress& progress) - { - progressCallback(progress); - }}; - - engine->load(forceReload, progressCallback ? progress : ProgressCallback {}); - - LMS_LOG(RECOMMENDATION, INFO) << "Initializing engine '" << engineTypeToString(engineType) << "': " << (_loadCancelled ? "aborted" : "complete"); - } - - { - std::scoped_lock lock {_controlMutex}; - _pendingEngines.erase(std::find(std::begin(_pendingEngines), std::end(_pendingEngines), engine.get())); - } - - if (!_loadCancelled) - { - std::unique_lock lock {_enginesMutex}; - _engines.emplace(engineType, std::move(engine)); - } - } - - void - RecommendationService::cancelLoad() - { - LMS_LOG(RECOMMENDATION, DEBUG) << "Cancelling loading..."; - - std::unique_lock controlLock {_controlMutex}; - - assert(!_loadCancelled); - _loadCancelled = true; - - LMS_LOG(RECOMMENDATION, DEBUG) << "Still " << _pendingEngines.size() << " pending engines!"; - - for (IEngine* engine : _pendingEngines) - { - engine->requestCancelLoad(); - } - - _pendingEnginesCondvar.wait(controlLock, [this] {return _pendingEngines.empty();}); - _loadCancelled = false; - - LMS_LOG(RECOMMENDATION, DEBUG) << "Cancelling loading DONE"; - } + case ScanSettings::SimilarityEngineType::Features: + case ScanSettings::SimilarityEngineType::None: + _engineType.reset(); + _engine.reset(); + break; + } + if (_engine) + _engine->load(false); + } } // ns Similarity diff --git a/src/libs/services/recommendation/impl/RecommendationService.hpp b/src/libs/services/recommendation/impl/RecommendationService.hpp index 11b10cfc..a27956e2 100644 --- a/src/libs/services/recommendation/impl/RecommendationService.hpp +++ b/src/libs/services/recommendation/impl/RecommendationService.hpp @@ -19,67 +19,49 @@ #pragma once -#include -#include -#include -#include -#include +#include #include "services/recommendation/IRecommendationService.hpp" #include "IEngine.hpp" namespace Database { - class Db; + class Db; } namespace Recommendation { - enum class EngineType - { - Clusters, - Features, - }; + enum class EngineType + { + Clusters, + Features, + }; - class RecommendationService : public IRecommendationService - { - public: - RecommendationService(Database::Db& db); - ~RecommendationService() = default; + class RecommendationService : public IRecommendationService + { + public: + RecommendationService(Database::Db& db); + ~RecommendationService() = default; - RecommendationService(const RecommendationService&) = delete; - RecommendationService(RecommendationService&&) = delete; - RecommendationService& operator=(const RecommendationService&) = delete; - RecommendationService& operator=(RecommendationService&&) = delete; + RecommendationService(const RecommendationService&) = delete; + RecommendationService& operator=(const RecommendationService&) = delete; - private: - void load(bool forceReload, const ProgressCallback& progressCallback) override; - void cancelLoad() override; + private: + void load() override; - TrackContainer findSimilarTracks(Database::TrackListId tracklistId, std::size_t maxCount) const override; - TrackContainer findSimilarTracks(const std::vector& tracksId, std::size_t maxCount) const override; - ReleaseContainer getSimilarReleases(Database::ReleaseId releaseId, std::size_t maxCount) const override; - ArtistContainer getSimilarArtists(Database::ArtistId artistId, EnumSet linkTypes, std::size_t maxCount) const override; + TrackContainer findSimilarTracks(Database::TrackListId tracklistId, std::size_t maxCount) const override; + TrackContainer findSimilarTracks(const std::vector& tracksId, std::size_t maxCount) const override; + ReleaseContainer getSimilarReleases(Database::ReleaseId releaseId, std::size_t maxCount) const override; + ArtistContainer getSimilarArtists(Database::ArtistId artistId, EnumSet linkTypes, std::size_t maxCount) const override; - void setEnginePriorities(const std::vector& engineTypes); - void clearEngines(); - void loadPendingEngine(EngineType engineType, std::unique_ptr engine, bool forceReload, const ProgressCallback& progressCallback); + void setEnginePriorities(const std::vector& engineTypes); + void clearEngines(); + void loadPendingEngine(EngineType engineType, std::unique_ptr engine, bool forceReload, const ProgressCallback& progressCallback); - Database::Db& _db; - - std::mutex _controlMutex; - bool _loadCancelled {}; - - using EngineContainer = std::unordered_map>; - EngineContainer _engines; - mutable std::shared_mutex _enginesMutex; - - std::vector _pendingEngines; - std::shared_mutex _pendingEnginesMutex; - std::condition_variable _pendingEnginesCondvar; - - std::vector _enginePriorities; // ordered by priority - }; + Database::Db& _db; + std::optional _engineType; + std::unique_ptr _engine; + }; } // ns Recommendation diff --git a/src/libs/services/recommendation/impl/features/FeaturesEngine.cpp b/src/libs/services/recommendation/impl/features/FeaturesEngine.cpp index 1cfb6811..60113e0b 100644 --- a/src/libs/services/recommendation/impl/features/FeaturesEngine.cpp +++ b/src/libs/services/recommendation/impl/features/FeaturesEngine.cpp @@ -120,7 +120,7 @@ FeaturesEngine::loadFromTraining(const TrainSettings& trainSettings, const Progr auto transaction {session.createSharedTransaction()}; LMS_LOG(RECOMMENDATION, DEBUG) << "Getting Track features..."; - trackFeaturesIds = TrackFeatures::find(session, Range {}); + trackFeaturesIds = TrackFeatures::find(session); LMS_LOG(RECOMMENDATION, DEBUG) << "Getting Track features DONE (found " << trackFeaturesIds.results.size() << " track features)"; } diff --git a/src/libs/services/recommendation/include/services/recommendation/IRecommendationService.hpp b/src/libs/services/recommendation/include/services/recommendation/IRecommendationService.hpp index f38f7bd0..7643d775 100644 --- a/src/libs/services/recommendation/include/services/recommendation/IRecommendationService.hpp +++ b/src/libs/services/recommendation/include/services/recommendation/IRecommendationService.hpp @@ -20,6 +20,7 @@ #pragma once #include +#include #include "utils/EnumSet.hpp" #include "services/database/TrackListId.hpp" #include "services/database/Types.hpp" @@ -37,8 +38,7 @@ namespace Recommendation public: virtual ~IRecommendationService() = default; - virtual void load(bool forceReload, const ProgressCallback& progressCallback = {}) = 0; - virtual void cancelLoad() = 0; // wait for cancel done + virtual void load() = 0; virtual TrackContainer findSimilarTracks(Database::TrackListId tracklistId, std::size_t maxCount) const = 0; virtual TrackContainer findSimilarTracks(const std::vector& tracksId, std::size_t maxCount) const = 0; diff --git a/src/libs/services/scanner/CMakeLists.txt b/src/libs/services/scanner/CMakeLists.txt index e913996f..8a0508e9 100644 --- a/src/libs/services/scanner/CMakeLists.txt +++ b/src/libs/services/scanner/CMakeLists.txt @@ -3,6 +3,7 @@ add_library(lmsscanner SHARED impl/ScannerService.cpp impl/ScannerStats.cpp impl/ScanStepCheckDuplicatedDbFiles.cpp + impl/ScanStepComputeClusterStats.cpp impl/ScanStepDiscoverFiles.cpp impl/ScanStepRemoveOrphanDbFiles.cpp impl/ScanStepScanFiles.cpp diff --git a/src/libs/services/scanner/impl/ScanStepCheckDuplicatedDbFiles.cpp b/src/libs/services/scanner/impl/ScanStepCheckDuplicatedDbFiles.cpp index 0e18f4a5..e1893a8a 100644 --- a/src/libs/services/scanner/impl/ScanStepCheckDuplicatedDbFiles.cpp +++ b/src/libs/services/scanner/impl/ScanStepCheckDuplicatedDbFiles.cpp @@ -37,7 +37,7 @@ namespace Scanner Session& session {_db.getTLSSession()}; auto transaction {session.createSharedTransaction()}; - const RangeResults tracks = Track::findIdsTrackMBIDDuplicates(session, Range {}); + const RangeResults tracks = Track::findIdsTrackMBIDDuplicates(session); for (const TrackId trackId : tracks.results) { const Track::pointer track {Track::find(session, trackId)}; diff --git a/src/libs/services/scanner/impl/ScanStepComputeClusterStats.cpp b/src/libs/services/scanner/impl/ScanStepComputeClusterStats.cpp new file mode 100644 index 00000000..38399599 --- /dev/null +++ b/src/libs/services/scanner/impl/ScanStepComputeClusterStats.cpp @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2023 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "ScanStepComputeClusterStats.hpp" +#include "services/database/Db.hpp" +#include "services/database/Cluster.hpp" +#include "services/database/Session.hpp" +#include "utils/Logger.hpp" +#include "utils/Path.hpp" + +namespace Scanner +{ + void ScanStepComputeClusterStats::process(ScanContext& context) + { + using namespace Database; + + if (context.stats.nbChanges() == 0) + return; + + Session& dbSession{ _db.getTLSSession() }; + + const std::size_t clusterCount{ [&] { + auto transaction{ dbSession.createSharedTransaction() }; + return Cluster::getCount(dbSession); + }() }; + + context.currentStepStats.totalElems = clusterCount; + + foreachSubRange(Range{ 0, clusterCount }, 100, [&](Range range) + { + const std::vector clusterIds{ [&] + { + Cluster::FindParameters params; + params.setRange(range); + + { + auto transaction{ dbSession.createSharedTransaction() }; + return std::move(Cluster::findIds(dbSession, params).results); + } + }() }; + + for (const ClusterId clusterId : clusterIds) + { + std::size_t trackCount; + std::size_t releaseCount; + + { + auto transaction{ dbSession.createSharedTransaction() }; + + trackCount = Cluster::computeTrackCount(dbSession, clusterId); + releaseCount = Cluster::computeReleaseCount(dbSession, clusterId); + } + + { + auto transaction{ dbSession.createUniqueTransaction() }; + + auto cluster{ Cluster::find(dbSession, clusterId) }; + cluster.modify()->setTrackCount(trackCount); + cluster.modify()->setReleaseCount(releaseCount); + } + + context.currentStepStats.processedElems++; + } + + return true; + }); + + LMS_LOG(DBUPDATER, DEBUG) << "Recomputed stats for " << clusterCount << " clusters!"; + } +} diff --git a/src/libs/av/include/av/TranscodeParameters.hpp b/src/libs/services/scanner/impl/ScanStepComputeClusterStats.hpp similarity index 56% rename from src/libs/av/include/av/TranscodeParameters.hpp rename to src/libs/services/scanner/impl/ScanStepComputeClusterStats.hpp index f709fef8..4ccd460d 100644 --- a/src/libs/av/include/av/TranscodeParameters.hpp +++ b/src/libs/services/scanner/impl/ScanStepComputeClusterStats.hpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015 Emeric Poupon + * Copyright (C) 2023 Emeric Poupon * * This file is part of LMS. * @@ -19,27 +19,18 @@ #pragma once -#include -#include -#include +#include "ScanStepBase.hpp" -#include "Types.hpp" - -namespace Av +namespace Scanner { - struct InputFileParameters - { - std::filesystem::path trackPath; - std::chrono::milliseconds duration; - }; - - struct TranscodeParameters - { - Format format; - std::size_t bitrate {128000}; - std::optional stream; // Id of the stream to be transcoded (auto detect by default) - std::chrono::milliseconds offset {0}; - bool stripMetadata {true}; - }; -} // namespace Av + class ScanStepComputeClusterStats : public ScanStepBase + { + public: + using ScanStepBase::ScanStepBase; + private: + ScanStep getStep() const override { return ScanStep::ComputeClusterStats; } + std::string_view getStepName() const override { return "Compute cluster stats"; } + void process(ScanContext& context) override; + }; +} diff --git a/src/libs/services/scanner/impl/ScanStepRemoveOrphanDbFiles.cpp b/src/libs/services/scanner/impl/ScanStepRemoveOrphanDbFiles.cpp index a9572fac..1ae6781d 100644 --- a/src/libs/services/scanner/impl/ScanStepRemoveOrphanDbFiles.cpp +++ b/src/libs/services/scanner/impl/ScanStepRemoveOrphanDbFiles.cpp @@ -30,174 +30,169 @@ namespace Scanner { - void - ScanStepRemoveOrphanDbFiles::process(ScanContext& context) - { - removeOrphanTracks(context); - removeOrphanClusters(); - removeOrphanArtists(); - removeOrphanReleases(); - } + void ScanStepRemoveOrphanDbFiles::process(ScanContext& context) + { + removeOrphanTracks(context); + removeOrphanClusters(); + removeOrphanArtists(); + removeOrphanReleases(); + } - void ScanStepRemoveOrphanDbFiles::removeOrphanTracks(ScanContext& context) - { - using namespace Database; + void ScanStepRemoveOrphanDbFiles::removeOrphanTracks(ScanContext& context) + { + using namespace Database; - if (_abortScan) - return; + if (_abortScan) + return; - static constexpr std::size_t batchSize {50}; - Session& session {_db.getTLSSession()}; + static constexpr std::size_t batchSize{ 50 }; + Session& session{ _db.getTLSSession() }; - LMS_LOG(DBUPDATER, DEBUG) << "Checking tracks to be removed..."; - std::size_t trackCount {}; + LMS_LOG(DBUPDATER, DEBUG) << "Checking tracks to be removed..."; + std::size_t trackCount{}; - { - auto transaction {session.createSharedTransaction()}; - trackCount = Track::getCount(session); - } - LMS_LOG(DBUPDATER, DEBUG) << trackCount << " tracks to be checked..."; + { + auto transaction{ session.createSharedTransaction() }; + trackCount = Track::getCount(session); + } + LMS_LOG(DBUPDATER, DEBUG) << trackCount << " tracks to be checked..."; - context.currentStepStats.totalElems = trackCount; + context.currentStepStats.totalElems = trackCount; - RangeResults trackPaths; - std::vector tracksToRemove; + RangeResults trackPaths; + std::vector tracksToRemove; - // TODO handle only files in context.directory - for (std::size_t i {trackCount < batchSize ? 0 : trackCount - batchSize}; ; i -= (i > batchSize ? batchSize : i)) - { - tracksToRemove.clear(); + // TODO handle only files in context.directory + for (std::size_t i{ trackCount < batchSize ? 0 : trackCount - batchSize }; ; i -= (i > batchSize ? batchSize : i)) + { + tracksToRemove.clear(); - { - auto transaction {session.createSharedTransaction()}; - trackPaths = Track::findPaths(session, Range {i, batchSize}); - } + { + auto transaction{ session.createSharedTransaction() }; + trackPaths = Track::findPaths(session, Range{ i, batchSize }); + } - for (const Track::PathResult& trackPath : trackPaths.results) - { - if (_abortScan) - return; + for (const Track::PathResult& trackPath : trackPaths.results) + { + if (_abortScan) + return; - if (!checkFile(trackPath.path)) - tracksToRemove.push_back(trackPath.trackId); + if (!checkFile(trackPath.path)) + tracksToRemove.push_back(trackPath.trackId); - context.currentStepStats.processedElems++; - } + context.currentStepStats.processedElems++; + } - if (!tracksToRemove.empty()) - { - auto transaction {session.createSharedTransaction()}; + if (!tracksToRemove.empty()) + { + auto transaction{ session.createSharedTransaction() }; - for (const TrackId trackId : tracksToRemove) - { - Track::pointer track {Track::find(session, trackId)}; - if (track) - { - track.remove(); - context.stats.deletions++; - } - } - } + for (const TrackId trackId : tracksToRemove) + { + Track::pointer track{ Track::find(session, trackId) }; + if (track) + { + track.remove(); + context.stats.deletions++; + } + } + } - _progressCallback(context.currentStepStats); + _progressCallback(context.currentStepStats); - if (i == 0) - break; - } + if (i == 0) + break; + } - LMS_LOG(DBUPDATER, DEBUG) << trackCount << " tracks checked!"; - } + LMS_LOG(DBUPDATER, DEBUG) << trackCount << " tracks checked!"; + } - void - ScanStepRemoveOrphanDbFiles::removeOrphanClusters() - { - using namespace Database; + void ScanStepRemoveOrphanDbFiles::removeOrphanClusters() + { + using namespace Database; - LMS_LOG(DBUPDATER, DEBUG) << "Checking orphan clusters..."; - Session& session {_db.getTLSSession()}; - auto transaction {session.createUniqueTransaction()}; + LMS_LOG(DBUPDATER, DEBUG) << "Checking orphan clusters..."; + Session& session{ _db.getTLSSession() }; + auto transaction{ session.createUniqueTransaction() }; - // Now process orphan Cluster (no track) - auto clusterIds {Cluster::findOrphans(session, Range {})}; - for (ClusterId clusterId : clusterIds.results) - { - Cluster::pointer cluster {Cluster::find(session, clusterId)}; - LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan cluster '" << cluster->getName() << "'"; - cluster.remove(); - } - } + // Now process orphan Cluster (no track) + auto clusterIds{ Cluster::findOrphans(session) }; + for (ClusterId clusterId : clusterIds.results) + { + Cluster::pointer cluster{ Cluster::find(session, clusterId) }; + LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan cluster '" << cluster->getName() << "'"; + cluster.remove(); + } + } - void - ScanStepRemoveOrphanDbFiles::removeOrphanArtists() - { - using namespace Database; + void ScanStepRemoveOrphanDbFiles::removeOrphanArtists() + { + using namespace Database; - LMS_LOG(DBUPDATER, DEBUG) << "Checking orphan artists..."; + LMS_LOG(DBUPDATER, DEBUG) << "Checking orphan artists..."; - Session& session {_db.getTLSSession()}; - auto transaction {session.createUniqueTransaction()}; + Session& session{ _db.getTLSSession() }; + auto transaction{ session.createUniqueTransaction() }; - auto artistIds {Artist::findOrphanIds(session, Range {})}; - for (const ArtistId artistId : artistIds.results) - { - Artist::pointer artist {Artist::find(session, artistId)}; - LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan artist '" << artist->getName() << "'"; - artist.remove(); - } - } + auto artistIds{ Artist::findOrphanIds(session) }; + for (const ArtistId artistId : artistIds.results) + { + Artist::pointer artist{ Artist::find(session, artistId) }; + LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan artist '" << artist->getName() << "'"; + artist.remove(); + } + } - void - ScanStepRemoveOrphanDbFiles::removeOrphanReleases() - { - using namespace Database; + void ScanStepRemoveOrphanDbFiles::removeOrphanReleases() + { + using namespace Database; - LMS_LOG(DBUPDATER, DEBUG) << "Checking orphan releases..."; + LMS_LOG(DBUPDATER, DEBUG) << "Checking orphan releases..."; - // TODO, by batch - Session& session {_db.getTLSSession()}; - auto transaction {session.createUniqueTransaction()}; + // TODO, by batch + Session& session{ _db.getTLSSession() }; + auto transaction{ session.createUniqueTransaction() }; - auto releases {Release::findOrphanIds(session, Range {})}; - for (const ReleaseId releaseId : releases.results) - { - Release::pointer release {Release::find(session, releaseId)}; - LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan release '" << release->getName() << "'"; - release.remove(); - } - } + auto releases{ Release::findOrphanIds(session) }; + for (const ReleaseId releaseId : releases.results) + { + Release::pointer release{ Release::find(session, releaseId) }; + LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan release '" << release->getName() << "'"; + release.remove(); + } + } - bool - ScanStepRemoveOrphanDbFiles::checkFile(const std::filesystem::path& p) - { - try - { - // For each track, make sure the the file still exists - // and still belongs to a media directory - if (!std::filesystem::exists( p ) - || !std::filesystem::is_regular_file( p ) ) - { - LMS_LOG(DBUPDATER, INFO) << "Removing '" << p.string() << "': missing"; - return false; - } + bool ScanStepRemoveOrphanDbFiles::checkFile(const std::filesystem::path& p) + { + try + { + // For each track, make sure the the file still exists + // and still belongs to a media directory + if (!std::filesystem::exists(p) + || !std::filesystem::is_regular_file(p)) + { + LMS_LOG(DBUPDATER, INFO) << "Removing '" << p.string() << "': missing"; + return false; + } - if (!PathUtils::isPathInRootPath(p, _settings.mediaDirectory, &excludeDirFileName)) - { - LMS_LOG(DBUPDATER, INFO) << "Removing '" << p.string() << "': out of media directory"; - return false; - } + if (!PathUtils::isPathInRootPath(p, _settings.mediaDirectory, &excludeDirFileName)) + { + LMS_LOG(DBUPDATER, INFO) << "Removing '" << p.string() << "': out of media directory"; + return false; + } - if (!PathUtils::hasFileAnyExtension(p, _settings.supportedExtensions)) - { - LMS_LOG(DBUPDATER, INFO) << "Removing '" << p.string() << "': file format no longer handled"; - return false; - } + if (!PathUtils::hasFileAnyExtension(p, _settings.supportedExtensions)) + { + LMS_LOG(DBUPDATER, INFO) << "Removing '" << p.string() << "': file format no longer handled"; + return false; + } - return true; - } - catch (std::filesystem::filesystem_error& e) - { - LMS_LOG(DBUPDATER, ERROR) << "Caught exception while checking file '" << p.string() << "': " << e.what(); - return false; - } - } + return true; + } + catch (std::filesystem::filesystem_error& e) + { + LMS_LOG(DBUPDATER, ERROR) << "Caught exception while checking file '" << p.string() << "': " << e.what(); + return false; + } + } } diff --git a/src/libs/services/scanner/impl/ScanStepScanFiles.cpp b/src/libs/services/scanner/impl/ScanStepScanFiles.cpp index 86820bd5..74a33540 100644 --- a/src/libs/services/scanner/impl/ScanStepScanFiles.cpp +++ b/src/libs/services/scanner/impl/ScanStepScanFiles.cpp @@ -37,500 +37,486 @@ using namespace Database; namespace { - Artist::pointer - createArtist(Session& session, const MetaData::Artist& artistInfo) - { - Artist::pointer artist {session.create(artistInfo.name)}; + Artist::pointer + createArtist(Session& session, const MetaData::Artist& artistInfo) + { + Artist::pointer artist{ session.create(artistInfo.name) }; - if (artistInfo.mbid) - artist.modify()->setMBID(*artistInfo.mbid); - if (artistInfo.sortName) - artist.modify()->setSortName(*artistInfo.sortName); + if (artistInfo.mbid) + artist.modify()->setMBID(*artistInfo.mbid); + if (artistInfo.sortName) + artist.modify()->setSortName(*artistInfo.sortName); - return artist; - } + return artist; + } - void - updateArtistIfNeeded(Artist::pointer artist, const MetaData::Artist& artistInfo) - { - // Name may have been updated - if (artist->getName() != artistInfo.name) - { - artist.modify()->setName(artistInfo.name); - } + void + updateArtistIfNeeded(Artist::pointer artist, const MetaData::Artist& artistInfo) + { + // Name may have been updated + if (artist->getName() != artistInfo.name) + { + artist.modify()->setName(artistInfo.name); + } - // Sortname may have been updated - if (artistInfo.sortName && *artistInfo.sortName != artist->getSortName() ) - { - artist.modify()->setSortName(*artistInfo.sortName); - } - } + // Sortname may have been updated + if (artistInfo.sortName && *artistInfo.sortName != artist->getSortName()) + { + artist.modify()->setSortName(*artistInfo.sortName); + } + } - std::vector - getOrCreateArtists(Session& session, const std::vector& artistsInfo, bool allowFallbackOnMBIDEntries) - { - std::vector artists; + std::vector + getOrCreateArtists(Session& session, const std::vector& artistsInfo, bool allowFallbackOnMBIDEntries) + { + std::vector artists; - for (const MetaData::Artist& artistInfo : artistsInfo) - { - Artist::pointer artist; + for (const MetaData::Artist& artistInfo : artistsInfo) + { + Artist::pointer artist; - // First try to get by MBID - if (artistInfo.mbid) - { - artist = Artist::find(session, *artistInfo.mbid); - if (!artist) - artist = createArtist(session, artistInfo); - else - updateArtistIfNeeded(artist, artistInfo); + // First try to get by MBID + if (artistInfo.mbid) + { + artist = Artist::find(session, *artistInfo.mbid); + if (!artist) + artist = createArtist(session, artistInfo); + else + updateArtistIfNeeded(artist, artistInfo); - artists.emplace_back(std::move(artist)); - continue; - } + artists.emplace_back(std::move(artist)); + continue; + } - // Fall back on artist name (collisions may occur) - if (!artistInfo.name.empty()) - { - for (const Artist::pointer& sameNamedArtist : Artist::find(session, artistInfo.name)) - { - // Do not fallback on artist that is correctly tagged - if (!allowFallbackOnMBIDEntries && sameNamedArtist->getMBID()) - continue; + // Fall back on artist name (collisions may occur) + if (!artistInfo.name.empty()) + { + for (const Artist::pointer& sameNamedArtist : Artist::find(session, artistInfo.name)) + { + // Do not fallback on artist that is correctly tagged + if (!allowFallbackOnMBIDEntries && sameNamedArtist->getMBID()) + continue; - artist = sameNamedArtist; - break; - } + artist = sameNamedArtist; + break; + } - // No Artist found with the same name and without MBID -> creating - if (!artist) - artist = createArtist(session, artistInfo); - else - updateArtistIfNeeded(artist, artistInfo); + // No Artist found with the same name and without MBID -> creating + if (!artist) + artist = createArtist(session, artistInfo); + else + updateArtistIfNeeded(artist, artistInfo); - artists.emplace_back(std::move(artist)); - continue; - } - } + artists.emplace_back(std::move(artist)); + continue; + } + } - return artists; - } + return artists; + } - ReleaseTypePrimary convertReleaseTypePrimary(MetaData::Release::PrimaryType type) - { - switch (type) - { - case MetaData::Release::PrimaryType::Album: return ReleaseTypePrimary::Album; - case MetaData::Release::PrimaryType::Single: return ReleaseTypePrimary::Single; - case MetaData::Release::PrimaryType::EP: return ReleaseTypePrimary::EP; - case MetaData::Release::PrimaryType::Broadcast: return ReleaseTypePrimary::Broadcast; - case MetaData::Release::PrimaryType::Other: return ReleaseTypePrimary::Other; - } + ReleaseTypePrimary convertReleaseTypePrimary(MetaData::Release::PrimaryType type) + { + switch (type) + { + case MetaData::Release::PrimaryType::Album: return ReleaseTypePrimary::Album; + case MetaData::Release::PrimaryType::Single: return ReleaseTypePrimary::Single; + case MetaData::Release::PrimaryType::EP: return ReleaseTypePrimary::EP; + case MetaData::Release::PrimaryType::Broadcast: return ReleaseTypePrimary::Broadcast; + case MetaData::Release::PrimaryType::Other: return ReleaseTypePrimary::Other; + } - return ReleaseTypePrimary::Other; - } + return ReleaseTypePrimary::Other; + } - EnumSet convertReleaseTypesSecondary(EnumSet types) - { - EnumSet res; + EnumSet convertReleaseTypesSecondary(EnumSet types) + { + EnumSet res; - for (MetaData::Release::SecondaryType type : types) - { - switch (type) - { - case MetaData::Release::SecondaryType::Compilation: - res.insert(ReleaseTypeSecondary::Compilation); - break; - case MetaData::Release::SecondaryType::Soundtrack: - res.insert(ReleaseTypeSecondary::Soundtrack); - break; - case MetaData::Release::SecondaryType::Spokenword: - res.insert(ReleaseTypeSecondary::Spokenword); - break; - case MetaData::Release::SecondaryType::Interview: - res.insert(ReleaseTypeSecondary::Interview); - break; - case MetaData::Release::SecondaryType::Audiobook: - res.insert(ReleaseTypeSecondary::Audiobook); - break; - case MetaData::Release::SecondaryType::AudioDrama: - res.insert(ReleaseTypeSecondary::AudioDrama); - break; - case MetaData::Release::SecondaryType::Live: - res.insert(ReleaseTypeSecondary::Live); - break; - case MetaData::Release::SecondaryType::Remix: - res.insert(ReleaseTypeSecondary::Remix); - break; - case MetaData::Release::SecondaryType::DJMix: - res.insert(ReleaseTypeSecondary::DJMix); - break; - case MetaData::Release::SecondaryType::Mixtape_Street: - res.insert(ReleaseTypeSecondary::Mixtape_Street); - break; - case MetaData::Release::SecondaryType::Demo: - res.insert(ReleaseTypeSecondary::Demo); - break; - } - } + for (MetaData::Release::SecondaryType type : types) + { + switch (type) + { + case MetaData::Release::SecondaryType::Compilation: + res.insert(ReleaseTypeSecondary::Compilation); + break; + case MetaData::Release::SecondaryType::Soundtrack: + res.insert(ReleaseTypeSecondary::Soundtrack); + break; + case MetaData::Release::SecondaryType::Spokenword: + res.insert(ReleaseTypeSecondary::Spokenword); + break; + case MetaData::Release::SecondaryType::Interview: + res.insert(ReleaseTypeSecondary::Interview); + break; + case MetaData::Release::SecondaryType::Audiobook: + res.insert(ReleaseTypeSecondary::Audiobook); + break; + case MetaData::Release::SecondaryType::AudioDrama: + res.insert(ReleaseTypeSecondary::AudioDrama); + break; + case MetaData::Release::SecondaryType::Live: + res.insert(ReleaseTypeSecondary::Live); + break; + case MetaData::Release::SecondaryType::Remix: + res.insert(ReleaseTypeSecondary::Remix); + break; + case MetaData::Release::SecondaryType::DJMix: + res.insert(ReleaseTypeSecondary::DJMix); + break; + case MetaData::Release::SecondaryType::Mixtape_Street: + res.insert(ReleaseTypeSecondary::Mixtape_Street); + break; + case MetaData::Release::SecondaryType::Demo: + res.insert(ReleaseTypeSecondary::Demo); + break; + } + } - return res; - } + return res; + } - void - updateReleaseIfNeeded(Release::pointer release, const MetaData::Release& releaseInfo) - { - if (release->getName() != releaseInfo.name) - release.modify()->setName(releaseInfo.name); - if (release->getTotalDisc() != releaseInfo.mediumCount) - release.modify()->setTotalDisc(releaseInfo.mediumCount); - if (releaseInfo.primaryType) - { - const ReleaseTypePrimary primaryType {convertReleaseTypePrimary(*releaseInfo.primaryType)}; - if (release->getPrimaryType() != primaryType) - release.modify()->setPrimaryType(primaryType); - } - const EnumSet secondaryTypes{ convertReleaseTypesSecondary(releaseInfo.secondaryTypes) }; - if (release->getSecondaryTypes() != secondaryTypes) - release.modify()->setSecondaryTypes(secondaryTypes); - if (release->getArtistDisplayName() != releaseInfo.artistDisplayName) - release.modify()->setArtistDisplayName(releaseInfo.artistDisplayName); - } + void + updateReleaseIfNeeded(Release::pointer release, const MetaData::Release& releaseInfo) + { + if (release->getName() != releaseInfo.name) + release.modify()->setName(releaseInfo.name); + if (release->getTotalDisc() != releaseInfo.mediumCount) + release.modify()->setTotalDisc(releaseInfo.mediumCount); + if (releaseInfo.primaryType) + { + const ReleaseTypePrimary primaryType{ convertReleaseTypePrimary(*releaseInfo.primaryType) }; + if (release->getPrimaryType() != primaryType) + release.modify()->setPrimaryType(primaryType); + } + const EnumSet secondaryTypes{ convertReleaseTypesSecondary(releaseInfo.secondaryTypes) }; + if (release->getSecondaryTypes() != secondaryTypes) + release.modify()->setSecondaryTypes(secondaryTypes); + if (release->getArtistDisplayName() != releaseInfo.artistDisplayName) + release.modify()->setArtistDisplayName(releaseInfo.artistDisplayName); + } - Release::pointer - getOrCreateRelease(Session& session, const MetaData::Release& releaseInfo) - { - Release::pointer release; + Release::pointer + getOrCreateRelease(Session& session, const MetaData::Release& releaseInfo) + { + Release::pointer release; - // First try to get by MBID - if (releaseInfo.mbid) - { - release = Release::find(session, *releaseInfo.mbid); - if (!release) - release = session.create(releaseInfo.name, releaseInfo.mbid); + // First try to get by MBID + if (releaseInfo.mbid) + { + release = Release::find(session, *releaseInfo.mbid); + if (!release) + release = session.create(releaseInfo.name, releaseInfo.mbid); - updateReleaseIfNeeded(release, releaseInfo); - return release; - } + updateReleaseIfNeeded(release, releaseInfo); + return release; + } - // Fall back on release name (collisions may occur) - if (!releaseInfo.name.empty()) - { - for (const Release::pointer& sameNamedRelease : Release::find(session, releaseInfo.name)) - { - // do not fallback on properly tagged releases - if (sameNamedRelease->getMBID()) - continue; + // Fall back on release name (collisions may occur) + if (!releaseInfo.name.empty()) + { + for (const Release::pointer& sameNamedRelease : Release::find(session, releaseInfo.name)) + { + // do not fallback on properly tagged releases + if (sameNamedRelease->getMBID()) + continue; - release = sameNamedRelease; - break; - } + release = sameNamedRelease; + break; + } - // No release found with the same name and without MBID -> creating - if (!release) - release = session.create(releaseInfo.name); + // No release found with the same name and without MBID -> creating + if (!release) + release = session.create(releaseInfo.name); - updateReleaseIfNeeded(release, releaseInfo); - return release; - } + updateReleaseIfNeeded(release, releaseInfo); + return release; + } - return Release::pointer{}; - } + return Release::pointer{}; + } - std::vector - getOrCreateClusters(Session& session, const MetaData::Tags& tags) - { - std::vector clusters; + std::vector + getOrCreateClusters(Session& session, const MetaData::Tags& tags) + { + std::vector clusters; - for (const auto& [tag, values] : tags) - { - auto clusterType = ClusterType::find(session, tag); - if (!clusterType) - continue; + for (const auto& [tag, values] : tags) + { + auto clusterType = ClusterType::find(session, tag); + if (!clusterType) + continue; - for (auto clusterName : values) - { - auto cluster = clusterType->getCluster(clusterName); - if (!cluster) - cluster = session.create(clusterType, clusterName); + for (auto clusterName : values) + { + auto cluster = clusterType->getCluster(clusterName); + if (!cluster) + cluster = session.create(clusterType, clusterName); - clusters.push_back(cluster); - } - } + clusters.push_back(cluster); + } + } - return clusters; - } + return clusters; + } - MetaData::ParserReadStyle - getParserReadStyle() - { - std::string_view readStyle {Service::get()->getString("scanner-parser-read-style", "accurate")}; + MetaData::ParserReadStyle + getParserReadStyle() + { + std::string_view readStyle{ Service::get()->getString("scanner-parser-read-style", "accurate") }; - if (readStyle == "fast") - return MetaData::ParserReadStyle::Fast; - else if (readStyle == "average") - return MetaData::ParserReadStyle::Average; - else if (readStyle == "accurate") - return MetaData::ParserReadStyle::Accurate; + if (readStyle == "fast") + return MetaData::ParserReadStyle::Fast; + else if (readStyle == "average") + return MetaData::ParserReadStyle::Average; + else if (readStyle == "accurate") + return MetaData::ParserReadStyle::Accurate; - throw LmsException {"Invalid value for 'scanner-parser-read-style'"}; - } + throw LmsException{ "Invalid value for 'scanner-parser-read-style'" }; + } } // namespace namespace Scanner { - ScanStepScanFiles::ScanStepScanFiles(InitParams& initParams) - : ScanStepBase {initParams} - , _metadataParser {MetaData::createParser(MetaData::ParserType::TagLib, getParserReadStyle())} // For now, always use TagLib - { - } + ScanStepScanFiles::ScanStepScanFiles(InitParams& initParams) + : ScanStepBase{ initParams } + , _metadataParser{ MetaData::createParser(MetaData::ParserType::TagLib, getParserReadStyle()) } // For now, always use TagLib + { + } - void - ScanStepScanFiles::process(ScanContext& context) - { - _metadataParser->setClusterTypeNames(_settings.clusterTypeNames); + void + ScanStepScanFiles::process(ScanContext& context) + { + _metadataParser->setClusterTypeNames(_settings.clusterTypeNames); - context.currentStepStats.totalElems = context.stats.filesScanned; + context.currentStepStats.totalElems = context.stats.filesScanned; - PathUtils::exploreFilesRecursive(context.directory, [&](std::error_code ec, const std::filesystem::path& path) - { - if (_abortScan) - return false; + PathUtils::exploreFilesRecursive(context.directory, [&](std::error_code ec, const std::filesystem::path& path) + { + if (_abortScan) + return false; - if (ec) - { - LMS_LOG(DBUPDATER, ERROR) << "Cannot process entry '" << path.string() << "': " << ec.message(); - context.stats.errors.emplace_back(ScanError {path, ScanErrorType::CannotReadFile, ec.message()}); - } - else if (PathUtils::hasFileAnyExtension(path, _settings.supportedExtensions)) - { - scanAudioFile(path, context); + if (ec) + { + LMS_LOG(DBUPDATER, ERROR) << "Cannot process entry '" << path.string() << "': " << ec.message(); + context.stats.errors.emplace_back(ScanError{ path, ScanErrorType::CannotReadFile, ec.message() }); + } + else if (PathUtils::hasFileAnyExtension(path, _settings.supportedExtensions)) + { + scanAudioFile(path, context); - context.currentStepStats.processedElems++; - _progressCallback(context.currentStepStats); + context.currentStepStats.processedElems++; + _progressCallback(context.currentStepStats); - // optimize the database during scan (if we import a very large database, it may be too late to do it once at end) - if ((context.stats.nbChanges() % 5'000) == 0) - _db.getTLSSession().optimize(); - } + // optimize the database during scan (if we import a very large database, it may be too late to do it once at end) + if ((context.currentStepStats.processedElems % 1'000) == 0) + _db.getTLSSession().optimize(); + } - return true; - }, &excludeDirFileName); - } + return true; + }, &excludeDirFileName); + } - void - ScanStepScanFiles::scanAudioFile(const std::filesystem::path& file, ScanContext& context) - { - ScanStats& stats {context.stats}; - Wt::WDateTime lastWriteTime; - try - { - lastWriteTime = PathUtils::getLastWriteTime(file); - } - catch (LmsException& e) - { - LMS_LOG(DBUPDATER, ERROR) << e.what(); - stats.skips++; - return; - } + void + ScanStepScanFiles::scanAudioFile(const std::filesystem::path& file, ScanContext& context) + { + ScanStats& stats{ context.stats }; + Wt::WDateTime lastWriteTime; + try + { + lastWriteTime = PathUtils::getLastWriteTime(file); + } + catch (LmsException& e) + { + LMS_LOG(DBUPDATER, ERROR) << e.what(); + stats.skips++; + return; + } - if (!context.forceScan) - { - // Skip file if last write is the same - Database::Session& dbSession {_db.getTLSSession()}; - auto transaction {_db.getTLSSession().createSharedTransaction()}; + if (!context.forceScan) + { + // Skip file if last write is the same + Database::Session& dbSession{ _db.getTLSSession() }; + auto transaction{ _db.getTLSSession().createSharedTransaction() }; - const Track::pointer track {Track::findByPath(dbSession, file)}; + const Track::pointer track{ Track::findByPath(dbSession, file) }; - if (track && track->getLastWriteTime().toTime_t() == lastWriteTime.toTime_t() - && track->getScanVersion() == _settings.scanVersion) - { - stats.skips++; - return; - } - } + if (track && track->getLastWriteTime().toTime_t() == lastWriteTime.toTime_t() + && track->getScanVersion() == _settings.scanVersion) + { + stats.skips++; + return; + } + } - std::optional trackInfo {_metadataParser->parse(file)}; - if (!trackInfo) - { - context.stats.errors.emplace_back(file, ScanErrorType::CannotParseFile); - return; - } + std::optional trackInfo{ _metadataParser->parse(file) }; + if (!trackInfo) + { + context.stats.errors.emplace_back(file, ScanErrorType::CannotParseFile); + return; + } - stats.scans++; + stats.scans++; - Database::Session& dbSession {_db.getTLSSession()}; - auto uniqueTransaction {dbSession.createUniqueTransaction()}; + Database::Session& dbSession{ _db.getTLSSession() }; + auto uniqueTransaction{ dbSession.createUniqueTransaction() }; - Track::pointer track {Track::findByPath(dbSession, file) }; + Track::pointer track{ Track::findByPath(dbSession, file) }; - if (trackInfo->mbid && (!track || _settings.skipDuplicateMBID)) - { - std::vector duplicateTracks {Track::findByMBID(dbSession, *trackInfo->mbid)}; + if (trackInfo->mbid && (!track || _settings.skipDuplicateMBID)) + { + std::vector duplicateTracks{ Track::findByMBID(dbSession, *trackInfo->mbid) }; - // find for existing MBIDs as the file may have just been moved - if (!track && duplicateTracks.size() == 1) - { - Track::pointer otherTrack {duplicateTracks.front()}; - std::error_code ec; - if (!std::filesystem::exists(otherTrack->getPath(), ec)) - { - LMS_LOG(DBUPDATER, DEBUG) << "Considering track '" << file.string() << "' moved from '" << otherTrack->getPath() << "'"; - track = otherTrack; - track.modify()->setPath(file); - } - } + // find for existing MBIDs as the file may have just been moved + if (!track && duplicateTracks.size() == 1) + { + Track::pointer otherTrack{ duplicateTracks.front() }; + std::error_code ec; + if (!std::filesystem::exists(otherTrack->getPath(), ec)) + { + LMS_LOG(DBUPDATER, DEBUG) << "Considering track '" << file.string() << "' moved from '" << otherTrack->getPath() << "'"; + track = otherTrack; + track.modify()->setPath(file); + } + } - // Skip duplicate track MBID - if (_settings.skipDuplicateMBID) - { - for (Track::pointer otherTrack : duplicateTracks) - { - // Skip ourselves - if (track && track->getId() == otherTrack->getId()) - continue; + // Skip duplicate track MBID + if (_settings.skipDuplicateMBID) + { + for (Track::pointer otherTrack : duplicateTracks) + { + // Skip ourselves + if (track && track->getId() == otherTrack->getId()) + continue; - // Skip if duplicate files no longer in media root: as it will be removed later, we will end up with no file - if (!PathUtils::isPathInRootPath(file, _settings.mediaDirectory, &excludeDirFileName)) - continue; + // Skip if duplicate files no longer in media root: as it will be removed later, we will end up with no file + if (!PathUtils::isPathInRootPath(file, _settings.mediaDirectory, &excludeDirFileName)) + continue; - LMS_LOG(DBUPDATER, DEBUG) << "Skipped '" << file.string() << "' (similar MBID in '" << otherTrack->getPath().string() << "')"; - // As this MBID already exists, just remove what we just scanned - if (track) - { - track.remove(); - stats.deletions++; - } - return; - } - } - } + LMS_LOG(DBUPDATER, DEBUG) << "Skipped '" << file.string() << "' (similar MBID in '" << otherTrack->getPath().string() << "')"; + // As this MBID already exists, just remove what we just scanned + if (track) + { + track.remove(); + stats.deletions++; + } + return; + } + } + } - // We estimate this is an audio file if: - // - we found a least one audio stream - // - the duration is not null - if (trackInfo->audioStreams.empty()) - { - LMS_LOG(DBUPDATER, DEBUG) << "Skipped '" << file.string() << "' (no audio stream found)"; + // We estimate this is an audio file if the duration is not null + if (trackInfo->duration == std::chrono::milliseconds::zero()) + { + LMS_LOG(DBUPDATER, DEBUG) << "Skipped '" << file.string() << "' (duration is 0)"; - // If Track exists here, delete it! - if (track) - { - track.remove(); - stats.deletions++; - } - stats.errors.emplace_back(ScanError {file, ScanErrorType::NoAudioTrack}); - return; - } - if (trackInfo->duration == std::chrono::milliseconds::zero()) - { - LMS_LOG(DBUPDATER, DEBUG) << "Skipped '" << file.string() << "' (duration is 0)"; + // If Track exists here, delete it! + if (track) + { + track.remove(); + stats.deletions++; + } + stats.errors.emplace_back(ScanError{ file, ScanErrorType::BadDuration }); + return; + } - // If Track exists here, delete it! - if (track) - { - track.remove(); - stats.deletions++; - } - stats.errors.emplace_back(ScanError {file, ScanErrorType::BadDuration}); - return; - } + // ***** Title + std::string title; + if (!trackInfo->title.empty()) + title = trackInfo->title; + else + { + // TODO parse file name guess track etc. + // For now juste use file name as title + title = file.filename().string(); + } - // ***** Title - std::string title; - if (!trackInfo->title.empty()) - title = trackInfo->title; - else - { - // TODO parse file name guess track etc. - // For now juste use file name as title - title = file.filename().string(); - } + // If file already exists, update its data + // Otherwise, create it + if (!track) + { + track = dbSession.create(file); + LMS_LOG(DBUPDATER, DEBUG) << "Adding '" << file.string() << "'"; + stats.additions++; + } + else + { + LMS_LOG(DBUPDATER, DEBUG) << "Updating '" << file.string() << "'"; - // If file already exists, update its data - // Otherwise, create it - if (!track) - { - track = dbSession.create(file); - LMS_LOG(DBUPDATER, DEBUG) << "Adding '" << file.string() << "'"; - stats.additions++; - } - else - { - LMS_LOG(DBUPDATER, DEBUG) << "Updating '" << file.string() << "'"; + stats.updates++; + } - stats.updates++; - } + // Track related data + assert(track); - // Track related data - assert(track); + track.modify()->clearArtistLinks(); + // Do not fallback on artists with the same name but having a MBID for artist and releaseArtists, as it may be corrected by properly tagging files + for (const Artist::pointer& artist : getOrCreateArtists(dbSession, trackInfo->artists, false)) + track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, artist, TrackArtistLinkType::Artist)); - track.modify()->clearArtistLinks(); - // Do not fallback on artists with the same name but having a MBID for artist and releaseArtists, as it may be corrected by properly tagging files - for (const Artist::pointer& artist : getOrCreateArtists(dbSession, trackInfo->artists, false)) - track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, artist, TrackArtistLinkType::Artist)); + if (trackInfo->medium && trackInfo->medium->release) + { + for (const Artist::pointer& releaseArtist : getOrCreateArtists(dbSession, trackInfo->medium->release->artists, false)) + track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, releaseArtist, TrackArtistLinkType::ReleaseArtist)); + } - if (trackInfo->medium && trackInfo->medium->release) - { - for (const Artist::pointer& releaseArtist : getOrCreateArtists(dbSession, trackInfo->medium->release->artists, false)) - track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, releaseArtist, TrackArtistLinkType::ReleaseArtist)); - } + // Allow fallbacks on artists with the same name even if they have MBID, since there is no tag to indicate the MBID of these artists + // We could ask MusicBrainz to get all the information, but that would heavily slow down the import process + for (const Artist::pointer& conductor : getOrCreateArtists(dbSession, trackInfo->conductorArtists, true)) + track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, conductor, TrackArtistLinkType::Conductor)); - // Allow fallbacks on artists with the same name even if they have MBID, since there is no tag to indicate the MBID of these artists - // We could ask MusicBrainz to get all the information, but that would heavily slow down the import process - for (const Artist::pointer& conductor : getOrCreateArtists(dbSession, trackInfo->conductorArtists, true)) - track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, conductor, TrackArtistLinkType::Conductor)); + for (const Artist::pointer& composer : getOrCreateArtists(dbSession, trackInfo->composerArtists, true)) + track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, composer, TrackArtistLinkType::Composer)); - for (const Artist::pointer& composer : getOrCreateArtists(dbSession, trackInfo->composerArtists, true)) - track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, composer, TrackArtistLinkType::Composer)); + for (const Artist::pointer& lyricist : getOrCreateArtists(dbSession, trackInfo->lyricistArtists, true)) + track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, lyricist, TrackArtistLinkType::Lyricist)); - for (const Artist::pointer& lyricist : getOrCreateArtists(dbSession, trackInfo->lyricistArtists, true)) - track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, lyricist, TrackArtistLinkType::Lyricist)); + for (const Artist::pointer& mixer : getOrCreateArtists(dbSession, trackInfo->mixerArtists, true)) + track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, mixer, TrackArtistLinkType::Mixer)); - for (const Artist::pointer& mixer : getOrCreateArtists(dbSession, trackInfo->mixerArtists, true)) - track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, mixer, TrackArtistLinkType::Mixer)); + for (const auto& [role, performers] : trackInfo->performerArtists) + { + for (const Artist::pointer& performer : getOrCreateArtists(dbSession, performers, true)) + track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, performer, TrackArtistLinkType::Performer, role)); + } - for (const auto& [role, performers] : trackInfo->performerArtists) - { - for (const Artist::pointer& performer : getOrCreateArtists(dbSession, performers, true)) - track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, performer, TrackArtistLinkType::Performer, role)); - } + for (const Artist::pointer& producer : getOrCreateArtists(dbSession, trackInfo->producerArtists, true)) + track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, producer, TrackArtistLinkType::Producer)); - for (const Artist::pointer& producer : getOrCreateArtists(dbSession, trackInfo->producerArtists, true)) - track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, producer, TrackArtistLinkType::Producer)); + for (const Artist::pointer& remixer : getOrCreateArtists(dbSession, trackInfo->remixerArtists, true)) + track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, remixer, TrackArtistLinkType::Remixer)); - for (const Artist::pointer& remixer : getOrCreateArtists(dbSession, trackInfo->remixerArtists, true)) - track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, remixer, TrackArtistLinkType::Remixer)); + track.modify()->setScanVersion(_settings.scanVersion); + if (trackInfo->medium && trackInfo->medium->release) + track.modify()->setRelease(getOrCreateRelease(dbSession, *trackInfo->medium->release)); + else + track.modify()->setRelease({}); + track.modify()->setTotalTrack(trackInfo->medium ? trackInfo->medium->trackCount : std::nullopt); + track.modify()->setReleaseReplayGain(trackInfo->medium ? trackInfo->medium->replayGain : std::nullopt); + track.modify()->setDiscSubtitle(trackInfo->medium ? trackInfo->medium->name : ""); + track.modify()->setClusters(getOrCreateClusters(dbSession, trackInfo->tags)); + track.modify()->setLastWriteTime(lastWriteTime); + track.modify()->setName(title); + track.modify()->setDuration(trackInfo->duration); + track.modify()->setBitrate(trackInfo->bitrate); + track.modify()->setAddedTime(Wt::WDateTime::currentDateTime()); + track.modify()->setTrackNumber(trackInfo->position); + track.modify()->setDiscNumber(trackInfo->medium ? trackInfo->medium->position : std::nullopt); + track.modify()->setDate(trackInfo->date); + track.modify()->setOriginalDate(trackInfo->originalDate); - track.modify()->setScanVersion(_settings.scanVersion); - if (trackInfo->medium && trackInfo->medium->release) - track.modify()->setRelease(getOrCreateRelease(dbSession, *trackInfo->medium->release)); - else - track.modify()->setRelease({}); - track.modify()->setTotalTrack(trackInfo->medium ? trackInfo->medium->trackCount : std::nullopt); - track.modify()->setReleaseReplayGain(trackInfo->medium ? trackInfo->medium->replayGain : std::nullopt); - track.modify()->setDiscSubtitle(trackInfo->medium ? trackInfo->medium->name : ""); - track.modify()->setClusters(getOrCreateClusters(dbSession, trackInfo->tags)); - track.modify()->setLastWriteTime(lastWriteTime); - track.modify()->setName(title); - track.modify()->setDuration(trackInfo->duration); - track.modify()->setAddedTime(Wt::WDateTime::currentDateTime()); - track.modify()->setTrackNumber(trackInfo->position); - track.modify()->setDiscNumber(trackInfo->medium ? trackInfo->medium->position : std::nullopt); - track.modify()->setDate(trackInfo->date); - track.modify()->setOriginalDate(trackInfo->originalDate); + // If a file has an OriginalYear but no Year, set it to ease filtering + if (!trackInfo->date.isValid() && trackInfo->originalDate.isValid()) + track.modify()->setDate(trackInfo->originalDate); - // If a file has an OriginalYear but no Year, set it to ease filtering - if (!trackInfo->date.isValid() && trackInfo->originalDate.isValid()) - track.modify()->setDate(trackInfo->originalDate); - - track.modify()->setRecordingMBID(trackInfo->recordingMBID); - track.modify()->setTrackMBID(trackInfo->mbid); - if (auto trackFeatures {TrackFeatures::find(dbSession, track->getId())}) - trackFeatures.remove(); // TODO: only if MBID changed? - track.modify()->setHasCover(trackInfo->hasCover); - track.modify()->setCopyright(trackInfo->copyright); - track.modify()->setCopyrightURL(trackInfo->copyrightURL); - track.modify()->setTrackReplayGain(trackInfo->replayGain); - track.modify()->setArtistDisplayName(trackInfo->artistDisplayName); - } + track.modify()->setRecordingMBID(trackInfo->recordingMBID); + track.modify()->setTrackMBID(trackInfo->mbid); + if (auto trackFeatures{ TrackFeatures::find(dbSession, track->getId()) }) + trackFeatures.remove(); // TODO: only if MBID changed? + track.modify()->setHasCover(trackInfo->hasCover); + track.modify()->setCopyright(trackInfo->copyright); + track.modify()->setCopyrightURL(trackInfo->copyrightURL); + track.modify()->setTrackReplayGain(trackInfo->replayGain); + track.modify()->setArtistDisplayName(trackInfo->artistDisplayName); + } } diff --git a/src/libs/services/scanner/impl/ScannerService.cpp b/src/libs/services/scanner/impl/ScannerService.cpp index 3fa41ed2..17eba786 100644 --- a/src/libs/services/scanner/impl/ScannerService.cpp +++ b/src/libs/services/scanner/impl/ScannerService.cpp @@ -25,7 +25,6 @@ #include "services/database/Cluster.hpp" #include "services/database/TrackFeatures.hpp" #include "services/database/ScanSettings.hpp" -#include "services/recommendation/IRecommendationService.hpp" #include "utils/Exception.hpp" #include "utils/IConfig.hpp" #include "utils/Logger.hpp" @@ -36,387 +35,363 @@ #include "ScanStepDiscoverFiles.hpp" #include "ScanStepRemoveOrphanDbFiles.hpp" #include "ScanStepScanFiles.hpp" +#include "ScanStepComputeClusterStats.hpp" -using namespace Database; - -namespace { - -Wt::WDate -getNextMonday(Wt::WDate current) +namespace Scanner { - do - { - current = current.addDays(1); - } while (current.dayOfWeek() != 1); - - return current; -} - -Wt::WDate -getNextFirstOfMonth(Wt::WDate current) -{ - do - { - current = current.addDays(1); - } while (current.day() != 1); - - return current; -} - -} // namespace - -namespace Scanner { - -std::unique_ptr -createScannerService(Db& db, Recommendation::IRecommendationService& recommendationService) -{ - return std::make_unique(db, recommendationService); -} - -ScannerService::ScannerService(Db& db, Recommendation::IRecommendationService& recommendationService) -: _recommendationService {recommendationService} -, _db {db} -, _dbSession {db} -{ - _ioService.setThreadCount(1); - - refreshScanSettings(); - - start(); -} - -ScannerService::~ScannerService() -{ - LMS_LOG(DBUPDATER, INFO) << "Stopping service..."; - stop(); - LMS_LOG(DBUPDATER, INFO) << "Service stopped!"; -} - -void -ScannerService::start() -{ - std::scoped_lock lock {_controlMutex}; - - _ioService.post([this] - { - if (_abortScan) - return; - - _recommendationService.load(false, - [](const Recommendation::Progress& progress) - { - LMS_LOG(DBUPDATER, DEBUG) << "Reloading recommendation : " << progress.processedElems << "/" << progress.totalElems; - }); - scheduleNextScan(); - }); - - _ioService.start(); -} - -void -ScannerService::stop() -{ - std::scoped_lock lock {_controlMutex}; - - _abortScan = true; - _scheduleTimer.cancel(); - _recommendationService.cancelLoad(); - _ioService.stop(); -} - -void -ScannerService::abortScan() -{ - LMS_LOG(DBUPDATER, DEBUG) << "Aborting scan..."; - std::scoped_lock lock {_controlMutex}; - - LMS_LOG(DBUPDATER, DEBUG) << "Waiting for the scan to abort..."; - - _abortScan = true; - _scheduleTimer.cancel(); - _recommendationService.cancelLoad(); - _ioService.stop(); - LMS_LOG(DBUPDATER, DEBUG) << "Scan abort done!"; - - _abortScan = false; - _ioService.start(); -} - -void -ScannerService::requestImmediateScan(bool force) -{ - abortScan(); - _ioService.post([=]() - { - if (_abortScan) - return; - - scheduleScan(force); - }); -} - -void -ScannerService::requestReload() -{ - abortScan(); - _ioService.post([=]() - { - if (_abortScan) - return; - - scheduleNextScan(); - }); -} - -ScannerService::Status -ScannerService::getStatus() const -{ - Status res; - - std::shared_lock lock {_statusMutex}; - - res.currentState = _curState; - res.nextScheduledScan = _nextScheduledScan; - res.lastCompleteScanStats = _lastCompleteScanStats; - res.currentScanStepStats = _currentScanStepStats; - - return res; -} - -void -ScannerService::scheduleNextScan() -{ - LMS_LOG(DBUPDATER, DEBUG) << "Scheduling next scan"; - - refreshScanSettings(); - - const Wt::WDateTime now {Wt::WDateTime::currentDateTime()}; - - Wt::WDateTime nextScanDateTime; - switch (_settings.updatePeriod) - { - case ScanSettings::UpdatePeriod::Daily: - if (now.time() < _settings.startTime) - nextScanDateTime = {now.date(), _settings.startTime}; - else - nextScanDateTime = {now.date().addDays(1), _settings.startTime}; - break; - - case ScanSettings::UpdatePeriod::Weekly: - if (now.time() < _settings.startTime && now.date().dayOfWeek() == 1) - nextScanDateTime = {now.date(), _settings.startTime}; - else - nextScanDateTime = {getNextMonday(now.date()), _settings.startTime}; - break; - - case ScanSettings::UpdatePeriod::Monthly: - if (now.time() < _settings.startTime && now.date().day() == 1) - nextScanDateTime = {now.date(), _settings.startTime}; - else - nextScanDateTime = {getNextFirstOfMonth(now.date()), _settings.startTime}; - break; - - case ScanSettings::UpdatePeriod::Hourly: - nextScanDateTime = {now.date(), now.time().addSecs(3600)}; - break; - - case ScanSettings::UpdatePeriod::Never: - LMS_LOG(DBUPDATER, INFO) << "Auto scan disabled!"; - break; - } - - if (nextScanDateTime.isValid()) - scheduleScan(false, nextScanDateTime); - - { - std::unique_lock lock {_statusMutex}; - _curState = nextScanDateTime.isValid() ? State::Scheduled : State::NotScheduled; - _nextScheduledScan = nextScanDateTime; - } - - _events.scanScheduled.emit(_nextScheduledScan); -} - -void -ScannerService::scheduleScan(bool force, const Wt::WDateTime& dateTime) -{ - auto cb {[=](boost::system::error_code ec) - { - if (ec) - return; - - scan(force); - }}; - - if (dateTime.isNull()) - { - LMS_LOG(DBUPDATER, INFO) << "Scheduling next scan right now"; - _scheduleTimer.expires_from_now(std::chrono::seconds {0}); - _scheduleTimer.async_wait(cb); - } - else - { - std::chrono::system_clock::time_point timePoint {dateTime.toTimePoint()}; - std::time_t t {std::chrono::system_clock::to_time_t(timePoint)}; - char ctimeStr[26]; - - LMS_LOG(DBUPDATER, INFO) << "Scheduling next scan at " << std::string(::ctime_r(&t, ctimeStr)); - _scheduleTimer.expires_at(timePoint); - _scheduleTimer.async_wait(cb); - } -} - -void -ScannerService::scan(bool forceScan) -{ - _events.scanStarted.emit(); - - { - std::unique_lock lock {_statusMutex}; - _curState = State::InProgress; - _nextScheduledScan = {}; - } - - - LMS_LOG(UI, INFO) << "New scan started!"; - - refreshScanSettings(); - - IScanStep::ScanContext scanContext {_settings.mediaDirectory, forceScan, ScanStats {}, ScanStepStats {}}; - ScanStats& stats {scanContext.stats}; - stats.startTime = Wt::WDateTime::currentDateTime(); - - for (auto& scanStep : _scanSteps) - { - LMS_LOG(DBUPDATER, DEBUG) << "Starting scan step '" << scanStep->getStepName() << "'"; - scanContext.currentStepStats = ScanStepStats {Wt::WDateTime::currentDateTime(), scanStep->getStep()}; - - notifyInProgress(scanContext.currentStepStats); - scanStep->process(scanContext); - notifyInProgress(scanContext.currentStepStats); - LMS_LOG(DBUPDATER, DEBUG) << "Completed scan step '" << scanStep->getStepName() << "'"; - } - - LMS_LOG(DBUPDATER, INFO) << "Scan " << (_abortScan ? "aborted" : "complete") << ". Changes = " << stats.nbChanges() << " (added = " << stats.additions << ", removed = " << stats.deletions << ", updated = " << stats.updates << "), Not changed = " << stats.skips << ", Scanned = " << stats.scans << " (errors = " << stats.errors.size() << "), features fetched = " << stats.featuresFetched << ", duplicates = " << stats.duplicates.size(); - - _dbSession.analyze(); - - if (!_abortScan) - { - stats.stopTime = Wt::WDateTime::currentDateTime(); - { - std::unique_lock lock {_statusMutex}; - - _lastCompleteScanStats = stats; - _currentScanStepStats.reset(); - } - - LMS_LOG(DBUPDATER, DEBUG) << "Scan not aborted, scheduling next scan!"; - scheduleNextScan(); - - _events.scanComplete.emit(stats); - } - else - { - LMS_LOG(DBUPDATER, DEBUG) << "Scan aborted, not scheduling next scan!"; - - std::unique_lock lock {_statusMutex}; - - _curState = State::NotScheduled; - _currentScanStepStats.reset(); - } -} - -void -ScannerService::refreshScanSettings() -{ - ScannerSettings newSettings {readSettings()}; - if (_settings == newSettings) - return; - - LMS_LOG(DBUPDATER, DEBUG) << "Scanner settings updated"; - LMS_LOG(DBUPDATER, DEBUG) << "skipDuplicateMBID = " << newSettings.skipDuplicateMBID; - LMS_LOG(DBUPDATER, DEBUG) << "Using scan settings version " << newSettings.scanVersion; - - _settings = std::move(newSettings); - - auto cbFunc {[this](const ScanStepStats& stats) - { - notifyInProgressIfNeeded(stats); - }}; - - ScanStepBase::InitParams params - { - _settings, - cbFunc, - _abortScan, - _db - }; - - _scanSteps.clear(); - _scanSteps.push_back(std::make_unique(params)); - _scanSteps.push_back(std::make_unique(params)); - _scanSteps.push_back(std::make_unique(params)); - _scanSteps.push_back(std::make_unique(params)); -} - -ScannerSettings -ScannerService::readSettings() -{ - ScannerSettings newSettings; - - newSettings.skipDuplicateMBID = Service::get()->getBool("scanner-skip-duplicate-mbid", false); - { - auto transaction {_dbSession.createSharedTransaction()}; - - const ScanSettings::pointer scanSettings {ScanSettings::get(_dbSession)}; - - newSettings.scanVersion = scanSettings->getScanVersion(); - newSettings.startTime = scanSettings->getUpdateStartTime(); - newSettings.updatePeriod = scanSettings->getUpdatePeriod(); - - { - const auto fileExtensions {scanSettings->getAudioFileExtensions()}; - newSettings.supportedExtensions.reserve(fileExtensions.size()); - std::transform(std::cbegin(fileExtensions), std::end(fileExtensions), std::back_inserter(newSettings.supportedExtensions), - [](const std::filesystem::path& extension) { return std::filesystem::path{ StringUtils::stringToLower(extension.string()) }; }); - } - newSettings.similarityServiceType = scanSettings->getSimilarityEngineType(); - newSettings.mediaDirectory = scanSettings->getMediaDirectory(); - - const auto clusterTypes = scanSettings->getClusterTypes(); - std::set clusterTypeNames; - - std::transform(std::cbegin(clusterTypes), std::cend(clusterTypes), - std::inserter(clusterTypeNames, clusterTypeNames.begin()), - [](ClusterType::pointer clusterType) { return clusterType->getName(); }); - - newSettings.clusterTypeNames = std::move(clusterTypeNames); - } - - return newSettings; -} - -void -ScannerService::notifyInProgress(const ScanStepStats& stepStats) -{ - { - std::unique_lock lock {_statusMutex}; - _currentScanStepStats = stepStats; - } - - const std::chrono::system_clock::time_point now {std::chrono::system_clock::now()}; - _events.scanInProgress(stepStats); - _lastScanInProgressEmit = now; -} - -void -ScannerService::notifyInProgressIfNeeded(const ScanStepStats& stepStats) -{ - std::chrono::system_clock::time_point now {std::chrono::system_clock::now()}; - - if (std::chrono::duration_cast(now - _lastScanInProgressEmit).count() > 1) - notifyInProgress(stepStats); -} + using namespace Database; + + namespace + { + Wt::WDate getNextMonday(Wt::WDate current) + { + do + { + current = current.addDays(1); + } while (current.dayOfWeek() != 1); + + return current; + } + + Wt::WDate getNextFirstOfMonth(Wt::WDate current) + { + do + { + current = current.addDays(1); + } while (current.day() != 1); + + return current; + } + } // namespace + + std::unique_ptr createScannerService(Db& db) + { + return std::make_unique(db); + } + + ScannerService::ScannerService(Db& db) + : _db{ db } + , _dbSession{ db } + { + _ioService.setThreadCount(1); + + refreshScanSettings(); + + start(); + } + + ScannerService::~ScannerService() + { + LMS_LOG(DBUPDATER, INFO) << "Stopping service..."; + stop(); + LMS_LOG(DBUPDATER, INFO) << "Service stopped!"; + } + + void ScannerService::start() + { + std::scoped_lock lock{ _controlMutex }; + + _ioService.post([this] + { + if (_abortScan) + return; + + scheduleNextScan(); + }); + + _ioService.start(); + } + + void ScannerService::stop() + { + std::scoped_lock lock{ _controlMutex }; + + _abortScan = true; + _scheduleTimer.cancel(); + _ioService.stop(); + } + + void ScannerService::abortScan() + { + LMS_LOG(DBUPDATER, DEBUG) << "Aborting scan..."; + std::scoped_lock lock{ _controlMutex }; + + LMS_LOG(DBUPDATER, DEBUG) << "Waiting for the scan to abort..."; + + _abortScan = true; + _scheduleTimer.cancel(); + _ioService.stop(); + LMS_LOG(DBUPDATER, DEBUG) << "Scan abort done!"; + + _abortScan = false; + _ioService.start(); + } + + void ScannerService::requestImmediateScan(bool force) + { + abortScan(); + _ioService.post([=]() + { + if (_abortScan) + return; + + scheduleScan(force); + }); + } + + void ScannerService::requestReload() + { + abortScan(); + _ioService.post([=]() + { + if (_abortScan) + return; + + scheduleNextScan(); + }); + } + + ScannerService::Status ScannerService::getStatus() const + { + Status res; + + std::shared_lock lock{ _statusMutex }; + + res.currentState = _curState; + res.nextScheduledScan = _nextScheduledScan; + res.lastCompleteScanStats = _lastCompleteScanStats; + res.currentScanStepStats = _currentScanStepStats; + + return res; + } + + void ScannerService::scheduleNextScan() + { + LMS_LOG(DBUPDATER, DEBUG) << "Scheduling next scan"; + + refreshScanSettings(); + + const Wt::WDateTime now{ Wt::WDateTime::currentDateTime() }; + + Wt::WDateTime nextScanDateTime; + switch (_settings.updatePeriod) + { + case ScanSettings::UpdatePeriod::Daily: + if (now.time() < _settings.startTime) + nextScanDateTime = { now.date(), _settings.startTime }; + else + nextScanDateTime = { now.date().addDays(1), _settings.startTime }; + break; + + case ScanSettings::UpdatePeriod::Weekly: + if (now.time() < _settings.startTime && now.date().dayOfWeek() == 1) + nextScanDateTime = { now.date(), _settings.startTime }; + else + nextScanDateTime = { getNextMonday(now.date()), _settings.startTime }; + break; + + case ScanSettings::UpdatePeriod::Monthly: + if (now.time() < _settings.startTime && now.date().day() == 1) + nextScanDateTime = { now.date(), _settings.startTime }; + else + nextScanDateTime = { getNextFirstOfMonth(now.date()), _settings.startTime }; + break; + + case ScanSettings::UpdatePeriod::Hourly: + nextScanDateTime = { now.date(), now.time().addSecs(3600) }; + break; + + case ScanSettings::UpdatePeriod::Never: + LMS_LOG(DBUPDATER, INFO) << "Auto scan disabled!"; + break; + } + + if (nextScanDateTime.isValid()) + scheduleScan(false, nextScanDateTime); + + { + std::unique_lock lock{ _statusMutex }; + _curState = nextScanDateTime.isValid() ? State::Scheduled : State::NotScheduled; + _nextScheduledScan = nextScanDateTime; + } + + _events.scanScheduled.emit(_nextScheduledScan); + } + + void ScannerService::scheduleScan(bool force, const Wt::WDateTime& dateTime) + { + auto cb{ [=](boost::system::error_code ec) + { + if (ec) + return; + + scan(force); + } }; + + if (dateTime.isNull()) + { + LMS_LOG(DBUPDATER, INFO) << "Scheduling next scan right now"; + _scheduleTimer.expires_from_now(std::chrono::seconds{ 0 }); + _scheduleTimer.async_wait(cb); + } + else + { + std::chrono::system_clock::time_point timePoint{ dateTime.toTimePoint() }; + std::time_t t{ std::chrono::system_clock::to_time_t(timePoint) }; + char ctimeStr[26]; + + LMS_LOG(DBUPDATER, INFO) << "Scheduling next scan at " << std::string(::ctime_r(&t, ctimeStr)); + _scheduleTimer.expires_at(timePoint); + _scheduleTimer.async_wait(cb); + } + } + + void ScannerService::scan(bool forceScan) + { + _events.scanStarted.emit(); + + { + std::unique_lock lock{ _statusMutex }; + _curState = State::InProgress; + _nextScheduledScan = {}; + } + + + LMS_LOG(UI, INFO) << "New scan started!"; + + refreshScanSettings(); + + IScanStep::ScanContext scanContext{ _settings.mediaDirectory, forceScan, ScanStats {}, ScanStepStats {} }; + ScanStats& stats{ scanContext.stats }; + stats.startTime = Wt::WDateTime::currentDateTime(); + + for (auto& scanStep : _scanSteps) + { + LMS_LOG(DBUPDATER, DEBUG) << "Starting scan step '" << scanStep->getStepName() << "'"; + scanContext.currentStepStats = ScanStepStats{ Wt::WDateTime::currentDateTime(), scanStep->getStep() }; + + notifyInProgress(scanContext.currentStepStats); + scanStep->process(scanContext); + notifyInProgress(scanContext.currentStepStats); + LMS_LOG(DBUPDATER, DEBUG) << "Completed scan step '" << scanStep->getStepName() << "'"; + } + + LMS_LOG(DBUPDATER, INFO) << "Scan " << (_abortScan ? "aborted" : "complete") << ". Changes = " << stats.nbChanges() << " (added = " << stats.additions << ", removed = " << stats.deletions << ", updated = " << stats.updates << "), Not changed = " << stats.skips << ", Scanned = " << stats.scans << " (errors = " << stats.errors.size() << "), features fetched = " << stats.featuresFetched << ", duplicates = " << stats.duplicates.size(); + + _dbSession.analyze(); + + if (!_abortScan) + { + stats.stopTime = Wt::WDateTime::currentDateTime(); + { + std::unique_lock lock{ _statusMutex }; + + _lastCompleteScanStats = stats; + _currentScanStepStats.reset(); + } + + LMS_LOG(DBUPDATER, DEBUG) << "Scan not aborted, scheduling next scan!"; + scheduleNextScan(); + + _events.scanComplete.emit(stats); + } + else + { + LMS_LOG(DBUPDATER, DEBUG) << "Scan aborted, not scheduling next scan!"; + + std::unique_lock lock{ _statusMutex }; + + _curState = State::NotScheduled; + _currentScanStepStats.reset(); + } + } + + void ScannerService::refreshScanSettings() + { + ScannerSettings newSettings{ readSettings() }; + if (_settings == newSettings) + return; + + LMS_LOG(DBUPDATER, DEBUG) << "Scanner settings updated"; + LMS_LOG(DBUPDATER, DEBUG) << "skipDuplicateMBID = " << newSettings.skipDuplicateMBID; + LMS_LOG(DBUPDATER, DEBUG) << "Using scan settings version " << newSettings.scanVersion; + + _settings = std::move(newSettings); + + auto cbFunc{ [this](const ScanStepStats& stats) + { + notifyInProgressIfNeeded(stats); + } }; + + ScanStepBase::InitParams params + { + _settings, + cbFunc, + _abortScan, + _db + }; + + _scanSteps.clear(); + _scanSteps.push_back(std::make_unique(params)); + _scanSteps.push_back(std::make_unique(params)); + _scanSteps.push_back(std::make_unique(params)); + _scanSteps.push_back(std::make_unique(params)); + _scanSteps.push_back(std::make_unique(params)); + } + + ScannerSettings ScannerService::readSettings() + { + ScannerSettings newSettings; + + newSettings.skipDuplicateMBID = Service::get()->getBool("scanner-skip-duplicate-mbid", false); + { + auto transaction{ _dbSession.createSharedTransaction() }; + + const ScanSettings::pointer scanSettings{ ScanSettings::get(_dbSession) }; + + newSettings.scanVersion = scanSettings->getScanVersion(); + newSettings.startTime = scanSettings->getUpdateStartTime(); + newSettings.updatePeriod = scanSettings->getUpdatePeriod(); + + { + const auto fileExtensions{ scanSettings->getAudioFileExtensions() }; + newSettings.supportedExtensions.reserve(fileExtensions.size()); + std::transform(std::cbegin(fileExtensions), std::end(fileExtensions), std::back_inserter(newSettings.supportedExtensions), + [](const std::filesystem::path& extension) { return std::filesystem::path{ StringUtils::stringToLower(extension.string()) }; }); + } + newSettings.mediaDirectory = scanSettings->getMediaDirectory(); + + const auto clusterTypes = scanSettings->getClusterTypes(); + std::set clusterTypeNames; + + std::transform(std::cbegin(clusterTypes), std::cend(clusterTypes), + std::inserter(clusterTypeNames, clusterTypeNames.begin()), + [](ClusterType::pointer clusterType) { return clusterType->getName(); }); + + newSettings.clusterTypeNames = std::move(clusterTypeNames); + } + + return newSettings; + } + + void ScannerService::notifyInProgress(const ScanStepStats& stepStats) + { + { + std::unique_lock lock{ _statusMutex }; + _currentScanStepStats = stepStats; + } + + const std::chrono::system_clock::time_point now{ std::chrono::system_clock::now() }; + _events.scanInProgress(stepStats); + _lastScanInProgressEmit = now; + } + + void ScannerService::notifyInProgressIfNeeded(const ScanStepStats& stepStats) + { + std::chrono::system_clock::time_point now{ std::chrono::system_clock::now() }; + + if (std::chrono::duration_cast(now - _lastScanInProgressEmit).count() > 1) + notifyInProgress(stepStats); + } } // namespace Scanner diff --git a/src/libs/services/scanner/impl/ScannerService.hpp b/src/libs/services/scanner/impl/ScannerService.hpp index 133dfef2..02493a5c 100644 --- a/src/libs/services/scanner/impl/ScannerService.hpp +++ b/src/libs/services/scanner/impl/ScannerService.hpp @@ -38,73 +38,65 @@ #include "IScanStep.hpp" #include "ScannerSettings.hpp" -namespace Recommendation -{ - class IRecommendationService; -} - namespace Scanner { - class ScannerService : public IScannerService - { - public: - ScannerService(Database::Db& db, Recommendation::IRecommendationService& recommendationService); - ~ScannerService(); + class ScannerService : public IScannerService + { + public: + ScannerService(Database::Db& db); + ~ScannerService(); - ScannerService(const ScannerService&) = delete; - ScannerService(ScannerService&&) = delete; - ScannerService& operator=(const ScannerService&) = delete; - ScannerService& operator=(ScannerService&&) = delete; + ScannerService(const ScannerService&) = delete; + ScannerService& operator=(const ScannerService&) = delete; - void requestReload() override; - void requestImmediateScan(bool force) override; + void requestReload() override; + void requestImmediateScan(bool force) override; - Status getStatus() const override; - Events& getEvents() override { return _events; } + Status getStatus() const override; + Events& getEvents() override { return _events; } - private: - void start(); - void stop(); + private: + void start(); + void stop(); - // Job handling - void scheduleNextScan(); - void scheduleScan(bool force, const Wt::WDateTime& dateTime = {}); + // Job handling + void scheduleNextScan(); + void scheduleScan(bool force, const Wt::WDateTime& dateTime = {}); - void abortScan(); + void abortScan(); - // Update database (scheduled callback) - void scan(bool force); + // Update database (scheduled callback) + void scan(bool force); - void scanMediaDirectory( const std::filesystem::path& mediaDirectory, bool forceScan, ScanStats& stats); + void scanMediaDirectory(const std::filesystem::path& mediaDirectory, bool forceScan, ScanStats& stats); - // Helpers - void refreshScanSettings(); - ScannerSettings readSettings(); + // Helpers + void refreshScanSettings(); + ScannerSettings readSettings(); + void reloadRecommendationService(); - void notifyInProgressIfNeeded(const ScanStepStats& stats); - void notifyInProgress(const ScanStepStats& stats); - void reloadSimilarityEngine(ScanStats& stats); + void notifyInProgressIfNeeded(const ScanStepStats& stats); + void notifyInProgress(const ScanStepStats& stats); + void reloadSimilarityEngine(ScanStats& stats); - Recommendation::IRecommendationService& _recommendationService; + std::vector> _scanSteps; - std::vector> _scanSteps; + std::mutex _controlMutex; + bool _abortScan{}; + Wt::WIOService _ioService; + boost::asio::system_timer _scheduleTimer{ _ioService }; + Events _events; + std::chrono::system_clock::time_point _lastScanInProgressEmit{}; + Database::Db& _db; + Database::Session _dbSession; - std::mutex _controlMutex; - bool _abortScan {}; - Wt::WIOService _ioService; - boost::asio::system_timer _scheduleTimer {_ioService}; - Events _events; - std::chrono::system_clock::time_point _lastScanInProgressEmit {}; - Database::Db& _db; - Database::Session _dbSession; + mutable std::shared_mutex _statusMutex; + State _curState{ State::NotScheduled }; + std::optional _lastCompleteScanStats; + std::optional _currentScanStepStats; + Wt::WDateTime _nextScheduledScan; - mutable std::shared_mutex _statusMutex; - State _curState {State::NotScheduled}; - std::optional _lastCompleteScanStats; - std::optional _currentScanStepStats; - Wt::WDateTime _nextScheduledScan; - - ScannerSettings _settings; - }; + ScannerSettings _settings; + }; } // Scanner diff --git a/src/libs/services/scanner/impl/ScannerSettings.hpp b/src/libs/services/scanner/impl/ScannerSettings.hpp index a102dea3..9a469c78 100644 --- a/src/libs/services/scanner/impl/ScannerSettings.hpp +++ b/src/libs/services/scanner/impl/ScannerSettings.hpp @@ -34,7 +34,6 @@ namespace Scanner Wt::WTime startTime; Database::ScanSettings::UpdatePeriod updatePeriod {Database::ScanSettings::UpdatePeriod::Never}; std::vector supportedExtensions; - Database::ScanSettings::SimilarityEngineType similarityServiceType; std::filesystem::path mediaDirectory; bool skipDuplicateMBID {}; std::set clusterTypeNames; @@ -45,7 +44,6 @@ namespace Scanner && startTime == rhs.startTime && updatePeriod == rhs.updatePeriod && supportedExtensions == rhs.supportedExtensions - && similarityServiceType == rhs.similarityServiceType && mediaDirectory == rhs.mediaDirectory && skipDuplicateMBID == rhs.skipDuplicateMBID && clusterTypeNames == rhs.clusterTypeNames; diff --git a/src/libs/services/scanner/include/services/scanner/IScannerService.hpp b/src/libs/services/scanner/include/services/scanner/IScannerService.hpp index 6085f6b8..77c694f1 100644 --- a/src/libs/services/scanner/include/services/scanner/IScannerService.hpp +++ b/src/libs/services/scanner/include/services/scanner/IScannerService.hpp @@ -29,11 +29,6 @@ namespace Database class Db; } -namespace Recommendation -{ - class IRecommendationService; -} - namespace Scanner { @@ -66,7 +61,7 @@ namespace Scanner virtual Events& getEvents() = 0; }; - std::unique_ptr createScannerService(Database::Db& db, Recommendation::IRecommendationService& recommendationEngine); + std::unique_ptr createScannerService(Database::Db& db); } // Scanner diff --git a/src/libs/services/scanner/include/services/scanner/ScannerStats.hpp b/src/libs/services/scanner/include/services/scanner/ScannerStats.hpp index c0a8bba8..5801269f 100644 --- a/src/libs/services/scanner/include/services/scanner/ScannerStats.hpp +++ b/src/libs/services/scanner/include/services/scanner/ScannerStats.hpp @@ -28,80 +28,81 @@ namespace Scanner { - enum class ScanErrorType - { - CannotReadFile, // cannot read file - CannotParseFile, // cannot parse file - NoAudioTrack, // no audio track found - BadDuration, // bad duration - }; + enum class ScanErrorType + { + CannotReadFile, // cannot read file + CannotParseFile, // cannot parse file + NoAudioTrack, // no audio track found + BadDuration, // bad duration + }; - enum class DuplicateReason - { - SameHash, - SameTrackMBID, - }; + enum class DuplicateReason + { + SameHash, + SameTrackMBID, + }; - struct ScanError - { - std::filesystem::path file; - ScanErrorType error; - std::string systemError; + struct ScanError + { + std::filesystem::path file; + ScanErrorType error; + std::string systemError; - ScanError(const std::filesystem::path& file, ScanErrorType error, const std::string& systemError = ""); - }; + ScanError(const std::filesystem::path& file, ScanErrorType error, const std::string& systemError = ""); + }; - struct ScanDuplicate - { - Database::TrackId trackId; - DuplicateReason reason; - }; + struct ScanDuplicate + { + Database::TrackId trackId; + DuplicateReason reason; + }; - enum class ScanStep - { - DiscoveringFiles, - ScanningFiles, - ChekingForMissingFiles, - CheckingForDuplicateFiles, - FetchingTrackFeatures, - ReloadingSimilarityEngine, - }; - static inline constexpr unsigned ScanProgressStepCount {5}; + enum class ScanStep + { + DiscoveringFiles, + ScanningFiles, + ChekingForMissingFiles, + CheckingForDuplicateFiles, + FetchingTrackFeatures, + ReloadingSimilarityEngine, + ComputeClusterStats, + }; + static inline constexpr unsigned ScanProgressStepCount{ 6 }; - // reduced scan stats - struct ScanStepStats - { - Wt::WDateTime startTime; + // reduced scan stats + struct ScanStepStats + { + Wt::WDateTime startTime; - ScanStep currentStep; + ScanStep currentStep; - std::size_t totalElems {}; - std::size_t processedElems {}; + std::size_t totalElems{}; + std::size_t processedElems{}; - unsigned progress() const; - }; + unsigned progress() const; + }; - struct ScanStats - { - Wt::WDateTime startTime; - Wt::WDateTime stopTime; + struct ScanStats + { + Wt::WDateTime startTime; + Wt::WDateTime stopTime; - std::size_t filesScanned {}; // Total number of files scanned (estimated) + std::size_t filesScanned{}; // Total number of files scanned (estimated) - std::size_t skips {}; // no change since last scan - std::size_t scans {}; // actually scanned filed + std::size_t skips{}; // no change since last scan + std::size_t scans{}; // actually scanned filed - std::size_t additions {}; // added in DB - std::size_t deletions {}; // removed from DB - std::size_t updates {}; // updated file in DB + std::size_t additions{}; // added in DB + std::size_t deletions{}; // removed from DB + std::size_t updates{}; // updated file in DB - std::size_t featuresFetched {}; // features fetched in DB + std::size_t featuresFetched{}; // features fetched in DB - std::vector errors; - std::vector duplicates; + std::vector errors; + std::vector duplicates; - std::size_t nbFiles() const; - std::size_t nbChanges() const; - }; + std::size_t nbFiles() const; + std::size_t nbChanges() const; + }; } // namespace Scanner diff --git a/src/libs/subsonic/impl/ProtocolVersion.hpp b/src/libs/subsonic/impl/ProtocolVersion.hpp index 1ebdf0f2..6dd17d54 100644 --- a/src/libs/subsonic/impl/ProtocolVersion.hpp +++ b/src/libs/subsonic/impl/ProtocolVersion.hpp @@ -31,7 +31,7 @@ namespace API::Subsonic }; static inline constexpr ProtocolVersion defaultServerProtocolVersion{ 1, 16, 0 }; - static inline constexpr std::string_view serverVersion{ "2" }; + static inline constexpr std::string_view serverVersion{ "4" }; } namespace StringUtils diff --git a/src/libs/subsonic/impl/RequestContext.hpp b/src/libs/subsonic/impl/RequestContext.hpp index 05de5b5a..61b23f0a 100644 --- a/src/libs/subsonic/impl/RequestContext.hpp +++ b/src/libs/subsonic/impl/RequestContext.hpp @@ -42,6 +42,7 @@ namespace API::Subsonic ClientInfo clientInfo; ProtocolVersion serverProtocolVersion; bool enableOpenSubsonic{ true }; + bool enableDefaultCover{ }; }; } diff --git a/src/libs/subsonic/impl/SubsonicResource.cpp b/src/libs/subsonic/impl/SubsonicResource.cpp index e5b0bb0d..7c22f043 100644 --- a/src/libs/subsonic/impl/SubsonicResource.cpp +++ b/src/libs/subsonic/impl/SubsonicResource.cpp @@ -66,7 +66,7 @@ namespace API::Subsonic { std::unordered_map res; - Service::get()->visitStrings("api-subsonic-report-old-server-protocol", + Service::get()->visitStrings("api-subsonic-old-server-protocol-clients", [&](std::string_view client) { res.emplace(std::string{ client }, ProtocolVersion{ 1, 12, 0 }); @@ -88,6 +88,19 @@ namespace API::Subsonic return res; } + std::unordered_set readDefaultCoverClients() + { + std::unordered_set res; + + Service::get()->visitStrings("api-subsonic-default-cover-clients", + [&](std::string_view client) + { + res.emplace(std::string{ client }); + }, { "DSub", "substreamer" }); + + return res; + } + std::string parameterMapToDebugString(const Wt::Http::ParameterMap& parameterMap) { auto censorValue = [](const std::string& type, const std::string& value) -> std::string @@ -266,6 +279,7 @@ namespace API::Subsonic SubsonicResource::SubsonicResource(Db& db) : _serverProtocolVersionsByClient{ readConfigProtocolVersions() } , _openSubsonicDisabledClients{ readOpenSubsonicDisabledClients() } + , _defaultCoverClients{ readDefaultCoverClients() } , _db{ db } { } @@ -286,13 +300,13 @@ namespace API::Subsonic const ResponseFormat format{ getParameterAs(request.getParameterMap(), "f").value_or("xml") == "json" ? ResponseFormat::json : ResponseFormat::xml }; ProtocolVersion protocolVersion{ defaultServerProtocolVersion }; - + try { // We need to parse client a soon as possible to make sure to answer with the right protocol version protocolVersion = getServerProtocolVersion(getMandatoryParameterAs(request.getParameterMap(), "c")); RequestContext requestContext{ buildRequestContext(request) }; - + auto itEntryPoint{ requestEntryPoints.find(requestPath) }; if (itEntryPoint != requestEntryPoints.end()) { @@ -378,12 +392,26 @@ namespace API::Subsonic const ClientInfo clientInfo{ getClientInfo(parameters) }; const Database::UserId userId{ authenticateUser(request, clientInfo) }; bool enableOpenSubsonic{ _openSubsonicDisabledClients.find(clientInfo.name) == std::cend(_openSubsonicDisabledClients) }; + bool enableDefaultCover{ _defaultCoverClients.find(clientInfo.name) != std::cend(_openSubsonicDisabledClients) }; - return { parameters, _db.getTLSSession(), userId, clientInfo, getServerProtocolVersion(clientInfo.name), enableOpenSubsonic }; + return { parameters, _db.getTLSSession(), userId, clientInfo, getServerProtocolVersion(clientInfo.name), enableOpenSubsonic, enableDefaultCover }; } Database::UserId SubsonicResource::authenticateUser(const Wt::Http::Request& request, const ClientInfo& clientInfo) { + // if the request if a continuation, the user is already authenticated + if (request.continuation()) + { + Database::Session& session{ _db.getTLSSession() }; + auto transaction{ session.createSharedTransaction() }; + + const auto user{ Database::User::find(session, clientInfo.user) }; + if (!user) + throw UserNotAuthorizedError{}; + + return user->getId(); + } + if (auto * authEnvService{ Service<::Auth::IEnvService>::get() }) { const auto checkResult{ authEnvService->processRequest(request) }; @@ -408,7 +436,7 @@ namespace API::Subsonic } } - throw InternalErrorGenericError{ "No service avalaible to authenticate user" }; + throw InternalErrorGenericError{ "No service available to authenticate user" }; } } // namespace api::subsonic diff --git a/src/libs/subsonic/impl/SubsonicResource.hpp b/src/libs/subsonic/impl/SubsonicResource.hpp index 63a04a5c..24c7025a 100644 --- a/src/libs/subsonic/impl/SubsonicResource.hpp +++ b/src/libs/subsonic/impl/SubsonicResource.hpp @@ -53,6 +53,8 @@ namespace API::Subsonic const std::unordered_map _serverProtocolVersionsByClient; const std::unordered_set _openSubsonicDisabledClients; + const std::unordered_set _defaultCoverClients; + Database::Db& _db; }; diff --git a/src/libs/subsonic/impl/SubsonicResponse.hpp b/src/libs/subsonic/impl/SubsonicResponse.hpp index 4414d4d1..60690dae 100644 --- a/src/libs/subsonic/impl/SubsonicResponse.hpp +++ b/src/libs/subsonic/impl/SubsonicResponse.hpp @@ -29,6 +29,8 @@ namespace API::Subsonic { + // Max count expected from all API methods that expose a count + static inline constexpr std::size_t defaultMaxCountSize{ 1000 }; enum class ResponseFormat { @@ -185,6 +187,18 @@ namespace API::Subsonic const std::string _parameterName; }; + class ParameterValueTooHighGenericError : public GenericError + { + public: + ParameterValueTooHighGenericError(std::string_view parameterName, std::size_t max) : _parameterName{ parameterName }, _max{ max } {} + + private: + std::string getMessage() const override { return "Parameter '" + _parameterName + "': bad value (max is " + std::to_string(_max) + ")"; } + + const std::string _parameterName; + std::size_t _max; + }; + class Response { public: diff --git a/src/libs/subsonic/impl/entrypoints/AlbumSongLists.cpp b/src/libs/subsonic/impl/entrypoints/AlbumSongLists.cpp index 721577d0..6a55ffc6 100644 --- a/src/libs/subsonic/impl/entrypoints/AlbumSongLists.cpp +++ b/src/libs/subsonic/impl/entrypoints/AlbumSongLists.cpp @@ -47,6 +47,8 @@ namespace API::Subsonic // Optional params const std::size_t size{ getParameterAs(context.parameters, "size").value_or(10) }; const std::size_t offset{ getParameterAs(context.parameters, "offset").value_or(0) }; + if (size > defaultMaxCountSize) + throw ParameterValueTooHighGenericError{ "size", defaultMaxCountSize }; const Range range{ offset, size }; @@ -120,7 +122,7 @@ namespace API::Subsonic // (no seed provided by subsonic, ot it would require to store some kind of context for each user/client when iterating over the random albums) Release::FindParameters params; params.setSortMethod(ReleaseSortMethod::Random); - params.setRange({ 0, size }); + params.setRange(Range{ 0, size }); releases = Release::findIds(context.dbSession, params); } @@ -130,10 +132,15 @@ namespace API::Subsonic } else if (type == "starred") { - releases = feedbackService.getStarredReleases(context.userId, {}, range); + Feedback::IFeedbackService::FindParameters params; + params.setUser(context.userId); + params.setRange(range); + releases = feedbackService.findStarredReleases(params); } else + { throw NotImplementedGenericError{}; + } Response response{ Response::createOkResponse(context.serverProtocolVersion) }; Response::Node& albumListNode{ response.createNode(id3 ? Response::Node::Key{ "albumList2" } : Response::Node::Key{ "albumList" }) }; @@ -160,19 +167,27 @@ namespace API::Subsonic Feedback::IFeedbackService& feedbackService{ *Service::get() }; - for (const ArtistId artistId : feedbackService.getStarredArtists(context.userId, {} /* clusters */, std::nullopt /* linkType */, ArtistSortMethod::BySortName, Range{}).results) + Feedback::IFeedbackService::FindParameters findParameters; + findParameters.setUser(context.userId); + { - if (auto artist{ Artist::find(context.dbSession, artistId) }) - starredNode.addArrayChild("artist", createArtistNode(context, artist, user, id3)); + Feedback::IFeedbackService::ArtistFindParameters artistFindParams; + artistFindParams.setUser(context.userId); + artistFindParams.setSortMethod(ArtistSortMethod::BySortName); + for (const ArtistId artistId : feedbackService.findStarredArtists(artistFindParams).results) + { + if (auto artist{ Artist::find(context.dbSession, artistId) }) + starredNode.addArrayChild("artist", createArtistNode(context, artist, user, id3)); + } } - for (const ReleaseId releaseId : feedbackService.getStarredReleases(context.userId, {} /* clusters */, Range{}).results) + for (const ReleaseId releaseId : feedbackService.findStarredReleases(findParameters).results) { if (auto release{ Release::find(context.dbSession, releaseId) }) starredNode.addArrayChild("album", createAlbumNode(context, release, user, id3)); } - for (const TrackId trackId : feedbackService.getStarredTracks(context.userId, {} /* clusters */, Range{}).results) + for (const TrackId trackId : feedbackService.findStarredTracks(findParameters).results) { if (auto track{ Track::find(context.dbSession, trackId) }) starredNode.addArrayChild("song", createSongNode(context, track, user)); @@ -196,7 +211,8 @@ namespace API::Subsonic { // Optional params std::size_t size{ getParameterAs(context.parameters, "size").value_or(50) }; - size = std::min(size, std::size_t{ 500 }); + if (size > defaultMaxCountSize) + throw ParameterValueTooHighGenericError{ "size", defaultMaxCountSize }; auto transaction{ context.dbSession.createSharedTransaction() }; @@ -204,13 +220,17 @@ namespace API::Subsonic if (!user) throw UserNotAuthorizedError{}; - const auto tracks{ Track::find(context.dbSession, Track::FindParameters {}.setSortMethod(TrackSortMethod::Random).setRange({0, size})) }; - Response response{ Response::createOkResponse(context.serverProtocolVersion) }; - Response::Node& randomSongsNode{ response.createNode("randomSongs") }; - for (const Track::pointer& track : tracks.results) - randomSongsNode.addArrayChild("song", createSongNode(context, track, user)); + + Track::FindParameters params; + params.setSortMethod(TrackSortMethod::Random); + params.setRange(Range{ 0, size }); + + Track::find(context.dbSession, params, [&](const Track::pointer& track) + { + randomSongsNode.addArrayChild("song", createSongNode(context, track, user)); + }); return response; } @@ -221,8 +241,9 @@ namespace API::Subsonic std::string genre{ getMandatoryParameterAs(context.parameters, "genre") }; // Optional params - std::size_t size{ getParameterAs(context.parameters, "count").value_or(10) }; - size = std::min(size, std::size_t{ 500 }); + std::size_t count{ getParameterAs(context.parameters, "count").value_or(10) }; + if (count > defaultMaxCountSize) + throw ParameterValueTooHighGenericError{"count", defaultMaxCountSize}; std::size_t offset{ getParameterAs(context.parameters, "offset").value_or(0) }; @@ -245,11 +266,12 @@ namespace API::Subsonic Track::FindParameters params; params.setClusters({ cluster->getId() }); - params.setRange({ offset, size }); + params.setRange(Range{ offset, count }); - const auto tracks{ Track::find(context.dbSession, params) }; - for (const Track::pointer& track : tracks.results) - songsByGenreNode.addArrayChild("song", createSongNode(context, track, user)); + Track::find(context.dbSession, params, [&](const Track::pointer& track) + { + songsByGenreNode.addArrayChild("song", createSongNode(context, track, user)); + }); return response; } diff --git a/src/libs/subsonic/impl/entrypoints/Bookmarks.cpp b/src/libs/subsonic/impl/entrypoints/Bookmarks.cpp index a149555c..1d20eb76 100644 --- a/src/libs/subsonic/impl/entrypoints/Bookmarks.cpp +++ b/src/libs/subsonic/impl/entrypoints/Bookmarks.cpp @@ -40,7 +40,7 @@ namespace API::Subsonic if (!user) throw UserNotAuthorizedError{}; - const auto bookmarkIds{ TrackBookmark::find(context.dbSession, user->getId(), Range {}) }; + const auto bookmarkIds{ TrackBookmark::find(context.dbSession, user->getId()) }; Response response{ Response::createOkResponse(context.serverProtocolVersion) }; Response::Node& bookmarksNode{ response.createNode("bookmarks") }; diff --git a/src/libs/subsonic/impl/entrypoints/Browsing.cpp b/src/libs/subsonic/impl/entrypoints/Browsing.cpp index 16b8ed8d..c9dcd7e0 100644 --- a/src/libs/subsonic/impl/entrypoints/Browsing.cpp +++ b/src/libs/subsonic/impl/entrypoints/Browsing.cpp @@ -124,7 +124,7 @@ namespace API::Subsonic // first pass: dispatch the artists by first letter LMS_LOG(API_SUBSONIC, DEBUG) << "GetArtists: fetching all artists..."; std::map> artistsSortedByFirstChar; - std::size_t currentArtistOffset{0}; + std::size_t currentArtistOffset{ 0 }; constexpr std::size_t batchSize{ 100 }; bool hasMoreArtists{ true }; while (hasMoreArtists) @@ -190,7 +190,7 @@ namespace API::Subsonic { Track::FindParameters params; params.setArtist(id); - params.setRange({ 0, meanTrackCountPerArtist }); + params.setRange(Range{ 0, meanTrackCountPerArtist }); params.setSortMethod(TrackSortMethod::Random); const auto artistTracks{ Track::findIds(context.dbSession, params) }; @@ -221,7 +221,7 @@ namespace API::Subsonic { Track::FindParameters params; params.setRelease(id); - params.setRange({ 0, meanTrackCountPerRelease }); + params.setRange(Range{ 0, meanTrackCountPerRelease }); params.setSortMethod(TrackSortMethod::Random); const auto releaseTracks{ Track::findIds(context.dbSession, params) }; @@ -242,6 +242,8 @@ namespace API::Subsonic { // Optional params std::size_t count{ getParameterAs(context.parameters, "count").value_or(50) }; + if (count > defaultMaxCountSize) + throw ParameterValueTooHighGenericError{ "count", defaultMaxCountSize }; std::vector tracks; @@ -316,9 +318,11 @@ namespace API::Subsonic directoryNode.setAttribute("id", idToString(RootId{})); directoryNode.setAttribute("name", "Music"); - const auto rootArtistIds{ Artist::find(context.dbSession, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::BySortName)) }; - for (const Artist::pointer& artist : rootArtistIds.results) - directoryNode.addArrayChild("child", createArtistNode(context, artist, user, false /* no id3 */)); + // TODO: this does not scale when a lot of artists are present + Artist::find(context.dbSession, Artist::FindParameters{}.setSortMethod(ArtistSortMethod::BySortName), [&](const Artist::pointer& artist) + { + directoryNode.addArrayChild("child", createArtistNode(context, artist, user, false /* no id3 */)); + }); } else if (artistId) { @@ -330,9 +334,10 @@ namespace API::Subsonic directoryNode.setAttribute("name", Utils::makeNameFilesystemCompatible(artist->getName())); - const auto artistReleases{ Release::find(context.dbSession, Release::FindParameters {}.setArtist(*artistId)) }; - for (const Release::pointer& release : artistReleases.results) - directoryNode.addArrayChild("child", createAlbumNode(context, release, user, false /* no id3 */)); + Release::find(context.dbSession, Release::FindParameters{}.setArtist(*artistId), [&](const Release::pointer& release) + { + directoryNode.addArrayChild("child", createAlbumNode(context, release, user, false /* no id3 */)); + }); } else if (releaseId) { @@ -344,9 +349,10 @@ namespace API::Subsonic directoryNode.setAttribute("name", Utils::makeNameFilesystemCompatible(release->getName())); - const auto tracks{ Track::find(context.dbSession, Track::FindParameters {}.setRelease(*releaseId).setSortMethod(TrackSortMethod::Release)) }; - for (const Track::pointer& track : tracks.results) - directoryNode.addArrayChild("child", createSongNode(context, track, user)); + Track::find(context.dbSession, Track::FindParameters{}.setRelease(*releaseId).setSortMethod(TrackSortMethod::Release), [&](const Track::pointer& track) + { + directoryNode.addArrayChild("child", createSongNode(context, track, user)); + }); } else throw BadParameterGenericError{ "id" }; diff --git a/src/libs/subsonic/impl/entrypoints/MediaRetrieval.cpp b/src/libs/subsonic/impl/entrypoints/MediaRetrieval.cpp index cddc758c..574fb546 100644 --- a/src/libs/subsonic/impl/entrypoints/MediaRetrieval.cpp +++ b/src/libs/subsonic/impl/entrypoints/MediaRetrieval.cpp @@ -20,8 +20,9 @@ #include "MediaRetrieval.hpp" #include "av/IAudioFile.hpp" -#include "av/TranscodeParameters.hpp" -#include "av/TranscodeResourceHandlerCreator.hpp" +#include "av/RawResourceHandlerCreator.hpp" +#include "av/TranscodingParameters.hpp" +#include "av/TranscodingResourceHandlerCreator.hpp" #include "av/Types.hpp" #include "services/cover/ICoverService.hpp" #include "services/database/Session.hpp" @@ -39,101 +40,148 @@ namespace API::Subsonic { using namespace Database; - namespace { - std::optional subsonicStreamFormatToAvFormat(std::string_view format) + namespace + { + std::optional subsonicStreamFormatToAvOutputFormat(std::string_view format) { - for (const auto& [str, avFormat] : std::initializer_list>{ - {"mp3", Av::Format::MP3}, - {"opus", Av::Format::OGG_OPUS}, - {"vorbis", Av::Format::OGG_VORBIS}, + for (const auto& [str, avFormat] : std::initializer_list>{ + {"mp3", Av::Transcoding::OutputFormat::MP3}, + {"opus", Av::Transcoding::OutputFormat::OGG_OPUS}, + {"vorbis", Av::Transcoding::OutputFormat::OGG_VORBIS}, }) { - if (StringUtils::stringCaseInsensitiveEqual("str", format)) + if (StringUtils::stringCaseInsensitiveEqual(str, format)) return avFormat; } return std::nullopt; } - Av::Format userTranscodeFormatToAvFormat(AudioFormat format) + Av::Transcoding::OutputFormat userTranscodeFormatToAvFormat(Database::TranscodingOutputFormat format) { switch (format) { - case Database::AudioFormat::MP3: return Av::Format::MP3; - case Database::AudioFormat::OGG_OPUS: return Av::Format::OGG_OPUS; - case Database::AudioFormat::MATROSKA_OPUS: return Av::Format::MATROSKA_OPUS; - case Database::AudioFormat::OGG_VORBIS: return Av::Format::OGG_VORBIS; - case Database::AudioFormat::WEBM_VORBIS: return Av::Format::WEBM_VORBIS; + case Database::TranscodingOutputFormat::MP3: return Av::Transcoding::OutputFormat::MP3; + case Database::TranscodingOutputFormat::OGG_OPUS: return Av::Transcoding::OutputFormat::OGG_OPUS; + case Database::TranscodingOutputFormat::MATROSKA_OPUS: return Av::Transcoding::OutputFormat::MATROSKA_OPUS; + case Database::TranscodingOutputFormat::OGG_VORBIS: return Av::Transcoding::OutputFormat::OGG_VORBIS; + case Database::TranscodingOutputFormat::WEBM_VORBIS: return Av::Transcoding::OutputFormat::WEBM_VORBIS; } - return Av::Format::OGG_OPUS; + return Av::Transcoding::OutputFormat::OGG_OPUS; + } + + bool isCodecCompatibleWithOutputFormat(Av::DecodingCodec codec, Av::Transcoding::OutputFormat outputFormat) + { + switch (outputFormat) + { + case Av::Transcoding::OutputFormat::MP3: + return codec == Av::DecodingCodec::MP3; + + case Av::Transcoding::OutputFormat::OGG_OPUS: + case Av::Transcoding::OutputFormat::MATROSKA_OPUS: + return codec == Av::DecodingCodec::OPUS; + + case Av::Transcoding::OutputFormat::OGG_VORBIS: + case Av::Transcoding::OutputFormat::WEBM_VORBIS: + return codec == Av::DecodingCodec::VORBIS; + } + + return true; } struct StreamParameters { - Av::InputFileParameters inputFileParameters; - std::optional transcodeParameters; + Av::Transcoding::InputParameters inputParameters; + std::optional outputParameters; bool estimateContentLength{}; }; + bool isOutputFormatCompatible(const std::filesystem::path& trackPath, Av::Transcoding::OutputFormat outputFormat) + { + try + { + const auto audioFile{ Av::parseAudioFile(trackPath) }; + + const auto streamInfo{ audioFile->getBestStreamInfo() }; + if (!streamInfo) + throw RequestedDataNotFoundError{}; // TODO 404? + + return isCodecCompatibleWithOutputFormat(streamInfo->codec, outputFormat); + } + catch (const Av::Exception& e) + { + // TODO 404? + throw RequestedDataNotFoundError{}; + } + } + StreamParameters getStreamParameters(RequestContext& context) { // Mandatory params const TrackId id{ getMandatoryParameterAs(context.parameters, "id") }; // Optional params - std::size_t maxBitRate{ getParameterAs(context.parameters, "maxBitRate").value_or(0) }; // "If set to zero, no limit is imposed" + std::size_t maxBitRate{ getParameterAs(context.parameters, "maxBitRate").value_or(0) * 1000 }; // "If set to zero, no limit is imposed", given in kpbs const std::string format{ getParameterAs(context.parameters, "format").value_or("") }; std::size_t timeOffset{ getParameterAs(context.parameters, "timeOffset").value_or(0) }; bool estimateContentLength{ getParameterAs(context.parameters, "estimateContentLength").value_or(false) }; StreamParameters parameters; - parameters.estimateContentLength = estimateContentLength; - auto transaction{ context.dbSession.createSharedTransaction() }; - { - const auto track{ Track::find(context.dbSession, id) }; - if (!track) - throw RequestedDataNotFoundError{}; - - parameters.inputFileParameters.trackPath = track->getPath(); - parameters.inputFileParameters.duration = track->getDuration(); - } - - if (format == "raw") // raw => no transcode - return parameters; - - const auto audioFile{ Av::parseAudioFile(parameters.inputFileParameters.trackPath) }; - - // check if transcode is really needed or not - // same format as requested, bitrate is lower than requested => no need to transcode - if (const auto streamInfo{ audioFile->getBestStreamInfo() }) - { - // assume reported codec is "mp3", "opus", "vorbis", etc. - if (StringUtils::stringCaseInsensitiveEqual(streamInfo->codec, format) && (maxBitRate == 0 || (streamInfo->bitrate / 1000) <= maxBitRate)) - { - LMS_LOG(API_SUBSONIC, DEBUG) << "stream parameters are compatible with actual file: no transcode"; - return parameters; - } - } - const User::pointer user{ User::find(context.dbSession, context.userId) }; if (!user) throw UserNotAuthorizedError{}; - Av::TranscodeParameters& transcodeParameters{ parameters.transcodeParameters.emplace() }; + const auto track{ Track::find(context.dbSession, id) }; + if (!track) + throw RequestedDataNotFoundError{}; - transcodeParameters.stripMetadata = false; // We want clients to use metadata (offline use, replay gain, etc.) - transcodeParameters.offset = std::chrono::seconds{ timeOffset }; + parameters.inputParameters.trackPath = track->getPath(); + parameters.inputParameters.duration = track->getDuration(); + parameters.estimateContentLength = estimateContentLength; - if (std::optional requestedFormat{ subsonicStreamFormatToAvFormat(format) }) - transcodeParameters.format = *requestedFormat; - else - transcodeParameters.format = userTranscodeFormatToAvFormat(user->getSubsonicDefaultTranscodeFormat()); + if (format == "raw") // raw => no transcoding + return parameters; - transcodeParameters.bitrate = user->getSubsonicDefaultTranscodeBitrate(); - if (maxBitRate != 0) - transcodeParameters.bitrate = Utils::clamp(transcodeParameters.bitrate, std::size_t{ 48000 }, maxBitRate * 1000); + std::optional requestedFormat{ subsonicStreamFormatToAvOutputFormat(format) }; + if (!requestedFormat) + { + if (user->getSubsonicEnableTranscodingByDefault()) + requestedFormat = userTranscodeFormatToAvFormat(user->getSubsonicDefaultTranscodingOutputFormat()); + } + + if (!requestedFormat && (maxBitRate == 0 || track->getBitrate() <= maxBitRate )) + { + LMS_LOG(API_SUBSONIC, DEBUG) << "File's bitrate is compatible with parameters => no transcoding"; + return parameters; // no transcoding needed + } + + // scan the file to check if its format is compatible with the actual requested format + // same codec => apply max bitrate + // otherwise => apply default bitrate (because we can't really compare bitrates between formats) + max bitrate) + std::size_t bitrate{}; + if (requestedFormat && isOutputFormatCompatible(track->getPath(), *requestedFormat)) + { + if (maxBitRate == 0 || track->getBitrate() <= maxBitRate) + { + LMS_LOG(API_SUBSONIC, DEBUG) << "File's bitrate and format are compatible with parameters => no transcoding"; + return parameters; // no transcoding needed + } + bitrate = maxBitRate; + } + + if (!requestedFormat) + requestedFormat = userTranscodeFormatToAvFormat(user->getSubsonicDefaultTranscodingOutputFormat()); + if (!bitrate) + bitrate = std::min(user->getSubsonicDefaultTranscodingOutputBitrate(), maxBitRate); + + Av::Transcoding::OutputParameters& outputParameters{ parameters.outputParameters.emplace() }; + + outputParameters.stripMetadata = false; // We want clients to use metadata (offline use, replay gain, etc.) + outputParameters.offset = std::chrono::seconds{ timeOffset }; + outputParameters.format = *requestedFormat; + outputParameters.bitrate = bitrate; return parameters; } @@ -160,7 +208,7 @@ namespace API::Subsonic trackPath = track->getPath(); } - resourceHandler = createFileResourceHandler(trackPath); + resourceHandler = Av::createRawResourceHandler(trackPath); } else { @@ -182,10 +230,10 @@ namespace API::Subsonic if (!continuation) { StreamParameters streamParameters{ getStreamParameters(context) }; - if (streamParameters.transcodeParameters) - resourceHandler = Av::createTranscodeResourceHandler(streamParameters.inputFileParameters, *streamParameters.transcodeParameters, streamParameters.estimateContentLength); + if (streamParameters.outputParameters) + resourceHandler = Av::Transcoding::createResourceHandler(streamParameters.inputParameters, *streamParameters.outputParameters, streamParameters.estimateContentLength); else - resourceHandler = createFileResourceHandler(streamParameters.inputFileParameters.trackPath); + resourceHandler = Av::createRawResourceHandler(streamParameters.inputParameters.trackPath); } else { @@ -207,8 +255,9 @@ namespace API::Subsonic // Mandatory params const auto trackId{ getParameterAs(context.parameters, "id") }; const auto releaseId{ getParameterAs(context.parameters, "id") }; + const auto artistId{ getParameterAs(context.parameters, "id") }; - if (!trackId && !releaseId) + if (!trackId && !releaseId && !artistId) throw BadParameterGenericError{ "id" }; std::size_t size{ getParameterAs(context.parameters, "size").value_or(1024) }; @@ -219,6 +268,21 @@ namespace API::Subsonic cover = Service::get()->getFromTrack(*trackId, size); else if (releaseId) cover = Service::get()->getFromRelease(*releaseId, size); + else if (artistId) + { + // TODO handle a placeholder for artists + response.setStatus(404); + return; + } + + if (!cover && context.enableDefaultCover) + cover = Service::get()->getDefault(size); + + if (!cover) + { + response.setStatus(404); + return; + } response.out().write(reinterpret_cast(cover->getData()), cover->getDataSize()); response.setMimeType(std::string{ cover->getMimeType() }); diff --git a/src/libs/subsonic/impl/entrypoints/Searching.cpp b/src/libs/subsonic/impl/entrypoints/Searching.cpp index a3ed2968..41d1e774 100644 --- a/src/libs/subsonic/impl/entrypoints/Searching.cpp +++ b/src/libs/subsonic/impl/entrypoints/Searching.cpp @@ -57,43 +57,56 @@ namespace API::Subsonic std::size_t songCount{ getParameterAs(context.parameters, "songCount").value_or(20) }; std::size_t songOffset{ getParameterAs(context.parameters, "songOffset").value_or(0) }; + if (artistCount > defaultMaxCountSize) + throw ParameterValueTooHighGenericError{ "artistCount", defaultMaxCountSize }; + else if (albumCount > defaultMaxCountSize) + throw ParameterValueTooHighGenericError{ "albumCount", defaultMaxCountSize }; + else if (songCount > defaultMaxCountSize) + throw ParameterValueTooHighGenericError{ "songCount", defaultMaxCountSize }; + + Response response{ Response::createOkResponse(context.serverProtocolVersion) }; + Response::Node& searchResult2Node{ response.createNode(id3 ? "searchResult3" : "searchResult2") }; + auto transaction{ context.dbSession.createSharedTransaction() }; User::pointer user{ User::find(context.dbSession, context.userId) }; if (!user) throw UserNotAuthorizedError{}; - Response response{ Response::createOkResponse(context.serverProtocolVersion) }; - Response::Node& searchResult2Node{ response.createNode(id3 ? "searchResult3" : "searchResult2") }; - if (artistCount > 0) { Artist::FindParameters params; params.setKeywords(keywords); - params.setRange({ artistOffset, artistCount }); + params.setRange(Range{ artistOffset, artistCount }); - for (const Artist::pointer& artist : Artist::find(context.dbSession, params).results) - searchResult2Node.addArrayChild("artist", createArtistNode(context, artist, user, id3)); + Artist::find(context.dbSession, params, [&](const Artist::pointer& artist) + { + searchResult2Node.addArrayChild("artist", createArtistNode(context, artist, user, id3)); + }); } if (albumCount > 0) { Release::FindParameters params; params.setKeywords(keywords); - params.setRange({ albumOffset, albumCount }); + params.setRange(Range{ albumOffset, albumCount }); - for (const Release::pointer& release : Release::find(context.dbSession, params).results) - searchResult2Node.addArrayChild("album", createAlbumNode(context, release, user, id3)); + Release::find(context.dbSession, params, [&](const Release::pointer& release) + { + searchResult2Node.addArrayChild("album", createAlbumNode(context, release, user, id3)); + }); } if (songCount > 0) { Track::FindParameters params; params.setKeywords(keywords); - params.setRange({ songOffset, songCount }); + params.setRange(Range{ songOffset, songCount }); - for (const Track::pointer& track : Track::find(context.dbSession, params).results) - searchResult2Node.addArrayChild("song", createSongNode(context, track, user)); + Track::find(context.dbSession, params, [&](const Track::pointer& track) + { + searchResult2Node.addArrayChild("song", createSongNode(context, track, user)); + }); } return response; diff --git a/src/libs/subsonic/impl/responses/Album.cpp b/src/libs/subsonic/impl/responses/Album.cpp index cbee51d6..b9bff4f5 100644 --- a/src/libs/subsonic/impl/responses/Album.cpp +++ b/src/libs/subsonic/impl/responses/Album.cpp @@ -168,7 +168,7 @@ namespace API::Subsonic params.setClusterType(clusterType->getId()); for (const auto& cluster : Cluster::find(context.dbSession, params).results) - albumNode.addArrayValue(field, std::get(cluster)); + albumNode.addArrayValue(field, cluster->getName()); } } }; @@ -183,7 +183,7 @@ namespace API::Subsonic params.setClusterType(genreClusterType->getId()); for (const auto& cluster : Cluster::find(context.dbSession, params).results) - albumNode.addArrayChild("genres", createItemGenreNode(std::get(cluster))); + albumNode.addArrayChild("genres", createItemGenreNode(cluster->getName())); } albumNode.createEmptyArrayChild("artists"); diff --git a/src/libs/subsonic/impl/responses/Song.cpp b/src/libs/subsonic/impl/responses/Song.cpp index 42150cfc..66ef8f57 100644 --- a/src/libs/subsonic/impl/responses/Song.cpp +++ b/src/libs/subsonic/impl/responses/Song.cpp @@ -21,6 +21,7 @@ #include +#include "av/IAudioFile.hpp" #include "services/database/Artist.hpp" #include "services/database/Cluster.hpp" #include "services/database/Listen.hpp" @@ -45,15 +46,15 @@ namespace API::Subsonic namespace { - std::string_view formatToSuffix(AudioFormat format) + std::string_view formatToSuffix(TranscodingOutputFormat format) { switch (format) { - case AudioFormat::MP3: return "mp3"; - case AudioFormat::OGG_OPUS: return "opus"; - case AudioFormat::MATROSKA_OPUS: return "mka"; - case AudioFormat::OGG_VORBIS: return "ogg"; - case AudioFormat::WEBM_VORBIS: return "webm"; + case TranscodingOutputFormat::MP3: return "mp3"; + case TranscodingOutputFormat::OGG_OPUS: return "opus"; + case TranscodingOutputFormat::MATROSKA_OPUS: return "mka"; + case TranscodingOutputFormat::OGG_VORBIS: return "ogg"; + case TranscodingOutputFormat::WEBM_VORBIS: return "webm"; } return ""; @@ -123,7 +124,11 @@ namespace API::Subsonic trackResponse.setAttribute("suffix", extension.string().substr(1)); } - trackResponse.setAttribute("transcodedSuffix", formatToSuffix(user->getSubsonicDefaultTranscodeFormat())); + { + const std::string fileSuffix{ formatToSuffix(user->getSubsonicDefaultTranscodingOutputFormat()) }; + trackResponse.setAttribute("transcodedSuffix", fileSuffix); + trackResponse.setAttribute("transcodedContentType", Av::getMimeType(std::filesystem::path{ "." + fileSuffix })); + } trackResponse.setAttribute("coverArt", idToString(track->getId())); @@ -148,8 +153,10 @@ namespace API::Subsonic } trackResponse.setAttribute("duration", std::chrono::duration_cast(track->getDuration()).count()); + trackResponse.setAttribute("bitRate", (track->getBitrate() / 1000)); trackResponse.setAttribute("type", "music"); trackResponse.setAttribute("created", StringUtils::toISO8601String(track->getLastWritten())); + trackResponse.setAttribute("contentType", Av::getMimeType(track->getPath().extension())); if (const Wt::WDateTime dateTime{ Service::get()->getStarredDateTime(user->getId(), track->getId()) }; dateTime.isValid()) trackResponse.setAttribute("starred", StringUtils::toISO8601String(dateTime)); @@ -228,7 +235,7 @@ namespace API::Subsonic params.setClusterType(clusterType->getId()); for (const auto& cluster : Cluster::find(context.dbSession, params).results) - trackResponse.addArrayValue(field, std::get(cluster)); + trackResponse.addArrayValue(field, cluster->getName()); } } }; @@ -243,7 +250,7 @@ namespace API::Subsonic params.setClusterType(genreClusterType->getId()); for (const auto& cluster : Cluster::find(context.dbSession, params).results) - trackResponse.addArrayChild("genres", createItemGenreNode(std::get(cluster))); + trackResponse.addArrayChild("genres", createItemGenreNode(cluster->getName())); } trackResponse.addChild("replayGain", createReplayGainNode(track)); diff --git a/src/libs/utils/impl/FileResourceHandler.cpp b/src/libs/utils/impl/FileResourceHandler.cpp index 8e3a6eb0..b0b4e53f 100644 --- a/src/libs/utils/impl/FileResourceHandler.cpp +++ b/src/libs/utils/impl/FileResourceHandler.cpp @@ -24,108 +24,106 @@ #include "utils/Logger.hpp" std::unique_ptr -createFileResourceHandler(const std::filesystem::path& path) +createFileResourceHandler(const std::filesystem::path& path, std::string_view mimeType) { - return std::make_unique(path); + return std::make_unique(path, mimeType); } - -FileResourceHandler::FileResourceHandler(const std::filesystem::path& path) -: _path {path} +FileResourceHandler::FileResourceHandler(const std::filesystem::path& path, std::string_view mimeType) + : _path{ path } + , _mimeType{ mimeType } { } - Wt::Http::ResponseContinuation* FileResourceHandler::processRequest(const Wt::Http::Request& request, Wt::Http::Response& response) { - ::uint64_t startByte {_offset}; - std::ifstream ifs {_path.string().c_str(), std::ios::in | std::ios::binary}; + ::uint64_t startByte{ _offset }; + std::ifstream ifs{ _path.string().c_str(), std::ios::in | std::ios::binary }; - if (startByte == 0) - { - if (!ifs) - { - LMS_LOG(UTILS, ERROR) << "Cannot open file stream for '" << _path.string() << "'"; - response.setStatus(404); - return {}; - } - else - { - response.setStatus(200); - } + if (startByte == 0) + { + if (!ifs) + { + LMS_LOG(UTILS, ERROR) << "Cannot open file stream for '" << _path.string() << "'"; + response.setStatus(404); + return {}; + } - ifs.seekg(0, std::ios::end); - const ::uint64_t fileSize {static_cast<::uint64_t>(ifs.tellg())}; - ifs.seekg(0, std::ios::beg); + ifs.seekg(0, std::ios::end); + const ::uint64_t fileSize{ static_cast<::uint64_t>(ifs.tellg()) }; + ifs.seekg(0, std::ios::beg); - LMS_LOG(UTILS, DEBUG) << "File '" << _path.string() << "', fileSize = " << fileSize; + LMS_LOG(UTILS, DEBUG) << "File '" << _path.string() << "', fileSize = " << fileSize; - const Wt::Http::Request::ByteRangeSpecifier ranges {request.getRanges(fileSize)}; - if (!ranges.isSatisfiable()) - { - std::ostringstream contentRange; - contentRange << "bytes */" << fileSize; - response.setStatus(416); // Requested range not satisfiable - response.addHeader("Content-Range", contentRange.str()); + const Wt::Http::Request::ByteRangeSpecifier ranges{ request.getRanges(fileSize) }; + if (!ranges.isSatisfiable()) + { + std::ostringstream contentRange; + contentRange << "bytes */" << fileSize; + response.setStatus(416); // Requested range not satisfiable + response.addHeader("Content-Range", contentRange.str()); - LMS_LOG(UTILS, DEBUG) << "Range not satisfiable"; - return {}; - } + LMS_LOG(UTILS, DEBUG) << "Range not satisfiable"; + return {}; + } - if (ranges.size() == 1) - { - LMS_LOG(UTILS, DEBUG) << "Range requested = " << ranges[0].firstByte() << "/" << ranges[0].lastByte(); + if (ranges.size() == 1) + { + LMS_LOG(UTILS, DEBUG) << "Range requested = " << ranges[0].firstByte() << "/" << ranges[0].lastByte(); - response.setStatus(206); - startByte = ranges[0].firstByte(); - _beyondLastByte = ranges[0].lastByte() + 1; + response.setStatus(206); + startByte = ranges[0].firstByte(); + _beyondLastByte = ranges[0].lastByte() + 1; - std::ostringstream contentRange; - contentRange << "bytes " << startByte << "-" - << _beyondLastByte - 1 << "/" << fileSize; + std::ostringstream contentRange; + contentRange << "bytes " << startByte << "-" + << _beyondLastByte - 1 << "/" << fileSize; - response.addHeader("Content-Range", contentRange.str()); - response.setContentLength(_beyondLastByte - startByte); - } - else - { - LMS_LOG(UTILS, DEBUG) << "No range requested"; + response.addHeader("Content-Range", contentRange.str()); + response.setContentLength(_beyondLastByte - startByte); + } + else + { + LMS_LOG(UTILS, DEBUG) << "No range requested"; - _beyondLastByte = fileSize; - response.setContentLength(_beyondLastByte); - } - } - else if (!ifs) - { - LMS_LOG(UTILS, ERROR) << "Cannot reopen file stream for '" << _path.string() << "'"; - return {}; - } + response.setStatus(200); + _beyondLastByte = fileSize; + response.setContentLength(_beyondLastByte); + } - ifs.seekg(static_cast(startByte)); + LMS_LOG(UTILS, DEBUG) << "Mimetype set to '" << _mimeType << "'"; + response.setMimeType(_mimeType); + } + else if (!ifs) + { + LMS_LOG(UTILS, ERROR) << "Cannot reopen file stream for '" << _path.string() << "'"; + return {}; + } - std::vector buf; - buf.resize(_chunkSize); + ifs.seekg(static_cast(startByte)); - ::uint64_t restSize = _beyondLastByte - startByte; - ::uint64_t pieceSize = buf.size() > restSize ? restSize : buf.size(); + std::vector buf; + buf.resize(_chunkSize); - ifs.read(&buf[0], pieceSize); - const ::uint64_t actualPieceSize {static_cast<::uint64_t>(ifs.gcount())}; - response.out().write(&buf[0], actualPieceSize); + ::uint64_t restSize = _beyondLastByte - startByte; + ::uint64_t pieceSize = buf.size() > restSize ? restSize : buf.size(); - LMS_LOG(UTILS, DEBUG) << "Written " << actualPieceSize << " bytes"; + ifs.read(&buf[0], pieceSize); + const ::uint64_t actualPieceSize{ static_cast<::uint64_t>(ifs.gcount()) }; + response.out().write(&buf[0], actualPieceSize); - LMS_LOG(UTILS, DEBUG) << "Progress: " << actualPieceSize << "/" << restSize; - if (ifs.good() && actualPieceSize < restSize) - { - _offset = startByte + actualPieceSize; - LMS_LOG(UTILS, DEBUG) << "Job not complete! Next chunk offset = " << _offset; + LMS_LOG(UTILS, DEBUG) << "Written " << actualPieceSize << " bytes"; - return response.createContinuation(); - } + LMS_LOG(UTILS, DEBUG) << "Progress: " << actualPieceSize << "/" << restSize; + if (ifs.good() && actualPieceSize < restSize) + { + _offset = startByte + actualPieceSize; + LMS_LOG(UTILS, DEBUG) << "Job not complete! Next chunk offset = " << _offset; - LMS_LOG(UTILS, DEBUG) << "Job complete!"; - return nullptr; + return response.createContinuation(); + } + + LMS_LOG(UTILS, DEBUG) << "Job complete!"; + return nullptr; } - diff --git a/src/libs/utils/impl/FileResourceHandler.hpp b/src/libs/utils/impl/FileResourceHandler.hpp index 8293f587..a6eab694 100644 --- a/src/libs/utils/impl/FileResourceHandler.hpp +++ b/src/libs/utils/impl/FileResourceHandler.hpp @@ -20,21 +20,24 @@ #pragma once #include +#include +#include #include "utils/IResourceHandler.hpp" class FileResourceHandler final : public IResourceHandler { - public: - FileResourceHandler(const std::filesystem::path& filePath); +public: + FileResourceHandler(const std::filesystem::path& filePath, std::string_view mimeType); - private: - Wt::Http::ResponseContinuation* processRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override; - void abort() override {}; +private: + Wt::Http::ResponseContinuation* processRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override; + void abort() override {}; - static constexpr std::size_t _chunkSize {65536}; + static constexpr std::size_t _chunkSize{ 262'144 }; - std::filesystem::path _path; - ::uint64_t _beyondLastByte {}; - ::uint64_t _offset {}; + std::filesystem::path _path; + std::string _mimeType; + ::uint64_t _beyondLastByte{}; + ::uint64_t _offset{}; }; diff --git a/src/libs/utils/impl/Logger.cpp b/src/libs/utils/impl/Logger.cpp index 604a2802..28176166 100644 --- a/src/libs/utils/impl/Logger.cpp +++ b/src/libs/utils/impl/Logger.cpp @@ -21,59 +21,59 @@ const char* getModuleName(Module mod) { - switch (mod) - { - case Module::API_SUBSONIC: return "API_SUBSONIC"; - case Module::AUTH: return "AUTH"; - case Module::AV: return "AV"; - case Module::CHILDPROCESS: return "CHILDPROC"; - case Module::COVER: return "COVER"; - case Module::DB: return "DB"; - case Module::DBUPDATER: return "DB UPDATER"; - case Module::FEATURE: return "FEATURE"; - case Module::FEEDBACK: return "FEEDBACK"; - case Module::HTTP: return "HTTP"; - case Module::MAIN: return "MAIN"; - case Module::METADATA: return "METADATA"; - case Module::REMOTE: return "REMOTE"; - case Module::SCROBBLING: return "SCROBBLING"; - case Module::SERVICE: return "SERVICE"; - case Module::RECOMMENDATION: return "RECOMMENDATION"; - case Module::TRANSCODE: return "TRANSCODE"; - case Module::UI: return "UI"; - case Module::UTILS: return "UTILS"; - } - return ""; + switch (mod) + { + case Module::API_SUBSONIC: return "API_SUBSONIC"; + case Module::AUTH: return "AUTH"; + case Module::AV: return "AV"; + case Module::CHILDPROCESS: return "CHILDPROC"; + case Module::COVER: return "COVER"; + case Module::DB: return "DB"; + case Module::DBUPDATER: return "DB UPDATER"; + case Module::FEATURE: return "FEATURE"; + case Module::FEEDBACK: return "FEEDBACK"; + case Module::HTTP: return "HTTP"; + case Module::MAIN: return "MAIN"; + case Module::METADATA: return "METADATA"; + case Module::REMOTE: return "REMOTE"; + case Module::SCROBBLING: return "SCROBBLING"; + case Module::SERVICE: return "SERVICE"; + case Module::RECOMMENDATION: return "RECOMMENDATION"; + case Module::TRANSCODING: return "TRANSCODING"; + case Module::UI: return "UI"; + case Module::UTILS: return "UTILS"; + } + return ""; } const char* getSeverityName(Severity sev) { - switch (sev) - { - case Severity::FATAL: return "fatal"; - case Severity::ERROR: return "error"; - case Severity::WARNING: return "warning"; - case Severity::INFO: return "info"; - case Severity::DEBUG: return "debug"; - } - return ""; + switch (sev) + { + case Severity::FATAL: return "fatal"; + case Severity::ERROR: return "error"; + case Severity::WARNING: return "warning"; + case Severity::INFO: return "info"; + case Severity::DEBUG: return "debug"; + } + return ""; } Log::Log(Logger* logger, Module module, Severity severity) - : _module {module}, - _severity {severity}, - _logger {logger} + : _module{ module }, + _severity{ severity }, + _logger{ logger } {} Log::~Log() { - if (_logger) - _logger->processLog(*this); + if (_logger) + _logger->processLog(*this); } std::string Log::getMessage() const { - return _oss.str(); + return _oss.str(); } diff --git a/src/libs/utils/include/utils/FileResourceHandlerCreator.hpp b/src/libs/utils/include/utils/FileResourceHandlerCreator.hpp index 8f69e18a..00106721 100644 --- a/src/libs/utils/include/utils/FileResourceHandlerCreator.hpp +++ b/src/libs/utils/include/utils/FileResourceHandlerCreator.hpp @@ -21,8 +21,9 @@ #include #include +#include #include "utils/IResourceHandler.hpp" -std::unique_ptr createFileResourceHandler(const std::filesystem::path& path); +std::unique_ptr createFileResourceHandler(const std::filesystem::path& path, std::string_view mimeType); diff --git a/src/libs/utils/include/utils/Logger.hpp b/src/libs/utils/include/utils/Logger.hpp index f7a48916..d14fe5ab 100644 --- a/src/libs/utils/include/utils/Logger.hpp +++ b/src/libs/utils/include/utils/Logger.hpp @@ -51,7 +51,7 @@ enum class Module SCROBBLING, SERVICE, RECOMMENDATION, - TRANSCODE, + TRANSCODING, UI, UTILS, }; diff --git a/src/lms/CMakeLists.txt b/src/lms/CMakeLists.txt index b42f9023..494d727e 100644 --- a/src/lms/CMakeLists.txt +++ b/src/lms/CMakeLists.txt @@ -44,7 +44,7 @@ add_executable(lms ui/explore/TrackListsView.cpp ui/explore/TracksView.cpp ui/resource/AudioFileResource.cpp - ui/resource/AudioTranscodeResource.cpp + ui/resource/AudioTranscodingResource.cpp ui/resource/CoverResource.cpp ui/resource/DownloadResource.cpp ) diff --git a/src/lms/main.cpp b/src/lms/main.cpp index 543c2475..ebef9dd4 100644 --- a/src/lms/main.cpp +++ b/src/lms/main.cpp @@ -270,7 +270,7 @@ int main(int argc, char* argv[]) Service coverService{ Cover::createCoverService(database, argv[0], server.appRoot() + "/images/unknown-cover.jpg") }; Service recommendationService{ Recommendation::createRecommendationService(database) }; Service playlistGeneratorService{ Recommendation::createPlaylistGeneratorService(database, *recommendationService.get()) }; - Service scannerService{ Scanner::createScannerService(database, *recommendationService) }; + Service scannerService{ Scanner::createScannerService(database) }; scannerService->getEvents().scanComplete.connect([&] { diff --git a/src/lms/ui/LmsApplication.cpp b/src/lms/ui/LmsApplication.cpp index 60de0f36..0b7463f0 100644 --- a/src/lms/ui/LmsApplication.cpp +++ b/src/lms/ui/LmsApplication.cpp @@ -49,7 +49,7 @@ #include "explore/Explore.hpp" #include "explore/Filters.hpp" #include "resource/AudioFileResource.hpp" -#include "resource/AudioTranscodeResource.hpp" +#include "resource/AudioTranscodingResource.hpp" #include "resource/DownloadResource.hpp" #include "resource/CoverResource.hpp" #include "Auth.hpp" diff --git a/src/lms/ui/MediaPlayer.cpp b/src/lms/ui/MediaPlayer.cpp index dfd9db66..96e7413f 100644 --- a/src/lms/ui/MediaPlayer.cpp +++ b/src/lms/ui/MediaPlayer.cpp @@ -35,7 +35,7 @@ #include "services/database/User.hpp" #include "resource/CoverResource.hpp" -#include "resource/AudioTranscodeResource.hpp" +#include "resource/AudioTranscodingResource.hpp" #include "resource/AudioFileResource.hpp" #include "utils/String.hpp" @@ -44,304 +44,295 @@ #include "LmsApplication.hpp" #include "Utils.hpp" -namespace UserInterface { - -static std::string settingsToJSString(const MediaPlayer::Settings& settings) +namespace UserInterface { - namespace Json = Wt::Json; + namespace + { + std::string settingsToJSString(const MediaPlayer::Settings& settings) + { + namespace Json = Wt::Json; - Json::Object res; + Json::Object res; - { - Json::Object transcode; - transcode["mode"] = static_cast(settings.transcode.mode); - transcode["format"] = static_cast(settings.transcode.format); - transcode["bitrate"] = static_cast(settings.transcode.bitrate); - res["transcode"] = std::move(transcode); - } + { + Json::Object transcoding; + transcoding["mode"] = static_cast(settings.transcoding.mode); + transcoding["format"] = static_cast(settings.transcoding.format); + transcoding["bitrate"] = static_cast(settings.transcoding.bitrate); + res["transcoding"] = std::move(transcoding); + } - { - Json::Object replayGain; - replayGain["mode"] = static_cast(settings.replayGain.mode); - replayGain["preAmpGain"] = settings.replayGain.preAmpGain; - replayGain["preAmpGainIfNoInfo"] = settings.replayGain.preAmpGainIfNoInfo; - res["replayGain"] = std::move(replayGain); - } + { + Json::Object replayGain; + replayGain["mode"] = static_cast(settings.replayGain.mode); + replayGain["preAmpGain"] = settings.replayGain.preAmpGain; + replayGain["preAmpGainIfNoInfo"] = settings.replayGain.preAmpGainIfNoInfo; + res["replayGain"] = std::move(replayGain); + } - return Json::serialize(res); -} + return Json::serialize(res); + } -static -std::optional -transcodeModeFromString(const std::string& str) -{ - const auto value {StringUtils::readAs(str)}; - if (!value) - return std::nullopt; + std::optional transcodingModeFromString(const std::string& str) + { + const auto value{ StringUtils::readAs(str) }; + if (!value) + return std::nullopt; - MediaPlayer::Settings::Transcode::Mode mode {static_cast(*value)}; - switch (mode) - { - case MediaPlayer::Settings::Transcode::Mode::Never: - case MediaPlayer::Settings::Transcode::Mode::Always: - case MediaPlayer::Settings::Transcode::Mode::IfFormatNotSupported: - return mode; - } + MediaPlayer::Settings::Transcoding::Mode mode{ static_cast(*value) }; + switch (mode) + { + case MediaPlayer::Settings::Transcoding::Mode::Never: + case MediaPlayer::Settings::Transcoding::Mode::Always: + case MediaPlayer::Settings::Transcoding::Mode::IfFormatNotSupported: + return mode; + } - return std::nullopt; -} + return std::nullopt; + } -static -std::optional -formatFromString(const std::string& str) -{ - const auto value {StringUtils::readAs(str)}; - if (!value) - return std::nullopt; + std::optional formatFromString(const std::string& str) + { + const auto value{ StringUtils::readAs(str) }; + if (!value) + return std::nullopt; - MediaPlayer::Format format {static_cast(*value)}; - switch (format) - { - case MediaPlayer::Format::MP3: - case MediaPlayer::Format::OGG_OPUS: - case MediaPlayer::Format::MATROSKA_OPUS: - case MediaPlayer::Format::OGG_VORBIS: - case MediaPlayer::Format::WEBM_VORBIS: - return format; - } + MediaPlayer::Format format{ static_cast(*value) }; + switch (format) + { + case MediaPlayer::Format::MP3: + case MediaPlayer::Format::OGG_OPUS: + case MediaPlayer::Format::MATROSKA_OPUS: + case MediaPlayer::Format::OGG_VORBIS: + case MediaPlayer::Format::WEBM_VORBIS: + return format; + } - return std::nullopt; -} + return std::nullopt; + } -static -std::optional -bitrateFromString(const std::string& str) -{ - const auto value {StringUtils::readAs(str)}; - if (!value) - return std::nullopt; + std::optional bitrateFromString(const std::string& str) + { + const auto value{ StringUtils::readAs(str) }; + if (!value) + return std::nullopt; - if (!Database::isAudioBitrateAllowed(*value)) - return std::nullopt; + if (!Database::isAudioBitrateAllowed(*value)) + return std::nullopt; - return *value; -} + return *value; + } -static -std::optional -replayGainModeFromString(const std::string& str) -{ - const auto value {StringUtils::readAs(str)}; - if (!value) - return std::nullopt; + std::optional replayGainModeFromString(const std::string& str) + { + const auto value{ StringUtils::readAs(str) }; + if (!value) + return std::nullopt; - MediaPlayer::Settings::ReplayGain::Mode mode {static_cast(*value)}; - switch (mode) - { - case MediaPlayer::Settings::ReplayGain::Mode::None: - case MediaPlayer::Settings::ReplayGain::Mode::Auto: - case MediaPlayer::Settings::ReplayGain::Mode::Track: - case MediaPlayer::Settings::ReplayGain::Mode::Release: - return mode; - } + MediaPlayer::Settings::ReplayGain::Mode mode{ static_cast(*value) }; + switch (mode) + { + case MediaPlayer::Settings::ReplayGain::Mode::None: + case MediaPlayer::Settings::ReplayGain::Mode::Auto: + case MediaPlayer::Settings::ReplayGain::Mode::Track: + case MediaPlayer::Settings::ReplayGain::Mode::Release: + return mode; + } - return std::nullopt; -} + return std::nullopt; + } -static -std::optional -replayGainPreAmpGainFromString(const std::string& str) -{ - const auto value {StringUtils::readAs(str)}; - if (!value) - return std::nullopt; + std::optional replayGainPreAmpGainFromString(const std::string& str) + { + const auto value{ StringUtils::readAs(str) }; + if (!value) + return std::nullopt; - return ::Utils::clamp(*value, (double)MediaPlayer::Settings::ReplayGain::minPreAmpGain, (double)MediaPlayer::Settings::ReplayGain::maxPreAmpGain); -} + return ::Utils::clamp(*value, (double)MediaPlayer::Settings::ReplayGain::minPreAmpGain, (double)MediaPlayer::Settings::ReplayGain::maxPreAmpGain); + } -static MediaPlayer::Settings settingsfromJSString(const std::string& strSettings) -{ - using Settings = MediaPlayer::Settings; - namespace Json = Wt::Json; - Json::Object parsedSettings; + MediaPlayer::Settings settingsfromJSString(const std::string& strSettings) + { + using Settings = MediaPlayer::Settings; + namespace Json = Wt::Json; + Json::Object parsedSettings; - Json::parse(strSettings, parsedSettings); + Json::parse(strSettings, parsedSettings); - MediaPlayer::Settings settings; + MediaPlayer::Settings settings; - { - const Json::Value transcodeValue {parsedSettings.get("transcode")}; - if (transcodeValue.type() == Json::Type::Object) - { - const Json::Object transcode {transcodeValue}; - settings.transcode.mode = transcodeModeFromString(transcode.get("mode").toString().orIfNull("")).value_or(Settings::Transcode::defaultMode); - settings.transcode.format = formatFromString(transcode.get("format").toString().orIfNull("")).value_or(Settings::Transcode::defaultFormat); - settings.transcode.bitrate = bitrateFromString(transcode.get("bitrate").toString().orIfNull("")).value_or(Settings::Transcode::defaultBitrate); - } - } - { - const Json::Value replayGainValue {parsedSettings.get("replayGain")}; - if (replayGainValue.type() == Json::Type::Object) - { - const Json::Object replayGain {replayGainValue}; - settings.replayGain.mode = replayGainModeFromString(replayGain.get("mode").toString().orIfNull("")).value_or(Settings::ReplayGain::defaultMode); - settings.replayGain.preAmpGain = replayGainPreAmpGainFromString(replayGain.get("preAmpGain").toString().orIfNull("")).value_or(Settings::ReplayGain::defaultPreAmpGain); - settings.replayGain.preAmpGainIfNoInfo = replayGainPreAmpGainFromString(replayGain.get("preAmpGainIfNoInfo").toString().orIfNull("")).value_or(Settings::ReplayGain::defaultPreAmpGain); - } - } + { + const Json::Value transcodingValue{ parsedSettings.get("transcoding") }; + if (transcodingValue.type() == Json::Type::Object) + { + const Json::Object transcoding{ transcodingValue }; + settings.transcoding.mode = transcodingModeFromString(transcoding.get("mode").toString().orIfNull("")).value_or(Settings::Transcoding::defaultMode); + settings.transcoding.format = formatFromString(transcoding.get("format").toString().orIfNull("")).value_or(Settings::Transcoding::defaultFormat); + settings.transcoding.bitrate = bitrateFromString(transcoding.get("bitrate").toString().orIfNull("")).value_or(Settings::Transcoding::defaultBitrate); + } + } + { + const Json::Value replayGainValue{ parsedSettings.get("replayGain") }; + if (replayGainValue.type() == Json::Type::Object) + { + const Json::Object replayGain{ replayGainValue }; + settings.replayGain.mode = replayGainModeFromString(replayGain.get("mode").toString().orIfNull("")).value_or(Settings::ReplayGain::defaultMode); + settings.replayGain.preAmpGain = replayGainPreAmpGainFromString(replayGain.get("preAmpGain").toString().orIfNull("")).value_or(Settings::ReplayGain::defaultPreAmpGain); + settings.replayGain.preAmpGainIfNoInfo = replayGainPreAmpGainFromString(replayGain.get("preAmpGainIfNoInfo").toString().orIfNull("")).value_or(Settings::ReplayGain::defaultPreAmpGain); + } + } - return settings; -} + return settings; + } + } -MediaPlayer::MediaPlayer() -: Wt::WTemplate {Wt::WString::tr("Lms.MediaPlayer.template")} -, playPrevious {this, "playPrevious"} -, playNext {this, "playNext"} -, scrobbleListenNow {this, "scrobbleListenNow"} -, scrobbleListenFinished {this, "scrobbleListenFinished"} -, playbackEnded {this, "playbackEnded"} -, _settingsLoaded {this, "settingsLoaded"} -{ - addFunction("tr", &Wt::WTemplate::Functions::tr); + MediaPlayer::MediaPlayer() + : Wt::WTemplate{ Wt::WString::tr("Lms.MediaPlayer.template") } + , playPrevious{ this, "playPrevious" } + , playNext{ this, "playNext" } + , scrobbleListenNow{ this, "scrobbleListenNow" } + , scrobbleListenFinished{ this, "scrobbleListenFinished" } + , playbackEnded{ this, "playbackEnded" } + , _settingsLoaded{ this, "settingsLoaded" } + { + addFunction("tr", &Wt::WTemplate::Functions::tr); - _audioTranscodeResource = std::make_unique(); - _audioFileResource = std::make_unique(); + _audioTranscodingResource = std::make_unique(); + _audioFileResource = std::make_unique(); - _title = bindNew("title"); - _artist = bindNew("artist"); - _release = bindNew("release"); - _separator = bindNew("separator"); - _playQueue = bindNew("playqueue-btn", Wt::WString::tr("Lms.MediaPlayer.template.playqueue-btn").arg(0), Wt::TextFormat::XHTML); - _playQueue->setLink(Wt::WLink {Wt::LinkType::InternalPath, "/playqueue"}); - _playQueue->setToolTip(tr("Lms.PlayQueue.playqueue")); + _title = bindNew("title"); + _artist = bindNew("artist"); + _release = bindNew("release"); + _separator = bindNew("separator"); + _playQueue = bindNew("playqueue-btn", Wt::WString::tr("Lms.MediaPlayer.template.playqueue-btn").arg(0), Wt::TextFormat::XHTML); + _playQueue->setLink(Wt::WLink{ Wt::LinkType::InternalPath, "/playqueue" }); + _playQueue->setToolTip(tr("Lms.PlayQueue.playqueue")); - _settingsLoaded.connect([this](const std::string& settings) - { - LMS_LOG(UI, DEBUG) << "Settings loaded! '" << settings << "'"; + _settingsLoaded.connect([this](const std::string& settings) + { + LMS_LOG(UI, DEBUG) << "Settings loaded! '" << settings << "'"; - _settings = settingsfromJSString(settings); + _settings = settingsfromJSString(settings); - settingsLoaded.emit(); - }); + settingsLoaded.emit(); + }); - { - Settings defaultSettings; + { + Settings defaultSettings; - std::ostringstream oss; - oss << "LMS.mediaplayer.init(" - << jsRef() - << ", defaultSettings = " << settingsToJSString(defaultSettings) - << ")"; + std::ostringstream oss; + oss << "LMS.mediaplayer.init(" + << jsRef() + << ", defaultSettings = " << settingsToJSString(defaultSettings) + << ")"; - LMS_LOG(UI, DEBUG) << "Running js = '" << oss.str() << "'"; - doJavaScript(oss.str()); - } -} + LMS_LOG(UI, DEBUG) << "Running js = '" << oss.str() << "'"; + doJavaScript(oss.str()); + } + } -void -MediaPlayer::loadTrack(Database::TrackId trackId, bool play, float replayGain) -{ - LMS_LOG(UI, DEBUG) << "Playing track ID = " << trackId.toString(); + void MediaPlayer::loadTrack(Database::TrackId trackId, bool play, float replayGain) + { + LMS_LOG(UI, DEBUG) << "Playing track ID = " << trackId.toString(); - std::ostringstream oss; - { - auto transaction {LmsApp->getDbSession().createSharedTransaction()}; + std::ostringstream oss; + { + auto transaction{ LmsApp->getDbSession().createSharedTransaction() }; - const auto track {Database::Track::find(LmsApp->getDbSession(), trackId)}; - if (!track) - return; + const auto track{ Database::Track::find(LmsApp->getDbSession(), trackId) }; + if (!track) + return; - const std::string transcodeResource {_audioTranscodeResource->getUrl(trackId)}; - const std::string nativeResource {_audioFileResource->getUrl(trackId)}; + const std::string transcodingResource{ _audioTranscodingResource->getUrl(trackId) }; + const std::string nativeResource{ _audioFileResource->getUrl(trackId) }; - const auto artists {track->getArtists({Database::TrackArtistLinkType::Artist})}; + const auto artists{ track->getArtists({Database::TrackArtistLinkType::Artist}) }; - oss - << "var params = {" - << " trackId :\"" << trackId.toString() << "\"," - << " nativeResource: \"" << nativeResource << "\"," - << " transcodeResource: \"" << transcodeResource << "\"," - << " duration: " << std::chrono::duration_cast(track->getDuration()).count() << "," - << " replayGain: " << replayGain << "," - << " title: \"" << StringUtils::jsEscape(track->getName()) << "\"," - << " artist: \"" << (!artists.empty() ? StringUtils::jsEscape(artists.front()->getName()) : "") << "\"," - << " release: \"" << (track->getRelease() ? StringUtils::jsEscape(track->getRelease()->getName()) : "") << "\"," - << " artwork: [" - << " { src: \"" << LmsApp->getCoverResource()->getTrackUrl(trackId, CoverResource::Size::Small) << "\", sizes: \"128x128\", type: \"image/jpeg\" }," - << " { src: \"" << LmsApp->getCoverResource()->getTrackUrl(trackId, CoverResource::Size::Large) << "\", sizes: \"512x512\", type: \"image/jpeg\" }," - << " ]" - << "};"; - oss << "LMS.mediaplayer.loadTrack(params, " << (play ? "true" : "false") << ")"; // true to autoplay + oss + << "var params = {" + << " trackId :\"" << trackId.toString() << "\"," + << " nativeResource: \"" << nativeResource << "\"," + << " transcodingResource: \"" << transcodingResource << "\"," + << " duration: " << std::chrono::duration_cast(track->getDuration()).count() << "," + << " replayGain: " << replayGain << "," + << " title: \"" << StringUtils::jsEscape(track->getName()) << "\"," + << " artist: \"" << (!artists.empty() ? StringUtils::jsEscape(artists.front()->getName()) : "") << "\"," + << " release: \"" << (track->getRelease() ? StringUtils::jsEscape(track->getRelease()->getName()) : "") << "\"," + << " artwork: [" + << " { src: \"" << LmsApp->getCoverResource()->getTrackUrl(trackId, CoverResource::Size::Small) << "\", sizes: \"128x128\", type: \"image/jpeg\" }," + << " { src: \"" << LmsApp->getCoverResource()->getTrackUrl(trackId, CoverResource::Size::Large) << "\", sizes: \"512x512\", type: \"image/jpeg\" }," + << " ]" + << "};"; + // Update 'sizes' above to match this: + static_assert(static_cast>(CoverResource::Size::Small) == 128); + static_assert(static_cast>(CoverResource::Size::Large) == 512); + oss << "LMS.mediaplayer.loadTrack(params, " << (play ? "true" : "false") << ")"; // true to autoplay - _title->setTextFormat(Wt::TextFormat::Plain); - _title->setText(Wt::WString::fromUTF8(track->getName())); + _title->setTextFormat(Wt::TextFormat::Plain); + _title->setText(Wt::WString::fromUTF8(track->getName())); - bool needSeparator {true}; + bool needSeparator{ true }; - if (!artists.empty()) - { - _artist->setTextFormat(Wt::TextFormat::Plain); - _artist->setText(Wt::WString::fromUTF8(artists.front()->getName())); - _artist->setLink(Utils::createArtistLink(artists.front())); - } - else - { - _artist->setText(""); - _artist->setLink({}); - needSeparator = false; - } + if (!artists.empty()) + { + _artist->setTextFormat(Wt::TextFormat::Plain); + _artist->setText(Wt::WString::fromUTF8(artists.front()->getName())); + _artist->setLink(Utils::createArtistLink(artists.front())); + } + else + { + _artist->setText(""); + _artist->setLink({}); + needSeparator = false; + } - if (track->getRelease()) - { - _release->setTextFormat(Wt::TextFormat::Plain); - _release->setText(Wt::WString::fromUTF8(track->getRelease()->getName())); - _release->setLink(Utils::createReleaseLink(track->getRelease())); - } - else - { - _release->setText(""); - _release->setLink({}); - needSeparator = false; - } + if (track->getRelease()) + { + _release->setTextFormat(Wt::TextFormat::Plain); + _release->setText(Wt::WString::fromUTF8(track->getRelease()->getName())); + _release->setLink(Utils::createReleaseLink(track->getRelease())); + } + else + { + _release->setText(""); + _release->setLink({}); + needSeparator = false; + } - if (needSeparator) - _separator->setText(" — "); - else - _separator->setText(""); - } + if (needSeparator) + _separator->setText(" — "); + else + _separator->setText(""); + } - LMS_LOG(UI, DEBUG) << "Running js = '" << oss.str() << "'"; - doJavaScript(oss.str()); + LMS_LOG(UI, DEBUG) << "Running js = '" << oss.str() << "'"; + doJavaScript(oss.str()); - _trackIdLoaded = trackId; - trackLoaded.emit(*_trackIdLoaded); -} + _trackIdLoaded = trackId; + trackLoaded.emit(*_trackIdLoaded); + } -void -MediaPlayer::stop() -{ - doJavaScript("LMS.mediaplayer.stop()"); -} + void MediaPlayer::stop() + { + doJavaScript("LMS.mediaplayer.stop()"); + } -void -MediaPlayer::setSettings(const Settings& settings) -{ - _settings = settings; + void MediaPlayer::setSettings(const Settings& settings) + { + _settings = settings; - { - std::ostringstream oss; - oss << "LMS.mediaplayer.setSettings(settings = " << settingsToJSString(settings) << ")"; + { + std::ostringstream oss; + oss << "LMS.mediaplayer.setSettings(settings = " << settingsToJSString(settings) << ")"; - LMS_LOG(UI, DEBUG) << "Running js = '" << oss.str() << "'"; - doJavaScript(oss.str()); - } -} + LMS_LOG(UI, DEBUG) << "Running js = '" << oss.str() << "'"; + doJavaScript(oss.str()); + } + } -void -MediaPlayer::onPlayQueueUpdated(std::size_t trackCount) -{ - _playQueue->setText(Wt::WString::tr("Lms.MediaPlayer.template.playqueue-btn").arg(trackCount)); -} + void MediaPlayer::onPlayQueueUpdated(std::size_t trackCount) + { + _playQueue->setText(Wt::WString::tr("Lms.MediaPlayer.template.playqueue-btn").arg(trackCount)); + } } // namespace UserInterface - diff --git a/src/lms/ui/MediaPlayer.hpp b/src/lms/ui/MediaPlayer.hpp index 544a307e..840398aa 100644 --- a/src/lms/ui/MediaPlayer.hpp +++ b/src/lms/ui/MediaPlayer.hpp @@ -19,118 +19,116 @@ #pragma once -#include #include #include #include +#include #include #include #include "services/database/TrackId.hpp" #include "services/database/Types.hpp" -namespace UserInterface { - -class AudioFileResource; -class AudioTranscodeResource; - -class MediaPlayer : public Wt::WTemplate +namespace UserInterface { - public: - using Bitrate = Database::Bitrate; - using Format = Database::AudioFormat; - using Gain = float; + class AudioFileResource; + class AudioTranscodingResource; - // Do not change enum values as they may be stored locally in browser - // Keep it sync with LMS.mediaplayer js + class MediaPlayer : public Wt::WTemplate + { + public: + using Bitrate = Database::Bitrate; + using Format = Database::TranscodingOutputFormat; + using Gain = float; - struct Settings - { - struct Transcode - { - enum class Mode - { - Never = 0, - Always = 1, - IfFormatNotSupported = 2, - }; - static inline constexpr Mode defaultMode {Mode::IfFormatNotSupported}; - static inline constexpr Format defaultFormat {Format::OGG_OPUS}; - static inline constexpr Bitrate defaultBitrate {128000}; + // Do not change enum values as they may be stored locally in browser + // Keep it sync with LMS.mediaplayer js - Mode mode {defaultMode}; - Format format {defaultFormat}; - Bitrate bitrate {defaultBitrate}; - }; + struct Settings + { + struct Transcoding + { + enum class Mode + { + Never = 0, + Always = 1, + IfFormatNotSupported = 2, + }; + static inline constexpr Mode defaultMode{ Mode::IfFormatNotSupported }; + static inline constexpr Format defaultFormat{ Format::OGG_OPUS }; + static inline constexpr Bitrate defaultBitrate{ 128000 }; - struct ReplayGain - { - enum class Mode - { - None = 0, - Auto = 1, - Track = 2, - Release = 3, - }; + Mode mode{ defaultMode }; + Format format{ defaultFormat }; + Bitrate bitrate{ defaultBitrate }; + }; - static inline constexpr Mode defaultMode {Mode::None}; - static inline constexpr Gain defaultPreAmpGain {}; - static inline constexpr Gain minPreAmpGain {-15}; - static inline constexpr Gain maxPreAmpGain {15}; + struct ReplayGain + { + enum class Mode + { + None = 0, + Auto = 1, + Track = 2, + Release = 3, + }; - Mode mode {defaultMode}; - Gain preAmpGain {defaultPreAmpGain}; - Gain preAmpGainIfNoInfo {defaultPreAmpGain}; - }; + static inline constexpr Mode defaultMode{ Mode::None }; + static inline constexpr Gain defaultPreAmpGain{}; + static inline constexpr Gain minPreAmpGain{ -15 }; + static inline constexpr Gain maxPreAmpGain{ 15 }; - Transcode transcode; - ReplayGain replayGain; - }; + Mode mode{ defaultMode }; + Gain preAmpGain{ defaultPreAmpGain }; + Gain preAmpGainIfNoInfo{ defaultPreAmpGain }; + }; - MediaPlayer(); + Transcoding transcoding; + ReplayGain replayGain; + }; - MediaPlayer(const MediaPlayer&) = delete; - MediaPlayer(MediaPlayer&&) = delete; - MediaPlayer& operator=(const MediaPlayer&) = delete; - MediaPlayer& operator=(MediaPlayer&&) = delete; + MediaPlayer(); + ~MediaPlayer() = default; + MediaPlayer(const MediaPlayer&) = delete; + MediaPlayer& operator=(const MediaPlayer&) = delete; - std::optional getTrackLoaded() const { return _trackIdLoaded; } + std::optional getTrackLoaded() const { return _trackIdLoaded; } - void loadTrack(Database::TrackId trackId, bool play, float replayGain); - void stop(); + void loadTrack(Database::TrackId trackId, bool play, float replayGain); + void stop(); - std::optional getSettings() const { return _settings; } - void setSettings(const Settings& settings); + std::optional getSettings() const { return _settings; } + void setSettings(const Settings& settings); - void onPlayQueueUpdated(std::size_t trackCount); + void onPlayQueueUpdated(std::size_t trackCount); - // Signals - Wt::JSignal<> playPrevious; - Wt::JSignal<> playNext; - Wt::Signal trackLoaded; - Wt::Signal<> settingsLoaded; + // Signals + Wt::JSignal<> playPrevious; + Wt::JSignal<> playNext; + Wt::Signal trackLoaded; + Wt::Signal<> settingsLoaded; - Wt::JSignal scrobbleListenNow; - Wt::JSignal scrobbleListenFinished; + Wt::JSignal scrobbleListenNow; + Wt::JSignal scrobbleListenFinished; - Wt::JSignal<> playbackEnded; + Wt::JSignal<> playbackEnded; - private: - std::unique_ptr _audioFileResource; - std::unique_ptr _audioTranscodeResource; + private: + std::unique_ptr _audioFileResource; + std::unique_ptr _audioTranscodingResource; - std::optional _trackIdLoaded; - std::optional _settings; + std::optional _trackIdLoaded; + std::optional _settings; - Wt::JSignal _settingsLoaded; + Wt::JSignal _settingsLoaded; - Wt::WText* _title {}; - Wt::WAnchor* _release {}; - Wt::WText* _separator {}; - Wt::WAnchor* _artist {}; - Wt::WPushButton* _playQueue {}; -}; + Wt::WText* _title{}; + Wt::WAnchor* _release{}; + Wt::WText* _separator{}; + Wt::WAnchor* _artist{}; + Wt::WPushButton* _playQueue{}; + }; } // namespace UserInterface diff --git a/src/lms/ui/PlayQueue.cpp b/src/lms/ui/PlayQueue.cpp index fab4cc70..bb414679 100644 --- a/src/lms/ui/PlayQueue.cpp +++ b/src/lms/ui/PlayQueue.cpp @@ -799,8 +799,9 @@ namespace UserInterface params.setDistinct(false); params.setSortMethod(TrackSortMethod::TrackList); - const auto tracks{ Track::find(session, params) }; - for (const Track::pointer& track : tracks.results) - session.create(track, trackList); + Track::find(session, params, [&](const Track::pointer& track) + { + session.create(track, trackList); + }); } } // namespace UserInterface diff --git a/src/lms/ui/SettingsView.cpp b/src/lms/ui/SettingsView.cpp index 03224b66..2c311077 100644 --- a/src/lms/ui/SettingsView.cpp +++ b/src/lms/ui/SettingsView.cpp @@ -44,23 +44,24 @@ #include "LmsApplication.hpp" #include "MediaPlayer.hpp" -namespace UserInterface { - +namespace UserInterface +{ using namespace Database; class SettingsModel : public Wt::WFormModel { public: // Associate each field with a unique string literal. - static inline const Field TranscodeModeField{ "transcode-mode" }; - static inline const Field TranscodeFormatField{ "transcode-format" }; - static inline const Field TranscodeBitrateField{ "transcode-bitrate" }; + static inline const Field TranscodingModeField{ "transcoding-mode" }; + static inline const Field TranscodeFormatField{ "transcoding-output-format" }; + static inline const Field TranscodeBitrateField{ "transcoding-output-bitrate" }; static inline const Field ReplayGainModeField{ "replaygain-mode" }; static inline const Field ReplayGainPreAmpGainField{ "replaygain-preamp" }; static inline const Field ReplayGainPreAmpGainIfNoInfoField{ "replaygain-preamp-no-rg-info" }; + static inline const Field SubsonicEnableTranscodingByDefault{ "subsonic-enable-transcoding-by-default" }; static inline const Field SubsonicArtistListModeField{ "subsonic-artist-list-mode" }; - static inline const Field SubsonicTranscodeFormatField{ "subsonic-transcode-format" }; - static inline const Field SubsonicTranscodeBitrateField{ "subsonic-transcode-bitrate" }; + static inline const Field SubsonicTranscodingOutputFormatField{ "subsonic-transcoding-output-format" }; + static inline const Field SubsonicTranscodingOutputBitrateField{ "subsonic-transcoding-output-bitrate" }; static inline const Field FeedbackBackendField{ "feedback-backend" }; static inline const Field ScrobblingBackendField{ "scrobbling-backend" }; static inline const Field ListenBrainzTokenField{ "listenbrainz-token" }; @@ -68,7 +69,7 @@ namespace UserInterface { static inline const Field PasswordField{ "password" }; static inline const Field PasswordConfirmField{ "password-confirm" }; - using TranscodeModeModel = ValueStringModel; + using TranscodingModeModel = ValueStringModel; using ReplayGainModeModel = ValueStringModel; using FeedbackBackendModel = ValueStringModel; using ScrobblingBackendModel = ValueStringModel; @@ -79,14 +80,15 @@ namespace UserInterface { { initializeModels(); - addField(TranscodeModeField); + addField(TranscodingModeField); addField(TranscodeBitrateField); addField(TranscodeFormatField); addField(ReplayGainModeField); addField(ReplayGainPreAmpGainField); addField(ReplayGainPreAmpGainIfNoInfoField); - addField(SubsonicTranscodeBitrateField); - addField(SubsonicTranscodeFormatField); + addField(SubsonicEnableTranscodingByDefault); + addField(SubsonicTranscodingOutputBitrateField); + addField(SubsonicTranscodingOutputFormatField); addField(FeedbackBackendField); addField(ScrobblingBackendField); addField(ListenBrainzTokenField); @@ -105,7 +107,7 @@ namespace UserInterface { addField(PasswordConfirmField); } - setValidator(TranscodeModeField, createMandatoryValidator()); + setValidator(TranscodingModeField, createMandatoryValidator()); setValidator(TranscodeBitrateField, createMandatoryValidator()); setValidator(TranscodeFormatField, createMandatoryValidator()); setValidator(ReplayGainModeField, createMandatoryValidator()); @@ -116,15 +118,15 @@ namespace UserInterface { setValidator(ReplayGainPreAmpGainField, createPreAmpValidator()); setValidator(ReplayGainPreAmpGainIfNoInfoField, createPreAmpValidator()); - setValidator(SubsonicTranscodeBitrateField, createMandatoryValidator()); - setValidator(SubsonicTranscodeFormatField, createMandatoryValidator()); + setValidator(SubsonicTranscodingOutputBitrateField, createMandatoryValidator()); + setValidator(SubsonicTranscodingOutputFormatField, createMandatoryValidator()); loadData(); } - std::shared_ptr getTranscodeModeModel() { return _transcodeModeModel; } - std::shared_ptr getTranscodeBitrateModel() { return _transcodeBitrateModel; } - std::shared_ptr getTranscodeFormatModel() { return _transcodeFormatModel; } + std::shared_ptr getTranscodingModeModel() { return _transcodingModeModeModel; } + std::shared_ptr getTranscodingOutputBitrateModel() { return _transcodingOutputBitrateModel; } + std::shared_ptr getTranscodingOutputFormatModel() { return _transcodingOutputFormatModel; } std::shared_ptr getReplayGainModeModel() { return _replayGainModeModel; } std::shared_ptr getSubsonicArtistListModeModel() { return _subsonicArtistListModeModel; } std::shared_ptr getFeedbackBackendModel() { return _feedbackBackendModel; } @@ -139,17 +141,17 @@ namespace UserInterface { { MediaPlayer::Settings settings; - auto transcodeModeRow{ _transcodeModeModel->getRowFromString(valueText(TranscodeModeField)) }; - if (transcodeModeRow) - settings.transcode.mode = _transcodeModeModel->getValue(*transcodeModeRow); + auto transcodingModeRow{ _transcodingModeModeModel->getRowFromString(valueText(TranscodingModeField)) }; + if (transcodingModeRow) + settings.transcoding.mode = _transcodingModeModeModel->getValue(*transcodingModeRow); - auto transcodeFormatRow{ _transcodeFormatModel->getRowFromString(valueText(TranscodeFormatField)) }; - if (transcodeFormatRow) - settings.transcode.format = _transcodeFormatModel->getValue(*transcodeFormatRow); + auto transcodingOutputFormatRow{ _transcodingOutputFormatModel->getRowFromString(valueText(TranscodeFormatField)) }; + if (transcodingOutputFormatRow) + settings.transcoding.format = _transcodingOutputFormatModel->getValue(*transcodingOutputFormatRow); - auto transcodeBitrateRow{ _transcodeBitrateModel->getRowFromString(valueText(TranscodeBitrateField)) }; - if (transcodeBitrateRow) - settings.transcode.bitrate = _transcodeBitrateModel->getValue(*transcodeBitrateRow); + auto transcodingOutputBitrateRow{ _transcodingOutputBitrateModel->getRowFromString(valueText(TranscodeBitrateField)) }; + if (transcodingOutputBitrateRow) + settings.transcoding.bitrate = _transcodingOutputBitrateModel->getValue(*transcodingOutputBitrateRow); auto replayGainModeRow{ _replayGainModeModel->getRowFromString(valueText(ReplayGainModeField)) }; if (replayGainModeRow) @@ -162,18 +164,20 @@ namespace UserInterface { } { - auto subsonicTranscodeBitrateRow{ _transcodeBitrateModel->getRowFromString(valueText(SubsonicTranscodeBitrateField)) }; - if (subsonicTranscodeBitrateRow) - user.modify()->setSubsonicDefaultTranscodeBitrate(_transcodeBitrateModel->getValue(*subsonicTranscodeBitrateRow)); + bool subsonicEnableTranscodingByDefault{ Wt::asNumber(value(SubsonicEnableTranscodingByDefault)) != 0 }; + user.modify()->setSubsonicEnableTranscodingByDefault(subsonicEnableTranscodingByDefault); - auto subsonicTranscodeFormatRow{ _transcodeFormatModel->getRowFromString(valueText(SubsonicTranscodeFormatField)) }; - if (subsonicTranscodeFormatRow) - user.modify()->setSubsonicDefaultTranscodeFormat(_transcodeFormatModel->getValue(*subsonicTranscodeFormatRow)); + auto subsonicTranscodingOutputBitrateRow{ _transcodingOutputBitrateModel->getRowFromString(valueText(SubsonicTranscodingOutputBitrateField)) }; + if (subsonicTranscodingOutputBitrateRow) + user.modify()->setSubsonicDefaultTranscodingOutputBitrate(_transcodingOutputBitrateModel->getValue(*subsonicTranscodingOutputBitrateRow)); + + auto subsonicTranscodingOutputFormatRow{ _transcodingOutputFormatModel->getRowFromString(valueText(SubsonicTranscodingOutputFormatField)) }; + if (subsonicTranscodingOutputFormatRow) + user.modify()->setSubsonicDefaultTranscodintOutputFormat(_transcodingOutputFormatModel->getValue(*subsonicTranscodingOutputFormatRow)); auto subsonicArtistListModeRow{ _subsonicArtistListModeModel->getRowFromString(valueText(SubsonicArtistListModeField)) }; if (subsonicArtistListModeRow) user.modify()->setSubsonicArtistListMode(_subsonicArtistListModeModel->getValue(*subsonicArtistListModeRow)); - } { @@ -205,22 +209,22 @@ namespace UserInterface { { const auto settings{ *LmsApp->getMediaPlayer().getSettings() }; - auto transcodeModeRow{ _transcodeModeModel->getRowFromValue(settings.transcode.mode) }; - if (transcodeModeRow) - setValue(TranscodeModeField, _transcodeModeModel->getString(*transcodeModeRow)); + auto transcodingModeRow{ _transcodingModeModeModel->getRowFromValue(settings.transcoding.mode) }; + if (transcodingModeRow) + setValue(TranscodingModeField, _transcodingModeModeModel->getString(*transcodingModeRow)); - auto transcodeFormatRow{ _transcodeFormatModel->getRowFromValue(settings.transcode.format) }; - if (transcodeFormatRow) - setValue(TranscodeFormatField, _transcodeFormatModel->getString(*transcodeFormatRow)); + auto transcodingOutputFormatRow{ _transcodingOutputFormatModel->getRowFromValue(settings.transcoding.format) }; + if (transcodingOutputFormatRow) + setValue(TranscodeFormatField, _transcodingOutputFormatModel->getString(*transcodingOutputFormatRow)); - auto transcodeBitrateRow{ _transcodeBitrateModel->getRowFromValue(settings.transcode.bitrate) }; - if (transcodeBitrateRow) - setValue(TranscodeBitrateField, _transcodeBitrateModel->getString(*transcodeBitrateRow)); + auto transcodingOutputBitrateRow{ _transcodingOutputBitrateModel->getRowFromValue(settings.transcoding.bitrate) }; + if (transcodingOutputBitrateRow) + setValue(TranscodeBitrateField, _transcodingOutputBitrateModel->getString(*transcodingOutputBitrateRow)); { - const bool usesTranscode{ settings.transcode.mode != MediaPlayer::Settings::Transcode::Mode::Never }; - setReadOnly(SettingsModel::TranscodeFormatField, !usesTranscode); - setReadOnly(SettingsModel::TranscodeBitrateField, !usesTranscode); + const bool useTranscoding{ settings.transcoding.mode != MediaPlayer::Settings::Transcoding::Mode::Never }; + setReadOnly(SettingsModel::TranscodeFormatField, !useTranscoding); + setReadOnly(SettingsModel::TranscodeBitrateField, !useTranscoding); } auto replayGainModeRow{ _replayGainModeModel->getRowFromValue(settings.replayGain.mode) }; @@ -232,13 +236,15 @@ namespace UserInterface { } { - auto subsonicTranscodeBitrateRow{ _transcodeBitrateModel->getRowFromValue(user->getSubsonicDefaultTranscodeBitrate()) }; - if (subsonicTranscodeBitrateRow) - setValue(SubsonicTranscodeBitrateField, _transcodeBitrateModel->getString(*subsonicTranscodeBitrateRow)); + setValue(SubsonicEnableTranscodingByDefault, user->getSubsonicEnableTranscodingByDefault()); - auto subsonicTranscodeFormatRow{ _transcodeFormatModel->getRowFromValue(user->getSubsonicDefaultTranscodeFormat()) }; - if (subsonicTranscodeFormatRow) - setValue(SubsonicTranscodeFormatField, _transcodeFormatModel->getString(*subsonicTranscodeFormatRow)); + auto subsonicTranscodingOutputBitrateRow{ _transcodingOutputBitrateModel->getRowFromValue(user->getSubsonicDefaultTranscodingOutputBitrate()) }; + if (subsonicTranscodingOutputBitrateRow) + setValue(SubsonicTranscodingOutputBitrateField, _transcodingOutputBitrateModel->getString(*subsonicTranscodingOutputBitrateRow)); + + auto subsonicTranscodingOutputFormatRow{ _transcodingOutputFormatModel->getRowFromValue(user->getSubsonicDefaultTranscodingOutputFormat()) }; + if (subsonicTranscodingOutputFormatRow) + setValue(SubsonicTranscodingOutputFormatField, _transcodingOutputFormatModel->getString(*subsonicTranscodingOutputFormatRow)); auto subsonicArtistListModeRow{ _subsonicArtistListModeModel->getRowFromValue(user->getSubsonicArtistListMode()) }; if (subsonicArtistListModeRow) @@ -259,7 +265,7 @@ namespace UserInterface { } { - const bool usesListenBrainz{ user->getScrobblingBackend() == ScrobblingBackend::ListenBrainz || user->getFeedbackBackend() == FeedbackBackend::ListenBrainz}; + const bool usesListenBrainz{ user->getScrobblingBackend() == ScrobblingBackend::ListenBrainz || user->getFeedbackBackend() == FeedbackBackend::ListenBrainz }; setReadOnly(SettingsModel::ListenBrainzTokenField, !usesListenBrainz); validator(SettingsModel::ListenBrainzTokenField)->setMandatory(usesListenBrainz); } @@ -315,23 +321,23 @@ namespace UserInterface { void initializeModels() { - _transcodeModeModel = std::make_shared(); - _transcodeModeModel->add(Wt::WString::tr("Lms.Settings.transcode-mode.always"), MediaPlayer::Settings::Transcode::Mode::Always); - _transcodeModeModel->add(Wt::WString::tr("Lms.Settings.transcode-mode.never"), MediaPlayer::Settings::Transcode::Mode::Never); - _transcodeModeModel->add(Wt::WString::tr("Lms.Settings.transcode-mode.if-format-not-supported"), MediaPlayer::Settings::Transcode::Mode::IfFormatNotSupported); + _transcodingModeModeModel = std::make_shared(); + _transcodingModeModeModel->add(Wt::WString::tr("Lms.Settings.transcoding-mode.always"), MediaPlayer::Settings::Transcoding::Mode::Always); + _transcodingModeModeModel->add(Wt::WString::tr("Lms.Settings.transcoding-mode.never"), MediaPlayer::Settings::Transcoding::Mode::Never); + _transcodingModeModeModel->add(Wt::WString::tr("Lms.Settings.transcoding-mode.if-format-not-supported"), MediaPlayer::Settings::Transcoding::Mode::IfFormatNotSupported); - _transcodeBitrateModel = std::make_shared>(); + _transcodingOutputBitrateModel = std::make_shared>(); visitAllowedAudioBitrates([&](const Bitrate bitrate) { - _transcodeBitrateModel->add(Wt::WString::fromUTF8(std::to_string(bitrate / 1000)), bitrate); + _transcodingOutputBitrateModel->add(Wt::WString::fromUTF8(std::to_string(bitrate / 1000)), bitrate); }); - _transcodeFormatModel = std::make_shared>(); - _transcodeFormatModel->add(Wt::WString::tr("Lms.Settings.transcode-format.mp3"), AudioFormat::MP3); - _transcodeFormatModel->add(Wt::WString::tr("Lms.Settings.transcode-format.ogg_opus"), AudioFormat::OGG_OPUS); - _transcodeFormatModel->add(Wt::WString::tr("Lms.Settings.transcode-format.matroska_opus"), AudioFormat::MATROSKA_OPUS); - _transcodeFormatModel->add(Wt::WString::tr("Lms.Settings.transcode-format.ogg_vorbis"), AudioFormat::OGG_VORBIS); - _transcodeFormatModel->add(Wt::WString::tr("Lms.Settings.transcode-format.webm_vorbis"), AudioFormat::WEBM_VORBIS); + _transcodingOutputFormatModel = std::make_shared>(); + _transcodingOutputFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding-output-format.mp3"), TranscodingOutputFormat::MP3); + _transcodingOutputFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding-output-format.ogg_opus"), TranscodingOutputFormat::OGG_OPUS); + _transcodingOutputFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding-output-format.matroska_opus"), TranscodingOutputFormat::MATROSKA_OPUS); + _transcodingOutputFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding-output-format.ogg_vorbis"), TranscodingOutputFormat::OGG_VORBIS); + _transcodingOutputFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding-output-format.webm_vorbis"), TranscodingOutputFormat::WEBM_VORBIS); _replayGainModeModel = std::make_shared(); _replayGainModeModel->add(Wt::WString::tr("Lms.Settings.replaygain-mode.none"), MediaPlayer::Settings::ReplayGain::Mode::None); @@ -356,13 +362,13 @@ namespace UserInterface { ::Auth::IPasswordService* _authPasswordService{}; bool _withOldPassword{}; - std::shared_ptr _transcodeModeModel; - std::shared_ptr> _transcodeBitrateModel; - std::shared_ptr> _transcodeFormatModel; - std::shared_ptr _replayGainModeModel; - std::shared_ptr> _subsonicArtistListModeModel; - std::shared_ptr _feedbackBackendModel; - std::shared_ptr _scrobblingBackendModel; + std::shared_ptr _transcodingModeModeModel; + std::shared_ptr> _transcodingOutputBitrateModel; + std::shared_ptr> _transcodingOutputFormatModel; + std::shared_ptr _replayGainModeModel; + std::shared_ptr> _subsonicArtistListModeModel; + std::shared_ptr _feedbackBackendModel; + std::shared_ptr _scrobblingBackendModel; }; SettingsView::SettingsView() @@ -431,24 +437,24 @@ namespace UserInterface { // Audio { // Transcode - auto transcodeMode{ std::make_unique() }; - auto* transcodeModeRaw{ transcodeMode.get() }; - transcodeMode->setModel(model->getTranscodeModeModel()); - t->setFormWidget(SettingsModel::TranscodeModeField, std::move(transcodeMode)); + auto transcodingMode{ std::make_unique() }; + auto* transcodingModeRaw{ transcodingMode.get() }; + transcodingMode->setModel(model->getTranscodingModeModel()); + t->setFormWidget(SettingsModel::TranscodingModeField, std::move(transcodingMode)); // Format - auto transcodeFormat{ std::make_unique() }; - transcodeFormat->setModel(model->getTranscodeFormatModel()); - t->setFormWidget(SettingsModel::TranscodeFormatField, std::move(transcodeFormat)); + auto transcodingOutputFormat{ std::make_unique() }; + transcodingOutputFormat->setModel(model->getTranscodingOutputFormatModel()); + t->setFormWidget(SettingsModel::TranscodeFormatField, std::move(transcodingOutputFormat)); // Bitrate - auto transcodeBitrate{ std::make_unique() }; - transcodeBitrate->setModel(model->getTranscodeBitrateModel()); - t->setFormWidget(SettingsModel::TranscodeBitrateField, std::move(transcodeBitrate)); + auto transcodingOutputBitrate{ std::make_unique() }; + transcodingOutputBitrate->setModel(model->getTranscodingOutputBitrateModel()); + t->setFormWidget(SettingsModel::TranscodeBitrateField, std::move(transcodingOutputBitrate)); - transcodeModeRaw->activated().connect([=](int row) + transcodingModeRaw->activated().connect([=](int row) { - const bool enable{ model->getTranscodeModeModel()->getValue(row) != MediaPlayer::Settings::Transcode::Mode::Never }; + const bool enable{ model->getTranscodingModeModel()->getValue(row) != MediaPlayer::Settings::Transcoding::Mode::Never }; model->setReadOnly(SettingsModel::TranscodeFormatField, !enable); model->setReadOnly(SettingsModel::TranscodeBitrateField, !enable); t->updateModel(model.get()); @@ -490,15 +496,18 @@ namespace UserInterface { { t->setCondition("if-has-subsonic-api", Service::get()->getBool("api-subsonic", true)); + // Enable transcoding by default + t->setFormWidget(SettingsModel::SubsonicEnableTranscodingByDefault, std::make_unique()); + // Format - auto transcodeFormat{ std::make_unique() }; - transcodeFormat->setModel(model->getTranscodeFormatModel()); - t->setFormWidget(SettingsModel::SubsonicTranscodeFormatField, std::move(transcodeFormat)); + auto transcodingOutputFormat{ std::make_unique() }; + transcodingOutputFormat->setModel(model->getTranscodingOutputFormatModel()); + t->setFormWidget(SettingsModel::SubsonicTranscodingOutputFormatField, std::move(transcodingOutputFormat)); // Bitrate - auto transcodeBitrate{ std::make_unique() }; - transcodeBitrate->setModel(model->getTranscodeBitrateModel()); - t->setFormWidget(SettingsModel::SubsonicTranscodeBitrateField, std::move(transcodeBitrate)); + auto transcodingOutputBitrate{ std::make_unique() }; + transcodingOutputBitrate->setModel(model->getTranscodingOutputBitrateModel()); + t->setFormWidget(SettingsModel::SubsonicTranscodingOutputBitrateField, std::move(transcodingOutputBitrate)); // Artist list mode auto artistListMode{ std::make_unique() }; diff --git a/src/lms/ui/Utils.cpp b/src/lms/ui/Utils.cpp index 12c5fb93..c18f53de 100644 --- a/src/lms/ui/Utils.cpp +++ b/src/lms/ui/Utils.cpp @@ -107,7 +107,7 @@ namespace UserInterface::Utils } }; const std::string styleClass{ getStyleClass(cluster) }; - auto res{ std::make_unique(std::string {} + (canDelete ? " " : "") + Wt::WString::fromUTF8(cluster->getName()), Wt::TextFormat::UnsafeXHTML) }; + auto res{ std::make_unique(std::string {} + (canDelete ? " " : "") + Wt::WString::fromUTF8(std::string{ cluster->getName() }), Wt::TextFormat::UnsafeXHTML) }; res->setStyleClass("Lms-badge-cluster badge me-1 " + styleClass); // HACK res->setToolTip(cluster->getType()->getName(), Wt::TextFormat::Plain); diff --git a/src/lms/ui/admin/DatabaseSettingsView.cpp b/src/lms/ui/admin/DatabaseSettingsView.cpp index 38d9877e..97db0680 100644 --- a/src/lms/ui/admin/DatabaseSettingsView.cpp +++ b/src/lms/ui/admin/DatabaseSettingsView.cpp @@ -29,6 +29,7 @@ #include "services/database/Cluster.hpp" #include "services/database/ScanSettings.hpp" #include "services/database/Session.hpp" +#include "services/recommendation/IRecommendationService.hpp" #include "services/scanner/IScannerService.hpp" #include "utils/Logger.hpp" #include "utils/Service.hpp" @@ -232,6 +233,7 @@ DatabaseSettingsView::refreshView() { model->saveData(); + Service::get()->load(); Service::get()->requestImmediateScan(false); LmsApp->notifyMsg(Notification::Type::Info, Wt::WString::tr("Lms.Admin.Database.database"), Wt::WString::tr("Lms.Admin.Database.settings-saved")); } diff --git a/src/lms/ui/admin/ScannerController.cpp b/src/lms/ui/admin/ScannerController.cpp index 4965eac4..cb4ea443 100644 --- a/src/lms/ui/admin/ScannerController.cpp +++ b/src/lms/ui/admin/ScannerController.cpp @@ -265,10 +265,15 @@ ScannerController::refreshContents() .arg(status.currentScanStepStats->totalElems) .arg(status.currentScanStepStats->progress())); break; + case Scanner::ScanStep::ReloadingSimilarityEngine: _stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-reloading-similarity-engine") .arg(status.currentScanStepStats->progress())); break; + + case Scanner::ScanStep::ComputeClusterStats: + _stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-compute-cluster-stats") + .arg(status.currentScanStepStats->progress())); } break; } diff --git a/src/lms/ui/explore/ArtistCollector.cpp b/src/lms/ui/explore/ArtistCollector.cpp index 67d08238..3f5ca7b5 100644 --- a/src/lms/ui/explore/ArtistCollector.cpp +++ b/src/lms/ui/explore/ArtistCollector.cpp @@ -33,16 +33,14 @@ namespace UserInterface { using namespace Database; - RangeResults ArtistCollector::get(Database::Range range) + RangeResults ArtistCollector::get(std::optional requestedRange) { Feedback::IFeedbackService& feedbackService{ *Service::get() }; Scrobbling::IScrobblingService& scrobblingService{ *Service::get() }; - RangeResults artists; + const Range range{ getActualRange(requestedRange) }; - range = getActualRange(range); - if (range.size == 0) - return artists; + RangeResults artists; switch (getMode()) { @@ -51,8 +49,16 @@ namespace UserInterface break; case Mode::Starred: - artists = feedbackService.getStarredArtists(LmsApp->getUserId(), getFilters().getClusterIds(), _linkType, ArtistSortMethod::StarredDateDesc, range); + { + Feedback::IFeedbackService::ArtistFindParameters params; + params.setUser(LmsApp->getUserId()); + params.setClusters(getFilters().getClusterIds()); + params.setLinkType(_linkType); + params.setSortMethod(ArtistSortMethod::StarredDateDesc); + params.setRange(range); + artists = feedbackService.findStarredArtists(params); break; + } case Mode::RecentlyPlayed: artists = scrobblingService.getRecentArtists(LmsApp->getUserId(), getFilters().getClusterIds(), _linkType, range); diff --git a/src/lms/ui/explore/ArtistCollector.hpp b/src/lms/ui/explore/ArtistCollector.hpp index ab9d43fc..2c918ed7 100644 --- a/src/lms/ui/explore/ArtistCollector.hpp +++ b/src/lms/ui/explore/ArtistCollector.hpp @@ -37,7 +37,7 @@ namespace UserInterface public: using DatabaseCollectorBase::DatabaseCollectorBase; - Database::RangeResults get(Database::Range range); + Database::RangeResults get(std::optional range = std::nullopt); void reset() { _randomArtists.reset(); } void setArtistLinkType(std::optional linkType) { _linkType = linkType; } diff --git a/src/lms/ui/explore/DatabaseCollectorBase.cpp b/src/lms/ui/explore/DatabaseCollectorBase.cpp index 16afcc7f..fcb80343 100644 --- a/src/lms/ui/explore/DatabaseCollectorBase.cpp +++ b/src/lms/ui/explore/DatabaseCollectorBase.cpp @@ -30,16 +30,25 @@ namespace UserInterface { } - DatabaseCollectorBase::Range DatabaseCollectorBase::getActualRange(Range range) const + DatabaseCollectorBase::Range DatabaseCollectorBase::getActualRange(std::optional requestedRange) const { - assert(range.size > 0); + Database::Range res; - if (range.offset < _maxCount) - range.size = std::min(_maxCount - range.offset, range.size); + if (!requestedRange) + { + res.offset = 0; + res.size = _maxCount; + } else - range.size = 0; + { + res.offset = requestedRange->offset; + if (requestedRange->offset < _maxCount) + res.size = std::min(_maxCount - requestedRange->offset, requestedRange->size); + else + res.size = 0; + } - return range; + return res; } std::size_t DatabaseCollectorBase::getMaxCount() const diff --git a/src/lms/ui/explore/DatabaseCollectorBase.hpp b/src/lms/ui/explore/DatabaseCollectorBase.hpp index 2d654779..52f3a458 100644 --- a/src/lms/ui/explore/DatabaseCollectorBase.hpp +++ b/src/lms/ui/explore/DatabaseCollectorBase.hpp @@ -56,9 +56,9 @@ namespace UserInterface void setSearch(std::string_view search); protected: - Range getActualRange(Range range) const; + Range getActualRange(std::optional range) const; std::size_t getMaxCount() const; - Filters& getFilters() { return _filters; } + Filters& getFilters() { return _filters; } const std::vector& getSearchKeywords() const { return _searchKeywords; } private: diff --git a/src/lms/ui/explore/Filters.cpp b/src/lms/ui/explore/Filters.cpp index 27d79f43..e6b45457 100644 --- a/src/lms/ui/explore/Filters.cpp +++ b/src/lms/ui/explore/Filters.cpp @@ -84,7 +84,7 @@ Filters::showDialog() { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - const auto clusterTypesIds {ClusterType::findUsed(LmsApp->getDbSession(), Range {})}; + const auto clusterTypesIds {ClusterType::findUsed(LmsApp->getDbSession())}; for (const ClusterTypeId clusterTypeId : clusterTypesIds.results) { const auto clusterType {ClusterType::find(LmsApp->getDbSession(), clusterTypeId)}; @@ -98,7 +98,7 @@ Filters::showDialog() for (const Cluster::pointer& cluster : clusterType->getClusters()) { if (std::find(std::cbegin(_clusterIds), std::cend(_clusterIds), cluster->getId()) == _clusterIds.end()) - valueCombo->addItem(Wt::WString::fromUTF8(cluster->getName())); + valueCombo->addItem(Wt::WString::fromUTF8(std::string{ cluster->getName() })); } } } @@ -115,7 +115,7 @@ Filters::showDialog() for (const Cluster::pointer& cluster : clusterType->getClusters()) { if (std::find(std::cbegin(_clusterIds), std::cend(_clusterIds), cluster->getId()) == _clusterIds.end()) - valueCombo->addItem(Wt::WString::fromUTF8(cluster->getName())); + valueCombo->addItem(Wt::WString::fromUTF8(std::string{ cluster->getName() })); } }); diff --git a/src/lms/ui/explore/PlayQueueController.cpp b/src/lms/ui/explore/PlayQueueController.cpp index 0329b880..74fc6911 100644 --- a/src/lms/ui/explore/PlayQueueController.cpp +++ b/src/lms/ui/explore/PlayQueueController.cpp @@ -45,7 +45,7 @@ namespace UserInterface params.setArtist(artistId); params.setSortMethod(Database::TrackSortMethod::DateDescAndRelease); params.setClusters(clusters); - params.setRange({ 0, maxTrackCount - res.size() }); + params.setRange(Database::Range{ 0, maxTrackCount - res.size() }); const auto tracks{ Database::Track::findIds(session, params) }; @@ -74,7 +74,7 @@ namespace UserInterface params.setRelease(releaseId); params.setSortMethod(Database::TrackSortMethod::Release); params.setClusters(clusters); - params.setRange({ 0, maxTrackCount - res.size() }); + params.setRange(Database::Range{ 0, maxTrackCount - res.size() }); const auto tracks{ Database::Track::findIds(session, params) }; @@ -98,7 +98,7 @@ namespace UserInterface Database::Track::FindParameters params; params.setTrackList(trackListId); params.setClusters(clusters); - params.setRange({ 0, maxTrackCount }); + params.setRange(Database::Range{ 0, maxTrackCount }); params.setSortMethod(TrackSortMethod::TrackList); params.setDistinct(false); diff --git a/src/lms/ui/explore/ReleaseCollector.cpp b/src/lms/ui/explore/ReleaseCollector.cpp index 4a421d4c..c3ae1e15 100644 --- a/src/lms/ui/explore/ReleaseCollector.cpp +++ b/src/lms/ui/explore/ReleaseCollector.cpp @@ -32,15 +32,14 @@ namespace UserInterface { using namespace Database; - RangeResults ReleaseCollector::get(Database::Range range) + RangeResults ReleaseCollector::get(std::optional requestedRange) { Feedback::IFeedbackService& feedbackService{ *Service::get() }; Scrobbling::IScrobblingService& scrobblingService{ *Service::get() }; + const Range range{ getActualRange(requestedRange) }; + RangeResults releases; - range = getActualRange(range); - if (range.size == 0) - return releases; switch (getMode()) { @@ -49,8 +48,14 @@ namespace UserInterface break; case Mode::Starred: - releases = feedbackService.getStarredReleases(LmsApp->getUserId(), getFilters().getClusterIds(), range); + { + Feedback::IFeedbackService::FindParameters params; + params.setUser(LmsApp->getUserId()); + params.setClusters(getFilters().getClusterIds()); + params.setRange(range); + releases = feedbackService.findStarredReleases(params); break; + } case ReleaseCollector::Mode::RecentlyPlayed: releases = scrobblingService.getRecentReleases(LmsApp->getUserId(), getFilters().getClusterIds(), range); @@ -118,7 +123,7 @@ namespace UserInterface Release::FindParameters params; params.setClusters(getFilters().getClusterIds()); params.setSortMethod(ReleaseSortMethod::Random); - params.setRange({ 0, getMaxCount() }); + params.setRange(Range{ 0, getMaxCount() }); { auto transaction{ LmsApp->getDbSession().createSharedTransaction() }; diff --git a/src/lms/ui/explore/ReleaseCollector.hpp b/src/lms/ui/explore/ReleaseCollector.hpp index 924aabe6..1a10c205 100644 --- a/src/lms/ui/explore/ReleaseCollector.hpp +++ b/src/lms/ui/explore/ReleaseCollector.hpp @@ -19,7 +19,7 @@ #pragma once -#include +#include #include "DatabaseCollectorBase.hpp" @@ -39,7 +39,7 @@ namespace UserInterface public: using DatabaseCollectorBase::DatabaseCollectorBase; - Database::RangeResults get(Database::Range range); + Database::RangeResults get(std::optional range = std::nullopt); void reset() { _randomReleases.reset(); } private: diff --git a/src/lms/ui/explore/ReleaseView.cpp b/src/lms/ui/explore/ReleaseView.cpp index 40754fc2..f9f61291 100644 --- a/src/lms/ui/explore/ReleaseView.cpp +++ b/src/lms/ui/explore/ReleaseView.cpp @@ -137,7 +137,7 @@ namespace UserInterface } } - // TODO: save in DB and mean all this + // TODO: save in DB and aggregate all this 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()) }) @@ -146,17 +146,18 @@ namespace UserInterface if (audioStream) { releaseInfo->setCondition("if-has-codec", true); - releaseInfo->bindString("codec", audioStream->codec); - if (audioStream->bitrate) - { - releaseInfo->setCondition("if-has-bitrate", true); - releaseInfo->bindString("bitrate", std::to_string(audioStream->bitrate / 1000) + " kbps"); - break; - } + releaseInfo->bindString("codec", audioStream->codecName); + break; } } } + if (const std::size_t meanBitrate{ release->getMeanBitrate() }) + { + releaseInfo->setCondition("if-has-bitrate", true); + releaseInfo->bindString("bitrate", std::to_string(meanBitrate / 1000) + " kbps"); + } + Wt::WPushButton* okBtn{ releaseInfo->bindNew("ok-btn", Wt::WString::tr("Lms.ok")) }; okBtn->clicked().connect([=] { @@ -373,100 +374,100 @@ namespace UserInterface params.setSortMethod(Database::TrackSortMethod::Release); params.setClusters(_filters.getClusterIds()); - for (const Database::Track::pointer& track : Database::Track::find(LmsApp->getDbSession(), params).results) - { - const Database::TrackId trackId{ track->getId() }; - const auto discNumber{ track->getDiscNumber() }; - - Wt::WContainerWidget* container; - if (isReleaseMultiDisc && discNumber) - container = getOrAddDiscContainer(*discNumber, track->getDiscSubtitle()); - else - container = getOrAddNoDiscContainer(); - - Template* entry{ container->addNew