diff --git a/Dockerfile-release b/Dockerfile-release index 8dd54d03..7a7c0485 100644 --- a/Dockerfile-release +++ b/Dockerfile-release @@ -1,4 +1,4 @@ -FROM alpine:3.19 AS build +FROM alpine:3.20 AS build WORKDIR /tmp/workdir @@ -56,8 +56,8 @@ RUN \ --disable-postproc \ --disable-pixelutils \ --disable-network \ - --enable-shared \ - --disable-static \ + --disable-shared \ + --enable-static \ --enable-gpl \ --enable-small \ --enable-version3 \ @@ -73,13 +73,14 @@ RUN \ --enable-muxer=ogg,matroska,mp3,webm \ --enable-protocol=file,pipe \ --enable-filter=aresample \ + --enable-lto \ --extra-libs=-ldl && \ make -j$(nproc) && \ make install && \ make distclean # WT -ARG WT_VERSION=4.10.4 +ARG WT_VERSION=4.11.1 RUN \ DIR=/tmp/wt && mkdir -p ${DIR} && cd ${DIR} && \ curl -sLO https://github.com/emweb/wt/archive/${WT_VERSION}.tar.gz && \ @@ -87,12 +88,12 @@ RUN \ RUN \ DIR=/tmp/wt && mkdir -p ${DIR} && cd ${DIR} && \ - cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${PREFIX} -DBUILD_EXAMPLES=OFF -DENABLE_LIBWTTEST=OFF -DCONNECTOR_FCGI=OFF -DUSE_SYSTEM_SQLITE3=ON && \ + cmake -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=TRUE -DSHARED_LIBS=OFF -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${PREFIX} -DBUILD_EXAMPLES=OFF -DENABLE_LIBWTTEST=OFF -DCONNECTOR_FCGI=OFF -DUSE_SYSTEM_SQLITE3=ON && \ make -j$(nproc) && \ make install # STB -ARG STB_VERSION=ae721c50eaf761660b4f90cc590453cdb0c2acd0 +ARG STB_VERSION=5c205738c191bcb0abc65c4febfa9bd25ff35234 RUN \ DIR=/tmp/stb && mkdir -p ${DIR} && cd ${DIR} && \ curl -sLO https://github.com/nothings/stb/archive/${STB_VERSION}.tar.gz && \ @@ -101,7 +102,7 @@ RUN \ cp ./*.h ${PREFIX}/include/stb # TAGLIB -ARG TAGLIB_VERSION=v2.0.1 +ARG TAGLIB_VERSION=v2.0.2 RUN \ DIR=/tmp/taglib && mkdir -p ${DIR} && cd ${DIR} && \ curl -sLO https://github.com/taglib/taglib/archive/${TAGLIB_VERSION}.tar.gz && \ @@ -109,7 +110,7 @@ RUN \ RUN \ DIR=/tmp/taglib && mkdir -p ${DIR} && cd ${DIR} && \ - CXXFLAGS="-I/usr/include/utf8cpp" cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${PREFIX} -DBUILD_SHARED_LIBS=ON -DBUILD_EXAMPLES=OFF -DBUILD_BINDINGS=OFF -DBUILD_TESTING=OFF -DTRACE_IN_RELEASE=OFF -DWITH_ZLIB=ON && \ + CXXFLAGS="-I/usr/include/utf8cpp" cmake -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=TRUE -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${PREFIX} -DBUILD_SHARED_LIBS=OFF -DBUILD_EXAMPLES=OFF -DBUILD_BINDINGS=OFF -DBUILD_TESTING=OFF -DTRACE_IN_RELEASE=OFF -DWITH_ZLIB=ON && \ make -j$(nproc) && \ make install @@ -117,7 +118,7 @@ RUN \ COPY . /tmp/lms/ RUN \ DIR=/tmp/lms/build && mkdir -p ${DIR} && cd ${DIR} && \ - PKG_CONFIG_PATH=/tmp/install/lib/pkgconfig CXXFLAGS="-I${PREFIX}/include" LDFLAGS="-L${PREFIX}/lib -Wl,--rpath-link=${PREFIX}/lib" cmake /tmp/lms/ -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${PREFIX} -DCMAKE_PREFIX_PATH=${PREFIX} && \ + PKG_CONFIG_PATH=/tmp/install/lib/pkgconfig CXXFLAGS="-I${PREFIX}/include" LDFLAGS="-L${PREFIX}/lib -Wl,--rpath-link=${PREFIX}/lib" cmake /tmp/lms/ -DCMAKE_BUILD_TYPE=Release -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=TRUE -DCMAKE_INSTALL_PREFIX=${PREFIX} -DCMAKE_PREFIX_PATH=${PREFIX} && \ LD_LIBRARY_PATH=${PREFIX}/lib make -j$(nproc) && \ LD_LIBRARY_PATH=${PREFIX}/lib make test && \ make install && \ @@ -132,11 +133,6 @@ RUN \ strip --strip-all $bin && \ cp $bin /tmp/fakeroot/bin/; \ done && \ - for lib in ${PREFIX}/lib/*.so; \ - do \ - strip --strip-all $lib; \ - done && \ - cp -r ${PREFIX}/lib /tmp/fakeroot/lib && \ cp -r ${PREFIX}/share /tmp/fakeroot/share && \ rm -rf /tmp/fakeroot/share/doc && \ rm -rf /tmp/fakeroot/share/man @@ -147,7 +143,7 @@ RUN \ rm -rf /tmp/fakeroot/share/Wt/resources/themes ## Release Stage -FROM alpine:3.19 AS release +FROM alpine:3.20 AS release LABEL maintainer="Emeric Poupon " ARG RUNTIME_PACKAGES=" \ diff --git a/README.md b/README.md index 31818935..a6dbca37 100644 --- a/README.md +++ b/README.md @@ -63,12 +63,15 @@ $setmulti(albumartistssort,%_albumartists_sort%) _LMS_ supports playlist files in `m3u` and `m3u8` formats. These playlists are synced during the scan process and are available as public shared playlists. ## Lyrics support -_LMS_ supports lyrics in `lrc` files and embedded track metadata. Both synchronized and unsynchronized lyrics are supported. +_LMS_ supports lyrics in `lrc` files, `txt` files, and embedded track metadata. Both synchronized and unsynchronized lyrics are supported. ## Keyboard shortcuts * Play/pause: Space * Previous track: Ctrl + Left * Next track: Ctrl + Right +* Decrease volume: Ctrl + Down +* Increase volume: Ctrl + Up + ## Installation See [INSTALL.md](INSTALL.md) file. diff --git a/approot/admin-scansettings.xml b/approot/admin-scansettings.xml index 17c244af..71339c39 100644 --- a/approot/admin-scansettings.xml +++ b/approot/admin-scansettings.xml @@ -4,7 +4,7 @@ -
+ ${tr:Lms.Admin.Database.scan-settings}
-
- - ${similarity-engine-type class="form-control"} -
- ${similarity-engine-type-info} -
-
+ ${tr:Lms.Admin.Database.tag-parsing}
${default-tag-delimiter-container class="row gy-3"}
+ ${tr:Lms.Admin.Database.misc}
- ${save-btn class="btn btn-primary me-1"}${discard-btn class="btn btn-secondary"} +
+ ${skip-single-release-playlists class="form-check-input"} + +
+ ${skip-single-release-playlists-info} +
+
+
+
+ + ${similarity-engine-type class="form-control"} +
+ ${similarity-engine-type-info} +
+
+ ${save-btn class="btn btn-primary me-1"}${discard-btn class="btn btn-secondary"}
diff --git a/approot/explore.xml b/approot/explore.xml index 21d93199..d3dc2f81 100644 --- a/approot/explore.xml +++ b/approot/explore.xml @@ -63,6 +63,7 @@
  • ${recently-played class="dropdown-item"}
  • ${most-played class="dropdown-item"}
  • ${recently-added class="dropdown-item"}
  • +
  • ${recently-modified class="dropdown-item"}
  • ${all class="dropdown-item"}
  • diff --git a/approot/messages.xml b/approot/messages.xml index 514b5aa7..b913e32e 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -78,6 +78,7 @@ Extra tags to scan Hourly Scan now! +Miscellaneous Monthly Never Scan aborted! @@ -87,7 +88,9 @@ Similarity engine Tag-based None +Skip playlists that contain tracks from the same album The tag delimiter must not consist solely of spaces +Tag parsing Update period Update start time Weekly diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml index e9d8e380..34bb5cda 100644 --- a/approot/messages_fr.xml +++ b/approot/messages_fr.xml @@ -78,6 +78,7 @@ Tags supplémentaires à scanner Toutes les heures Scanner maintenant ! +Divers Tous les mois Jamais Scan interrompu ! @@ -87,7 +88,9 @@ Moteur de similarité Basé sur les tags Aucun +Ignorer les playlists contenant des pistes d'un même album Le délimiteur de tag ne doit pas comporter uniquement des espaces +Analyse des tags Périodicité des mises à jour Heure de départ de la mise à jour Toutes les semaines diff --git a/approot/messages_it.xml b/approot/messages_it.xml index ab11f507..6c18325b 100644 --- a/approot/messages_it.xml +++ b/approot/messages_it.xml @@ -78,6 +78,7 @@ Tag aggiuntivi da scansionare Ogni ora Scansiona ora! +Varie Mensile Mai Scansione annullata! @@ -87,7 +88,9 @@ Motore di similarità Basato su tag Nessuno +Salta le playlist che contengono brani dello stesso album Il delimitatore del tag non deve consistere esclusivamente di spazi +Analisi dei tag Frequenza di aggiornamento Orario di aggiornamento Settimanale diff --git a/approot/messages_pl.xml b/approot/messages_pl.xml index f699761d..5ece9b62 100644 --- a/approot/messages_pl.xml +++ b/approot/messages_pl.xml @@ -79,6 +79,7 @@ Szukaj dodatkowych znaczników Co godzinę Skanuj teraz! +Różne Co miesiąc Nigdy Skanowanie przerwane! @@ -88,7 +89,9 @@ Metoda sprawdzania podobieństwa Oparta o znaczniki Żadna +Pomiń playlisty zawierające utwory z tego samego albumu Rozdzielacz nie może się składać z samych białych znaków +Analiza tagów Okres aktualizacji Czas startu aktualizacji Co tydzień diff --git a/approot/messages_zh.xml b/approot/messages_zh.xml index 70ca3916..de9ebbc1 100644 --- a/approot/messages_zh.xml +++ b/approot/messages_zh.xml @@ -78,6 +78,7 @@ 每小时 立即扫描! + 每月 从不 @@ -88,6 +89,8 @@ + + 更新周期 更新开始时间 每周 diff --git a/approot/settings.xml b/approot/settings.xml index e839a8e1..54644c47 100644 --- a/approot/settings.xml +++ b/approot/settings.xml @@ -209,10 +209,9 @@ ${} -
    - ${save-btn class="btn btn-primary me-1"}${discard-btn class="btn btn-secondary"} -
    +
    + ${save-btn class="btn btn-primary me-1"}${discard-btn class="btn btn-secondary"}
    diff --git a/conf/lms.conf b/conf/lms.conf index d64477a7..eaa1921f 100644 --- a/conf/lms.conf +++ b/conf/lms.conf @@ -1,8 +1,8 @@ # LMS Sample configuration file -# Path to the working directory -# Must have write privileges in order to create and modify this directory -working-dir = "/var/lms/"; +# Path to the working directory where the database and other cached files will be written to. +# Ensure this directory exists and has write permissions for the application +working-dir = "/var/lms"; # ffmpeg location ffmpeg-file = "/usr/bin/ffmpeg"; diff --git a/docroot/js/mediaplayer.js b/docroot/js/mediaplayer.js index f966e446..d926c5aa 100644 --- a/docroot/js/mediaplayer.js +++ b/docroot/js/mediaplayer.js @@ -15,6 +15,9 @@ const Mode = { } Object.freeze(Mode); +// How much to increase / decrease volume when adjusting it with keyboard shortcuts +const volumeStepAmount = 0.05; + LMS.mediaplayer = function () { let _root = {}; let _elems = {}; @@ -199,6 +202,20 @@ LMS.mediaplayer = function () { } } + let _stepVolumeDown = function() { + let currentVolume = _elems.audio.volume; + let remainder = (currentVolume * 10) % (volumeStepAmount * 10); + let newVolume = remainder === 0 ? currentVolume - volumeStepAmount : currentVolume - (remainder / 10); + _setVolume(Math.max(newVolume, 0)); + } + + let _stepVolumeUp = function() { + let currentVolume = _elems.audio.volume; + let remainder = (currentVolume * 10) % (volumeStepAmount * 10); + let newVolume = remainder === 0 ? currentVolume + volumeStepAmount : currentVolume + (volumeStepAmount - (remainder / 10)); + _setVolume(Math.min(newVolume, 1)); + } + let _setReplayGain = function (replayGain) { _gainNode.gain.value = Math.pow(10, (_settings.replayGain.preAmpGain + replayGain) / 20); } @@ -337,6 +354,14 @@ LMS.mediaplayer = function () { _playNext(); handled = true; } + else if (event.ctrlKey && event.keyCode == 40) { + _stepVolumeDown(); + handled = true; + } + else if (event.ctrlKey && event.keyCode == 38) { + _stepVolumeUp(); + handled = true; + } if (handled) event.preventDefault(); diff --git a/src/libs/av/CMakeLists.txt b/src/libs/av/CMakeLists.txt index be2bd834..5c63d843 100644 --- a/src/libs/av/CMakeLists.txt +++ b/src/libs/av/CMakeLists.txt @@ -1,6 +1,6 @@ pkg_check_modules(LIBAV IMPORTED_TARGET libavcodec libavutil libavformat) -add_library(lmsav SHARED +add_library(lmsav STATIC impl/AudioFile.cpp impl/RawResourceHandlerCreator.cpp impl/Transcoder.cpp @@ -26,5 +26,3 @@ target_link_libraries(lmsav PUBLIC target_link_libraries(lmsav PRIVATE PkgConfig::LIBAV ) - -install(TARGETS lmsav DESTINATION ${CMAKE_INSTALL_LIBDIR}) diff --git a/src/libs/av/impl/AudioFile.cpp b/src/libs/av/impl/AudioFile.cpp index 593ebbe4..5432bfc6 100644 --- a/src/libs/av/impl/AudioFile.cpp +++ b/src/libs/av/impl/AudioFile.cpp @@ -130,17 +130,17 @@ namespace lms::av AudioFile::AudioFile(const std::filesystem::path& p) : _p{ p } { - int error{ avformat_open_input(&_context, _p.string().c_str(), nullptr, nullptr) }; + int error{ avformat_open_input(&_context, _p.c_str(), nullptr, nullptr) }; if (error < 0) { - LMS_LOG(AV, ERROR, "Cannot open " << _p.string() << ": " << averror_to_string(error)); + LMS_LOG(AV, ERROR, "Cannot open " << _p << ": " << averror_to_string(error)); throw AudioFileException{ error }; } 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)); + LMS_LOG(AV, ERROR, "Cannot find stream information on " << _p << ": " << averror_to_string(error)); avformat_close_input(&_context); throw AudioFileException{ error }; } @@ -353,7 +353,7 @@ namespace lms::av { ".mka", "audio/x-matroska" }, }; - auto it{ entries.find(core::stringUtils::stringToLower(fileExtension.string())) }; + auto it{ entries.find(core::stringUtils::stringToLower(fileExtension.c_str())) }; if (it == std::cend(entries)) return ""; diff --git a/src/libs/av/impl/Transcoder.cpp b/src/libs/av/impl/Transcoder.cpp index d9c9821f..efc0a304 100644 --- a/src/libs/av/impl/Transcoder.cpp +++ b/src/libs/av/impl/Transcoder.cpp @@ -90,7 +90,7 @@ namespace lms::av::transcoding throw Exception{ "File error '" + _inputParameters.trackPath.string() + "': " + e.what() }; } - LOG(INFO, "Transcoding file '" << _inputParameters.trackPath.string() << "'"); + LOG(INFO, "Transcoding file " << _inputParameters.trackPath); std::vector args; diff --git a/src/libs/core/CMakeLists.txt b/src/libs/core/CMakeLists.txt index 7e22c4be..cb66dd13 100644 --- a/src/libs/core/CMakeLists.txt +++ b/src/libs/core/CMakeLists.txt @@ -1,7 +1,7 @@ pkg_check_modules(Config++ REQUIRED IMPORTED_TARGET libconfig++) pkg_check_modules(Archive REQUIRED IMPORTED_TARGET libarchive) -add_library(lmscore SHARED +add_library(lmscore STATIC impl/http/Client.cpp impl/http/SendQueue.cpp impl/ArchiveZipper.cpp @@ -12,6 +12,7 @@ add_library(lmscore SHARED impl/IOContextRunner.cpp impl/Logger.cpp impl/NetAddress.cpp + impl/PartialDateTime.cpp impl/Path.cpp impl/Random.cpp impl/RecursiveSharedMutex.cpp @@ -41,8 +42,6 @@ target_link_libraries(lmscore PUBLIC Wt::Wt ) -install(TARGETS lmscore DESTINATION ${CMAKE_INSTALL_LIBDIR}) - if(BUILD_TESTING) add_subdirectory(test) endif() diff --git a/src/libs/core/impl/ArchiveZipper.cpp b/src/libs/core/impl/ArchiveZipper.cpp index b677ac2e..909a5b31 100644 --- a/src/libs/core/impl/ArchiveZipper.cpp +++ b/src/libs/core/impl/ArchiveZipper.cpp @@ -238,7 +238,7 @@ namespace lms::zip { assert(_currentEntry != std::cend(_entries)); - std::ifstream ifs{ _currentEntry->filePath.c_str(), std::ios_base::binary }; + std::ifstream ifs{ _currentEntry->filePath, std::ios_base::binary }; if (!ifs) throw FileException{ _currentEntry->filePath, "cannot open file", errno }; diff --git a/src/libs/core/impl/Config.cpp b/src/libs/core/impl/Config.cpp index eef30ad4..c65aa464 100644 --- a/src/libs/core/impl/Config.cpp +++ b/src/libs/core/impl/Config.cpp @@ -32,7 +32,7 @@ namespace lms::core { try { - _config.readFile(p.string().c_str()); + _config.readFile(p.c_str()); } catch (libconfig::FileIOException& e) { diff --git a/src/libs/core/impl/Config.hpp b/src/libs/core/impl/Config.hpp index 03bd4737..c19efa8c 100644 --- a/src/libs/core/impl/Config.hpp +++ b/src/libs/core/impl/Config.hpp @@ -38,12 +38,12 @@ namespace lms::core private: // Default values are returned in case of setting not found - std::string_view getString(std::string_view setting, std::string_view def = "") override; + std::string_view getString(std::string_view setting, std::string_view def) override; void visitStrings(std::string_view setting, std::function _func, std::initializer_list defs) override; - std::filesystem::path getPath(std::string_view setting, const std::filesystem::path& def = std::filesystem::path()) override; - unsigned long getULong(std::string_view setting, unsigned long def = 0) override; - long getLong(std::string_view setting, long def = 0) override; - bool getBool(std::string_view setting, bool def = false) override; + std::filesystem::path getPath(std::string_view setting, const std::filesystem::path& def) override; + unsigned long getULong(std::string_view setting, unsigned long def) override; + long getLong(std::string_view setting, long def) override; + bool getBool(std::string_view setting, bool def) override; libconfig::Config _config; }; diff --git a/src/libs/core/impl/FileResourceHandler.cpp b/src/libs/core/impl/FileResourceHandler.cpp index 49f871c5..130671dc 100644 --- a/src/libs/core/impl/FileResourceHandler.cpp +++ b/src/libs/core/impl/FileResourceHandler.cpp @@ -39,13 +39,13 @@ namespace lms 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 }; + std::ifstream ifs{ _path, std::ios::in | std::ios::binary }; if (startByte == 0) { if (!ifs) { - LMS_LOG(UTILS, ERROR, "Cannot open file stream for '" << _path.string() << "'"); + LMS_LOG(UTILS, ERROR, "Cannot open file stream for " << _path); response.setStatus(404); return {}; } @@ -54,7 +54,7 @@ namespace lms 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 << ", fileSize = " << fileSize); response.addHeader("Accept-Ranges", "bytes"); @@ -99,7 +99,7 @@ namespace lms } else if (!ifs) { - LMS_LOG(UTILS, ERROR, "Cannot reopen file stream for '" << _path.string() << "'"); + LMS_LOG(UTILS, ERROR, "Cannot reopen file stream for " << _path); return {}; } diff --git a/src/libs/core/impl/PartialDateTime.cpp b/src/libs/core/impl/PartialDateTime.cpp new file mode 100644 index 00000000..24173de9 --- /dev/null +++ b/src/libs/core/impl/PartialDateTime.cpp @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2025 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 "core/PartialDateTime.hpp" + +#include +#include +#include + +namespace lms::core +{ + PartialDateTime::PartialDateTime(int year) + : _year{ static_cast(year) } + , _precision{ Precision::Year } + { + } + + PartialDateTime::PartialDateTime(int year, unsigned month) + : _year{ static_cast(year) } + , _month{ static_cast(month) } + , _precision{ Precision::Month } + { + } + + PartialDateTime::PartialDateTime(int year, unsigned month, unsigned day) + : _year{ static_cast(year) } + , _month{ static_cast(month) } + , _day{ static_cast(day) } + , _precision{ Precision::Day } + { + } + + PartialDateTime::PartialDateTime(int year, unsigned month, unsigned day, unsigned hour, unsigned min, unsigned sec) + : _year{ static_cast(year) } + , _month{ static_cast(month) } + , _day{ static_cast(day) } + , _hour{ static_cast(hour) } + , _min{ static_cast(min) } + , _sec{ static_cast(sec) } + , _precision{ Precision::Sec } + { + } + + PartialDateTime PartialDateTime::fromString(std::string_view str) + { + PartialDateTime res; + + const std::string dateTimeStr{ str }; + + static constexpr const char* formats[]{ + "%Y-%m-%dT%H:%M:%S", + "%Y-%m-%d %H:%M:%S", + "%Y/%m/%d %H:%M:%S", + "%Y-%m", + "%Y/%m", + "%Y", + }; + + for (const char* format : formats) + { + PartialDateTime candidate{ parseDateTime(format, dateTimeStr) }; + if (candidate > res) + res = candidate; + + if (res._precision == Precision::Sec) + break; + } + + return res; + } + + std::string PartialDateTime::toISO8601String() const + { + if (_precision == Precision::Invalid) + return ""; + + std::ostringstream ss; + + ss << std::setfill('0') << std::setw(4) << _year; + if (_precision >= Precision::Month) + ss << "-" << std::setw(2) << static_cast(_month); + if (_precision >= Precision::Day) + ss << "-" << std::setw(2) << static_cast(_day); + if (_precision >= Precision::Hour) + ss << 'T' << std::setw(2) << static_cast(_hour); + if (_precision >= Precision::Min) + ss << ':' << std::setw(2) << static_cast(_min); + if (_precision >= Precision::Sec) + ss << ':' << std::setw(2) << static_cast(_sec); + + return ss.str(); + } + + PartialDateTime PartialDateTime::parseDateTime(const char* format, const std::string& dateTimeStr) + { + PartialDateTime res; + + std::tm tm{}; + tm.tm_year = std::numeric_limits::min(); + tm.tm_mon = std::numeric_limits::min(); + tm.tm_mday = std::numeric_limits::min(); + tm.tm_hour = std::numeric_limits::min(); + tm.tm_min = std::numeric_limits::min(); + tm.tm_sec = std::numeric_limits::min(); + + std::istringstream ss{ dateTimeStr }; + ss >> std::get_time(&tm, format); + if (ss.fail()) + return res; + + if (tm.tm_sec != std::numeric_limits::min()) + { + res._sec = tm.tm_sec; + res._precision = Precision::Sec; + } + if (tm.tm_min != std::numeric_limits::min()) + { + res._min = tm.tm_min; + if (res._precision == Precision::Invalid) + res._precision = Precision::Min; + } + if (tm.tm_hour != std::numeric_limits::min()) + { + res._hour = tm.tm_hour; + if (res._precision == Precision::Invalid) + res._precision = Precision::Hour; + } + if (tm.tm_mday != std::numeric_limits::min()) + { + res._day = tm.tm_mday; + if (res._precision == Precision::Invalid) + res._precision = Precision::Day; + } + if (tm.tm_mon != std::numeric_limits::min()) + { + res._month = tm.tm_mon + 1; // tm.tm_mon is [0, 11] + if (res._precision == Precision::Invalid) + res._precision = Precision::Month; + } + if (tm.tm_year != std::numeric_limits::min()) + { + res._year = tm.tm_year + 1900; // tm.tm_year is years since 1900 + if (res._precision == Precision::Invalid) + res._precision = Precision::Year; + } + + return res; + } +} // namespace lms::core diff --git a/src/libs/core/impl/Path.cpp b/src/libs/core/impl/Path.cpp index ff44a485..d0f983e7 100644 --- a/src/libs/core/impl/Path.cpp +++ b/src/libs/core/impl/Path.cpp @@ -38,7 +38,7 @@ namespace lms::core::pathUtils { core::Crc32Calculator crc32; - std::ifstream ifs{ p.string().c_str(), std::ios_base::binary }; + std::ifstream ifs{ p, std::ios_base::binary }; if (ifs) { do @@ -51,7 +51,7 @@ namespace lms::core::pathUtils } else { - LMS_LOG(DBUPDATER, ERROR, "Failed to open file '" << p.string() << "'"); + LMS_LOG(DBUPDATER, ERROR, "Failed to open file " << p); throw LmsException("Failed to open file '" + p.string() + "'"); } @@ -72,7 +72,7 @@ namespace lms::core::pathUtils { }; - if (stat(file.string().c_str(), &sb) == -1) + if (stat(file.c_str(), &sb) == -1) throw LmsException("Failed to get stats on file '" + file.string() + "'"); return Wt::WDateTime::fromTime_t(sb.st_mtime); @@ -95,7 +95,7 @@ namespace lms::core::pathUtils if (std::filesystem::exists(excludePath, ec)) { - LMS_LOG(DBUPDATER, DEBUG, "Found '" << excludePath.string() << "': skipping directory"); + LMS_LOG(DBUPDATER, DEBUG, "Found " << excludePath << ": skipping directory"); return true; } } @@ -135,7 +135,7 @@ namespace lms::core::pathUtils bool hasFileAnyExtension(const std::filesystem::path& file, std::span supportedExtensions) { - const std::filesystem::path extension{ stringUtils::stringToLower(file.extension().string()) }; + const std::filesystem::path extension{ stringUtils::stringToLower(file.extension().c_str()) }; return (std::find(std::cbegin(supportedExtensions), std::cend(supportedExtensions), extension) != std::cend(supportedExtensions)); } diff --git a/src/libs/core/impl/String.cpp b/src/libs/core/impl/String.cpp index 5677b34c..3be9817b 100644 --- a/src/libs/core/impl/String.cpp +++ b/src/libs/core/impl/String.cpp @@ -476,6 +476,12 @@ namespace lms::core::stringUtils return date.toString("yyyy-MM-dd").toUTF8(); } + Wt::WDateTime fromISO8601String(std::string_view dateTime) + { + // assume UTC + return Wt::WDateTime::fromString(Wt::WString{ std::string{ dateTime } }, "yyyy-MM-ddThh:mm:ss.zzz"); + } + std::string formatTimestamp(std::chrono::milliseconds timestamp) { using namespace std::chrono; diff --git a/src/libs/core/include/core/IConfig.hpp b/src/libs/core/include/core/IConfig.hpp index 2d703552..c281c4b5 100644 --- a/src/libs/core/include/core/IConfig.hpp +++ b/src/libs/core/include/core/IConfig.hpp @@ -31,12 +31,12 @@ namespace lms::core virtual ~IConfig() = default; // Default values are returned in case of setting not found - virtual std::string_view getString(std::string_view setting, std::string_view def = "") = 0; - virtual void visitStrings(std::string_view setting, std::function _func, std::initializer_list def = {}) = 0; - virtual std::filesystem::path getPath(std::string_view setting, const std::filesystem::path& def = std::filesystem::path()) = 0; - virtual unsigned long getULong(std::string_view setting, unsigned long def = 0) = 0; - virtual long getLong(std::string_view setting, long def = 0) = 0; - virtual bool getBool(std::string_view setting, bool def = false) = 0; + virtual std::string_view getString(std::string_view setting, std::string_view def) = 0; + virtual void visitStrings(std::string_view setting, std::function _func, std::initializer_list def) = 0; + virtual std::filesystem::path getPath(std::string_view setting, const std::filesystem::path& def) = 0; + virtual unsigned long getULong(std::string_view setting, unsigned long def) = 0; + virtual long getLong(std::string_view setting, long def) = 0; + virtual bool getBool(std::string_view setting, bool def) = 0; }; std::unique_ptr createConfig(const std::filesystem::path& p); diff --git a/src/libs/core/include/core/PartialDateTime.hpp b/src/libs/core/include/core/PartialDateTime.hpp new file mode 100644 index 00000000..a16a44d8 --- /dev/null +++ b/src/libs/core/include/core/PartialDateTime.hpp @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2025 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 + +namespace lms::core +{ + class PartialDateTime + { + public: + constexpr PartialDateTime() = default; + PartialDateTime(int year); + PartialDateTime(int year, unsigned month); + PartialDateTime(int year, unsigned month, unsigned day); + PartialDateTime(int year, unsigned month, unsigned day, unsigned hour, unsigned min, unsigned sec); + + static PartialDateTime fromString(std::string_view str); + std::string toISO8601String() const; + + bool isValid() const { return _precision != Precision::Invalid; } + + constexpr std::optional getYear() const { return (_precision >= Precision::Year ? std::make_optional(_year) : std::nullopt); } + constexpr std::optional getMonth() const { return (_precision >= Precision::Month ? std::make_optional(_month) : std::nullopt); } + constexpr std::optional getDay() const { return (_precision >= Precision::Day ? std::make_optional(_day) : std::nullopt); } + + constexpr auto operator<=>(const PartialDateTime& other) const = default; + + private: + static PartialDateTime parseDateTime(const char* format, const std::string& dateTimeStr); + + std::int16_t _year{}; + std::uint8_t _month{}; // 1 to 12 + std::uint8_t _day{}; // 1 to 31 + std::uint8_t _hour{}; // 0 to 23 + std::uint8_t _min{}; // 0 to 59 + std::uint8_t _sec{}; // 0 to 59 + enum class Precision : std::uint8_t + { + Invalid, + Year, + Month, + Day, + Hour, + Min, + Sec, + }; + Precision _precision{ Precision::Invalid }; + }; +} // namespace lms::core \ No newline at end of file diff --git a/src/libs/core/include/core/String.hpp b/src/libs/core/include/core/String.hpp index 652ac009..31542808 100644 --- a/src/libs/core/include/core/String.hpp +++ b/src/libs/core/include/core/String.hpp @@ -116,6 +116,8 @@ namespace lms::core::stringUtils [[nodiscard]] std::string toISO8601String(const Wt::WDateTime& dateTime); [[nodiscard]] std::string toISO8601String(const Wt::WDate& date); + [[nodiscard]] Wt::WDateTime fromISO8601String(std::string_view dateTime); + // to "[minutes:seconds.milliseconds]" std::string formatTimestamp(std::chrono::milliseconds timestamp); } // namespace lms::core::stringUtils \ No newline at end of file diff --git a/src/libs/core/test/CMakeLists.txt b/src/libs/core/test/CMakeLists.txt index 7b6aced5..7b255e10 100644 --- a/src/libs/core/test/CMakeLists.txt +++ b/src/libs/core/test/CMakeLists.txt @@ -3,6 +3,7 @@ include(GoogleTest) add_executable(test-core EnumSet.cpp LiteralString.cpp + PartialDateTime.cpp Path.cpp RecursiveSharedMutex.cpp Service.cpp diff --git a/src/libs/core/test/PartialDateTime.cpp b/src/libs/core/test/PartialDateTime.cpp new file mode 100644 index 00000000..cd5b1fc1 --- /dev/null +++ b/src/libs/core/test/PartialDateTime.cpp @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2025 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 + +#include "core/PartialDateTime.hpp" + +namespace lms::core::stringUtils::tests +{ + TEST(PartialDateTime, year) + { + EXPECT_EQ(PartialDateTime{}.getYear(), std::nullopt); + EXPECT_EQ(PartialDateTime{ 1992 }.getYear(), 1992); + } + + TEST(PartialDateTime, month) + { + EXPECT_EQ(PartialDateTime{}.getMonth(), std::nullopt); + EXPECT_EQ((PartialDateTime{ 1992, 3 }.getMonth()), std::optional{ 3 }); + } + TEST(PartialDateTime, day) + { + EXPECT_EQ(PartialDateTime{}.getDay(), std::nullopt); + EXPECT_EQ((PartialDateTime{ 1992, 3, 27 }.getDay()), std::optional{ 27 }); + } + + TEST(PartialDateTime, comparison) + { + EXPECT_EQ((PartialDateTime{ 1992, 3, 27 }), (PartialDateTime{ 1992, 3, 27 })); + EXPECT_EQ((PartialDateTime{ 1992, 3 }), (PartialDateTime{ 1992, 3 })); + EXPECT_EQ(PartialDateTime{ 1992 }, PartialDateTime{ 1992 }); + EXPECT_NE((PartialDateTime{ 1992, 3, 27 }), (PartialDateTime{ 1992, 3 })); + EXPECT_NE((PartialDateTime{ 1992, 3, 27 }), (PartialDateTime{ 1992 })); + EXPECT_NE((PartialDateTime{ 1992, 3 }), (PartialDateTime{ 1992 })); + EXPECT_NE((PartialDateTime{ 1992, 3, 27 }), (PartialDateTime{ 1992, 3, 28 })); + EXPECT_NE((PartialDateTime{ 1992, 3, 27 }), (PartialDateTime{ 1992, 4, 27 })); + EXPECT_NE((PartialDateTime{ 1992, 3, 27 }), (PartialDateTime{ 1993, 3, 27 })); + EXPECT_GT((PartialDateTime{ 1993, 3, 28 }), (PartialDateTime{ 1993, 3, 27 })); + EXPECT_GT((PartialDateTime{ 1993, 4 }), (PartialDateTime{ 1993, 3, 27 })); + EXPECT_GT((PartialDateTime{ 1994 }), (PartialDateTime{ 1993, 3, 27 })); + EXPECT_LT((PartialDateTime{ 1993, 3, 27 }), (PartialDateTime{ 1993, 3, 28 })); + EXPECT_LT((PartialDateTime{ 1993, 3, 27 }), (PartialDateTime{ 1993, 4 })); + EXPECT_LT((PartialDateTime{ 1993, 3, 27 }), (PartialDateTime{ 1994 })); + } + + TEST(PartialDateTime, stringComparison) + { + EXPECT_EQ((PartialDateTime{ 1992, 3, 27 }.toISO8601String()), (PartialDateTime{ 1992, 3, 27 }.toISO8601String())); + EXPECT_EQ((PartialDateTime{ 1992, 3 }.toISO8601String()), (PartialDateTime{ 1992, 3 }.toISO8601String())); + EXPECT_EQ(PartialDateTime{ 1992 }.toISO8601String(), PartialDateTime{ 1992 }.toISO8601String()); + EXPECT_NE((PartialDateTime{ 1992, 3, 27 }.toISO8601String()), (PartialDateTime{ 1992, 3 }.toISO8601String())); + EXPECT_NE((PartialDateTime{ 1992, 3, 27 }.toISO8601String()), (PartialDateTime{ 1992 }.toISO8601String())); + EXPECT_NE((PartialDateTime{ 1992, 3 }.toISO8601String()), (PartialDateTime{ 1992 }.toISO8601String())); + EXPECT_NE((PartialDateTime{ 1992, 3, 27 }.toISO8601String()), (PartialDateTime{ 1992, 3, 28 }.toISO8601String())); + EXPECT_NE((PartialDateTime{ 1992, 3, 27 }.toISO8601String()), (PartialDateTime{ 1992, 4, 27 }.toISO8601String())); + EXPECT_NE((PartialDateTime{ 1992, 3, 27 }.toISO8601String()), (PartialDateTime{ 1993, 3, 27 }.toISO8601String())); + EXPECT_GT((PartialDateTime{ 1993, 3, 28 }.toISO8601String()), (PartialDateTime{ 1993, 3, 27 }.toISO8601String())); + EXPECT_GT((PartialDateTime{ 1993, 4 }.toISO8601String()), (PartialDateTime{ 1993, 3, 27 }.toISO8601String())); + EXPECT_GT((PartialDateTime{ 1994 }.toISO8601String()), (PartialDateTime{ 1993, 3, 27 }.toISO8601String())); + EXPECT_LT((PartialDateTime{ 1993, 3, 27 }.toISO8601String()), (PartialDateTime{ 1993, 3, 28 }.toISO8601String())); + EXPECT_LT((PartialDateTime{ 1993, 3, 27 }.toISO8601String()), (PartialDateTime{ 1993, 4 }.toISO8601String())); + EXPECT_LT((PartialDateTime{ 1993, 3, 27 }.toISO8601String()), (PartialDateTime{ 1994 }.toISO8601String())); + } + + TEST(PartialDateTime, stringConversions) + { + struct TestCase + { + std::string_view input; + std::string_view expectedOutput; + }; + + constexpr TestCase tests[]{ + { "", "" }, + { "aaaa", "" }, + { "aaaa-bb", "" }, + { "1992", "1992" }, + { "1992-03", "1992-03" }, + { "1992-00", "1992" }, + { "1992-03-00", "1992-03" }, + { "1992-03-27", "1992-03-27" }, + { "1992-00-00", "1992" }, + { "1992-03-27T15", "1992-03-27T15" }, + { "1992-03-27T15:08", "1992-03-27T15:08" }, + { "1992-03-27T15:08:57", "1992-03-27T15:08:57" }, + { "1992-03-00T15:08:57", "1992-03" }, + + { "1992-03-27 15", "1992-03-27T15" }, + { "1992-03-27 15:08", "1992-03-27T15:08" }, + { "1992-03-27 15:08:57", "1992-03-27T15:08:57" }, + + { "aaaa/bb", "" }, + { "1992", "1992" }, + { "1992/00", "1992" }, + { "1992/03", "1992-03" }, + { "1992/03/00", "1992-03" }, + { "1992/03/27", "1992-03-27" }, + { "1992/03/27 15", "1992-03-27T15" }, + { "1992/03/27 15:08", "1992-03-27T15:08" }, + { "1992/03/27 15:08:57", "1992-03-27T15:08:57" }, + { "1992/03/00 15:08:57", "1992-03" }, + }; + + for (const TestCase& test : tests) + { + const PartialDateTime dateTime{ PartialDateTime::fromString(test.input) }; + EXPECT_EQ(dateTime.toISO8601String(), test.expectedOutput) << "Input = '" << test.input; + } + } +} // namespace lms::core::stringUtils::tests diff --git a/src/libs/core/test/String.cpp b/src/libs/core/test/String.cpp index 7bae9e3e..a1fa628e 100644 --- a/src/libs/core/test/String.cpp +++ b/src/libs/core/test/String.cpp @@ -299,18 +299,25 @@ namespace lms::core::stringUtils::tests } } - TEST(Stringutils, date) + TEST(Stringutils, DateToString) { const Wt::WDate date{ 2020, 01, 03 }; EXPECT_EQ(toISO8601String(date), "2020-01-03"); } - TEST(Stringutils, dateTime) + TEST(Stringutils, DateTimeToString) { const Wt::WDateTime dateTime{ Wt::WDate{ 2020, 01, 03 }, Wt::WTime{ 9, 8, 11, 75 } }; EXPECT_EQ(toISO8601String(dateTime), "2020-01-03T09:08:11.075"); } + TEST(Stringutils, DateTimeFromString) + { + EXPECT_EQ(fromISO8601String("2020-01-03T09:08:11.075"), (Wt::WDateTime{ Wt::WDate{ 2020, 01, 03 }, Wt::WTime{ 9, 8, 11, 75 } })); + EXPECT_EQ(fromISO8601String("2020-01-03"), Wt::WDateTime{}); + EXPECT_EQ(fromISO8601String(""), Wt::WDateTime{}); + } + TEST(StringUtils, stringEndsWith) { EXPECT_TRUE(stringEndsWith("FooBar", "Bar")); diff --git a/src/libs/database/CMakeLists.txt b/src/libs/database/CMakeLists.txt index 2fe78ac7..a2aece03 100644 --- a/src/libs/database/CMakeLists.txt +++ b/src/libs/database/CMakeLists.txt @@ -1,4 +1,4 @@ -add_library(lmsdatabase SHARED +add_library(lmsdatabase STATIC impl/Artist.cpp impl/AuthToken.cpp impl/Cluster.cpp @@ -54,8 +54,6 @@ target_link_libraries(lmsdatabase PUBLIC Wt::Dbo ) -install(TARGETS lmsdatabase DESTINATION ${CMAKE_INSTALL_LIBDIR}) - if(BUILD_TESTING) add_subdirectory(test) endif() diff --git a/src/libs/database/impl/Artist.cpp b/src/libs/database/impl/Artist.cpp index 1b8a2cca..b602ae0b 100644 --- a/src/libs/database/impl/Artist.cpp +++ b/src/libs/database/impl/Artist.cpp @@ -42,9 +42,9 @@ namespace lms::db { session.checkReadTransaction(); - // TODO remove distinct and use group by auto query{ session.getDboSession()->query("SELECT " + std::string{ itemToSelect } + " FROM artist a") }; - if (params.sortMethod == ArtistSortMethod::LastWritten + if (params.sortMethod == ArtistSortMethod::LastWrittenDesc + || params.sortMethod == ArtistSortMethod::AddedDesc || params.writtenAfter.isValid() || params.linkType || params.track.isValid() @@ -55,7 +55,8 @@ namespace lms::db query.join("track_artist_link t_a_l ON t_a_l.artist_id = a.id"); } - if (params.sortMethod == ArtistSortMethod::LastWritten + if (params.sortMethod == ArtistSortMethod::LastWrittenDesc + || params.sortMethod == ArtistSortMethod::AddedDesc || params.writtenAfter.isValid() || params.release.isValid() || params.mediaLibrary.isValid()) @@ -151,8 +152,11 @@ namespace lms::db case ArtistSortMethod::Random: query.orderBy("RANDOM()"); break; - case ArtistSortMethod::LastWritten: - query.orderBy("t.file_last_write DESC"); + case ArtistSortMethod::LastWrittenDesc: + query.orderBy("MAX(t.file_last_write) DESC"); + break; + case ArtistSortMethod::AddedDesc: + query.orderBy("MAX(t.file_added) DESC"); break; case ArtistSortMethod::StarredDateDesc: assert(params.starringUser.isValid()); diff --git a/src/libs/database/impl/Db.cpp b/src/libs/database/impl/Db.cpp index b033a143..ff696b7a 100644 --- a/src/libs/database/impl/Db.cpp +++ b/src/libs/database/impl/Db.cpp @@ -48,9 +48,12 @@ namespace lms::db { prepare(); } + ~Connection() override = default; private: Connection& operator=(const Connection&) = delete; + Connection(Connection&&) = delete; + Connection&& operator=(Connection&&) = delete; std::unique_ptr clone() const override { @@ -72,9 +75,9 @@ namespace lms::db // Session living class handling the database and the login Db::Db(const std::filesystem::path& dbPath, std::size_t connectionCount) { - LMS_LOG(DB, INFO, "Creating connection pool on file " << dbPath.string()); + LMS_LOG(DB, INFO, "Creating connection pool on file " << dbPath); - auto connection{ std::make_unique(dbPath.string()) }; + auto connection{ std::make_unique(dbPath) }; if (core::IConfig * config{ core::Service::get() }) // may not be here on testU connection->setProperty("show-queries", config->getBool("db-show-queries", false) ? "true" : "false"); diff --git a/src/libs/database/impl/Directory.cpp b/src/libs/database/impl/Directory.cpp index 3824bb78..d335cace 100644 --- a/src/libs/database/impl/Directory.cpp +++ b/src/libs/database/impl/Directory.cpp @@ -21,6 +21,7 @@ #include "database/MediaLibrary.hpp" #include "database/Session.hpp" +#include "database/Types.hpp" #include "IdTypeTraits.hpp" #include "PathTraits.hpp" @@ -81,6 +82,15 @@ namespace lms::db if (params.withNoTrack) query.where("NOT EXISTS (SELECT 1 FROM track t WHERE t.directory_id = d.id)"); + switch (params.sortMethod) + { + case DirectorySortMethod::None: + break; + case DirectorySortMethod::Name: + query.orderBy("name COLLATE NOCASE"); + break; + } + return query; } @@ -89,17 +99,12 @@ namespace lms::db if (path.empty()) return path; - // Convert the path to string std::string pathStr{ path.string() }; // Check if the last character is a directory separator if (pathStr.back() != std::filesystem::path::preferred_separator) - { - // If not, add the preferred separator pathStr += std::filesystem::path::preferred_separator; - } - // Return the new path return std::filesystem::path{ pathStr }; } } // namespace diff --git a/src/libs/database/impl/Migration.cpp b/src/libs/database/impl/Migration.cpp index 4a83942d..c5be754e 100644 --- a/src/libs/database/impl/Migration.cpp +++ b/src/libs/database/impl/Migration.cpp @@ -35,7 +35,7 @@ namespace lms::db { namespace { - static constexpr Version LMS_DATABASE_VERSION{ 77 }; + static constexpr Version LMS_DATABASE_VERSION{ 80 }; } VersionInfo::VersionInfo() @@ -88,6 +88,14 @@ namespace lms::db::Migration namespace { + void dropIndexes(Session& session) + { + // Make sure we remove all the previoulsy created index, the createIndexesIfNeeded will recreate them all + std::vector indexeNames{ utils::fetchQueryResults(session.getDboSession()->query(R"(SELECT name FROM sqlite_master WHERE type = 'index' AND name LIKE '%_idx')")) }; + for (const auto& indexName : indexeNames) + utils::executeCommand(*session.getDboSession(), "DROP INDEX " + indexName); + } + void migrateFromV33(Session& session) { // remove name from track_artist_link @@ -461,9 +469,7 @@ SELECT void migrateFromV56(Session& session) { // Make sure we remove all the previoulsy created index, the createIndexesIfNeeded will recreate them all - std::vector indexeNames{ utils::fetchQueryResults(session.getDboSession()->query(R"(SELECT name FROM sqlite_master WHERE type = 'index' AND name LIKE '%_idx')")) }; - for (const auto& indexName : indexeNames) - utils::executeCommand(*session.getDboSession(), "DROP INDEX " + indexName); + dropIndexes(session); } void migrateFromV57(Session& session) @@ -1029,6 +1035,34 @@ FROM tracklist)"); utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1"); } + void migrateFromV77(Session& session) + { + // added new scan settings: skip single release playlists (default value is conservative, no need to rescan) + utils::executeCommand(*session.getDboSession(), "ALTER TABLE scan_settings ADD COLUMN skip_single_release_playlists BOOLEAN NOT NULL DEFAULT(FALSE)"); + } + + void migrateFromV78(Session& session) + { + // added advisory tag support + utils::executeCommand(*session.getDboSession(), "ALTER TABLE track ADD COLUMN advisory INTEGER NOT NULL DEFAULT(0)"); // 0 means unset + + // Just increment the scan version of the settings to make the next scan rescan everything + utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1"); + } + + void migrateFromV79(Session& session) + { + // Make sure we remove all the previoulsy created index, the createIndexesIfNeeded will recreate them all + dropIndexes(session); + + // New partial date/time support + utils::executeCommand(*session.getDboSession(), "ALTER TABLE track DROP COLUMN year"); + utils::executeCommand(*session.getDboSession(), "ALTER TABLE track DROP COLUMN original_year"); + + // Just increment the scan version of the settings to make the next scan rescan everything + utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1"); + } + bool doDbMigration(Session& session) { constexpr std::string_view outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" }; @@ -1082,6 +1116,9 @@ FROM tracklist)"); { 74, migrateFromV74 }, { 75, migrateFromV75 }, { 76, migrateFromV76 }, + { 77, migrateFromV77 }, + { 78, migrateFromV78 }, + { 79, migrateFromV79 }, }; bool migrationPerformed{}; @@ -1127,4 +1164,4 @@ FROM tracklist)"); return migrationPerformed; } -} // namespace lms::db::Migration +} // namespace lms::db::Migration \ No newline at end of file diff --git a/src/libs/database/impl/PartialDateTimeTraits.hpp b/src/libs/database/impl/PartialDateTimeTraits.hpp new file mode 100644 index 00000000..f2b35ace --- /dev/null +++ b/src/libs/database/impl/PartialDateTimeTraits.hpp @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2025 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 "core/PartialDateTime.hpp" + +#include + +namespace Wt::Dbo +{ + template<> + struct sql_value_traits + { + static std::string type(SqlConnection* conn, int /*size*/) + { + return conn->dateTimeType(SqlDateTimeType::DateTime); + } + + static void bind(const lms::core::PartialDateTime& dateTime, SqlStatement* statement, int column, int /* size */) + { + if (!dateTime.isValid()) + statement->bindNull(column); + else + statement->bind(column, dateTime.toISO8601String()); + } + + static bool read(lms::core::PartialDateTime& dateTime, SqlStatement* statement, int column, int size) + { + std::string str; + if (!statement->getResult(column, &str, size)) + return false; + + dateTime = lms::core::PartialDateTime::fromString(str); + return true; + } + }; +} // namespace Wt::Dbo diff --git a/src/libs/database/impl/PlayListFile.cpp b/src/libs/database/impl/PlayListFile.cpp index 0bbc4db1..20df473a 100644 --- a/src/libs/database/impl/PlayListFile.cpp +++ b/src/libs/database/impl/PlayListFile.cpp @@ -56,7 +56,7 @@ namespace lms::db { session.checkReadTransaction(); - return utils::fetchQuerySingleResult(session.getDboSession()->query>("SELECT pl_f from playlist_file pl_f").where("pl_f.absolute_file_path = ?").bind(p.string())); + return utils::fetchQuerySingleResult(session.getDboSession()->query>("SELECT pl_f from playlist_file pl_f").where("pl_f.absolute_file_path = ?").bind(p)); } void PlayListFile::find(Session& session, PlayListFileId& lastRetrievedId, std::size_t count, const std::function& func) diff --git a/src/libs/database/impl/Release.cpp b/src/libs/database/impl/Release.cpp index 4ab4437a..9cfb04e9 100644 --- a/src/libs/database/impl/Release.cpp +++ b/src/libs/database/impl/Release.cpp @@ -21,7 +21,7 @@ #include -#include "core/ILogger.hpp" +#include "core/PartialDateTime.hpp" #include "database/Artist.hpp" #include "database/Cluster.hpp" #include "database/Directory.hpp" @@ -32,6 +32,7 @@ #include "EnumSetTraits.hpp" #include "IdTypeTraits.hpp" +#include "PartialDateTimeTraits.hpp" #include "SqlQuery.hpp" #include "StringViewTraits.hpp" #include "Utils.hpp" @@ -49,7 +50,8 @@ namespace lms::db auto query{ session.getDboSession()->query("SELECT " + std::string{ itemToSelect } + " from release r") }; if (params.sortMethod == ReleaseSortMethod::ArtistNameThenName - || params.sortMethod == ReleaseSortMethod::LastWritten + || params.sortMethod == ReleaseSortMethod::LastWrittenDesc + || params.sortMethod == ReleaseSortMethod::AddedDesc || params.sortMethod == ReleaseSortMethod::DateAsc || params.sortMethod == ReleaseSortMethod::DateDesc || params.sortMethod == ReleaseSortMethod::OriginalDate @@ -90,8 +92,8 @@ namespace lms::db if (params.dateRange) { - query.where("COALESCE(CAST(SUBSTR(t.date, 1, 4) AS INTEGER), t.year) >= ?").bind(params.dateRange->begin); - query.where("COALESCE(CAST(SUBSTR(t.date, 1, 4) AS INTEGER), t.year) <= ?").bind(params.dateRange->end); + query.where("CAST(SUBSTR(t.date, 1, 4) AS INTEGER) >= ?").bind(params.dateRange->begin); + query.where("CAST(SUBSTR(t.date, 1, 4) AS INTEGER) <= ?").bind(params.dateRange->end); } if (!params.name.empty()) @@ -206,20 +208,23 @@ namespace lms::db case ReleaseSortMethod::Random: query.orderBy("RANDOM()"); break; - case ReleaseSortMethod::LastWritten: + case ReleaseSortMethod::LastWrittenDesc: query.orderBy("t.file_last_write DESC"); break; + case ReleaseSortMethod::AddedDesc: + query.orderBy("t.file_added DESC"); + break; case ReleaseSortMethod::DateAsc: - query.orderBy("COALESCE(t.date, CAST(t.year AS TEXT)) ASC, r.name COLLATE NOCASE"); + query.orderBy("t.date ASC, r.name COLLATE NOCASE"); break; case ReleaseSortMethod::DateDesc: - query.orderBy("COALESCE(t.date, CAST(t.year AS TEXT)) DESC, r.name COLLATE NOCASE"); + query.orderBy("t.date DESC, r.name COLLATE NOCASE"); break; case ReleaseSortMethod::OriginalDate: - query.orderBy("COALESCE(original_date, CAST(original_year AS TEXT), date, CAST(year AS TEXT)), r.name COLLATE NOCASE"); + query.orderBy("COALESCE(t.original_date, t.date), r.name COLLATE NOCASE"); break; case ReleaseSortMethod::OriginalDateDesc: - query.orderBy("COALESCE(original_date, CAST(original_year AS TEXT), date, CAST(year AS TEXT)) DESC, r.name COLLATE NOCASE"); + query.orderBy("COALESCE(t.original_date, t.date) DESC, r.name COLLATE NOCASE"); break; case ReleaseSortMethod::StarredDateDesc: assert(params.starringUser.isValid()); @@ -453,22 +458,22 @@ namespace lms::db return discs; } - Wt::WDate Release::getDate() const + core::PartialDateTime Release::getDate() const { return getDate(false); } - Wt::WDate Release::getOriginalDate() const + core::PartialDateTime Release::getOriginalDate() const { return getDate(true); } - Wt::WDate Release::getDate(bool original) const + core::PartialDateTime Release::getDate(bool original) const { assert(session()); const char* field{ original ? "original_date" : "date" }; - auto query{ (session()->query(std::string{ "SELECT " } + "t." + field + " FROM track t").where("t.release_id = ?").groupBy(field).bind(getId())) }; + auto query{ (session()->query(std::string{ "SELECT " } + "t." + field + " FROM track t").where("t.release_id = ?").groupBy(field).bind(getId())) }; const auto dates{ utils::fetchQueryResults(query) }; @@ -493,17 +498,28 @@ namespace lms::db { assert(session()); - const char* field{ original ? "original_year" : "year" }; - auto query{ session()->query>(std::string{ "SELECT " } + "t." + field + " FROM track t").where("t.release_id = ?").bind(getId()).groupBy(field) }; + const char* field{ original ? "original_date" : "date" }; + auto query{ session()->query(std::string{ "SELECT " } + "t." + field + " FROM track t").where("t.release_id = ?").bind(getId()).groupBy(field) }; - const auto years{ utils::fetchQueryResults(query) }; + bool valid{ true }; + std::optional year{}; + utils::forEachQueryResult(query, [&](core::PartialDateTime dateTime) { + if (!dateTime.isValid()) + valid = false; + else if (!year) + year = dateTime.getYear(); + else if (year != dateTime.getYear().value()) + valid = false; + }); - // various years => invalid years - const std::size_t count{ years.size() }; - if (count == 0 || count > 1) + if (!year) + valid = false; + + if (!valid) return std::nullopt; - return years.front(); + assert(year); + return *year; } std::optional Release::getCopyright() const @@ -666,6 +682,19 @@ namespace lms::db utils::forEachQueryResult(query, _func); } + core::EnumSet Release::getAdvisories() const + { + core::EnumSet res; + + auto query{ session()->query("SELECT DISTINCT advisory FROM track t").where("t.release_id = ?").bind(getId()) }; + + utils::forEachQueryResult(query, [&](Advisory advisory) { + res.insert(advisory); + }); + + return res; + } + std::chrono::milliseconds Release::getDuration() const { assert(session()); diff --git a/src/libs/database/impl/ScanSettings.cpp b/src/libs/database/impl/ScanSettings.cpp index 4168e402..da2dc361 100644 --- a/src/libs/database/impl/ScanSettings.cpp +++ b/src/libs/database/impl/ScanSettings.cpp @@ -94,6 +94,15 @@ namespace lms::db } } + void ScanSettings::setSkipSingleReleasePlayLists(bool value) + { + if (_skipSingleReleasePlayLists != value) + { + _skipSingleReleasePlayLists = value; + incScanVersion(); + } + } + void ScanSettings::incScanVersion() { _scanVersion += 1; diff --git a/src/libs/database/impl/Session.cpp b/src/libs/database/impl/Session.cpp index 2cb88d37..25af8d32 100644 --- a/src/libs/database/impl/Session.cpp +++ b/src/libs/database/impl/Session.cpp @@ -52,6 +52,7 @@ #include "EnumSetTraits.hpp" #include "Migration.hpp" +#include "PartialDateTimeTraits.hpp" #include "PathTraits.hpp" #include "Utils.hpp" @@ -202,6 +203,7 @@ namespace lms::db utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS directory_parent_directory_idx ON directory(parent_directory_id)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS directory_path_idx ON directory(absolute_path)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS directory_media_library_idx ON directory(media_library_id)"); + utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS directory_name_idx ON directory(name COLLATE NOCASE)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS image_directory_stem_idx ON image(directory_id, stem COLLATE NOCASE)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS image_id_idx ON image(id)"); @@ -238,19 +240,25 @@ namespace lms::db utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_date_idx ON track(date)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_directory_release_idx ON track(directory_id, release_id);"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_directory_file_stem_idx ON track(directory_id, file_stem);"); + utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_file_added_idx ON track(file_added)"); + utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_file_added_desc_idx ON track(file_added DESC)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_file_last_write_idx ON track(file_last_write)"); + utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_file_last_write_desc_idx ON track(file_last_write DESC)"); + utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_file_name_idx ON track(file_name COLLATE NOCASE)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_media_library_idx ON track(media_library_id)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_media_library_release_idx ON track(media_library_id, release_id)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_mbid_idx ON track(mbid)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_name_idx ON track(name)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_name_nocase_idx ON track(name COLLATE NOCASE)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_original_date_idx ON track(original_date)"); - utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_original_year_idx ON track(original_year)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_recording_mbid_idx ON track(recording_mbid)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_release_idx ON track(release_id)"); + utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_release_date_idx ON track(release_id, date)"); + utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_release_date_desc_idx ON track(release_id, date DESC)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_release_file_last_write_idx ON track(release_id, file_last_write)"); - utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_release_year_idx ON track(release_id, year)"); - utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_year_idx ON track(year)"); + utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_release_file_last_write_desc_idx ON track(release_id, file_last_write DESC)"); + utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_release_file_added_idx ON track(release_id, file_added)"); + utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_release_file_added_desc_idx ON track(release_id, file_added DESC)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS tracklist_name_idx ON tracklist(name)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS tracklist_user_type_idx ON tracklist(user_id, type)"); diff --git a/src/libs/database/impl/Track.cpp b/src/libs/database/impl/Track.cpp index 43418b8e..058ff7eb 100644 --- a/src/libs/database/impl/Track.cpp +++ b/src/libs/database/impl/Track.cpp @@ -34,6 +34,7 @@ #include "database/User.hpp" #include "IdTypeTraits.hpp" +#include "PartialDateTimeTraits.hpp" #include "PathTraits.hpp" #include "SqlQuery.hpp" #include "StringViewTraits.hpp" @@ -174,9 +175,12 @@ namespace lms::db case TrackSortMethod::Id: query.orderBy("t.id"); break; - case TrackSortMethod::LastWritten: + case TrackSortMethod::LastWrittenDesc: query.orderBy("t.file_last_write DESC"); break; + case TrackSortMethod::AddedDesc: + query.orderBy("t.file_added DESC"); + break; case TrackSortMethod::Random: query.orderBy("RANDOM()"); break; @@ -187,6 +191,9 @@ namespace lms::db case TrackSortMethod::Name: query.orderBy("t.name COLLATE NOCASE"); break; + case TrackSortMethod::FileName: + query.orderBy("t.file_name COLLATE NOCASE"); + break; case TrackSortMethod::DateDescAndRelease: query.orderBy("COALESCE(t.date, CAST(t.year AS TEXT)) DESC,t.release_id,t.disc_number,t.track_number"); break; @@ -233,7 +240,7 @@ namespace lms::db { session.checkReadTransaction(); - return utils::fetchQuerySingleResult(session.getDboSession()->query>("SELECT t from track t").where("t.absolute_file_path = ?").bind(p.string())); + return utils::fetchQuerySingleResult(session.getDboSession()->query>("SELECT t from track t").where("t.absolute_file_path = ?").bind(p)); } Track::pointer Track::find(Session& session, TrackId id) @@ -447,6 +454,16 @@ namespace lms::db _trackLyrics.insert(getDboPtr(lyrics)); } + std::optional Track::getYear() const + { + return _date.getYear(); + } + + std::optional Track::getOriginalYear() const + { + return _originalDate.getYear(); + } + bool Track::hasLyrics() const { return !_trackLyrics.empty(); diff --git a/src/libs/database/impl/Types.cpp b/src/libs/database/impl/Types.cpp index fb5220f9..6100b86a 100644 --- a/src/libs/database/impl/Types.cpp +++ b/src/libs/database/impl/Types.cpp @@ -41,9 +41,4 @@ namespace lms::db { return allowedAudioBitrates.find(bitrate) != std::cend(allowedAudioBitrates); } - - DateRange DateRange::fromYearRange(int from, int to) - { - return DateRange{ from, to }; - } } // namespace lms::db diff --git a/src/libs/database/include/database/Directory.hpp b/src/libs/database/include/database/Directory.hpp index 31ee3033..45f17743 100644 --- a/src/libs/database/include/database/Directory.hpp +++ b/src/libs/database/include/database/Directory.hpp @@ -55,6 +55,7 @@ namespace lms::db DirectoryId parentDirectory; // If set, directories that have this parent bool withNoTrack{}; // If set, directories that do not contain any track MediaLibraryId mediaLibrary; // If set, directories in this library + DirectorySortMethod sortMethod{ DirectorySortMethod::None }; FindParameters& setRange(std::optional _range) { @@ -92,6 +93,11 @@ namespace lms::db mediaLibrary = _mediaLibrary; return *this; } + FindParameters& setSortMethod(DirectorySortMethod _method) + { + sortMethod = _method; + return *this; + } }; // find diff --git a/src/libs/database/include/database/Release.hpp b/src/libs/database/include/database/Release.hpp index bebe3d1b..699c0236 100644 --- a/src/libs/database/include/database/Release.hpp +++ b/src/libs/database/include/database/Release.hpp @@ -19,7 +19,6 @@ #pragma once -#include #include #include #include @@ -30,6 +29,7 @@ #include #include "core/EnumSet.hpp" +#include "core/PartialDateTime.hpp" #include "core/UUID.hpp" #include "database/ArtistId.hpp" #include "database/ClusterId.hpp" @@ -126,7 +126,7 @@ namespace lms::db ReleaseSortMethod sortMethod{ ReleaseSortMethod::None }; std::optional range; Wt::WDateTime writtenAfter; - std::optional dateRange; + std::optional dateRange; UserId starringUser; // only releases starred by this user std::optional feedbackBackend; // and for this backend ArtistId artist; // only releases that involved this user @@ -167,7 +167,7 @@ namespace lms::db writtenAfter = _after; return *this; } - FindParameters& setDateRange(const std::optional& _dateRange) + FindParameters& setDateRange(const std::optional& _dateRange) { dateRange = _dateRange; return *this; @@ -227,9 +227,9 @@ namespace lms::db std::vector>> getClusterGroups(const std::vector& clusterTypeIds, std::size_t size) const; // Utility functions (if all tracks have the same values, which is legit to not be the case) - Wt::WDate getDate() const; + core::PartialDateTime getDate() const; std::optional getYear() const; - Wt::WDate getOriginalDate() const; + core::PartialDateTime getOriginalDate() const; std::optional getOriginalYear() const; std::optional getCopyright() const; std::optional getCopyrightURL() const; @@ -252,6 +252,7 @@ namespace lms::db std::vector getLabelNames() const; std::vector getReleaseTypeNames() const; void visitLabels(const std::function& _func) const; + core::EnumSet getAdvisories() const; std::string_view getBarcode() const { return _barcode; } ObjectPtr getImage() const; @@ -301,7 +302,7 @@ namespace lms::db Release(const std::string& name, const std::optional& MBID = {}); static pointer create(Session& session, const std::string& name, const std::optional& MBID = {}); - Wt::WDate getDate(bool original) const; + core::PartialDateTime getDate(bool original) const; std::optional getYear(bool original) const; static constexpr std::size_t _maxNameLength{ 512 }; diff --git a/src/libs/database/include/database/ScanSettings.hpp b/src/libs/database/include/database/ScanSettings.hpp index 11d7beab..54ff5ec3 100644 --- a/src/libs/database/include/database/ScanSettings.hpp +++ b/src/libs/database/include/database/ScanSettings.hpp @@ -70,6 +70,7 @@ namespace lms::db SimilarityEngineType getSimilarityEngineType() const { return _similarityEngineType; } std::vector getArtistTagDelimiters() const; std::vector getDefaultTagDelimiters() const; + bool getSkipSingleReleasePlayLists() const { return _skipSingleReleasePlayLists; } // Setters void setUpdateStartTime(Wt::WTime t) { _startTime = t; } @@ -78,6 +79,7 @@ namespace lms::db void setSimilarityEngineType(SimilarityEngineType type) { _similarityEngineType = type; } void setArtistTagDelimiters(std::span delimiters); void setDefaultTagDelimiters(std::span delimiters); + void setSkipSingleReleasePlayLists(bool value); void incScanVersion(); template @@ -90,6 +92,7 @@ namespace lms::db Wt::Dbo::field(a, _extraTagsToScan, "extra_tags_to_scan"); Wt::Dbo::field(a, _artistTagDelimiters, "artist_tag_delimiters"); Wt::Dbo::field(a, _defaultTagDelimiters, "default_tag_delimiters"); + Wt::Dbo::field(a, _skipSingleReleasePlayLists, "skip_single_release_playlists"); } private: @@ -100,5 +103,6 @@ namespace lms::db std::string _extraTagsToScan; std::string _artistTagDelimiters; std::string _defaultTagDelimiters; + bool _skipSingleReleasePlayLists{ false }; }; } // namespace lms::db diff --git a/src/libs/database/include/database/Track.hpp b/src/libs/database/include/database/Track.hpp index 606977e4..f42089d3 100644 --- a/src/libs/database/include/database/Track.hpp +++ b/src/libs/database/include/database/Track.hpp @@ -33,6 +33,7 @@ #include #include "core/EnumSet.hpp" +#include "core/PartialDateTime.hpp" #include "core/UUID.hpp" #include "database/ArtistId.hpp" #include "database/ClusterId.hpp" @@ -193,12 +194,6 @@ namespace lms::db } }; - struct PathResult - { - TrackId trackId; - std::filesystem::path path; - }; - Track() = default; // Find utility functions @@ -229,21 +224,20 @@ namespace lms::db void setRelativeFilePath(const std::filesystem::path& filePath); void setFileSize(std::size_t fileSize) { _fileSize = fileSize; } void setLastWriteTime(Wt::WDateTime time) { _fileLastWrite = time; } - void setAddedTime(Wt::WDateTime time) { _fileAdded = time; } + void setAddedTime(core::PartialDateTime time) { _fileAdded = time; } void setBitrate(std::size_t bitrate) { _bitrate = bitrate; } void setBitsPerSample(std::size_t bitsPerSample) { _bitsPerSample = bitsPerSample; } void setDuration(std::chrono::milliseconds duration) { _duration = duration; } void setChannelCount(std::size_t channelCount) { _channelCount = channelCount; } void setSampleRate(std::size_t channelCount) { _sampleRate = channelCount; } - void setDate(const Wt::WDate& date) { _date = date; } - void setYear(std::optional year) { _year = year; } - void setOriginalDate(const Wt::WDate& date) { _originalDate = date; } - void setOriginalYear(std::optional year) { _originalYear = year; } + void setDate(const core::PartialDateTime& date) { _date = date; } + void setOriginalDate(const core::PartialDateTime& 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(std::string_view copyright); void setCopyrightURL(std::string_view copyrightURL); + void setAdvisory(Advisory advisory) { _advisory = advisory; } 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; } @@ -273,18 +267,19 @@ namespace lms::db std::chrono::milliseconds getDuration() const { return _duration; } std::size_t getSampleRate() const { return _sampleRate; } const Wt::WDateTime& getLastWritten() const { return _fileLastWrite; } - const Wt::WDate& getDate() const { return _date; } - std::optional getYear() const { return _year; } - const Wt::WDate& getOriginalDate() const { return _originalDate; } - std::optional getOriginalYear() const { return _originalYear; }; + const core::PartialDateTime& getDate() const { return _date; } + std::optional getYear() const; + const core::PartialDateTime& getOriginalDate() const { return _originalDate; } + std::optional getOriginalYear() const; const Wt::WDateTime& getLastWriteTime() const { return _fileLastWrite; } - const Wt::WDateTime& getAddedTime() const { return _fileAdded; } + const core::PartialDateTime& getAddedTime() const { return _fileAdded; } bool hasCover() const { return _hasCover; } bool hasLyrics() const; std::optional getTrackMBID() const { return core::UUID::fromString(_trackMBID); } std::optional getRecordingMBID() const { return core::UUID::fromString(_recordingMBID); } std::optional getCopyright() const; std::optional getCopyrightURL() const; + Advisory getAdvisory() const { return _advisory; } std::optional getTrackReplayGain() const { return _trackReplayGain; } std::optional getReleaseReplayGain() const { return _releaseReplayGain; } std::string_view getArtistDisplayName() const { return _artistDisplayName; } @@ -294,6 +289,7 @@ namespace lms::db std::vector> getArtists(core::EnumSet artistLinkTypes) const; // no type means all std::vector getArtistIds(core::EnumSet artistLinkTypes) const; // no type means all std::vector> getArtistLinks() const; + ReleaseId getReleaseId() const { return _release.id(); } ObjectPtr getRelease() const { return _release; } std::vector> getClusters() const; std::vector getClusterIds() const; @@ -317,9 +313,7 @@ namespace lms::db Wt::Dbo::field(a, _channelCount, "channel_count"); Wt::Dbo::field(a, _sampleRate, "sample_rate"); Wt::Dbo::field(a, _date, "date"); - Wt::Dbo::field(a, _year, "year"); Wt::Dbo::field(a, _originalDate, "original_date"); - Wt::Dbo::field(a, _originalYear, "original_year"); Wt::Dbo::field(a, _absoluteFilePath, "absolute_file_path"); Wt::Dbo::field(a, _relativeFilePath, "relative_file_path"); Wt::Dbo::field(a, _fileStem, "file_stem"); @@ -332,6 +326,7 @@ namespace lms::db Wt::Dbo::field(a, _recordingMBID, "recording_mbid"); Wt::Dbo::field(a, _copyright, "copyright"); Wt::Dbo::field(a, _copyrightURL, "copyright_url"); + Wt::Dbo::field(a, _advisory, "advisory"); 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"); @@ -364,27 +359,25 @@ namespace lms::db int _channelCount{}; std::chrono::duration _duration{}; int _sampleRate{}; - Wt::WDate _date; - std::optional _year; - Wt::WDate _originalDate; - std::optional _originalYear; + core::PartialDateTime _date; + core::PartialDateTime _originalDate; std::filesystem::path _absoluteFilePath; // full path std::filesystem::path _relativeFilePath; // relative to root (that may be deleted) std::filesystem::path _fileStem; std::filesystem::path _fileName; long long _fileSize{}; Wt::WDateTime _fileLastWrite; - Wt::WDateTime _fileAdded; + core::PartialDateTime _fileAdded; bool _hasCover{}; std::string _trackMBID; std::string _recordingMBID; std::string _copyright; std::string _copyrightURL; + Advisory _advisory{ Advisory::UnSet }; std::optional _trackReplayGain; std::optional _releaseReplayGain; std::string _artistDisplayName; std::string _comment; - Wt::Dbo::ptr _release; Wt::Dbo::ptr _mediaLibrary; Wt::Dbo::ptr _directory; diff --git a/src/libs/database/include/database/Types.hpp b/src/libs/database/include/database/Types.hpp index 151a4791..7d8d955b 100644 --- a/src/libs/database/include/database/Types.hpp +++ b/src/libs/database/include/database/Types.hpp @@ -100,12 +100,10 @@ namespace lms::db } }; - struct DateRange + struct YearRange { - int begin; - int end; - - static DateRange fromYearRange(int from, int to); + int begin{}; + int end{}; }; struct DiscInfo @@ -121,7 +119,8 @@ namespace lms::db Name, SortName, Random, - LastWritten, + LastWrittenDesc, + AddedDesc, StarredDateDesc, }; @@ -131,6 +130,12 @@ namespace lms::db Name, }; + enum class DirectorySortMethod + { + None, + Name, + }; + enum class ReleaseSortMethod { None, @@ -142,7 +147,8 @@ namespace lms::db OriginalDate, OriginalDateDesc, Random, - LastWritten, + LastWrittenDesc, + AddedDesc, StarredDateDesc, }; @@ -158,8 +164,10 @@ namespace lms::db None, Id, Random, - LastWritten, + LastWrittenDesc, + AddedDesc, StarredDateDesc, + FileName, Name, DateDescAndRelease, Release, // order by disc/track number @@ -249,4 +257,12 @@ namespace lms::db PlayList = 0, // user controlled playlists Internal = 1, // internal usage (current playqueue, history, ...) }; + + enum class Advisory + { + UnSet = 0, + Unknown = 1, + Clean = 2, + Explicit = 3, + }; } // namespace lms::db diff --git a/src/libs/database/test/Artist.cpp b/src/libs/database/test/Artist.cpp index 85f6ece5..0ee1c8da 100644 --- a/src/libs/database/test/Artist.cpp +++ b/src/libs/database/test/Artist.cpp @@ -669,7 +669,8 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Artist_findByRelease) { ScopedArtist artist{ session, "artist" }; - ScopedTrack track{ session }; + ScopedTrack track1{ session }; + ScopedTrack track2{ session }; ScopedRelease release{ session, "MyRelease" }; { @@ -680,7 +681,8 @@ namespace lms::db::tests { auto transaction{ session.createWriteTransaction() }; - TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); + TrackArtistLink::create(session, track1.get(), artist.get(), TrackArtistLinkType::Artist); + TrackArtistLink::create(session, track2.get(), artist.get(), TrackArtistLinkType::Artist); } { @@ -691,7 +693,8 @@ namespace lms::db::tests { auto transaction{ session.createWriteTransaction() }; - track.get().modify()->setRelease(release.get()); + track1.get().modify()->setRelease(release.get()); + track2.get().modify()->setRelease(release.get()); } { @@ -700,6 +703,12 @@ namespace lms::db::tests ASSERT_EQ(artists.results.size(), 1); EXPECT_EQ(artists.results.front(), artist.getId()); } + + { + auto transaction{ session.createReadTransaction() }; + const std::size_t count{ Release::getCount(session, Release::FindParameters{}.setArtist(artist.getId())) }; + EXPECT_EQ(count, 1); + } } TEST_F(DatabaseFixture, Artist_image) @@ -725,4 +734,84 @@ namespace lms::db::tests EXPECT_EQ(artistImage->getId(), image.getId()); } } + + TEST_F(DatabaseFixture, Artist_sortDateAdded) + { + ScopedArtist artistA{ session, "artistA" }; + ScopedArtist artistB{ session, "artistB" }; + ScopedArtist artistC{ session, "artistC" }; + ScopedArtist artistD{ session, "artistD" }; + + ScopedTrack trackA1{ session }; + ScopedTrack trackB1{ session }; + ScopedTrack trackC1{ session }; + ScopedTrack trackD1{ session }; + + ScopedTrack trackA2{ session }; + + { + auto transaction{ session.createWriteTransaction() }; + trackA1.get().modify()->setAddedTime(core::PartialDateTime{ 2021, 1, 2 }); + trackB1.get().modify()->setAddedTime(core::PartialDateTime{ 2021, 1, 1 }); + trackD1.get().modify()->setAddedTime(core::PartialDateTime{ 2021, 1, 2, 15, 36, 24 }); + trackD1.get().modify()->setAddedTime(core::PartialDateTime{ 2021, 1, 3 }); + trackA2.get().modify()->setAddedTime(core::PartialDateTime{ 2021, 1, 4 }); + + TrackArtistLink::create(session, trackA1.get(), artistA.get(), TrackArtistLinkType::Artist); + TrackArtistLink::create(session, trackA2.get(), artistA.get(), TrackArtistLinkType::Producer); + TrackArtistLink::create(session, trackB1.get(), artistB.get(), TrackArtistLinkType::Artist); + TrackArtistLink::create(session, trackC1.get(), artistC.get(), TrackArtistLinkType::Artist); + TrackArtistLink::create(session, trackD1.get(), artistD.get(), TrackArtistLinkType::Artist); + } + + { + auto transaction{ session.createReadTransaction() }; + const auto artists{ Artist::findIds(session, Artist::FindParameters{}.setSortMethod(ArtistSortMethod::AddedDesc)) }; + ASSERT_EQ(artists.results.size(), 4); + EXPECT_EQ(artists.results[0], artistA.getId()); + EXPECT_EQ(artists.results[1], artistD.getId()); + EXPECT_EQ(artists.results[2], artistB.getId()); + EXPECT_EQ(artists.results[3], artistC.getId()); + } + } + + TEST_F(DatabaseFixture, Artist_sortLastWritten) + { + ScopedArtist artistA{ session, "artistA" }; + ScopedArtist artistB{ session, "artistB" }; + ScopedArtist artistC{ session, "artistC" }; + ScopedArtist artistD{ session, "artistD" }; + + ScopedTrack trackA1{ session }; + ScopedTrack trackB1{ session }; + ScopedTrack trackC1{ session }; + ScopedTrack trackD1{ session }; + + ScopedTrack trackA2{ session }; + + { + auto transaction{ session.createWriteTransaction() }; + trackA1.get().modify()->setLastWriteTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 2 } }); + trackB1.get().modify()->setLastWriteTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 1 } }); + trackD1.get().modify()->setLastWriteTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 2 }, Wt::WTime{ 15, 36, 24 } }); + trackD1.get().modify()->setLastWriteTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 3 } }); + trackA2.get().modify()->setLastWriteTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 4 } }); + + TrackArtistLink::create(session, trackA1.get(), artistA.get(), TrackArtistLinkType::Artist); + TrackArtistLink::create(session, trackA2.get(), artistA.get(), TrackArtistLinkType::Producer); + TrackArtistLink::create(session, trackB1.get(), artistB.get(), TrackArtistLinkType::Artist); + TrackArtistLink::create(session, trackC1.get(), artistC.get(), TrackArtistLinkType::Artist); + TrackArtistLink::create(session, trackD1.get(), artistD.get(), TrackArtistLinkType::Artist); + } + + { + auto transaction{ session.createReadTransaction() }; + const auto artists{ Artist::findIds(session, Artist::FindParameters{}.setSortMethod(ArtistSortMethod::LastWrittenDesc)) }; + ASSERT_EQ(artists.results.size(), 4); + EXPECT_EQ(artists.results[0], artistA.getId()); + EXPECT_EQ(artists.results[1], artistD.getId()); + EXPECT_EQ(artists.results[2], artistB.getId()); + EXPECT_EQ(artists.results[3], artistC.getId()); + } + } } // namespace lms::db::tests diff --git a/src/libs/database/test/Release.cpp b/src/libs/database/test/Release.cpp index d3ee941a..21416541 100644 --- a/src/libs/database/test/Release.cpp +++ b/src/libs/database/test/Release.cpp @@ -19,6 +19,7 @@ #include "Common.hpp" +#include "core/PartialDateTime.hpp" #include "database/Image.hpp" namespace lms::db::tests @@ -462,8 +463,8 @@ namespace lms::db::tests { ScopedRelease release1{ session, "MyRelease1" }; ScopedRelease release2{ session, "MyRelease2" }; - const Wt::WDate release1Date{ Wt::WDate{ 1994, 2, 3 } }; - const Wt::WDate release1OriginalDate{ Wt::WDate{ 1993, 4, 5 } }; + const core::PartialDateTime release1Date{ 1994, 2, 3 }; + const core::PartialDateTime release1OriginalDate{ 1993, 4, 5 }; ScopedTrack track1A{ session }; ScopedTrack track1B{ session }; @@ -473,7 +474,7 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - const auto releases{ Release::findIds(session, Release::FindParameters{}.setDateRange(DateRange::fromYearRange(0, 3000))) }; + const auto releases{ Release::findIds(session, Release::FindParameters{}.setDateRange(YearRange{ -3000, 3000 })) }; EXPECT_EQ(releases.results.size(), 0); } @@ -492,20 +493,23 @@ namespace lms::db::tests EXPECT_EQ(release1.get()->getDate(), release1Date); EXPECT_EQ(release1.get()->getOriginalDate(), release1OriginalDate); + + EXPECT_EQ(release1.get()->getYear(), release1Date.getYear()); + EXPECT_EQ(release1.get()->getOriginalYear(), release1OriginalDate.getYear()); } { auto transaction{ session.createReadTransaction() }; - auto releases{ Release::findIds(session, Release::FindParameters{}.setDateRange(DateRange::fromYearRange(1950, 2000))) }; + auto releases{ Release::findIds(session, Release::FindParameters{}.setDateRange(YearRange{ 1950, 2000 })) }; ASSERT_EQ(releases.results.size(), 1); EXPECT_EQ(releases.results.front(), release1.getId()); - releases = Release::findIds(session, Release::FindParameters{}.setDateRange(DateRange::fromYearRange(1994, 1994))); + releases = Release::findIds(session, Release::FindParameters{}.setDateRange(YearRange{ 1994, 1994 })); ASSERT_EQ(releases.results.size(), 1); EXPECT_EQ(releases.results.front(), release1.getId()); - releases = Release::findIds(session, Release::FindParameters{}.setDateRange(DateRange::fromYearRange(1993, 1993))); + releases = Release::findIds(session, Release::FindParameters{}.setDateRange(YearRange{ 1993, 1993 })); ASSERT_EQ(releases.results.size(), 0); } } @@ -525,7 +529,7 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - const auto releases{ Release::findIds(session, Release::FindParameters{}.setDateRange(DateRange::fromYearRange(0, 3000))) }; + const auto releases{ Release::findIds(session, Release::FindParameters{}.setDateRange(YearRange{ 0, 3000 })) }; EXPECT_EQ(releases.results.size(), 0); } @@ -537,10 +541,10 @@ namespace lms::db::tests track2A.get().modify()->setRelease(release2.get()); track2B.get().modify()->setRelease(release2.get()); - track1A.get().modify()->setYear(release1Year); - track1B.get().modify()->setYear(release1Year); - track1A.get().modify()->setOriginalYear(release1OriginalYear); - track1B.get().modify()->setOriginalYear(release1OriginalYear); + track1A.get().modify()->setDate(core::PartialDateTime{ release1Year }); + track1B.get().modify()->setDate(core::PartialDateTime{ release1Year }); + track1A.get().modify()->setOriginalDate(core::PartialDateTime{ release1OriginalYear }); + track1B.get().modify()->setOriginalDate(core::PartialDateTime{ release1OriginalYear }); EXPECT_EQ(release1.get()->getYear(), release1Year); EXPECT_EQ(release1.get()->getOriginalYear(), release1OriginalYear); @@ -549,15 +553,15 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - auto releases{ Release::findIds(session, Release::FindParameters{}.setDateRange(DateRange::fromYearRange(1950, 2000))) }; + auto releases{ Release::findIds(session, Release::FindParameters{}.setDateRange(YearRange{ 1950, 2000 })) }; ASSERT_EQ(releases.results.size(), 1); EXPECT_EQ(releases.results.front(), release1.getId()); - releases = Release::findIds(session, Release::FindParameters{}.setDateRange(DateRange::fromYearRange(1994, 1994))); + releases = Release::findIds(session, Release::FindParameters{}.setDateRange(YearRange{ 1994, 1994 })); ASSERT_EQ(releases.results.size(), 1); EXPECT_EQ(releases.results.front(), release1.getId()); - releases = Release::findIds(session, Release::FindParameters{}.setDateRange(DateRange::fromYearRange(1993, 1993))); + releases = Release::findIds(session, Release::FindParameters{}.setDateRange(YearRange{ 1993, 1993 })); ASSERT_EQ(releases.results.size(), 0); } } @@ -957,11 +961,11 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Release_sortMethod) { ScopedRelease release1{ session, "MyRelease1" }; - const Wt::WDate release1Date{ Wt::WDate{ 2000, 2, 3 } }; - const Wt::WDate release1OriginalDate{ Wt::WDate{ 1993, 4, 5 } }; + const core::PartialDateTime release1Date{ 2000, 2, 3 }; + const core::PartialDateTime release1OriginalDate{ 1993, 4, 5 }; ScopedRelease release2{ session, "MyRelease2" }; - const Wt::WDate release2Date{ Wt::WDate{ 1994, 2, 3 } }; + const core::PartialDateTime release2Date{ 1994, 2, 3 }; ScopedTrack track1{ session }; ScopedTrack track2{ session }; @@ -1117,4 +1121,84 @@ namespace lms::db::tests EXPECT_EQ(releaseImage->getId(), image.getId()); } } + + TEST_F(DatabaseFixture, Release_sortDateAdded) + { + ScopedRelease releaseA{ session, "relA" }; + ScopedRelease releaseB{ session, "relB" }; + ScopedRelease releaseC{ session, "relC" }; + ScopedRelease releaseD{ session, "relD" }; + + ScopedTrack trackA1{ session }; + ScopedTrack trackB1{ session }; + ScopedTrack trackC1{ session }; + ScopedTrack trackD1{ session }; + + ScopedTrack trackA2{ session }; + + { + auto transaction{ session.createWriteTransaction() }; + trackA1.get().modify()->setAddedTime(core::PartialDateTime{ 2021, 1, 2 }); + trackB1.get().modify()->setAddedTime(core::PartialDateTime{ 2021, 1, 1 }); + trackD1.get().modify()->setAddedTime(core::PartialDateTime{ 2021, 1, 2, 15, 36, 24 }); + trackD1.get().modify()->setAddedTime(core::PartialDateTime{ 2021, 1, 3 }); + trackA2.get().modify()->setAddedTime(core::PartialDateTime{ 2021, 1, 4 }); + + trackA1.get().modify()->setRelease(releaseA.get()); + trackA2.get().modify()->setRelease(releaseA.get()); + trackB1.get().modify()->setRelease(releaseB.get()); + trackC1.get().modify()->setRelease(releaseC.get()); + trackD1.get().modify()->setRelease(releaseD.get()); + } + + { + auto transaction{ session.createReadTransaction() }; + const auto releases{ Release::findIds(session, Release::FindParameters{}.setSortMethod(ReleaseSortMethod::AddedDesc)) }; + ASSERT_EQ(releases.results.size(), 4); + EXPECT_EQ(releases.results[0], releaseA.getId()); + EXPECT_EQ(releases.results[1], releaseD.getId()); + EXPECT_EQ(releases.results[2], releaseB.getId()); + EXPECT_EQ(releases.results[3], releaseC.getId()); + } + } + + TEST_F(DatabaseFixture, Release_sortLastWritten) + { + ScopedRelease releaseA{ session, "relA" }; + ScopedRelease releaseB{ session, "relB" }; + ScopedRelease releaseC{ session, "relC" }; + ScopedRelease releaseD{ session, "relD" }; + + ScopedTrack trackA1{ session }; + ScopedTrack trackB1{ session }; + ScopedTrack trackC1{ session }; + ScopedTrack trackD1{ session }; + + ScopedTrack trackA2{ session }; + + { + auto transaction{ session.createWriteTransaction() }; + trackA1.get().modify()->setLastWriteTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 2 } }); + trackB1.get().modify()->setLastWriteTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 1 } }); + trackD1.get().modify()->setLastWriteTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 2 }, Wt::WTime{ 15, 36, 24 } }); + trackD1.get().modify()->setLastWriteTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 3 } }); + trackA2.get().modify()->setLastWriteTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 4 } }); + + trackA1.get().modify()->setRelease(releaseA.get()); + trackA2.get().modify()->setRelease(releaseA.get()); + trackB1.get().modify()->setRelease(releaseB.get()); + trackC1.get().modify()->setRelease(releaseC.get()); + trackD1.get().modify()->setRelease(releaseD.get()); + } + + { + auto transaction{ session.createReadTransaction() }; + const auto releases{ Release::findIds(session, Release::FindParameters{}.setSortMethod(ReleaseSortMethod::LastWrittenDesc)) }; + ASSERT_EQ(releases.results.size(), 4); + EXPECT_EQ(releases.results[0], releaseA.getId()); + EXPECT_EQ(releases.results[1], releaseD.getId()); + EXPECT_EQ(releases.results[2], releaseB.getId()); + EXPECT_EQ(releases.results[3], releaseC.getId()); + } + } } // namespace lms::db::tests \ No newline at end of file diff --git a/src/libs/database/test/Track.cpp b/src/libs/database/test/Track.cpp index 8c9a13d8..f5ab80fc 100644 --- a/src/libs/database/test/Track.cpp +++ b/src/libs/database/test/Track.cpp @@ -259,8 +259,8 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Track_date) { ScopedTrack track{ session }; - const Wt::WDate date{ 1995, 5, 5 }; - const Wt::WDate originalDate{ 1994, 2, 2 }; + const core::PartialDateTime date{ 1995, 5, 5 }; + const core::PartialDateTime originalDate{ 1994, 2, 2 }; { auto transaction{ session.createReadTransaction() }; EXPECT_EQ(track->getYear(), std::nullopt); @@ -275,22 +275,16 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - EXPECT_EQ(track->getYear(), std::nullopt); - EXPECT_EQ(track->getOriginalYear(), std::nullopt); + EXPECT_EQ(track->getYear(), 1995); + EXPECT_EQ(track->getOriginalYear(), 1994); EXPECT_EQ(track->getDate(), date); EXPECT_EQ(track->getOriginalDate(), originalDate); } - { - auto transaction{ session.createWriteTransaction() }; - track.get().modify()->setYear(date.year()); - track.get().modify()->setOriginalYear(originalDate.year()); - } - { auto transaction{ session.createReadTransaction() }; - EXPECT_EQ(track->getYear(), date.year()); - EXPECT_EQ(track->getOriginalYear(), originalDate.year()); + EXPECT_EQ(track->getYear(), date.getYear()); + EXPECT_EQ(track->getOriginalYear(), originalDate.getYear()); } } @@ -383,4 +377,54 @@ namespace lms::db::tests EXPECT_EQ(track->getComment(), "MyComment"); } } + + TEST_F(DatabaseFixture, Track_sortDateAdded) + { + ScopedTrack track1{ session }; + ScopedTrack track2{ session }; + ScopedTrack track3{ session }; + ScopedTrack track4{ session }; + + { + auto transaction{ session.createWriteTransaction() }; + track1.get().modify()->setAddedTime(core::PartialDateTime{ 2021, 1, 2 }); + track2.get().modify()->setAddedTime(core::PartialDateTime{ 2021, 1, 1 }); + track4.get().modify()->setAddedTime(core::PartialDateTime{ 2021, 1, 2, 15, 36, 24 }); + } + + { + auto transaction{ session.createReadTransaction() }; + const auto tracks{ Track::findIds(session, Track::FindParameters{}.setSortMethod(TrackSortMethod::AddedDesc)) }; + ASSERT_EQ(tracks.results.size(), 4); + EXPECT_EQ(tracks.results[0], track4.getId()); + EXPECT_EQ(tracks.results[1], track1.getId()); + EXPECT_EQ(tracks.results[2], track2.getId()); + EXPECT_EQ(tracks.results[3], track3.getId()); + } + } + + TEST_F(DatabaseFixture, Track_sortLastWritten) + { + ScopedTrack track1{ session }; + ScopedTrack track2{ session }; + ScopedTrack track3{ session }; + ScopedTrack track4{ session }; + + { + auto transaction{ session.createWriteTransaction() }; + track1.get().modify()->setLastWriteTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 2 } }); + track2.get().modify()->setLastWriteTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 1 } }); + track4.get().modify()->setLastWriteTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 2 }, Wt::WTime{ 15, 36, 24 } }); + } + + { + auto transaction{ session.createReadTransaction() }; + const auto tracks{ Track::findIds(session, Track::FindParameters{}.setSortMethod(TrackSortMethod::LastWrittenDesc)) }; + ASSERT_EQ(tracks.results.size(), 4); + EXPECT_EQ(tracks.results[0], track4.getId()); + EXPECT_EQ(tracks.results[1], track1.getId()); + EXPECT_EQ(tracks.results[2], track2.getId()); + EXPECT_EQ(tracks.results[3], track3.getId()); + } + } } // namespace lms::db::tests \ No newline at end of file diff --git a/src/libs/image/CMakeLists.txt b/src/libs/image/CMakeLists.txt index 15555911..015feaac 100644 --- a/src/libs/image/CMakeLists.txt +++ b/src/libs/image/CMakeLists.txt @@ -1,5 +1,5 @@ -add_library(lmsimage SHARED +add_library(lmsimage STATIC impl/EncodedImage.cpp ) @@ -27,6 +27,9 @@ if (${LMS_IMAGE_BACKEND} STREQUAL "stb") target_sources(lmsimage PRIVATE impl/stb/Image.cpp impl/stb/RawImage.cpp + impl/stb/StbImage.cpp + impl/stb/StbImageResize.cpp + impl/stb/StbImageWrite.cpp ) target_compile_options(lmsimage PRIVATE "-DSTB_IMAGE_RESIZE_VERSION=${STB_IMAGE_RESIZE_VERSION}") target_include_directories(lmsimage PRIVATE ${STB_IMAGE_INCLUDE_DIR}) @@ -44,6 +47,3 @@ elseif (${LMS_IMAGE_BACKEND} STREQUAL "graphicsmagick") else () message(FATAL_ERROR "Invalid image library") endif () - -install(TARGETS lmsimage DESTINATION ${CMAKE_INSTALL_LIBDIR}) - diff --git a/src/libs/image/impl/EncodedImage.cpp b/src/libs/image/impl/EncodedImage.cpp index bef1cbca..53ea72c2 100644 --- a/src/libs/image/impl/EncodedImage.cpp +++ b/src/libs/image/impl/EncodedImage.cpp @@ -53,7 +53,7 @@ namespace lms::image { LMS_SCOPED_TRACE_DETAILED("Image", "ReadFile"); - std::ifstream ifs{ p.string(), std::ios::binary }; + std::ifstream ifs{ p, std::ios::binary }; if (!ifs.is_open()) throw Exception{ "Cannot open file '" + p.string() + "' for reading purpose" }; diff --git a/src/libs/image/impl/graphicsmagick/Image.cpp b/src/libs/image/impl/graphicsmagick/Image.cpp index 16f54fbe..8f4c7605 100644 --- a/src/libs/image/impl/graphicsmagick/Image.cpp +++ b/src/libs/image/impl/graphicsmagick/Image.cpp @@ -21,6 +21,8 @@ #include +#include + #include "core/ILogger.hpp" #include "core/ITraceLogger.hpp" #include "image/Exception.hpp" @@ -32,7 +34,7 @@ namespace lms::image { void init(const std::filesystem::path& path) { - Magick::InitializeMagick(path.string().c_str()); + Magick::InitializeMagick(path.c_str()); if (auto nbThreads{ MagickLib::GetMagickResourceLimit(MagickLib::ThreadsResource) }; nbThreads != 1) LMS_LOG(COVER, WARNING, "Consider setting env var OMP_NUM_THREADS=1 to save resources"); @@ -53,6 +55,28 @@ namespace lms::image return fileExtensions; } + ImageProperties probeImage(const std::filesystem::path& path) + { + LMS_SCOPED_TRACE_DETAILED("Image", "ProbeFile"); + + try + { + Magick::Image image; + image.ping(path.c_str()); + + ImageProperties properties; + properties.width = image.size().width(); + properties.height = image.size().height(); + + return properties; + } + catch (Magick::Exception& e) + { + LMS_LOG(COVER, ERROR, "Caught Magick exception: " << e.what()); + throw Exception{ std::string{ "Magick probe error: " } + e.what() }; + } + } + std::unique_ptr decodeImage(std::span encodedData) { LMS_SCOPED_TRACE_DETAILED("Image", "DecodeBuffer"); diff --git a/src/libs/image/impl/graphicsmagick/RawImage.cpp b/src/libs/image/impl/graphicsmagick/RawImage.cpp index 3e7d017a..eec24463 100644 --- a/src/libs/image/impl/graphicsmagick/RawImage.cpp +++ b/src/libs/image/impl/graphicsmagick/RawImage.cpp @@ -54,7 +54,7 @@ namespace lms::image::GraphicsMagick { try { - _image.read(p.string().c_str()); + _image.read(p.c_str()); } catch (Magick::WarningCoder& e) { diff --git a/src/libs/image/impl/stb/Image.cpp b/src/libs/image/impl/stb/Image.cpp index 28e443d2..ec9432bf 100644 --- a/src/libs/image/impl/stb/Image.cpp +++ b/src/libs/image/impl/stb/Image.cpp @@ -21,14 +21,14 @@ #include -#define STB_IMAGE_WRITE_IMPLEMENTATION -#include +#include "StbImage.hpp" +#include "StbImageWrite.hpp" #include "core/ITraceLogger.hpp" +#include "image/Exception.hpp" #include "EncodedImage.hpp" #include "RawImage.hpp" -#include "image/Exception.hpp" namespace lms::image { @@ -42,6 +42,24 @@ namespace lms::image return fileExtensions; } + ImageProperties probeImage(const std::filesystem::path& path) + { + LMS_SCOPED_TRACE_DETAILED("Image", "ProbeFile"); + + int x{}; + int y{}; + int comp{}; + + if (::stbi_info(path.c_str(), &x, &y, &comp) == 0) + throw StbiException{ "Probe failed" }; + + ImageProperties properties; + properties.width = x; + properties.height = y; + + return properties; + } + std::unique_ptr decodeImage(std::span encodedData) { LMS_SCOPED_TRACE_DETAILED("Image", "DecodeBuffer"); diff --git a/src/libs/image/impl/stb/RawImage.cpp b/src/libs/image/impl/stb/RawImage.cpp index 99021bf1..f4c420af 100644 --- a/src/libs/image/impl/stb/RawImage.cpp +++ b/src/libs/image/impl/stb/RawImage.cpp @@ -19,42 +19,33 @@ #include "RawImage.hpp" -#define STB_IMAGE_IMPLEMENTATION -#define STB_IMAGE_RESIZE_IMPLEMENTATION - -#define STBIR_DEFAULT_FILTER_DOWNSAMPLE STBIR_FILTER_MITCHELL -#define STBIR_DEFAULT_FILTER_UPSAMPLE STBIR_FILTER_CATMULLROM - -#define STBI_FAILURE_USERMSG - -#include -#if STB_IMAGE_RESIZE_VERSION == 1 - #include -#elif STB_IMAGE_RESIZE_VERSION == 2 - #include -#else - #error "Unhandled STB image resize version"! -#endif +#include "StbImage.hpp" +#include "StbImageResize.hpp" #include "core/ITraceLogger.hpp" #include "image/Exception.hpp" namespace lms::image::STB { + namespace + { + + } // namespace + RawImage::RawImage(std::span encodedData) { int n{}; _data = UniquePtrFree{ ::stbi_load_from_memory(reinterpret_cast(encodedData.data()), encodedData.size(), &_width, &_height, &n, 3), std::free }; if (!_data) - throw Exception{ "Cannot load image from memory: " + std::string{ ::stbi_failure_reason() } }; + throw StbiException{ "Cannot load image from memory" }; } RawImage::RawImage(const std::filesystem::path& p) { int n{}; - _data = UniquePtrFree{ stbi_load(p.string().c_str(), &_width, &_height, &n, 3), std::free }; + _data = UniquePtrFree{ stbi_load(p.c_str(), &_width, &_height, &n, 3), std::free }; if (!_data) - throw Exception{ "Cannot load image from file: " + std::string{ ::stbi_failure_reason() } }; + throw StbiException{ "Cannot load image from file" }; } void RawImage::resize(ImageSize width) @@ -94,7 +85,7 @@ namespace lms::image::STB #error "Unhandled STB image resize version"! #endif { - throw Exception{ "Failed to resize image:" + std::string{ ::stbi_failure_reason() } }; + throw StbiException{ "Failed to resize image" }; } _data = std::move(resizedData); diff --git a/src/libs/image/impl/stb/StbImage.cpp b/src/libs/image/impl/stb/StbImage.cpp new file mode 100644 index 00000000..7432daad --- /dev/null +++ b/src/libs/image/impl/stb/StbImage.cpp @@ -0,0 +1,35 @@ +/* + * 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 . + */ + +#define STB_IMAGE_IMPLEMENTATION +#include "StbImage.hpp" + +namespace lms::image +{ + StbiException::StbiException(std::string_view desc) + : Exception{ std::string{ desc } + ": " + getLastFailureReason() } + { + } + + std::string StbiException::getLastFailureReason() + { + const char* failureReason{ ::stbi_failure_reason() }; + return failureReason ? failureReason : "unknown reason"; + } +} // namespace lms::image \ No newline at end of file diff --git a/src/libs/image/impl/stb/StbImage.hpp b/src/libs/image/impl/stb/StbImage.hpp new file mode 100644 index 00000000..09bc88ac --- /dev/null +++ b/src/libs/image/impl/stb/StbImage.hpp @@ -0,0 +1,42 @@ +/* + * 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 . + */ +#pragma once + +#define STBI_ONLY_JPEG +#define STBI_ONLY_PNG +#define STBI_ONLY_BMP +#define STBI_FAILURE_USERMSG + +#include + +#include + +#include "image/Exception.hpp" + +namespace lms::image +{ + class StbiException : public Exception + { + public: + StbiException(std::string_view desc); + + private: + static std::string getLastFailureReason(); + }; +} // namespace lms::image \ No newline at end of file diff --git a/src/libs/image/impl/stb/StbImageResize.cpp b/src/libs/image/impl/stb/StbImageResize.cpp new file mode 100644 index 00000000..35054deb --- /dev/null +++ b/src/libs/image/impl/stb/StbImageResize.cpp @@ -0,0 +1,21 @@ +/* + * 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 . + */ + +#define STB_IMAGE_RESIZE_IMPLEMENTATION +#include "StbImageResize.hpp" diff --git a/src/libs/image/impl/stb/StbImageResize.hpp b/src/libs/image/impl/stb/StbImageResize.hpp new file mode 100644 index 00000000..53de1342 --- /dev/null +++ b/src/libs/image/impl/stb/StbImageResize.hpp @@ -0,0 +1,31 @@ +/* + * 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 . + */ + +#pragma once + +#define STBIR_DEFAULT_FILTER_DOWNSAMPLE STBIR_FILTER_MITCHELL +#define STBIR_DEFAULT_FILTER_UPSAMPLE STBIR_FILTER_CATMULLROM + +#if STB_IMAGE_RESIZE_VERSION == 1 + #include +#elif STB_IMAGE_RESIZE_VERSION == 2 + #include +#else + #error "Unhandled STB image resize version"! +#endif diff --git a/src/libs/image/impl/stb/StbImageWrite.cpp b/src/libs/image/impl/stb/StbImageWrite.cpp new file mode 100644 index 00000000..bca30deb --- /dev/null +++ b/src/libs/image/impl/stb/StbImageWrite.cpp @@ -0,0 +1,21 @@ +/* + * 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 . + */ + +#define STB_IMAGE_WRITE_IMPLEMENTATION +#include "StbImageWrite.hpp" diff --git a/src/libs/image/impl/stb/StbImageWrite.hpp b/src/libs/image/impl/stb/StbImageWrite.hpp new file mode 100644 index 00000000..ff741442 --- /dev/null +++ b/src/libs/image/impl/stb/StbImageWrite.hpp @@ -0,0 +1,21 @@ +/* + * 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 . + */ +#pragma once + +#include \ No newline at end of file diff --git a/src/libs/image/include/image/Image.hpp b/src/libs/image/include/image/Image.hpp index 39fc67f8..594d78fd 100644 --- a/src/libs/image/include/image/Image.hpp +++ b/src/libs/image/include/image/Image.hpp @@ -31,6 +31,9 @@ namespace lms::image void init(const std::filesystem::path& path); std::span getSupportedFileExtensions(); + // All these methods may throw Exception + ImageProperties probeImage(const std::filesystem::path& path); + std::unique_ptr decodeImage(std::span encodedData); std::unique_ptr decodeImage(const std::filesystem::path& path); diff --git a/src/libs/image/include/image/Types.hpp b/src/libs/image/include/image/Types.hpp index ccd56849..f513f969 100644 --- a/src/libs/image/include/image/Types.hpp +++ b/src/libs/image/include/image/Types.hpp @@ -22,4 +22,10 @@ namespace lms::image { using ImageSize = std::size_t; -} \ No newline at end of file + + struct ImageProperties + { + ImageSize width{}; + ImageSize height{}; + }; +} // namespace lms::image \ No newline at end of file diff --git a/src/libs/metadata/CMakeLists.txt b/src/libs/metadata/CMakeLists.txt index 67614df0..8fa1167c 100644 --- a/src/libs/metadata/CMakeLists.txt +++ b/src/libs/metadata/CMakeLists.txt @@ -8,7 +8,7 @@ if (BUILD_BENCHMARKS) add_subdirectory(bench) endif() -add_library(lmsmetadata SHARED +add_library(lmsmetadata STATIC impl/AvFormatTagReader.cpp impl/Lyrics.cpp impl/Parser.cpp @@ -34,6 +34,3 @@ target_link_libraries(lmsmetadata PUBLIC lmscore std::filesystem ) - -install(TARGETS lmsmetadata DESTINATION ${CMAKE_INSTALL_LIBDIR}) - diff --git a/src/libs/metadata/impl/AvFormatTagReader.cpp b/src/libs/metadata/impl/AvFormatTagReader.cpp index 7891890a..c3e52bcc 100644 --- a/src/libs/metadata/impl/AvFormatTagReader.cpp +++ b/src/libs/metadata/impl/AvFormatTagReader.cpp @@ -32,6 +32,7 @@ namespace lms::metadata // Mapping to internal avformat names and/or common alternative custom names static const std::unordered_map> tagMapping{ { TagType::AcoustID, { "ACOUSTID_ID", "ACOUSTID ID" } }, + { TagType::Advisory, { "ITUNESADVISORY" } }, { TagType::Album, { "ALBUM", "TALB", "WM/ALBUMTITLE" } }, { TagType::AlbumArtist, { "ALBUMARTIST", "ALBUM_ARTIST" } }, { TagType::AlbumArtistSortOrder, { "ALBUMARTISTSORT", "TSO2" } }, @@ -63,6 +64,7 @@ namespace lms::metadata { TagType::DiscNumber, { "TPOS", "DISC", "DISK", "DISCNUMBER", "WM/PARTOFSET" } }, { TagType::DiscSubtitle, { "TSST", "DISCSUBTITLE", "SETSUBTITLE" } }, { TagType::EncodedBy, { "ENCODEDBY" } }, + { TagType::EncodingTime, { "ENCODINGTIME", "TDEN" } }, { TagType::Engineer, { "ENGINEER" } }, { TagType::GaplessPlayback, { "GAPLESSPLAYBACK" } }, { TagType::Genre, { "GENRE" } }, @@ -105,7 +107,7 @@ namespace lms::metadata { TagType::ProducerSortOrder, { "PRODUCERSORTORDER" } }, { TagType::Producers, { "PRODUCERS" } }, { TagType::ProducersSortOrder, { "PRODUCERSSORTORDER" } }, - { TagType::RecordLabel, { "LABEL", "PUBLISHER" } }, + { TagType::RecordLabel, { "LABEL", "PUBLISHER", "ORGANIZATION" } }, { TagType::ReleaseCountry, { "RELEASECOUNTRY" } }, { TagType::ReleaseDate, { "RELEASEDATE" } }, { TagType::ReleaseStatus, { "RELEASESTATUS" } }, diff --git a/src/libs/metadata/impl/ITagReader.hpp b/src/libs/metadata/impl/ITagReader.hpp index 5cad170a..ce120b94 100644 --- a/src/libs/metadata/impl/ITagReader.hpp +++ b/src/libs/metadata/impl/ITagReader.hpp @@ -26,12 +26,13 @@ namespace lms::metadata { - // using picard internal names + // prefer using picard internal names // see https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html enum class TagType { AcoustID, AcoustIDFingerprint, + Advisory, // non standard Album, AlbumArtist, AlbumArtists, // non standard @@ -64,6 +65,7 @@ namespace lms::metadata DiscSubtitle, EncodedBy, EncoderSettings, + EncodingTime, Engineer, GaplessPlayback, Genre, diff --git a/src/libs/metadata/impl/Lyrics.cpp b/src/libs/metadata/impl/Lyrics.cpp index 10b0d86b..028d7cbf 100644 --- a/src/libs/metadata/impl/Lyrics.cpp +++ b/src/libs/metadata/impl/Lyrics.cpp @@ -28,7 +28,7 @@ namespace lms::metadata { std::span getSupportedLyricsFileExtensions() { - static const std::array fileExtensions{ ".lrc" }; // TODO handle ".txt" and ".elrc" + static const std::array fileExtensions{ ".lrc", ".txt" }; // TODO handle ".elrc" return fileExtensions; } diff --git a/src/libs/metadata/impl/Parser.cpp b/src/libs/metadata/impl/Parser.cpp index ed013ad0..564ce5d6 100644 --- a/src/libs/metadata/impl/Parser.cpp +++ b/src/libs/metadata/impl/Parser.cpp @@ -22,6 +22,7 @@ #include #include "core/ILogger.hpp" +#include "core/PartialDateTime.hpp" #include "core/String.hpp" #include "metadata/Exception.hpp" @@ -246,6 +247,25 @@ namespace lms::metadata return artistDisplayName; } + + std::optional getAdvisory(const ITagReader& tagReader) + { + if (const auto value{ getTagValueAs(tagReader, TagType::Advisory) }) + { + switch (*value) + { + case 1: + case 4: + return Track::Advisory::Explicit; + case 2: + return Track::Advisory::Clean; + case 0: + return Track::Advisory::Unknown; + } + } + + return std::nullopt; + } } // namespace std::unique_ptr createParser(ParserBackend parserBackend, ParserReadStyle parserReadStyle) @@ -317,7 +337,7 @@ namespace lms::metadata } catch (const Exception& e) { - LMS_LOG(METADATA, ERROR, "File '" << p.string() << "': parsing failed"); + LMS_LOG(METADATA, ERROR, "File " << p << ": parsing failed"); throw ParseException{}; } } @@ -341,35 +361,27 @@ namespace lms::metadata track.recordingMBID = getTagValueAs(tagReader, TagType::MusicBrainzRecordingID); track.acoustID = getTagValueAs(tagReader, TagType::AcoustID); track.position = getTagValueAs(tagReader, TagType::TrackNumber); // May parse 'Number/Total', that's fine - if (auto dateStr = getTagValueAs(tagReader, TagType::Date)) + if (const auto dateStr{ getTagValueAs(tagReader, TagType::Date) }) { - if (const Wt::WDate date{ utils::parseDate(*dateStr) }; date.isValid()) - { + if (const core::PartialDateTime date{ core::PartialDateTime::fromString(*dateStr) }; date.isValid()) track.date = date; - track.year = date.year(); - } - else - { - track.year = utils::parseYear(*dateStr); - } } - if (auto dateStr = getTagValueAs(tagReader, TagType::OriginalReleaseDate)) + if (const auto dateStr = getTagValueAs(tagReader, TagType::OriginalReleaseDate)) { - if (const Wt::WDate date{ utils::parseDate(*dateStr) }; date.isValid()) - { + if (const core::PartialDateTime date{ core::PartialDateTime::fromString(*dateStr) }; date.isValid()) track.originalDate = date; - track.originalYear = date.year(); - } - else - { - track.originalYear = utils::parseYear(*dateStr); - } } - if (auto dateStr = getTagValueAs(tagReader, TagType::OriginalReleaseYear)) - { + if (const auto dateStr{ getTagValueAs(tagReader, TagType::OriginalReleaseYear) }) track.originalYear = utils::parseYear(*dateStr); + + if (const auto encodingTimeStr{ getTagValueAs(tagReader, TagType::EncodingTime) }) + { + if (const core::PartialDateTime date{ core::PartialDateTime::fromString(*encodingTimeStr) }; date.isValid()) + track.encodingTime = date; } + track.advisory = getAdvisory(tagReader); + track.lyrics = getLyrics(tagReader); // no custom delimiter on lyrics track.comments = getTagValuesAs(tagReader, TagType::Comment, {} /* no custom delimiter on comments */); track.copyright = getTagValueAs(tagReader, TagType::Copyright).value_or(""); @@ -404,13 +416,9 @@ namespace lms::metadata track.remixerArtists = getArtists(tagReader, { TagType::Remixers, TagType::Remixer }, { TagType::RemixersSortOrder, TagType::RemixerSortOrder }, {}, _artistTagDelimiters, _defaultTagDelimiters); track.performerArtists = getPerformerArtists(tagReader); // artistDelimiters not supported - // If a file has date but no year, set it - if (!track.year && track.date.isValid()) - track.year = track.date.year(); - // If a file has originalDate but no originalYear, set it - if (!track.originalYear && track.originalDate.isValid()) - track.originalYear = track.originalDate.year(); + if (!track.originalYear) + track.originalYear = track.originalDate.getYear(); } std::optional Parser::getMedium(const ITagReader& tagReader) diff --git a/src/libs/metadata/impl/TagLibTagReader.cpp b/src/libs/metadata/impl/TagLibTagReader.cpp index c955c802..984409cf 100644 --- a/src/libs/metadata/impl/TagLibTagReader.cpp +++ b/src/libs/metadata/impl/TagLibTagReader.cpp @@ -48,6 +48,10 @@ #include "core/String.hpp" #include "metadata/Exception.hpp" +#if (TAGLIB_MAJOR_VERSION > 2) || (TAGLIB_MAJOR_VERSION == 2 && TAGLIB_MINOR_VERSION > 0) + #define TAGLIB_HAS_MP4_ITEM_TYPE +#endif + namespace lms::metadata { namespace @@ -59,6 +63,7 @@ namespace lms::metadata // Mapping to internal taglib names and/or common alternative custom names const std::unordered_map> tagMapping{ { TagType::AcoustID, { "ACOUSTID_ID", "ACOUSTID ID" } }, + { TagType::Advisory, { "ITUNESADVISORY" } }, { TagType::Album, { "ALBUM" } }, { TagType::AlbumArtist, { "ALBUMARTIST" } }, { TagType::AlbumArtistSortOrder, { "ALBUMARTISTSORT" } }, @@ -91,6 +96,7 @@ namespace lms::metadata { TagType::DiscSubtitle, { "DISCSUBTITLE", "SETSUBTITLE" } }, { TagType::EncodedBy, { "ENCODEDBY" } }, { TagType::Engineer, { "ENGINEER" } }, + { TagType::EncodingTime, { "ENCODINGTIME" } }, { TagType::GaplessPlayback, { "GAPLESSPLAYBACK" } }, { TagType::Genre, { "GENRE" } }, { TagType::Grouping, { "GROUPING", "ALBUMGROUPING" } }, @@ -132,7 +138,7 @@ namespace lms::metadata { TagType::ProducerSortOrder, { "PRODUCERSORTORDER" } }, { TagType::Producers, { "PRODUCERS" } }, { TagType::ProducersSortOrder, { "PRODUCERSSORTORDER" } }, - { TagType::RecordLabel, { "LABEL" } }, + { TagType::RecordLabel, { "LABEL", "PUBLISHER", "ORGANIZATION" } }, { TagType::ReleaseCountry, { "RELEASECOUNTRY" } }, { TagType::ReleaseDate, { "RELEASEDATE" } }, { TagType::ReleaseStatus, { "RELEASESTATUS" } }, @@ -188,7 +194,7 @@ namespace lms::metadata { LMS_SCOPED_TRACE_DETAILED("MetaData", "TagLibParseFile"); - return TagLib::FileRef{ p.string().c_str(), true // read audio properties + return TagLib::FileRef{ p.c_str(), true // read audio properties , readStyleToTagLibReadStyle(parserReadStyle) }; } @@ -199,13 +205,13 @@ namespace lms::metadata { if (_file.isNull()) { - LMS_LOG(METADATA, ERROR, "File '" << p.string() << "': parsing failed"); + LMS_LOG(METADATA, ERROR, "File " << p << ": parsing failed"); throw ParsingFailedException{}; } if (!_file.audioProperties()) { - LMS_LOG(METADATA, ERROR, "File '" << p.string() << "': no audio properties"); + LMS_LOG(METADATA, ERROR, "File " << p << ": no audio properties"); throw ParsingFailedException{}; } @@ -306,6 +312,12 @@ namespace lms::metadata if (!attributes.isEmpty()) _propertyMap[strName] = std::move(attributes); } + + if (auto itAuthor{ _propertyMap.find("AUTHOR") }; itAuthor != _propertyMap.end() && _propertyMap.unsupportedData().contains("Author")) + { + if (!_propertyMap.contains("ARTISTS")) + _propertyMap["ARTIST"].append(itAuthor->second); + } } } // MP3 @@ -319,10 +331,22 @@ namespace lms::metadata // MP4 else if (TagLib::MP4::File * mp4File{ dynamic_cast(_file.file()) }) { - TagLib::MP4::Item coverItem{ mp4File->tag()->item("covr") }; - TagLib::MP4::CoverArtList coverArtList{ coverItem.toCoverArtList() }; - if (!coverArtList.isEmpty()) - _hasEmbeddedCover = true; + if (const TagLib::MP4::Item coverItem{ mp4File->tag()->item("covr") }; coverItem.isValid()) + { +#if TAGLIB_HAS_MP4_ITEM_TYPE + if (coverItem.type() == TagLib::MP4::Item::Type::CoverArtList) +#endif + _hasEmbeddedCover = true; + } + + // Taglib does not expose rtng in properties + if (const TagLib::MP4::Item rtngItem{ mp4File->tag()->item("rtng") }; rtngItem.isValid()) + { +#if TAGLIB_HAS_MP4_ITEM_TYPE + if (rtngItem.type() == TagLib::MP4::Item::Type::Byte) +#endif + _propertyMap["ITUNESADVISORY"] = TagLib::String{ std::to_string(rtngItem.toByte()) }; + } if (!_propertyMap.contains("ORIGINALDATE")) { diff --git a/src/libs/metadata/impl/Utils.cpp b/src/libs/metadata/impl/Utils.cpp index 6f87545f..1f7158a9 100644 --- a/src/libs/metadata/impl/Utils.cpp +++ b/src/libs/metadata/impl/Utils.cpp @@ -18,6 +18,7 @@ */ #include "Utils.hpp" + #include #include #include diff --git a/src/libs/metadata/include/metadata/Types.hpp b/src/libs/metadata/include/metadata/Types.hpp index 1674b927..021f5668 100644 --- a/src/libs/metadata/include/metadata/Types.hpp +++ b/src/libs/metadata/include/metadata/Types.hpp @@ -26,8 +26,7 @@ #include #include -#include - +#include "core/PartialDateTime.hpp" #include "core/UUID.hpp" #include "Lyrics.hpp" @@ -102,6 +101,12 @@ namespace lms::metadata struct Track { + enum class Advisory + { + Unknown, + Explicit, + Clean, + }; AudioProperties audioProperties; std::optional mbid; std::optional recordingMBID; @@ -113,10 +118,11 @@ namespace lms::metadata std::vector moods; std::vector languages; Tags userExtraTags; - std::optional year{}; - Wt::WDate date; - std::optional originalYear{}; - Wt::WDate originalDate; + core::PartialDateTime date; + std::optional originalYear; + core::PartialDateTime originalDate; + std::optional advisory; + core::PartialDateTime encodingTime; bool hasCover{}; std::optional acoustID; std::string copyright; diff --git a/src/libs/metadata/test/Parser.cpp b/src/libs/metadata/test/Parser.cpp index 3345e604..7d0a5e14 100644 --- a/src/libs/metadata/test/Parser.cpp +++ b/src/libs/metadata/test/Parser.cpp @@ -17,11 +17,12 @@ * along with LMS. If not, see . */ -#include #include #include +#include + #include "Parser.hpp" #include "TestTagReader.hpp" @@ -33,6 +34,7 @@ namespace lms::metadata TestTagReader testTags{ { { TagType::AcoustID, { "e987a441-e134-4960-8019-274eddacc418" } }, + { TagType::Advisory, { "2" } }, { TagType::Album, { "MyAlbum" } }, { TagType::AlbumSortOrder, { "MyAlbumSortName" } }, { TagType::Artist, { "MyArtist1 & MyArtist2" } }, @@ -99,6 +101,8 @@ namespace lms::metadata } EXPECT_EQ(track->acoustID, core::UUID::fromString("e987a441-e134-4960-8019-274eddacc418")); + ASSERT_TRUE(track->advisory.has_value()); + EXPECT_EQ(track->advisory.value(), Track::Advisory::Clean); EXPECT_EQ(track->artistDisplayName, "MyArtist1 & MyArtist2"); ASSERT_EQ(track->artists.size(), 2); EXPECT_EQ(track->artists[0].name, "MyArtist1"); @@ -121,9 +125,9 @@ namespace lms::metadata EXPECT_EQ(track->copyright, "MyCopyright"); EXPECT_EQ(track->copyrightURL, "MyCopyrightURL"); ASSERT_TRUE(track->date.isValid()); - EXPECT_EQ(track->date.year(), 2020); - EXPECT_EQ(track->date.month(), 3); - EXPECT_EQ(track->date.day(), 4); + EXPECT_EQ(track->date.getYear(), 2020); + EXPECT_EQ(track->date.getMonth(), 3); + EXPECT_EQ(track->date.getDay(), 4); EXPECT_FALSE(track->hasCover); ASSERT_EQ(track->genres.size(), 2); EXPECT_EQ(track->genres[0], "Genre1"); @@ -153,9 +157,9 @@ namespace lms::metadata EXPECT_EQ(track->moods[0], "Mood1"); EXPECT_EQ(track->moods[1], "Mood2"); ASSERT_TRUE(track->originalDate.isValid()); - EXPECT_EQ(track->originalDate.year(), 2019); - EXPECT_EQ(track->originalDate.month(), 2); - EXPECT_EQ(track->originalDate.day(), 3); + EXPECT_EQ(track->originalDate.getYear(), 2019); + EXPECT_EQ(track->originalDate.getMonth(), 2); + EXPECT_EQ(track->originalDate.getDay(), 3); ASSERT_TRUE(track->originalYear.has_value()); EXPECT_EQ(track->originalYear.value(), 2019); ASSERT_TRUE(track->performerArtists.contains("Rolea")); @@ -184,8 +188,6 @@ namespace lms::metadata ASSERT_EQ(track->userExtraTags["MY_AWESOME_TAG_B"].size(), 2); EXPECT_EQ(track->userExtraTags["MY_AWESOME_TAG_B"][0], "MyTagValue1ForTagB"); EXPECT_EQ(track->userExtraTags["MY_AWESOME_TAG_B"][1], "MyTagValue2ForTagB"); - ASSERT_TRUE(track->year.has_value()); - EXPECT_EQ(track->year.value(), 2020); // Medium ASSERT_TRUE(track->medium.has_value()); @@ -581,4 +583,79 @@ namespace lms::metadata EXPECT_EQ(track->artists[1].mbid, std::nullopt); EXPECT_EQ(track->artistDisplayName, "Artist1, Artist2"); // reconstruct the artist display name } + + TEST(Parser, advisory) + { + auto doTest = [](std::string_view value, std::optional expectedValue) { + const TestTagReader testTags{ + { + { TagType::Advisory, { value } }, + } + }; + + Parser parser; + std::unique_ptr track{ Parser{}.parse(testTags) }; + + ASSERT_EQ(track->advisory.has_value(), expectedValue.has_value()) << "Value = '" << value << "'"; + if (track->advisory.has_value()) + { + EXPECT_EQ(track->advisory.value(), expectedValue); + } + }; + + doTest("0", Track::Advisory::Unknown); + doTest("1", Track::Advisory::Explicit); + doTest("4", Track::Advisory::Explicit); + doTest("2", Track::Advisory::Clean); + doTest("", std::nullopt); + doTest("3", std::nullopt); + } + + TEST(Parser, encodingTime) + { + auto doTest = [](std::string_view value, core::PartialDateTime expectedValue) { + const TestTagReader testTags{ + { + { TagType::EncodingTime, { value } }, + } + }; + + Parser parser; + std::unique_ptr track{ Parser{}.parse(testTags) }; + + ASSERT_EQ(track->encodingTime, expectedValue) << "Value = '" << value << "'"; + }; + + doTest("", core::PartialDateTime{}); + doTest("foo", core::PartialDateTime{}); + doTest("2020-01-03T09:08:11.075", core::PartialDateTime{ 2020, 01, 03, 9, 8, 11 }); + doTest("2020-01-03", core::PartialDateTime{ 2020, 01, 03 }); + doTest("2020/01/03", core::PartialDateTime{ 2020, 01, 03 }); + } + + TEST(Parser, date) + { + auto doTest = [](std::string_view value, core::PartialDateTime expectedValue) { + const TestTagReader testTags{ + { + { TagType::Date, { value } }, + } + }; + + Parser parser; + std::unique_ptr track{ Parser{}.parse(testTags) }; + + ASSERT_EQ(track->date, expectedValue) << "Value = '" << value << "'"; + }; + + doTest("", core::PartialDateTime{}); + doTest("foo", core::PartialDateTime{}); + doTest("2020-01-03", core::PartialDateTime{ 2020, 01, 03 }); + doTest("2020-01", core::PartialDateTime{ 2020, 1 }); + doTest("2020", core::PartialDateTime{ 2020 }); + doTest("2020/01/03", core::PartialDateTime{ 2020, 01, 03 }); + doTest("2020/01", core::PartialDateTime{ 2020, 1 }); + doTest("2020", core::PartialDateTime{ 2020 }); + } + } // namespace lms::metadata diff --git a/src/libs/services/artwork/CMakeLists.txt b/src/libs/services/artwork/CMakeLists.txt index 1d18565a..74c3f91c 100644 --- a/src/libs/services/artwork/CMakeLists.txt +++ b/src/libs/services/artwork/CMakeLists.txt @@ -1,5 +1,5 @@ -add_library(lmsartwork SHARED +add_library(lmsartwork STATIC impl/ImageCache.cpp impl/ArtworkService.cpp ) @@ -24,6 +24,3 @@ target_link_libraries(lmsartwork PUBLIC lmscore std::filesystem ) - -install(TARGETS lmsartwork DESTINATION ${CMAKE_INSTALL_LIBDIR}) - diff --git a/src/libs/services/artwork/impl/ArtworkService.cpp b/src/libs/services/artwork/impl/ArtworkService.cpp index fccd74b0..3b02b1ef 100644 --- a/src/libs/services/artwork/impl/ArtworkService.cpp +++ b/src/libs/services/artwork/impl/ArtworkService.cpp @@ -59,7 +59,7 @@ namespace lms::cover { setJpegQuality(core::Service::get()->getULong("cover-jpeg-quality", 75)); - LMS_LOG(COVER, INFO, "Default release cover path = '" << defaultReleaseCoverSvgPath.string() << "'"); + LMS_LOG(COVER, INFO, "Default release cover path = " << defaultReleaseCoverSvgPath); LMS_LOG(COVER, INFO, "Max cache size = " << _cache.getMaxCacheSize()); _defaultReleaseCover = image::readImage(defaultReleaseCoverSvgPath); // may throw @@ -93,7 +93,7 @@ namespace lms::cover std::vector candidatePictures; std::size_t pictureIndex{}; input.visitAttachedPictures([&](const av::Picture& picture, const av::IAudioFile::MetadataMap& metadata) { - candidatePictures.emplace_back(picture, metadataHasFrontKeyword(metadata), pictureIndex++); + candidatePictures.emplace_back(CandidatePicture{ picture, metadataHasFrontKeyword(metadata), pictureIndex++ }); }); std::stable_sort(std::begin(candidatePictures), std::end(candidatePictures), std::greater<>()); @@ -141,7 +141,7 @@ namespace lms::cover } catch (const image::Exception& e) { - LMS_LOG(COVER, ERROR, "Cannot read cover in file '" << p.string() << "': " << e.what()); + LMS_LOG(COVER, ERROR, "Cannot read cover in file " << p << ": " << e.what()); } return image; @@ -183,7 +183,7 @@ namespace lms::cover } catch (av::Exception& e) { - LMS_LOG(COVER, ERROR, "Cannot get covers from track " << p.string() << ": " << e.what()); + LMS_LOG(COVER, ERROR, "Cannot get covers from track " << p << ": " << e.what()); } return image; diff --git a/src/libs/services/auth/CMakeLists.txt b/src/libs/services/auth/CMakeLists.txt index 45c6557d..3558812d 100644 --- a/src/libs/services/auth/CMakeLists.txt +++ b/src/libs/services/auth/CMakeLists.txt @@ -1,5 +1,5 @@ -add_library(lmsauth SHARED +add_library(lmsauth STATIC impl/AuthTokenService.cpp impl/AuthServiceBase.cpp impl/EnvService.cpp @@ -50,6 +50,3 @@ if (USE_PAM) target_include_directories(lmsauth PRIVATE ${PAM_INCLUDE_DIR}) target_link_libraries(lmsauth PRIVATE ${PAM_LIBRARIES}) endif (USE_PAM) - -install(TARGETS lmsauth DESTINATION ${CMAKE_INSTALL_LIBDIR}) - diff --git a/src/libs/services/feedback/CMakeLists.txt b/src/libs/services/feedback/CMakeLists.txt index d5d143cb..ca9c9682 100644 --- a/src/libs/services/feedback/CMakeLists.txt +++ b/src/libs/services/feedback/CMakeLists.txt @@ -1,5 +1,5 @@ -add_library(lmsfeedback SHARED +add_library(lmsfeedback STATIC impl/internal/InternalBackend.cpp impl/listenbrainz/FeedbacksParser.cpp impl/listenbrainz/FeedbacksSynchronizer.cpp @@ -25,5 +25,3 @@ target_link_libraries(lmsfeedback PRIVATE target_link_libraries(lmsfeedback PUBLIC lmsdatabase ) - -install(TARGETS lmsfeedback DESTINATION ${CMAKE_INSTALL_LIBDIR}) diff --git a/src/libs/services/recommendation/CMakeLists.txt b/src/libs/services/recommendation/CMakeLists.txt index 603a66b0..b31cf280 100644 --- a/src/libs/services/recommendation/CMakeLists.txt +++ b/src/libs/services/recommendation/CMakeLists.txt @@ -1,5 +1,5 @@ -add_library(lmsrecommendation SHARED +add_library(lmsrecommendation STATIC impl/clusters/ClustersEngine.cpp impl/features/FeaturesEngineCache.cpp impl/features/FeaturesEngine.cpp @@ -25,6 +25,3 @@ target_link_libraries(lmsrecommendation PRIVATE lmssom std::filesystem ) - -install(TARGETS lmsrecommendation DESTINATION ${CMAKE_INSTALL_LIBDIR}) - diff --git a/src/libs/services/recommendation/impl/features/FeaturesEngineCache.cpp b/src/libs/services/recommendation/impl/features/FeaturesEngineCache.cpp index 3934e3a1..5734fe45 100644 --- a/src/libs/services/recommendation/impl/features/FeaturesEngineCache.cpp +++ b/src/libs/services/recommendation/impl/features/FeaturesEngineCache.cpp @@ -32,7 +32,7 @@ namespace lms::recommendation { std::filesystem::path getCacheDirectory() { - return core::Service::get()->getPath("working-dir") / "cache" / "features"; + return core::Service::get()->getPath("working-dir", "/var/lms") / "cache" / "features"; } std::filesystem::path getCacheNetworkFilePath() @@ -230,7 +230,7 @@ namespace lms::recommendation void FeaturesEngineCache::write() const { - std::filesystem::create_directories(core::Service::get()->getPath("working-dir") / "cache" / "features"); + std::filesystem::create_directories(core::Service::get()->getPath("working-dir", "/var/lms") / "cache" / "features"); if (!networkToCacheFile(_network, getCacheNetworkFilePath()) || !objectPositionToCacheFile(_trackPositions, getCacheTrackPositionsFilePath())) diff --git a/src/libs/services/scanner/CMakeLists.txt b/src/libs/services/scanner/CMakeLists.txt index ee13b263..b4072e3a 100644 --- a/src/libs/services/scanner/CMakeLists.txt +++ b/src/libs/services/scanner/CMakeLists.txt @@ -1,4 +1,4 @@ -add_library(lmsscanner SHARED +add_library(lmsscanner STATIC impl/scanners/AudioFileScanner.cpp impl/scanners/ImageFileScanner.cpp impl/scanners/LyricsFileScanner.cpp @@ -43,6 +43,3 @@ target_link_libraries(lmsscanner PUBLIC std::filesystem Wt::Wt ) - -install(TARGETS lmsscanner DESTINATION ${CMAKE_INSTALL_LIBDIR}) - diff --git a/src/libs/services/scanner/impl/ScannerService.cpp b/src/libs/services/scanner/impl/ScannerService.cpp index 8c3003b9..fc4af817 100644 --- a/src/libs/services/scanner/impl/ScannerService.cpp +++ b/src/libs/services/scanner/impl/ScannerService.cpp @@ -338,7 +338,6 @@ namespace lms::scanner 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); @@ -406,6 +405,8 @@ namespace lms::scanner newSettings.artistTagDelimiters = scanSettings->getArtistTagDelimiters(); newSettings.defaultTagDelimiters = scanSettings->getDefaultTagDelimiters(); + + newSettings.skipSingleReleasePlayLists = scanSettings->getSkipSingleReleasePlayLists(); } return newSettings; diff --git a/src/libs/services/scanner/impl/ScannerSettings.hpp b/src/libs/services/scanner/impl/ScannerSettings.hpp index 9605003a..03a83e3f 100644 --- a/src/libs/services/scanner/impl/ScannerSettings.hpp +++ b/src/libs/services/scanner/impl/ScannerSettings.hpp @@ -42,6 +42,8 @@ namespace lms::scanner std::vector extraTags; std::vector artistTagDelimiters; std::vector defaultTagDelimiters; + bool skipSingleReleasePlayLists{}; + std::vector mediaLibraries; bool operator==(const ScannerSettings& rhs) const = default; diff --git a/src/libs/services/scanner/impl/scanners/AudioFileScanner.cpp b/src/libs/services/scanner/impl/scanners/AudioFileScanner.cpp index e440a030..ad226eee 100644 --- a/src/libs/services/scanner/impl/scanners/AudioFileScanner.cpp +++ b/src/libs/services/scanner/impl/scanners/AudioFileScanner.cpp @@ -22,6 +22,7 @@ #include "core/IConfig.hpp" #include "core/ILogger.hpp" #include "core/ITraceLogger.hpp" +#include "core/PartialDateTime.hpp" #include "core/Path.hpp" #include "core/Service.hpp" #include "database/Artist.hpp" @@ -35,6 +36,7 @@ #include "database/TrackArtistLink.hpp" #include "database/TrackFeatures.hpp" #include "database/TrackLyrics.hpp" +#include "database/Types.hpp" #include "metadata/Exception.hpp" #include "metadata/IParser.hpp" @@ -309,6 +311,24 @@ namespace lms::scanner return lyrics; } + db::Advisory getAdvisory(std::optional advisory) + { + if (!advisory) + return db::Advisory::UnSet; + + switch (advisory.value()) + { + case metadata::Track::Advisory::Clean: + return db::Advisory::Clean; + case metadata::Track::Advisory::Explicit: + return db::Advisory::Explicit; + case metadata::Track::Advisory::Unknown: + return db::Advisory::Unknown; + } + + return db::Advisory::UnSet; + } + class AudioFileScanOperation : public IFileScanOperation { public: @@ -349,7 +369,7 @@ namespace lms::scanner } catch (const metadata::Exception& e) { - LMS_LOG(DBUPDATER, INFO, "Failed to parse audio file '" << _file.string() << "'"); + LMS_LOG(DBUPDATER, ERROR, "Failed to parse audio file " << _file); } } @@ -391,7 +411,7 @@ namespace lms::scanner std::error_code ec; if (!std::filesystem::exists(otherTrack->getAbsoluteFilePath(), ec)) { - LMS_LOG(DBUPDATER, DEBUG, "Considering track '" << _file.string() << "' moved from '" << otherTrack->getAbsoluteFilePath() << "'"); + LMS_LOG(DBUPDATER, DEBUG, "Considering track " << _file << " moved from " << otherTrack->getAbsoluteFilePath()); track = otherTrack; track.modify()->setAbsoluteFilePath(_file); } @@ -415,7 +435,7 @@ namespace lms::scanner continue; } - LMS_LOG(DBUPDATER, DEBUG, "Skipped '" << _file.string() << "' (similar MBID in '" << otherTrack->getAbsoluteFilePath().string() << "')"); + LMS_LOG(DBUPDATER, DEBUG, "Skipped " << _file << " (similar MBID in " << otherTrack->getAbsoluteFilePath() << ")"); // As this MBID already exists, just remove what we just scanned if (track) { @@ -430,7 +450,7 @@ namespace lms::scanner // We estimate this is an audio file if the duration is not null if (_parsedTrack->audioProperties.duration == std::chrono::milliseconds::zero()) { - LMS_LOG(DBUPDATER, DEBUG, "Skipped '" << _file.string() << "' (duration is 0)"); + LMS_LOG(DBUPDATER, DEBUG, "Skipped " << _file << " (duration is 0)"); // If Track exists here, delete it! if (track) @@ -460,6 +480,17 @@ namespace lms::scanner { track = dbSession.create(); track.modify()->setAbsoluteFilePath(_file); + + const core::PartialDateTime addedTime{ + fileInfo->lastWriteTime.date().year(), + static_cast(fileInfo->lastWriteTime.date().month()), + static_cast(fileInfo->lastWriteTime.date().day()), + static_cast(fileInfo->lastWriteTime.time().hour()), + static_cast(fileInfo->lastWriteTime.time().minute()), + static_cast(fileInfo->lastWriteTime.time().second()) + }; + + track.modify()->setAddedTime(addedTime); // may be erased by encodingTime added = true; } @@ -477,6 +508,9 @@ namespace lms::scanner track.modify()->setFileSize(fileInfo->fileSize); track.modify()->setLastWriteTime(fileInfo->lastWriteTime); + if (_parsedTrack->encodingTime.isValid()) + track.modify()->setAddedTime(_parsedTrack->encodingTime); + db::MediaLibrary::pointer mediaLibrary{ db::MediaLibrary::find(dbSession, _mediaLibrary.id) }; // may be null if settings are updated in // => next scan will correct this track.modify()->setMediaLibrary(mediaLibrary); db::Directory::pointer directory{ utils::getOrCreateDirectory(dbSession, _file.parent_path(), mediaLibrary) }; @@ -529,22 +563,17 @@ namespace lms::scanner track.modify()->setDiscSubtitle(_parsedTrack->medium ? _parsedTrack->medium->name : ""); track.modify()->setClusters(getOrCreateClusters(dbSession, *_parsedTrack)); track.modify()->setName(title); - track.modify()->setAddedTime(Wt::WDateTime::currentDateTime()); track.modify()->setTrackNumber(_parsedTrack->position); track.modify()->setDiscNumber(_parsedTrack->medium ? _parsedTrack->medium->position : std::nullopt); track.modify()->setDate(_parsedTrack->date); - track.modify()->setYear(_parsedTrack->year); track.modify()->setOriginalDate(_parsedTrack->originalDate); - track.modify()->setOriginalYear(_parsedTrack->originalYear); + if (!track->getOriginalDate().isValid() && _parsedTrack->originalYear) + track.modify()->setOriginalDate(core::PartialDateTime{ *_parsedTrack->originalYear }); // If a file has an OriginalDate but no date, set it to ease filtering if (!_parsedTrack->date.isValid() && _parsedTrack->originalDate.isValid()) track.modify()->setDate(_parsedTrack->originalDate); - // If a file has an OriginalYear but no Year, set it to ease filtering - if (!_parsedTrack->year && _parsedTrack->originalYear) - track.modify()->setYear(_parsedTrack->originalYear); - track.modify()->setRecordingMBID(_parsedTrack->recordingMBID); track.modify()->setTrackMBID(_parsedTrack->mbid); if (auto trackFeatures{ db::TrackFeatures::find(dbSession, track->getId()) }) @@ -552,6 +581,7 @@ namespace lms::scanner track.modify()->setHasCover(_parsedTrack->hasCover); track.modify()->setCopyright(_parsedTrack->copyright); track.modify()->setCopyrightURL(_parsedTrack->copyrightURL); + track.modify()->setAdvisory(getAdvisory(_parsedTrack->advisory)); track.modify()->setComment(!_parsedTrack->comments.empty() ? _parsedTrack->comments.front() : ""); // only take the first one for now track.modify()->setTrackReplayGain(_parsedTrack->replayGain); track.modify()->setArtistDisplayName(_parsedTrack->artistDisplayName); @@ -565,12 +595,12 @@ namespace lms::scanner if (added) { - LMS_LOG(DBUPDATER, DEBUG, "Added audio file '" << _file.string() << "'"); + LMS_LOG(DBUPDATER, DEBUG, "Added audio file " << _file); stats.additions++; } else { - LMS_LOG(DBUPDATER, DEBUG, "Updated audio file '" << _file.string() << "'"); + LMS_LOG(DBUPDATER, DEBUG, "Updated audio file " << _file); stats.updates++; } } diff --git a/src/libs/services/scanner/impl/scanners/ImageFileScanner.cpp b/src/libs/services/scanner/impl/scanners/ImageFileScanner.cpp index 8f1278b8..d84a3c65 100644 --- a/src/libs/services/scanner/impl/scanners/ImageFileScanner.cpp +++ b/src/libs/services/scanner/impl/scanners/ImageFileScanner.cpp @@ -27,6 +27,7 @@ #include "database/MediaLibrary.hpp" #include "database/Session.hpp" #include "image/Exception.hpp" +#include "image/IRawImage.hpp" #include "image/Image.hpp" #include "IFileScanOperation.hpp" @@ -55,29 +56,19 @@ namespace lms::scanner const MediaLibraryInfo _mediaLibrary; db::Db& _db; - struct ImageInfo - { - std::size_t height{}; - std::size_t width{}; - }; - std::optional _parsedImageInfo; + std::optional _parsedImageProperties; }; void ImageFileScanOperation::scan() { try { - std::unique_ptr rawImage{ image::decodeImage(_file) }; - - ImageInfo imageInfo; - imageInfo.width = rawImage->getWidth(); - imageInfo.height = rawImage->getHeight(); - - _parsedImageInfo = imageInfo; + _parsedImageProperties = image::probeImage(_file); } catch (const image::Exception& e) { - LMS_LOG(DBUPDATER, ERROR, "Cannot read image in file '" << _file.string() << "': " << e.what()); + _parsedImageProperties.reset(); + LMS_LOG(DBUPDATER, ERROR, "Cannot read image in file " << _file << ": " << e.what()); } } @@ -95,12 +86,13 @@ namespace lms::scanner db::Session& dbSession{ _db.getTLSSession() }; db::Image::pointer image{ db::Image::find(dbSession, _file) }; - if (!_parsedImageInfo) + if (!_parsedImageProperties) { if (image) { image.remove(); stats.deletions++; + LMS_LOG(DBUPDATER, DEBUG, "Removed image " << _file); } context.stats.errors.emplace_back(_file, ScanErrorType::CannotReadImageFile); return; @@ -112,19 +104,19 @@ namespace lms::scanner image.modify()->setLastWriteTime(fileInfo->lastWriteTime); image.modify()->setFileSize(fileInfo->fileSize); - image.modify()->setHeight(_parsedImageInfo->height); - image.modify()->setWidth(_parsedImageInfo->width); + image.modify()->setHeight(_parsedImageProperties->height); + image.modify()->setWidth(_parsedImageProperties->width); db::MediaLibrary::pointer mediaLibrary{ db::MediaLibrary::find(dbSession, _mediaLibrary.id) }; // may be null if settings are updated in // => next scan will correct this image.modify()->setDirectory(utils::getOrCreateDirectory(dbSession, _file.parent_path(), mediaLibrary)); if (added) { - LMS_LOG(DBUPDATER, DEBUG, "Added image '" << _file.string() << "'"); + LMS_LOG(DBUPDATER, DEBUG, "Added image " << _file); stats.additions++; } else { - LMS_LOG(DBUPDATER, DEBUG, "Updated image '" << _file.string() << "'"); + LMS_LOG(DBUPDATER, DEBUG, "Updated image " << _file); stats.updates++; } } diff --git a/src/libs/services/scanner/impl/scanners/LyricsFileScanner.cpp b/src/libs/services/scanner/impl/scanners/LyricsFileScanner.cpp index 12e3e604..181f5efc 100644 --- a/src/libs/services/scanner/impl/scanners/LyricsFileScanner.cpp +++ b/src/libs/services/scanner/impl/scanners/LyricsFileScanner.cpp @@ -62,15 +62,15 @@ namespace lms::scanner { try { - std::ifstream ifs{ _file.string() }; + std::ifstream ifs{ _file }; if (!ifs) - LMS_LOG(DBUPDATER, ERROR, "Cannot open file '" << _file.string() << "'"); + LMS_LOG(DBUPDATER, ERROR, "Cannot open file " << _file); else _parsedLyrics = metadata::parseLyrics(ifs); } catch (const metadata::Exception& e) { - LMS_LOG(DBUPDATER, ERROR, "Cannot read lyrics in file '" << _file.string() << "': " << e.what()); + LMS_LOG(DBUPDATER, ERROR, "Cannot read lyrics in file " << _file << ": " << e.what()); } } @@ -122,12 +122,12 @@ namespace lms::scanner if (added) { - LMS_LOG(DBUPDATER, DEBUG, "Added external lyrics '" << _file.string() << "'"); + LMS_LOG(DBUPDATER, DEBUG, "Added external lyrics " << _file); stats.additions++; } else { - LMS_LOG(DBUPDATER, DEBUG, "Updated external lyrics '" << _file.string() << "'"); + LMS_LOG(DBUPDATER, DEBUG, "Updated external lyrics " << _file); stats.updates++; } } diff --git a/src/libs/services/scanner/impl/scanners/PlayListFileScanner.cpp b/src/libs/services/scanner/impl/scanners/PlayListFileScanner.cpp index 4efdb469..4a7b8c1c 100644 --- a/src/libs/services/scanner/impl/scanners/PlayListFileScanner.cpp +++ b/src/libs/services/scanner/impl/scanners/PlayListFileScanner.cpp @@ -66,15 +66,15 @@ namespace lms::scanner { try { - std::ifstream ifs{ _file.string() }; + std::ifstream ifs{ _file }; if (!ifs) - LMS_LOG(DBUPDATER, ERROR, "Cannot open file '" << _file.string() << "'"); + LMS_LOG(DBUPDATER, ERROR, "Cannot open file " << _file); else _parsedPlayList = metadata::parsePlayList(ifs); } catch (const metadata::Exception& e) { - LMS_LOG(DBUPDATER, ERROR, "Cannot read playlist in file '" << _file.string() << "': " << e.what()); + LMS_LOG(DBUPDATER, ERROR, "Cannot read playlist in file " << _file << ": " << e.what()); } } @@ -100,6 +100,7 @@ namespace lms::scanner stats.deletions++; } context.stats.errors.emplace_back(_file, ScanErrorType::CannotReadPlayListFile); + LMS_LOG(DBUPDATER, DEBUG, "Removed playlist file " << _file); return; } @@ -120,13 +121,12 @@ namespace lms::scanner if (added) { - LMS_LOG(DBUPDATER, DEBUG, "Added playlist file '" << _file.string() << "'"); - LMS_LOG(DBUPDATER, DEBUG, "db playlist file = '" << playList->getAbsoluteFilePath().string() << "'"); + LMS_LOG(DBUPDATER, DEBUG, "Added playlist file " << _file); stats.additions++; } else { - LMS_LOG(DBUPDATER, DEBUG, "Updated playlist file '" << _file.string() << "'"); + LMS_LOG(DBUPDATER, DEBUG, "Updated playlist file '" << _file); stats.updates++; } } diff --git a/src/libs/services/scanner/impl/steps/FileScanQueue.cpp b/src/libs/services/scanner/impl/steps/FileScanQueue.cpp index d5b580a9..4c71027e 100644 --- a/src/libs/services/scanner/impl/steps/FileScanQueue.cpp +++ b/src/libs/services/scanner/impl/steps/FileScanQueue.cpp @@ -53,7 +53,7 @@ namespace lms::scanner { { LMS_SCOPED_TRACE_OVERVIEW("Scanner", operation->getName()); - LMS_LOG(DBUPDATER, DEBUG, operation->getName() << ": scanning file '" << operation->getFile().string() << "'"); + LMS_LOG(DBUPDATER, DEBUG, operation->getName() << ": scanning file " << operation->getFile()); operation->scan(); } diff --git a/src/libs/services/scanner/impl/steps/ScanStepAssociatePlayListTracks.cpp b/src/libs/services/scanner/impl/steps/ScanStepAssociatePlayListTracks.cpp index fd8297c8..b3852f9f 100644 --- a/src/libs/services/scanner/impl/steps/ScanStepAssociatePlayListTracks.cpp +++ b/src/libs/services/scanner/impl/steps/ScanStepAssociatePlayListTracks.cpp @@ -26,10 +26,13 @@ #include "database/Db.hpp" #include "database/Directory.hpp" #include "database/PlayListFile.hpp" +#include "database/ReleaseId.hpp" #include "database/Session.hpp" #include "database/Track.hpp" #include "database/TrackList.hpp" +#include "ScannerSettings.hpp" + namespace lms::scanner { namespace @@ -37,10 +40,17 @@ namespace lms::scanner constexpr std::size_t readBatchSize{ 20 }; constexpr std::size_t writeBatchSize{ 5 }; + struct TrackInfo + { + db::TrackId trackId; + db::ReleaseId releaseId; + }; + struct PlayListFileAssociation { db::PlayListFileId playListFileIdId; - std::vector trackIds; + + std::vector tracks; }; using PlayListFileAssociationContainer = std::deque; @@ -49,6 +59,7 @@ namespace lms::scanner db::Session& session; db::PlayListFileId lastRetrievedPlayListFileId; std::size_t processedPlayListFileCount{}; + const ScannerSettings& settings; }; db::Track::pointer getMatchingTrack(db::Session& session, const std::filesystem::path& filePath, const db::Directory::pointer& playListDirectory) @@ -67,7 +78,19 @@ namespace lms::scanner return matchingTrack; } - bool trackListNeedsUpdate(db::Session& session, std::string_view name, std::span trackIds, const db::TrackList::pointer& trackList) + bool isSingleReleasePlayList(std::span tracks) + { + if (tracks.empty()) + return true; + + const db::ReleaseId releaseId{ tracks.front().releaseId }; + if (std::all_of(std::cbegin(tracks) + 1, std::cend(tracks), [=](const TrackInfo& trackInfo) { return trackInfo.releaseId == releaseId; })) + return true; + + return false; + } + + bool trackListNeedsUpdate(db::Session& session, std::string_view name, std::span tracks, const db::TrackList::pointer& trackList) { if (trackList->getName() != name) return true; @@ -78,13 +101,13 @@ namespace lms::scanner bool needUpdate{}; std::size_t currentIndex{}; db::TrackListEntry::find(session, params, [&](const db::TrackListEntry::pointer& entry) { - if (currentIndex > trackIds.size() || trackIds[currentIndex] != entry->getTrackId()) + if (currentIndex > tracks.size() || tracks[currentIndex].trackId != entry->getTrackId()) needUpdate = true; currentIndex += 1; }); - if (currentIndex != trackIds.size()) + if (currentIndex != tracks.size()) needUpdate = true; return needUpdate; @@ -108,20 +131,27 @@ namespace lms::scanner // TODO optim: no need to fetch the whole track db::Track::pointer track{ getMatchingTrack(searchContext.session, file, playListFile->getDirectory()) }; if (track) - playListAssociation.trackIds.push_back(track->getId()); + playListAssociation.tracks.push_back(TrackInfo{ .trackId = track->getId(), .releaseId = track->getReleaseId() }); else - LMS_LOG(DBUPDATER, DEBUG, "Track '" << file.string() << "' not found in playlist '" << playListFile->getAbsoluteFilePath().string() << "'"); + LMS_LOG(DBUPDATER, DEBUG, "Track " << file << " not found in playlist " << playListFile->getAbsoluteFilePath()); + } + + if (playListAssociation.tracks.empty() + || (searchContext.settings.skipSingleReleasePlayLists && isSingleReleasePlayList(playListAssociation.tracks))) + { + playListAssociation.tracks.clear(); } bool needUpdate{ true }; if (const db::TrackList::pointer trackList{ playListFile->getTrackList() }) - needUpdate = trackListNeedsUpdate(searchContext.session, playListFile->getName(), playListAssociation.trackIds, trackList); + { + if (!playListAssociation.tracks.empty()) + needUpdate = trackListNeedsUpdate(searchContext.session, playListFile->getName(), playListAssociation.tracks, trackList); + } if (needUpdate) - { - LMS_LOG(DBUPDATER, DEBUG, "Updating PlayList '" << playListFile->getAbsoluteFilePath().string() << "' (" << playListAssociation.trackIds.size() << " files)"); playListFileAssociations.emplace_back(std::move(playListAssociation)); - } + searchContext.processedPlayListFileCount++; }); } @@ -135,7 +165,19 @@ namespace lms::scanner assert(playListFile); db::TrackList::pointer trackList{ playListFile->getTrackList() }; - if (!trackList) + if (playListFileAssociation.tracks.empty()) + { + if (trackList) + { + LMS_LOG(DBUPDATER, DEBUG, "Removed associated tracklist for " << playListFile->getAbsoluteFilePath() << ""); + trackList.remove(); + } + + return; + } + + const bool createTrackList{ !trackList }; + if (createTrackList) { trackList = session.create(playListFile->getName(), db::TrackListType::PlayList); playListFile.modify()->setTrackList(trackList); @@ -146,11 +188,13 @@ namespace lms::scanner trackList.modify()->setName(playListFile->getName()); trackList.modify()->clear(); - for (const db::TrackId trackId : playListFileAssociation.trackIds) + for (const TrackInfo trackInfo : playListFileAssociation.tracks) { - if (db::Track::pointer track{ db::Track::find(session, trackId) }) + if (db::Track::pointer track{ db::Track::find(session, trackInfo.trackId) }) session.create(track, trackList, playListFile->getLastWriteTime()); } + + LMS_LOG(DBUPDATER, DEBUG, std::string_view{ createTrackList ? "Created" : "Updated" } << " associated tracklist for " << playListFile->getAbsoluteFilePath() << " (" << playListFileAssociation.tracks.size() << " tracks)"); } void updatePlayListFiles(db::Session& session, PlayListFileAssociationContainer& playListFileAssociations) @@ -186,6 +230,7 @@ namespace lms::scanner SearchPlayListFileContext searchContext{ .session = session, .lastRetrievedPlayListFileId = {}, + .settings = _settings, }; PlayListFileAssociationContainer playListFileAssociations; diff --git a/src/libs/services/scanner/impl/steps/ScanStepScanFiles.cpp b/src/libs/services/scanner/impl/steps/ScanStepScanFiles.cpp index cebece5b..12962d5d 100644 --- a/src/libs/services/scanner/impl/steps/ScanStepScanFiles.cpp +++ b/src/libs/services/scanner/impl/steps/ScanStepScanFiles.cpp @@ -57,7 +57,7 @@ namespace lms::scanner { [[maybe_unused]] auto [it, inserted]{ _scannerByExtension.emplace(extension, scanner) }; assert(inserted); - LMS_LOG(DBUPDATER, INFO, "Registered extension '" << extension.string() << "' for '" << scanner->getName() << "'"); + LMS_LOG(DBUPDATER, INFO, "Registered extension " << extension << " for " << scanner->getName()); } } @@ -88,12 +88,12 @@ namespace lms::scanner if (ec) { - LMS_LOG(DBUPDATER, ERROR, "Cannot scan file '" << path.string() << "': " << ec.message()); + LMS_LOG(DBUPDATER, ERROR, "Cannot scan file " << path << ": " << ec.message()); context.stats.errors.emplace_back(ScanError{ path, ScanErrorType::CannotReadFile, ec.message() }); } else { - auto itScanner{ _scannerByExtension.find(core::stringUtils::stringToLower(path.extension().string())) }; + auto itScanner{ _scannerByExtension.find(core::stringUtils::stringToLower(path.extension().c_str())) }; if (itScanner != std::cend(_scannerByExtension)) { IFileScanner& scanner{ *itScanner->second }; @@ -140,7 +140,7 @@ namespace lms::scanner if (_abortScan) return; - LMS_LOG(DBUPDATER, DEBUG, scanOperation->getName() << ": processing result for '" << scanOperation->getFile().string() << "'"); + LMS_LOG(DBUPDATER, DEBUG, scanOperation->getName() << ": processing result for " << scanOperation->getFile()); scanOperation->processResult(context); context.stats.scans++; } diff --git a/src/libs/services/scanner/impl/steps/ScanStepScanFiles.hpp b/src/libs/services/scanner/impl/steps/ScanStepScanFiles.hpp index beac2383..e15c54a2 100644 --- a/src/libs/services/scanner/impl/steps/ScanStepScanFiles.hpp +++ b/src/libs/services/scanner/impl/steps/ScanStepScanFiles.hpp @@ -28,7 +28,7 @@ namespace lms::scanner { class IFileScanner; - class MediaLibraryInfo; + struct MediaLibraryInfo; class ScanStepScanFiles : public ScanStepBase { diff --git a/src/libs/services/scanner/impl/steps/ScanStepUpdateLibraryFields.hpp b/src/libs/services/scanner/impl/steps/ScanStepUpdateLibraryFields.hpp index 2d168626..5acb0f10 100644 --- a/src/libs/services/scanner/impl/steps/ScanStepUpdateLibraryFields.hpp +++ b/src/libs/services/scanner/impl/steps/ScanStepUpdateLibraryFields.hpp @@ -23,7 +23,7 @@ namespace lms::scanner { - class MediaLibraryInfo; + struct MediaLibraryInfo; class ScanStepUpdateLibraryFields : public ScanStepBase { diff --git a/src/libs/services/scrobbling/CMakeLists.txt b/src/libs/services/scrobbling/CMakeLists.txt index efcf7147..55b5e825 100644 --- a/src/libs/services/scrobbling/CMakeLists.txt +++ b/src/libs/services/scrobbling/CMakeLists.txt @@ -1,5 +1,5 @@ -add_library(lmsscrobbling SHARED +add_library(lmsscrobbling STATIC impl/internal/InternalBackend.cpp impl/listenbrainz/ListenBrainzBackend.cpp impl/listenbrainz/ListenTypes.cpp @@ -26,8 +26,6 @@ target_link_libraries(lmsscrobbling PUBLIC lmsdatabase ) -install(TARGETS lmsscrobbling DESTINATION ${CMAKE_INSTALL_LIBDIR}) - if(BUILD_TESTING) add_subdirectory(test) endif() diff --git a/src/libs/som/CMakeLists.txt b/src/libs/som/CMakeLists.txt index 9000eb6f..79c1ac54 100644 --- a/src/libs/som/CMakeLists.txt +++ b/src/libs/som/CMakeLists.txt @@ -1,4 +1,4 @@ -add_library(lmssom SHARED +add_library(lmssom STATIC impl/DataNormalizer.cpp impl/Network.cpp ) @@ -17,8 +17,6 @@ target_link_libraries(lmssom PUBLIC set_property(TARGET lmssom PROPERTY POSITION_INDEPENDENT_CODE ON) -install(TARGETS lmssom DESTINATION ${CMAKE_INSTALL_LIBDIR}) - if(BUILD_TESTING) add_subdirectory(test) endif() diff --git a/src/libs/subsonic/CMakeLists.txt b/src/libs/subsonic/CMakeLists.txt index 875fce08..3149d14f 100644 --- a/src/libs/subsonic/CMakeLists.txt +++ b/src/libs/subsonic/CMakeLists.txt @@ -1,5 +1,5 @@ -add_library(lmssubsonic SHARED +add_library(lmssubsonic STATIC impl/endpoints/AlbumSongLists.cpp impl/endpoints/Bookmarks.cpp impl/endpoints/Browsing.cpp @@ -60,8 +60,6 @@ target_link_libraries(lmssubsonic PUBLIC Wt::Wt ) -install(TARGETS lmssubsonic DESTINATION ${CMAKE_INSTALL_LIBDIR}) - if (BUILD_BENCHMARKS) add_subdirectory(bench) endif() \ No newline at end of file diff --git a/src/libs/subsonic/impl/endpoints/AlbumSongLists.cpp b/src/libs/subsonic/impl/endpoints/AlbumSongLists.cpp index 916086be..8c04c158 100644 --- a/src/libs/subsonic/impl/endpoints/AlbumSongLists.cpp +++ b/src/libs/subsonic/impl/endpoints/AlbumSongLists.cpp @@ -25,6 +25,7 @@ #include "database/Release.hpp" #include "database/Session.hpp" #include "database/Track.hpp" +#include "database/Types.hpp" #include "database/User.hpp" #include "services/feedback/IFeedbackService.hpp" #include "services/scrobbling/IScrobblingService.hpp" @@ -106,7 +107,7 @@ namespace lms::api::subsonic Release::FindParameters params; params.setSortMethod(fromYear > toYear ? ReleaseSortMethod::DateDesc : ReleaseSortMethod::DateAsc); params.setRange(range); - params.setDateRange(DateRange::fromYearRange(std::min(fromYear, toYear), std::max(fromYear, toYear))); + params.setDateRange(YearRange{ std::min(fromYear, toYear), std::max(fromYear, toYear) }); params.setMediaLibrary(mediaLibraryId); releases = Release::findIds(context.dbSession, params); @@ -123,7 +124,7 @@ namespace lms::api::subsonic else if (type == "newest") { Release::FindParameters params; - params.setSortMethod(ReleaseSortMethod::LastWritten); + params.setSortMethod(ReleaseSortMethod::AddedDesc); params.setRange(range); params.setMediaLibrary(mediaLibraryId); diff --git a/src/libs/subsonic/impl/endpoints/Browsing.cpp b/src/libs/subsonic/impl/endpoints/Browsing.cpp index aa5a25fe..8cdf0ae3 100644 --- a/src/libs/subsonic/impl/endpoints/Browsing.cpp +++ b/src/libs/subsonic/impl/endpoints/Browsing.cpp @@ -88,6 +88,7 @@ namespace lms::api::subsonic { Directory::FindParameters params; params.setParentDirectory(parentDirectory->getId()); + params.setSortMethod(DirectorySortMethod::Name); Directory::find(context.dbSession, params, [&](const Directory::pointer& directory) { const std::string_view name{ directory->getName() }; @@ -313,6 +314,7 @@ namespace lms::api::subsonic { Directory::FindParameters params; params.setParentDirectory(directory->getId()); + params.setSortMethod(DirectorySortMethod::Name); Directory::find(context.dbSession, params, [&](const Directory::pointer& subDirectory) { const Release::pointer release{ getReleaseFromDirectory(context.dbSession, subDirectory->getId()) }; @@ -338,6 +340,7 @@ namespace lms::api::subsonic { Track::FindParameters params; params.setDirectory(directory->getId()); + params.setSortMethod(TrackSortMethod::FileName); Track::find(context.dbSession, params, [&](const Track::pointer& track) { directoryNode.addArrayChild("child", createSongNode(context, track, context.user)); diff --git a/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp b/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp index c5a738f6..4e63ee45 100644 --- a/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp +++ b/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp @@ -176,10 +176,13 @@ namespace lms::api::subsonic bitrate = maxBitRate; } + // Need to transcode here if (!requestedFormat) requestedFormat = userTranscodeFormatToAvFormat(context.user->getSubsonicDefaultTranscodingOutputFormat()); if (!bitrate) - bitrate = std::min(context.user->getSubsonicDefaultTranscodingOutputBitrate(), maxBitRate); + bitrate = context.user->getSubsonicDefaultTranscodingOutputBitrate(); + if (maxBitRate) + bitrate = std::min(bitrate, maxBitRate); av::transcoding::OutputParameters& outputParameters{ parameters.outputParameters.emplace() }; diff --git a/src/libs/subsonic/impl/endpoints/MediaRetrieval.hpp b/src/libs/subsonic/impl/endpoints/MediaRetrieval.hpp index 4291466c..45da25a9 100644 --- a/src/libs/subsonic/impl/endpoints/MediaRetrieval.hpp +++ b/src/libs/subsonic/impl/endpoints/MediaRetrieval.hpp @@ -26,7 +26,7 @@ namespace lms::api::subsonic { - class RequestContext; + struct RequestContext; Response handleGetLyrics(RequestContext& context); Response handleGetLyricsBySongId(RequestContext& context); diff --git a/src/libs/subsonic/impl/responses/Album.cpp b/src/libs/subsonic/impl/responses/Album.cpp index 3ace6ba1..051ff869 100644 --- a/src/libs/subsonic/impl/responses/Album.cpp +++ b/src/libs/subsonic/impl/responses/Album.cpp @@ -28,6 +28,7 @@ #include "database/Image.hpp" #include "database/Release.hpp" #include "database/Track.hpp" +#include "database/Types.hpp" #include "database/User.hpp" #include "services/feedback/IFeedbackService.hpp" #include "services/scrobbling/IScrobblingService.hpp" @@ -203,7 +204,7 @@ namespace lms::api::subsonic albumNode.createEmptyArrayChild("artists"); albumNode.setAttribute("displayArtist", ""); } - albumNode.addChild("originalReleaseDate", createItemDateNode(release->getOriginalDate(), release->getOriginalYear())); + albumNode.addChild("originalReleaseDate", createItemDateNode(release->getOriginalDate())); albumNode.setAttribute("isCompilation", release->isCompilation()); @@ -223,6 +224,18 @@ namespace lms::api::subsonic albumNode.addArrayChild("recordLabels", createRecordLabel(label)); }); + auto advisoryToExplicitStatus = [&](const core::EnumSet advisories) -> std::string_view { + if (advisories.contains(db::Advisory::Explicit)) + return "explicit"; + + if (advisories.contains(db::Advisory::Clean)) + return "clean"; + + return ""; + }; + + albumNode.setAttribute("explicitStatus", advisoryToExplicitStatus(release->getAdvisories())); + return albumNode; } } // namespace lms::api::subsonic \ No newline at end of file diff --git a/src/libs/subsonic/impl/responses/ItemDate.cpp b/src/libs/subsonic/impl/responses/ItemDate.cpp index 703e2557..c5c92c99 100644 --- a/src/libs/subsonic/impl/responses/ItemDate.cpp +++ b/src/libs/subsonic/impl/responses/ItemDate.cpp @@ -23,20 +23,16 @@ namespace lms::api::subsonic { - Response::Node createItemDateNode(const Wt::WDate& date, std::optional year) + Response::Node createItemDateNode(const core::PartialDateTime& date) { Response::Node itemDateNode; - if (date.isValid()) - { - itemDateNode.setAttribute("year", date.year()); - itemDateNode.setAttribute("month", date.month()); - itemDateNode.setAttribute("day", date.day()); - } - else if (year) - { + if (auto year{ date.getYear() }) itemDateNode.setAttribute("year", *year); - } + if (auto month{ date.getMonth() }) + itemDateNode.setAttribute("month", *month); + if (auto day{ date.getDay() }) + itemDateNode.setAttribute("day", *day); return itemDateNode; } diff --git a/src/libs/subsonic/impl/responses/ItemDate.hpp b/src/libs/subsonic/impl/responses/ItemDate.hpp index c071c136..ffa3a1e7 100644 --- a/src/libs/subsonic/impl/responses/ItemDate.hpp +++ b/src/libs/subsonic/impl/responses/ItemDate.hpp @@ -19,11 +19,11 @@ #pragma once -#include +#include "core/PartialDateTime.hpp" #include "SubsonicResponse.hpp" namespace lms::api::subsonic { - Response::Node createItemDateNode(const Wt::WDate& date, std::optional year); + Response::Node createItemDateNode(const core::PartialDateTime& date); } diff --git a/src/libs/subsonic/impl/responses/Song.cpp b/src/libs/subsonic/impl/responses/Song.cpp index bc74729a..bdc8b3a6 100644 --- a/src/libs/subsonic/impl/responses/Song.cpp +++ b/src/libs/subsonic/impl/responses/Song.cpp @@ -32,6 +32,7 @@ #include "database/Release.hpp" #include "database/Track.hpp" #include "database/TrackArtistLink.hpp" +#include "database/Types.hpp" #include "database/User.hpp" #include "services/feedback/IFeedbackService.hpp" #include "services/scrobbling/IScrobblingService.hpp" @@ -227,6 +228,22 @@ namespace lms::api::subsonic for (const auto& genre : genres) trackResponse.addArrayChild("genres", createItemGenreNode(genre->getName())); + auto advisoryToExplicitStatus = [](db::Advisory advisory) -> std::string_view { + switch (advisory) + { + case db::Advisory::Clean: + return "clean"; + case db::Advisory::Explicit: + return "expicit"; + case db::Advisory::Unknown: + case db::Advisory::UnSet: + break; + } + + return ""; + }; + trackResponse.setAttribute("explicitStatus", advisoryToExplicitStatus(track->getAdvisory())); + trackResponse.addChild("replayGain", createReplayGainNode(track)); return trackResponse; diff --git a/src/lms/main.cpp b/src/lms/main.cpp index c0ce07c3..39cacc87 100644 --- a/src/lms/main.cpp +++ b/src/lms/main.cpp @@ -109,33 +109,35 @@ namespace lms std::vector generateWtConfig(std::string execPath, core::logging::Severity minSeverity) { + core::IConfig& config{ *core::Service::get() }; + std::vector args; - const std::filesystem::path wtConfigPath{ core::Service::get()->getPath("working-dir") / "wt_config.xml" }; - const std::filesystem::path wtLogFilePath{ core::Service::get()->getPath("log-file", "/var/log/lms.log") }; - const std::filesystem::path wtAccessLogFilePath{ core::Service::get()->getPath("access-log-file", "/var/log/lms.access.log") }; - const std::filesystem::path wtResourcesPath{ core::Service::get()->getPath("wt-resources", "/usr/share/Wt/resources") }; + const std::filesystem::path wtConfigPath{ config.getPath("working-dir", "/var/lms") / "wt_config.xml" }; + const std::filesystem::path wtLogFilePath{ config.getPath("log-file", "") }; + const std::filesystem::path wtAccessLogFilePath{ config.getPath("access-log-file", "") }; + const std::filesystem::path wtResourcesPath{ config.getPath("wt-resources", "/usr/share/Wt/resources") }; args.push_back(execPath); args.push_back("--config=" + wtConfigPath.string()); - args.push_back("--docroot=" + std::string{ core::Service::get()->getString("docroot") }); - args.push_back("--approot=" + std::string{ core::Service::get()->getString("approot") }); - args.push_back("--deploy-path=" + std::string{ core::Service::get()->getString("deploy-path", "/") }); + args.push_back("--docroot=" + std::string{ config.getString("docroot", "/usr/share/lms/docroot/;/resources,/css,/images,/js,/favicon.ico") }); + args.push_back("--approot=" + std::string{ config.getString("approot", "/usr/share/lms/approot") }); + args.push_back("--deploy-path=" + std::string{ config.getString("deploy-path", "/") }); if (!wtResourcesPath.empty()) args.push_back("--resources-dir=" + wtResourcesPath.string()); if (core::Service::get()->getBool("tls-enable", false)) { - args.push_back("--https-port=" + std::to_string(core::Service::get()->getULong("listen-port", 5082))); - args.push_back("--https-address=" + std::string{ core::Service::get()->getString("listen-addr", "0.0.0.0") }); - args.push_back("--ssl-certificate=" + std::string{ core::Service::get()->getString("tls-cert") }); - args.push_back("--ssl-private-key=" + std::string{ core::Service::get()->getString("tls-key") }); - args.push_back("--ssl-tmp-dh=" + std::string{ core::Service::get()->getString("tls-dh") }); + args.push_back("--https-port=" + std::to_string(config.getULong("listen-port", 5082))); + args.push_back("--https-address=" + std::string{ config.getString("listen-addr", "0.0.0.0") }); + args.push_back("--ssl-certificate=" + std::string{ config.getString("tls-cert", "/var/lms/cert.pem") }); + args.push_back("--ssl-private-key=" + std::string{ config.getString("tls-key", "/var/lms/privkey.pem") }); + args.push_back("--ssl-tmp-dh=" + std::string{ config.getString("tls-dh", "/var/lms/dh2048.pem") }); } else { - args.push_back("--http-port=" + std::to_string(core::Service::get()->getULong("listen-port", 5082))); - args.push_back("--http-address=" + std::string{ core::Service::get()->getString("listen-addr", "0.0.0.0") }); + args.push_back("--http-port=" + std::to_string(config.getULong("listen-port", 5082))); + args.push_back("--http-address=" + std::string{ config.getString("listen-addr", "0.0.0.0") }); } if (!wtAccessLogFilePath.empty()) @@ -153,10 +155,10 @@ namespace lms pt.put("server.application-settings.log-config", core::logging::WtLogger::computeLogConfig(minSeverity)); // Reverse proxy - if (core::Service::get()->getBool("behind-reverse-proxy", false)) + if (config.getBool("behind-reverse-proxy", false)) { - pt.put("server.application-settings.trusted-proxy-config.original-ip-header", core::Service::get()->getString("original-ip-header", "X-Forwarded-For")); - core::Service::get()->visitStrings("trusted-proxies", [&](std::string_view trustedProxy) { + pt.put("server.application-settings.trusted-proxy-config.original-ip-header", config.getString("original-ip-header", "X-Forwarded-For")); + config.visitStrings("trusted-proxies", [&](std::string_view trustedProxy) { pt.add("server.application-settings.trusted-proxy-config.trusted-proxies.proxy", std::string{ trustedProxy }); }, { "127.0.0.1", "::1" }); @@ -176,7 +178,7 @@ namespace lms } { - std::ofstream oss{ wtConfigPath.string().c_str(), std::ios::out }; + std::ofstream oss{ wtConfigPath, std::ios::out }; if (!oss) throw core::LmsException{ "Can't open '" + wtConfigPath.string() + "' for writing!" }; @@ -284,8 +286,8 @@ namespace lms LMS_LOG(MAIN, WARNING, "Cannot set locale from system"); // Make sure the working directory exists - std::filesystem::create_directories(config->getPath("working-dir")); - std::filesystem::create_directories(config->getPath("working-dir") / "cache"); + std::filesystem::create_directories(config->getPath("working-dir", "/var/lms")); + std::filesystem::create_directories(config->getPath("working-dir", "/var/lms") / "cache"); // Construct WT configuration and get the argc/argv back const std::vector wtServerArgs{ generateWtConfig(argv[0], minLogSeverity) }; @@ -313,7 +315,7 @@ namespace lms core::IOContextRunner ioContextRunner{ ioContext, getThreadCount(), "Misc" }; // Connection pool size must be twice the number of threads: we have at least 2 io pools with getThreadCount() each and they all may access the database - db::Db database{ config->getPath("working-dir") / "lms.db", getThreadCount() * 2 }; + db::Db database{ config->getPath("working-dir", "/var/lms") / "lms.db", getThreadCount() * 2 }; { db::Session session{ database }; session.prepareTablesIfNeeded(); diff --git a/src/lms/ui/MediaPlayer.cpp b/src/lms/ui/MediaPlayer.cpp index a165aa15..73c5bec6 100644 --- a/src/lms/ui/MediaPlayer.cpp +++ b/src/lms/ui/MediaPlayer.cpp @@ -108,16 +108,12 @@ namespace lms::ui return std::nullopt; } - std::optional bitrateFromString(const std::string& str) + std::optional bitrateFromInt(int bitRate) { - const auto value{ core::stringUtils::readAs(str) }; - if (!value) + if (!db::isAudioBitrateAllowed(bitRate)) return std::nullopt; - if (!db::isAudioBitrateAllowed(*value)) - return std::nullopt; - - return *value; + return bitRate; } std::optional replayGainModeFromString(const std::string& str) @@ -165,7 +161,7 @@ namespace lms::ui 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); + settings.transcoding.bitrate = bitrateFromInt(transcoding.get("bitrate").toNumber().orIfNull(0)).value_or(Settings::Transcoding::defaultBitrate); } } { diff --git a/src/lms/ui/admin/ScanSettingsView.cpp b/src/lms/ui/admin/ScanSettingsView.cpp index 872bd2c7..100b04f6 100644 --- a/src/lms/ui/admin/ScanSettingsView.cpp +++ b/src/lms/ui/admin/ScanSettingsView.cpp @@ -19,6 +19,7 @@ #include "ScanSettingsView.hpp" +#include #include #include #include @@ -69,6 +70,7 @@ namespace lms::ui static inline constexpr Field UpdatePeriodField{ "update-period" }; static inline constexpr Field UpdateStartTimeField{ "update-start-time" }; static inline constexpr Field SimilarityEngineTypeField{ "similarity-engine-type" }; + static inline constexpr Field SkipSingleReleasePlayLists{ "skip-single-release-playlists" }; using UpdatePeriodModel = ValueStringModel; @@ -79,10 +81,12 @@ namespace lms::ui addField(UpdatePeriodField); addField(UpdateStartTimeField); addField(SimilarityEngineTypeField); + addField(SkipSingleReleasePlayLists); setValidator(UpdatePeriodField, createMandatoryValidator()); setValidator(UpdateStartTimeField, createMandatoryValidator()); setValidator(SimilarityEngineTypeField, createMandatoryValidator()); + setValidator(SkipSingleReleasePlayLists, createMandatoryValidator()); } std::shared_ptr updatePeriodModel() { return _updatePeriodModel; } @@ -109,6 +113,8 @@ namespace lms::ui setReadOnly(DatabaseSettingsModel::UpdateStartTimeField, true); } + setValue(SkipSingleReleasePlayLists, scanSettings->getSkipSingleReleasePlayLists()); + auto similarityEngineTypeRow{ _similarityEngineTypeModel->getRowFromValue(scanSettings->getSimilarityEngineType()) }; if (similarityEngineTypeRow) setValue(SimilarityEngineTypeField, _similarityEngineTypeModel->getString(*similarityEngineTypeRow)); @@ -126,17 +132,28 @@ namespace lms::ui ScanSettings::pointer scanSettings{ ScanSettings::get(LmsApp->getDbSession()) }; - auto updatePeriodRow{ _updatePeriodModel->getRowFromString(valueText(UpdatePeriodField)) }; - if (updatePeriodRow) - scanSettings.modify()->setUpdatePeriod(_updatePeriodModel->getValue(*updatePeriodRow)); + { + const auto updatePeriodRow{ _updatePeriodModel->getRowFromString(valueText(UpdatePeriodField)) }; + if (updatePeriodRow) + scanSettings.modify()->setUpdatePeriod(_updatePeriodModel->getValue(*updatePeriodRow)); + } - auto startTimeRow{ _updateStartTimeModel->getRowFromString(valueText(UpdateStartTimeField)) }; - if (startTimeRow) - scanSettings.modify()->setUpdateStartTime(_updateStartTimeModel->getValue(*startTimeRow)); + { + const auto startTimeRow{ _updateStartTimeModel->getRowFromString(valueText(UpdateStartTimeField)) }; + if (startTimeRow) + scanSettings.modify()->setUpdateStartTime(_updateStartTimeModel->getValue(*startTimeRow)); + } - auto similarityEngineTypeRow{ _similarityEngineTypeModel->getRowFromString(valueText(SimilarityEngineTypeField)) }; - if (similarityEngineTypeRow) - scanSettings.modify()->setSimilarityEngineType(_similarityEngineTypeModel->getValue(*similarityEngineTypeRow)); + { + const bool skipSingleReleasePlayLists{ Wt::asNumber(value(SkipSingleReleasePlayLists)) != 0 }; + scanSettings.modify()->setSkipSingleReleasePlayLists(skipSingleReleasePlayLists); + } + + { + const auto similarityEngineTypeRow{ _similarityEngineTypeModel->getRowFromString(valueText(SimilarityEngineTypeField)) }; + if (similarityEngineTypeRow) + scanSettings.modify()->setSimilarityEngineType(_similarityEngineTypeModel->getValue(*similarityEngineTypeRow)); + } scanSettings.modify()->setExtraTagsToScan(extraTagsToScan); scanSettings.modify()->setArtistTagDelimiters(artistDelimiters); @@ -318,6 +335,9 @@ namespace lms::ui updateStartTime->setModel(model->updateStartTimeModel()); t->setFormWidget(DatabaseSettingsModel::UpdateStartTimeField, std::move(updateStartTime)); + // Skip playlists + t->setFormWidget(DatabaseSettingsModel::SkipSingleReleasePlayLists, std::make_unique()); + // Similarity engine type auto similarityEngineType{ std::make_unique() }; similarityEngineType->setModel(model->similarityEngineTypeModel()); diff --git a/src/lms/ui/explore/ArtistCollector.cpp b/src/lms/ui/explore/ArtistCollector.cpp index eaf08daa..6b7a165f 100644 --- a/src/lms/ui/explore/ArtistCollector.cpp +++ b/src/lms/ui/explore/ArtistCollector.cpp @@ -99,7 +99,24 @@ namespace lms::ui params.setKeywords(getSearchKeywords()); params.setMediaLibrary(filters.getMediaLibrary()); params.setLinkType(_linkType); - params.setSortMethod(ArtistSortMethod::LastWritten); + params.setSortMethod(ArtistSortMethod::AddedDesc); + params.setRange(range); + + { + auto transaction{ LmsApp->getDbSession().createReadTransaction() }; + artists = Artist::findIds(LmsApp->getDbSession(), params); + } + break; + } + + case Mode::RecentlyModified: + { + Artist::FindParameters params; + params.setClusters(filters.getClusters()); + params.setKeywords(getSearchKeywords()); + params.setMediaLibrary(filters.getMediaLibrary()); + params.setLinkType(_linkType); + params.setSortMethod(ArtistSortMethod::LastWrittenDesc); params.setRange(range); { diff --git a/src/lms/ui/explore/DatabaseCollectorBase.hpp b/src/lms/ui/explore/DatabaseCollectorBase.hpp index 956abbe8..5bc756ae 100644 --- a/src/lms/ui/explore/DatabaseCollectorBase.hpp +++ b/src/lms/ui/explore/DatabaseCollectorBase.hpp @@ -43,6 +43,7 @@ namespace lms::ui Starred, RecentlyPlayed, RecentlyAdded, + RecentlyModified, MostPlayed, All }; diff --git a/src/lms/ui/explore/ReleaseCollector.cpp b/src/lms/ui/explore/ReleaseCollector.cpp index 8d60907c..df5e52fe 100644 --- a/src/lms/ui/explore/ReleaseCollector.cpp +++ b/src/lms/ui/explore/ReleaseCollector.cpp @@ -92,7 +92,23 @@ namespace lms::ui params.setClusters(getFilters().getClusters()); params.setMediaLibrary(getFilters().getMediaLibrary()); params.setKeywords(getSearchKeywords()); - params.setSortMethod(ReleaseSortMethod::LastWritten); + params.setSortMethod(ReleaseSortMethod::AddedDesc); + params.setRange(range); + + { + auto transaction{ LmsApp->getDbSession().createReadTransaction() }; + releases = Release::findIds(LmsApp->getDbSession(), params); + } + break; + } + + case Mode::RecentlyModified: + { + Release::FindParameters params; + params.setClusters(getFilters().getClusters()); + params.setMediaLibrary(getFilters().getMediaLibrary()); + params.setKeywords(getSearchKeywords()); + params.setSortMethod(ReleaseSortMethod::LastWrittenDesc); params.setRange(range); { diff --git a/src/lms/ui/explore/ReleaseHelpers.cpp b/src/lms/ui/explore/ReleaseHelpers.cpp index 20465ae8..09bda9be 100644 --- a/src/lms/ui/explore/ReleaseHelpers.cpp +++ b/src/lms/ui/explore/ReleaseHelpers.cpp @@ -174,7 +174,9 @@ namespace lms::ui::releaseHelpers { Wt::WString res; - // Year can be here, but originalYear can't be here without year (enforced by scanner) + // Year could be here, but originalYear can't be here without year (enforced by scanner) + assert(year || !originalYear); + if (!year) return res; diff --git a/src/lms/ui/explore/SortModeSelector.hpp b/src/lms/ui/explore/SortModeSelector.hpp index a160f20c..2c79e77f 100644 --- a/src/lms/ui/explore/SortModeSelector.hpp +++ b/src/lms/ui/explore/SortModeSelector.hpp @@ -36,6 +36,7 @@ namespace lms::ui bindItem("recently-played", Wt::WString::tr("Lms.Explore.recently-played"), DatabaseCollectorBase::Mode::RecentlyPlayed); bindItem("most-played", Wt::WString::tr("Lms.Explore.most-played"), DatabaseCollectorBase::Mode::MostPlayed); bindItem("recently-added", Wt::WString::tr("Lms.Explore.recently-added"), DatabaseCollectorBase::Mode::RecentlyAdded); + bindItem("recently-modified", Wt::WString::tr("Lms.Explore.recently-modified"), DatabaseCollectorBase::Mode::RecentlyModified); bindItem("all", Wt::WString::tr("Lms.Explore.all"), DatabaseCollectorBase::Mode::All); } }; diff --git a/src/lms/ui/explore/TrackCollector.cpp b/src/lms/ui/explore/TrackCollector.cpp index 4bb2a338..7e5500c9 100644 --- a/src/lms/ui/explore/TrackCollector.cpp +++ b/src/lms/ui/explore/TrackCollector.cpp @@ -93,7 +93,23 @@ namespace lms::ui params.setClusters(getFilters().getClusters()); params.setMediaLibrary(getFilters().getMediaLibrary()); params.setKeywords(getSearchKeywords()); - params.setSortMethod(TrackSortMethod::LastWritten); + params.setSortMethod(TrackSortMethod::AddedDesc); + params.setRange(range); + + { + auto transaction{ LmsApp->getDbSession().createReadTransaction() }; + tracks = Track::findIds(LmsApp->getDbSession(), params); + } + break; + } + + case Mode::RecentlyModified: + { + Track::FindParameters params; + params.setClusters(getFilters().getClusters()); + params.setMediaLibrary(getFilters().getMediaLibrary()); + params.setKeywords(getSearchKeywords()); + params.setSortMethod(TrackSortMethod::LastWrittenDesc); params.setRange(range); { diff --git a/src/tools/cover/LmsCover.cpp b/src/tools/cover/LmsCover.cpp index c5e08921..2eaa8039 100644 --- a/src/tools/cover/LmsCover.cpp +++ b/src/tools/cover/LmsCover.cpp @@ -89,7 +89,7 @@ int main(int argc, char* argv[]) image::init(argv[0]); core::Service config{ core::createConfig(vm["conf"].as()) }; - db::Db db{ config->getPath("working-dir") / "lms.db" }; + db::Db db{ config->getPath("working-dir", "/var/lms") / "lms.db" }; core::Service coverArtService{ cover::createArtworkService(db, vm["default-release-cover,"].as(), vm["default-artist-image"].as()) }; coverArtService->setJpegQuality(config->getULong("cover-jpeg-quality", vm["quality"].as())); diff --git a/src/tools/db-generator/LmsDbGenerator.cpp b/src/tools/db-generator/LmsDbGenerator.cpp index 13effd41..f251facb 100644 --- a/src/tools/db-generator/LmsDbGenerator.cpp +++ b/src/tools/db-generator/LmsDbGenerator.cpp @@ -203,7 +203,7 @@ int main(int argc, char* argv[]) throw std::runtime_error{ "File '" + genParams.trackPath.string() + "' does not exist!" }; core::Service config{ core::createConfig(vm["conf"].as()) }; - db::Db db{ config->getPath("working-dir") / "lms.db" }; + db::Db db{ config->getPath("working-dir", "/var/lms") / "lms.db" }; db::Session session{ db }; std::cout << "Starting generation..." << std::endl; diff --git a/src/tools/metadata/CMakeLists.txt b/src/tools/metadata/CMakeLists.txt index 30decda4..dcdd2394 100644 --- a/src/tools/metadata/CMakeLists.txt +++ b/src/tools/metadata/CMakeLists.txt @@ -10,4 +10,3 @@ target_link_libraries(lms-metadata PRIVATE ) install(TARGETS lms-metadata DESTINATION bin) - diff --git a/src/tools/metadata/LmsMetadata.cpp b/src/tools/metadata/LmsMetadata.cpp index 0ed739af..dcec02f9 100644 --- a/src/tools/metadata/LmsMetadata.cpp +++ b/src/tools/metadata/LmsMetadata.cpp @@ -135,6 +135,24 @@ namespace lms::metadata return os; } + std::ostream& operator<<(std::ostream& os, Track::Advisory advisory) + { + switch (advisory) + { + case Track::Advisory::Clean: + os << "clean"; + break; + case Track::Advisory::Explicit: + os << "Explicit"; + break; + case Track::Advisory::Unknown: + os << "Unknown"; + break; + } + + return os; + } + void parse(IParser& parser, const std::filesystem::path& file) { using namespace metadata; @@ -184,9 +202,6 @@ namespace lms::metadata for (const Artist& artist : track->remixerArtists) std::cout << "Remixer: " << artist << std::endl; - if (track->medium) - std::cout << "Medium: " << *track->medium; - std::cout << "Title: " << track->title << std::endl; if (track->mbid) @@ -220,12 +235,10 @@ namespace lms::metadata std::cout << "Position: " << *track->position << std::endl; if (track->date.isValid()) - std::cout << "Date: " << track->date.toString("yyyy-MM-dd") << std::endl; - if (track->year) - std::cout << "Year: " << *track->year << std::endl; + std::cout << "Date: " << track->date.toISO8601String() << std::endl; if (track->originalDate.isValid()) - std::cout << "Original date: " << track->originalDate.toString("yyyy-MM-dd") << std::endl; + std::cout << "Original date: " << track->originalDate.toISO8601String() << std::endl; if (track->originalYear) std::cout << "Original year: " << *track->originalYear << std::endl; @@ -250,6 +263,15 @@ namespace lms::metadata if (!track->copyrightURL.empty()) std::cout << "CopyrightURL: " << track->copyrightURL << std::endl; + if (track->advisory) + std::cout << "Advisory: " << *track->advisory << std::endl; + + if (track->encodingTime.isValid()) + std::cout << "Encoding time: " << track->encodingTime.toISO8601String() << std::endl; + + if (track->medium) + std::cout << "Medium: " << *track->medium; + std::cout << std::endl; } } // namespace lms::metadata @@ -365,14 +387,14 @@ int main(int argc, char* argv[]) { std::cout << "Using Lyrics:" << std::endl; - std::ifstream ifs{ file.string() }; + std::ifstream ifs{ file }; if (ifs) { const metadata::Lyrics lyrics{ metadata::parseLyrics(ifs) }; std::cout << lyrics << std::endl; } else - std::cerr << "Cannot open file '" << file.string() << "'"; + std::cerr << "Cannot open file " << file; } catch (metadata::Exception& e) { @@ -424,4 +446,4 @@ int main(int argc, char* argv[]) } return EXIT_SUCCESS; -} +} \ No newline at end of file diff --git a/src/tools/recommendation/CMakeLists.txt b/src/tools/recommendation/CMakeLists.txt index 12f20ef0..a4923a32 100644 --- a/src/tools/recommendation/CMakeLists.txt +++ b/src/tools/recommendation/CMakeLists.txt @@ -8,5 +8,3 @@ target_link_libraries(lms-recommendation PRIVATE lmsrecommendation Boost::program_options ) - -install(TARGETS lms-recommendation DESTINATION bin) diff --git a/src/tools/recommendation/LmsRecommendation.cpp b/src/tools/recommendation/LmsRecommendation.cpp index c2e45a5e..42acf637 100644 --- a/src/tools/recommendation/LmsRecommendation.cpp +++ b/src/tools/recommendation/LmsRecommendation.cpp @@ -145,7 +145,7 @@ int main(int argc, char* argv[]) core::Service config{ core::createConfig(vm["conf"].as()) }; - Db db{ config->getPath("working-dir") / "lms.db" }; + Db db{ config->getPath("working-dir", "/var/lms") / "lms.db" }; Session session{ db }; std::cout << "Creating recommendation service..." << std::endl; diff --git a/src/tools/similarity-parameters/LmsSimilarityParameters.cpp b/src/tools/similarity-parameters/LmsSimilarityParameters.cpp index 952978a2..65fe041b 100644 --- a/src/tools/similarity-parameters/LmsSimilarityParameters.cpp +++ b/src/tools/similarity-parameters/LmsSimilarityParameters.cpp @@ -384,7 +384,7 @@ int main(int argc, char* argv[]) ServiceProvider::create(configFilePath); - db::Db db{ ServiceProvider::get()->getPath("working-dir") / "lms.db" }; + db::Db db{ ServiceProvider::get()->getPath("working-dir", "/var/lms") / "lms.db" }; db::SessionPool sessionPool{ db, nbWorkers }; std::cout << "Caching all features..." << std::endl;