Merge branch 'develop' for release v3.63.0

This commit is contained in:
emeric
2025-01-22 18:48:50 +01:00
117 changed files with 1773 additions and 451 deletions
+11 -15
View File
@@ -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 <itmfr@yahoo.fr>"
ARG RUNTIME_PACKAGES=" \
+4 -1
View File
@@ -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: <kbd>Space</kbd>
* Previous track: <kbd>Ctrl</kbd> + <kbd>Left</kbd>
* Next track: <kbd>Ctrl</kbd> + <kbd>Right</kbd>
* Decrease volume: <kbd>Ctrl</kbd> + <kbd>Down</kbd>
* Increase volume: <kbd>Ctrl</kbd> + <kbd>Up</kbd>
## Installation
See [INSTALL.md](INSTALL.md) file.
+23 -11
View File
@@ -4,7 +4,7 @@
<!--FORMS message blocks-->
<message id="Lms.Admin.Database.template">
<form class="row g-3">
<form class="row g-3 mb-3">
<legend>${tr:Lms.Admin.Database.scan-settings}</legend>
<div class="col-sm-6">
<label class="form-label" for="${id:update-period}">
@@ -24,15 +24,7 @@
${update-start-time-info}
</div>
</div>
<div class="col-12">
<label class="form-label" for="${id:similarity-engine-type}">
${tr:Lms.Admin.Database.similarity-engine-type}
</label>
${similarity-engine-type class="form-control"}
<div class="invalid-feedback">
${similarity-engine-type-info}
</div>
</div>
<legend>${tr:Lms.Admin.Database.tag-parsing}</legend>
<div class="col-12">
<label class="form-label" for="${id:extra-tags-to-scan}">
${tr:Lms.Admin.Database.extra-tags-to-scan}
@@ -54,10 +46,30 @@
</label>
${default-tag-delimiter-container class="row gy-3"}
</div>
<legend>${tr:Lms.Admin.Database.misc}</legend>
<div class="col-12">
${save-btn class="btn btn-primary me-1"}${discard-btn class="btn btn-secondary"}
<div class="form-check">
${skip-single-release-playlists class="form-check-input"}
<label class="form-check-label" for="${id:skip-single-release-playlists}">
${tr:Lms.Admin.Database.skip-single-release-playlists}
</label>
<div class="invalid-feedback">
${skip-single-release-playlists-info}
</div>
</div>
</div>
<div class="col-12">
<label class="form-label" for="${id:similarity-engine-type}">
${tr:Lms.Admin.Database.similarity-engine-type}
</label>
${similarity-engine-type class="form-control"}
<div class="invalid-feedback">
${similarity-engine-type-info}
</div>
</div>
</form>
<hr/>
${save-btn class="btn btn-primary me-1"}${discard-btn class="btn btn-secondary"}
</message>
<message id="Lms.Admin.Database.template.line-edit-entry">
+1
View File
@@ -63,6 +63,7 @@
<li>${recently-played class="dropdown-item"}</li>
<li>${most-played class="dropdown-item"}</li>
<li>${recently-added class="dropdown-item"}</li>
<li>${recently-modified class="dropdown-item"}</li>
<li>${all class="dropdown-item"}</li>
</ul>
</div>
+3
View File
@@ -78,6 +78,7 @@
<message id="Lms.Admin.Database.extra-tags-to-scan">Extra tags to scan</message>
<message id="Lms.Admin.Database.hourly">Hourly</message>
<message id="Lms.Admin.Database.immediate-scan">Scan now!</message>
<message id="Lms.Admin.Database.misc">Miscellaneous</message>
<message id="Lms.Admin.Database.monthly">Monthly</message>
<message id="Lms.Admin.Database.never">Never</message>
<message id="Lms.Admin.Database.scan-aborted">Scan aborted!</message>
@@ -87,7 +88,9 @@
<message id="Lms.Admin.Database.similarity-engine-type">Similarity engine</message>
<message id="Lms.Admin.Database.similarity-engine-type.clusters">Tag-based</message>
<message id="Lms.Admin.Database.similarity-engine-type.none">None</message>
<message id="Lms.Admin.Database.skip-single-release-playlists">Skip playlists that contain tracks from the same album</message>
<message id="Lms.Admin.Database.tag-delimiter-must-not-contain-only-spaces">The tag delimiter must not consist solely of spaces</message>
<message id="Lms.Admin.Database.tag-parsing">Tag parsing</message>
<message id="Lms.Admin.Database.update-period">Update period</message>
<message id="Lms.Admin.Database.update-start-time">Update start time</message>
<message id="Lms.Admin.Database.weekly">Weekly</message>
+3
View File
@@ -78,6 +78,7 @@
<message id="Lms.Admin.Database.extra-tags-to-scan">Tags supplémentaires à scanner</message>
<message id="Lms.Admin.Database.hourly">Toutes les heures</message>
<message id="Lms.Admin.Database.immediate-scan">Scanner maintenant !</message>
<message id="Lms.Admin.Database.misc">Divers</message>
<message id="Lms.Admin.Database.monthly">Tous les mois</message>
<message id="Lms.Admin.Database.never">Jamais</message>
<message id="Lms.Admin.Database.scan-aborted">Scan interrompu !</message>
@@ -87,7 +88,9 @@
<message id="Lms.Admin.Database.similarity-engine-type">Moteur de similarité</message>
<message id="Lms.Admin.Database.similarity-engine-type.clusters">Basé sur les tags</message>
<message id="Lms.Admin.Database.similarity-engine-type.none">Aucun</message>
<message id="Lms.Admin.Database.skip-single-release-playlists">Ignorer les playlists contenant des pistes d'un même album</message>
<message id="Lms.Admin.Database.tag-delimiter-must-not-contain-only-spaces">Le délimiteur de tag ne doit pas comporter uniquement des espaces</message>
<message id="Lms.Admin.Database.tag-parsing">Analyse des tags</message>
<message id="Lms.Admin.Database.update-period">Périodicité des mises à jour</message>
<message id="Lms.Admin.Database.update-start-time">Heure de départ de la mise à jour</message>
<message id="Lms.Admin.Database.weekly">Toutes les semaines</message>
+3
View File
@@ -78,6 +78,7 @@
<message id="Lms.Admin.Database.extra-tags-to-scan">Tag aggiuntivi da scansionare</message>
<message id="Lms.Admin.Database.hourly">Ogni ora</message>
<message id="Lms.Admin.Database.immediate-scan">Scansiona ora!</message>
<message id="Lms.Admin.Database.misc">Varie</message>
<message id="Lms.Admin.Database.monthly">Mensile</message>
<message id="Lms.Admin.Database.never">Mai</message>
<message id="Lms.Admin.Database.scan-aborted">Scansione annullata!</message>
@@ -87,7 +88,9 @@
<message id="Lms.Admin.Database.similarity-engine-type">Motore di similarità</message>
<message id="Lms.Admin.Database.similarity-engine-type.clusters">Basato su tag</message>
<message id="Lms.Admin.Database.similarity-engine-type.none">Nessuno</message>
<message id="Lms.Admin.Database.skip-single-release-playlists">Salta le playlist che contengono brani dello stesso album</message>
<message id="Lms.Admin.Database.tag-delimiter-must-not-contain-only-spaces">Il delimitatore del tag non deve consistere esclusivamente di spazi</message>
<message id="Lms.Admin.Database.tag-parsing">Analisi dei tag</message>
<message id="Lms.Admin.Database.update-period">Frequenza di aggiornamento</message>
<message id="Lms.Admin.Database.update-start-time">Orario di aggiornamento</message>
<message id="Lms.Admin.Database.weekly">Settimanale</message>
+3
View File
@@ -79,6 +79,7 @@
<message id="Lms.Admin.Database.extra-tags-to-scan">Szukaj dodatkowych znaczników</message>
<message id="Lms.Admin.Database.hourly">Co godzinę</message>
<message id="Lms.Admin.Database.immediate-scan">Skanuj teraz!</message>
<message id="Lms.Admin.Database.misc">Różne</message>
<message id="Lms.Admin.Database.monthly">Co miesiąc</message>
<message id="Lms.Admin.Database.never">Nigdy</message>
<message id="Lms.Admin.Database.scan-aborted">Skanowanie przerwane!</message>
@@ -88,7 +89,9 @@
<message id="Lms.Admin.Database.similarity-engine-type">Metoda sprawdzania podobieństwa</message>
<message id="Lms.Admin.Database.similarity-engine-type.clusters">Oparta o znaczniki</message>
<message id="Lms.Admin.Database.similarity-engine-type.none">Żadna</message>
<message id="Lms.Admin.Database.skip-single-release-playlists">Pomiń playlisty zawierające utwory z tego samego albumu</message>
<message id="Lms.Admin.Database.tag-delimiter-must-not-contain-only-spaces">Rozdzielacz nie może się składać z samych białych znaków</message>
<message id="Lms.Admin.Database.tag-parsing">Analiza tagów</message>
<message id="Lms.Admin.Database.update-period">Okres aktualizacji</message>
<message id="Lms.Admin.Database.update-start-time">Czas startu aktualizacji</message>
<message id="Lms.Admin.Database.weekly">Co tydzień</message>
+3
View File
@@ -78,6 +78,7 @@
<message id="Lms.Admin.Database.hourly">每小时</message>
<message id="Lms.Admin.Database.immediate-scan">立即扫描!</message>
<message id="Lms.Admin.Database.monthly">每月</message>
<message id="Lms.Admin.Database.never">从不</message>
@@ -88,6 +89,8 @@
<message id="Lms.Admin.Database.update-period">更新周期</message>
<message id="Lms.Admin.Database.update-start-time">更新开始时间</message>
<message id="Lms.Admin.Database.weekly">每周</message>
+2 -3
View File
@@ -209,10 +209,9 @@
</div>
</div>
${</if-has-change-password>}
<div class="col-12">
${save-btn class="btn btn-primary me-1"}${discard-btn class="btn btn-secondary"}
</div>
</form>
<hr/>
${save-btn class="btn btn-primary me-1"}${discard-btn class="btn btn-secondary"}
</message>
</messages>
+3 -3
View File
@@ -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";
+25
View File
@@ -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();
+1 -3
View File
@@ -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})
+4 -4
View File
@@ -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 "";
+1 -1
View File
@@ -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<std::string> args;
+2 -3
View File
@@ -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()
+1 -1
View File
@@ -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 };
+1 -1
View File
@@ -32,7 +32,7 @@ namespace lms::core
{
try
{
_config.readFile(p.string().c_str());
_config.readFile(p.c_str());
}
catch (libconfig::FileIOException& e)
{
+5 -5
View File
@@ -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<void(std::string_view)> _func, std::initializer_list<std::string_view> 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;
};
+4 -4
View File
@@ -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 {};
}
+164
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include "core/PartialDateTime.hpp"
#include <iomanip>
#include <limits>
#include <sstream>
namespace lms::core
{
PartialDateTime::PartialDateTime(int year)
: _year{ static_cast<std::int16_t>(year) }
, _precision{ Precision::Year }
{
}
PartialDateTime::PartialDateTime(int year, unsigned month)
: _year{ static_cast<std::int16_t>(year) }
, _month{ static_cast<std::uint8_t>(month) }
, _precision{ Precision::Month }
{
}
PartialDateTime::PartialDateTime(int year, unsigned month, unsigned day)
: _year{ static_cast<std::int16_t>(year) }
, _month{ static_cast<std::uint8_t>(month) }
, _day{ static_cast<std::uint8_t>(day) }
, _precision{ Precision::Day }
{
}
PartialDateTime::PartialDateTime(int year, unsigned month, unsigned day, unsigned hour, unsigned min, unsigned sec)
: _year{ static_cast<std::int16_t>(year) }
, _month{ static_cast<std::uint8_t>(month) }
, _day{ static_cast<std::uint8_t>(day) }
, _hour{ static_cast<std::uint8_t>(hour) }
, _min{ static_cast<std::uint8_t>(min) }
, _sec{ static_cast<std::uint8_t>(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<int>(_month);
if (_precision >= Precision::Day)
ss << "-" << std::setw(2) << static_cast<int>(_day);
if (_precision >= Precision::Hour)
ss << 'T' << std::setw(2) << static_cast<int>(_hour);
if (_precision >= Precision::Min)
ss << ':' << std::setw(2) << static_cast<int>(_min);
if (_precision >= Precision::Sec)
ss << ':' << std::setw(2) << static_cast<int>(_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<decltype(tm.tm_year)>::min();
tm.tm_mon = std::numeric_limits<decltype(tm.tm_mon)>::min();
tm.tm_mday = std::numeric_limits<decltype(tm.tm_mday)>::min();
tm.tm_hour = std::numeric_limits<decltype(tm.tm_hour)>::min();
tm.tm_min = std::numeric_limits<decltype(tm.tm_min)>::min();
tm.tm_sec = std::numeric_limits<decltype(tm.tm_sec)>::min();
std::istringstream ss{ dateTimeStr };
ss >> std::get_time(&tm, format);
if (ss.fail())
return res;
if (tm.tm_sec != std::numeric_limits<decltype(tm.tm_sec)>::min())
{
res._sec = tm.tm_sec;
res._precision = Precision::Sec;
}
if (tm.tm_min != std::numeric_limits<decltype(tm.tm_min)>::min())
{
res._min = tm.tm_min;
if (res._precision == Precision::Invalid)
res._precision = Precision::Min;
}
if (tm.tm_hour != std::numeric_limits<decltype(tm.tm_hour)>::min())
{
res._hour = tm.tm_hour;
if (res._precision == Precision::Invalid)
res._precision = Precision::Hour;
}
if (tm.tm_mday != std::numeric_limits<decltype(tm.tm_mday)>::min())
{
res._day = tm.tm_mday;
if (res._precision == Precision::Invalid)
res._precision = Precision::Day;
}
if (tm.tm_mon != std::numeric_limits<decltype(tm.tm_mon)>::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<decltype(tm.tm_year)>::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
+5 -5
View File
@@ -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<const std::filesystem::path> 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));
}
+6
View File
@@ -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;
+6 -6
View File
@@ -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<void(std::string_view)> _func, std::initializer_list<std::string_view> 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<void(std::string_view)> _func, std::initializer_list<std::string_view> 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<IConfig> createConfig(const std::filesystem::path& p);
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <cstdint>
#include <optional>
#include <string>
#include <string_view>
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<int> getYear() const { return (_precision >= Precision::Year ? std::make_optional(_year) : std::nullopt); }
constexpr std::optional<int> getMonth() const { return (_precision >= Precision::Month ? std::make_optional(_month) : std::nullopt); }
constexpr std::optional<int> 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
+2
View File
@@ -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
+1
View File
@@ -3,6 +3,7 @@ include(GoogleTest)
add_executable(test-core
EnumSet.cpp
LiteralString.cpp
PartialDateTime.cpp
Path.cpp
RecursiveSharedMutex.cpp
Service.cpp
+126
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include <gtest/gtest.h>
#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<int>{ 3 });
}
TEST(PartialDateTime, day)
{
EXPECT_EQ(PartialDateTime{}.getDay(), std::nullopt);
EXPECT_EQ((PartialDateTime{ 1992, 3, 27 }.getDay()), std::optional<int>{ 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
+9 -2
View File
@@ -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"));
+1 -3
View File
@@ -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()
+9 -5
View File
@@ -42,9 +42,9 @@ namespace lms::db
{
session.checkReadTransaction();
// TODO remove distinct and use group by
auto query{ session.getDboSession()->query<ResultType>("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());
+5 -2
View File
@@ -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<SqlConnection> 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<Connection>(dbPath.string()) };
auto connection{ std::make_unique<Connection>(dbPath) };
if (core::IConfig * config{ core::Service<core::IConfig>::get() }) // may not be here on testU
connection->setProperty("show-queries", config->getBool("db-show-queries", false) ? "true" : "false");
+10 -5
View File
@@ -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
+42 -5
View File
@@ -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<std::string> indexeNames{ utils::fetchQueryResults(session.getDboSession()->query<std::string>(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<std::string> indexeNames{ utils::fetchQueryResults(session.getDboSession()->query<std::string>(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
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "core/PartialDateTime.hpp"
#include <Wt/Dbo/SqlTraits.h>
namespace Wt::Dbo
{
template<>
struct sql_value_traits<lms::core::PartialDateTime>
{
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
+1 -1
View File
@@ -56,7 +56,7 @@ namespace lms::db
{
session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<PlayListFile>>("SELECT pl_f from playlist_file pl_f").where("pl_f.absolute_file_path = ?").bind(p.string()));
return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<PlayListFile>>("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<void(const pointer&)>& func)
+49 -20
View File
@@ -21,7 +21,7 @@
#include <Wt/Dbo/WtSqlTraits.h>
#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<ResultType>("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<Wt::WDate>(std::string{ "SELECT " } + "t." + field + " FROM track t").where("t.release_id = ?").groupBy(field).bind(getId())) };
auto query{ (session()->query<core::PartialDateTime>(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::optional<int>>(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<core::PartialDateTime>(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<int> 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<std::string> Release::getCopyright() const
@@ -666,6 +682,19 @@ namespace lms::db
utils::forEachQueryResult(query, _func);
}
core::EnumSet<Advisory> Release::getAdvisories() const
{
core::EnumSet<Advisory> res;
auto query{ session()->query<Advisory>("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());
+9
View File
@@ -94,6 +94,15 @@ namespace lms::db
}
}
void ScanSettings::setSkipSingleReleasePlayLists(bool value)
{
if (_skipSingleReleasePlayLists != value)
{
_skipSingleReleasePlayLists = value;
incScanVersion();
}
}
void ScanSettings::incScanVersion()
{
_scanVersion += 1;
+11 -3
View File
@@ -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)");
+19 -2
View File
@@ -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<Wt::Dbo::ptr<Track>>("SELECT t from track t").where("t.absolute_file_path = ?").bind(p.string()));
return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<Track>>("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<int> Track::getYear() const
{
return _date.getYear();
}
std::optional<int> Track::getOriginalYear() const
{
return _originalDate.getYear();
}
bool Track::hasLyrics() const
{
return !_trackLyrics.empty();
-5
View File
@@ -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
@@ -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> _range)
{
@@ -92,6 +93,11 @@ namespace lms::db
mediaLibrary = _mediaLibrary;
return *this;
}
FindParameters& setSortMethod(DirectorySortMethod _method)
{
sortMethod = _method;
return *this;
}
};
// find
@@ -19,7 +19,6 @@
#pragma once
#include <filesystem>
#include <optional>
#include <span>
#include <string>
@@ -30,6 +29,7 @@
#include <Wt/WDateTime.h>
#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> range;
Wt::WDateTime writtenAfter;
std::optional<DateRange> dateRange;
std::optional<YearRange> dateRange;
UserId starringUser; // only releases starred by this user
std::optional<FeedbackBackend> 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>& _dateRange)
FindParameters& setDateRange(const std::optional<YearRange>& _dateRange)
{
dateRange = _dateRange;
return *this;
@@ -227,9 +227,9 @@ namespace lms::db
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(const std::vector<ClusterTypeId>& 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<int> getYear() const;
Wt::WDate getOriginalDate() const;
core::PartialDateTime getOriginalDate() const;
std::optional<int> getOriginalYear() const;
std::optional<std::string> getCopyright() const;
std::optional<std::string> getCopyrightURL() const;
@@ -252,6 +252,7 @@ namespace lms::db
std::vector<std::string> getLabelNames() const;
std::vector<std::string> getReleaseTypeNames() const;
void visitLabels(const std::function<void(const Label::pointer& label)>& _func) const;
core::EnumSet<Advisory> getAdvisories() const;
std::string_view getBarcode() const { return _barcode; }
ObjectPtr<Image> getImage() const;
@@ -301,7 +302,7 @@ namespace lms::db
Release(const std::string& name, const std::optional<core::UUID>& MBID = {});
static pointer create(Session& session, const std::string& name, const std::optional<core::UUID>& MBID = {});
Wt::WDate getDate(bool original) const;
core::PartialDateTime getDate(bool original) const;
std::optional<int> getYear(bool original) const;
static constexpr std::size_t _maxNameLength{ 512 };
@@ -70,6 +70,7 @@ namespace lms::db
SimilarityEngineType getSimilarityEngineType() const { return _similarityEngineType; }
std::vector<std::string> getArtistTagDelimiters() const;
std::vector<std::string> 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<const std::string_view> delimiters);
void setDefaultTagDelimiters(std::span<const std::string_view> delimiters);
void setSkipSingleReleasePlayLists(bool value);
void incScanVersion();
template<class Action>
@@ -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
+17 -24
View File
@@ -33,6 +33,7 @@
#include <Wt/WDateTime.h>
#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<int> year) { _year = year; }
void setOriginalDate(const Wt::WDate& date) { _originalDate = date; }
void setOriginalYear(std::optional<int> 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<core::UUID>& MBID) { _trackMBID = MBID ? MBID->getAsString() : ""; }
void setRecordingMBID(const std::optional<core::UUID>& 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<float> replayGain) { _trackReplayGain = replayGain; }
void setReleaseReplayGain(std::optional<float> 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<int> getYear() const { return _year; }
const Wt::WDate& getOriginalDate() const { return _originalDate; }
std::optional<int> getOriginalYear() const { return _originalYear; };
const core::PartialDateTime& getDate() const { return _date; }
std::optional<int> getYear() const;
const core::PartialDateTime& getOriginalDate() const { return _originalDate; }
std::optional<int> 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<core::UUID> getTrackMBID() const { return core::UUID::fromString(_trackMBID); }
std::optional<core::UUID> getRecordingMBID() const { return core::UUID::fromString(_recordingMBID); }
std::optional<std::string> getCopyright() const;
std::optional<std::string> getCopyrightURL() const;
Advisory getAdvisory() const { return _advisory; }
std::optional<float> getTrackReplayGain() const { return _trackReplayGain; }
std::optional<float> getReleaseReplayGain() const { return _releaseReplayGain; }
std::string_view getArtistDisplayName() const { return _artistDisplayName; }
@@ -294,6 +289,7 @@ namespace lms::db
std::vector<ObjectPtr<Artist>> getArtists(core::EnumSet<TrackArtistLinkType> artistLinkTypes) const; // no type means all
std::vector<ArtistId> getArtistIds(core::EnumSet<TrackArtistLinkType> artistLinkTypes) const; // no type means all
std::vector<ObjectPtr<TrackArtistLink>> getArtistLinks() const;
ReleaseId getReleaseId() const { return _release.id(); }
ObjectPtr<Release> getRelease() const { return _release; }
std::vector<ObjectPtr<Cluster>> getClusters() const;
std::vector<ClusterId> 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<int, std::milli> _duration{};
int _sampleRate{};
Wt::WDate _date;
std::optional<int> _year;
Wt::WDate _originalDate;
std::optional<int> _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<float> _trackReplayGain;
std::optional<float> _releaseReplayGain;
std::string _artistDisplayName;
std::string _comment;
Wt::Dbo::ptr<Release> _release;
Wt::Dbo::ptr<MediaLibrary> _mediaLibrary;
Wt::Dbo::ptr<Directory> _directory;
+24 -8
View File
@@ -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
+92 -3
View File
@@ -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
+101 -17
View File
@@ -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
+56 -12
View File
@@ -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
+4 -4
View File
@@ -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})
+1 -1
View File
@@ -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" };
+25 -1
View File
@@ -21,6 +21,8 @@
#include <memory>
#include <Magick++/Image.h>
#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<IRawImage> decodeImage(std::span<const std::byte> encodedData)
{
LMS_SCOPED_TRACE_DETAILED("Image", "DecodeBuffer");
@@ -54,7 +54,7 @@ namespace lms::image::GraphicsMagick
{
try
{
_image.read(p.string().c_str());
_image.read(p.c_str());
}
catch (Magick::WarningCoder& e)
{
+21 -3
View File
@@ -21,14 +21,14 @@
#include <array>
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include <stb_image_write.h>
#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<IRawImage> decodeImage(std::span<const std::byte> encodedData)
{
LMS_SCOPED_TRACE_DETAILED("Image", "DecodeBuffer");
+11 -20
View File
@@ -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 <stb_image.h>
#if STB_IMAGE_RESIZE_VERSION == 1
#include <stb_image_resize.h>
#elif STB_IMAGE_RESIZE_VERSION == 2
#include <stb_image_resize2.h>
#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<const std::byte> encodedData)
{
int n{};
_data = UniquePtrFree{ ::stbi_load_from_memory(reinterpret_cast<const stbi_uc*>(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);
+35
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#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
+42
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#define STBI_ONLY_JPEG
#define STBI_ONLY_PNG
#define STBI_ONLY_BMP
#define STBI_FAILURE_USERMSG
#include <stb_image.h>
#include <string_view>
#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
@@ -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 <http://www.gnu.org/licenses/>.
*/
#define STB_IMAGE_RESIZE_IMPLEMENTATION
#include "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 <http://www.gnu.org/licenses/>.
*/
#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 <stb_image_resize.h>
#elif STB_IMAGE_RESIZE_VERSION == 2
#include <stb_image_resize2.h>
#else
#error "Unhandled STB image resize version"!
#endif
+21
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "StbImageWrite.hpp"
+21
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <stb_image_write.h>
+3
View File
@@ -31,6 +31,9 @@ namespace lms::image
void init(const std::filesystem::path& path);
std::span<const std::filesystem::path> getSupportedFileExtensions();
// All these methods may throw Exception
ImageProperties probeImage(const std::filesystem::path& path);
std::unique_ptr<IRawImage> decodeImage(std::span<const std::byte> encodedData);
std::unique_ptr<IRawImage> decodeImage(const std::filesystem::path& path);
+7 -1
View File
@@ -22,4 +22,10 @@
namespace lms::image
{
using ImageSize = std::size_t;
}
struct ImageProperties
{
ImageSize width{};
ImageSize height{};
};
} // namespace lms::image
+1 -4
View File
@@ -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})
+3 -1
View File
@@ -32,6 +32,7 @@ namespace lms::metadata
// Mapping to internal avformat names and/or common alternative custom names
static const std::unordered_map<TagType, std::vector<std::string>> 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" } },
+3 -1
View File
@@ -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,
+1 -1
View File
@@ -28,7 +28,7 @@ namespace lms::metadata
{
std::span<const std::filesystem::path> getSupportedLyricsFileExtensions()
{
static const std::array<std::filesystem::path, 1> fileExtensions{ ".lrc" }; // TODO handle ".txt" and ".elrc"
static const std::array<std::filesystem::path, 2> fileExtensions{ ".lrc", ".txt" }; // TODO handle ".elrc"
return fileExtensions;
}
+35 -27
View File
@@ -22,6 +22,7 @@
#include <span>
#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<Track::Advisory> getAdvisory(const ITagReader& tagReader)
{
if (const auto value{ getTagValueAs<int>(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<IParser> 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<core::UUID>(tagReader, TagType::MusicBrainzRecordingID);
track.acoustID = getTagValueAs<core::UUID>(tagReader, TagType::AcoustID);
track.position = getTagValueAs<std::size_t>(tagReader, TagType::TrackNumber); // May parse 'Number/Total', that's fine
if (auto dateStr = getTagValueAs<std::string>(tagReader, TagType::Date))
if (const auto dateStr{ getTagValueAs<std::string>(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<std::string>(tagReader, TagType::OriginalReleaseDate))
if (const auto dateStr = getTagValueAs<std::string>(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<std::string>(tagReader, TagType::OriginalReleaseYear))
{
if (const auto dateStr{ getTagValueAs<std::string>(tagReader, TagType::OriginalReleaseYear) })
track.originalYear = utils::parseYear(*dateStr);
if (const auto encodingTimeStr{ getTagValueAs<std::string>(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<std::string>(tagReader, TagType::Comment, {} /* no custom delimiter on comments */);
track.copyright = getTagValueAs<std::string>(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<Medium> Parser::getMedium(const ITagReader& tagReader)
+32 -8
View File
@@ -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<TagType, std::vector<std::string>> 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<TagLib::MP4::File*>(_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"))
{
+1
View File
@@ -18,6 +18,7 @@
*/
#include "Utils.hpp"
#include <ctime>
#include <iomanip>
#include <sstream>
+12 -6
View File
@@ -26,8 +26,7 @@
#include <string_view>
#include <vector>
#include <Wt/WDate.h>
#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<core::UUID> mbid;
std::optional<core::UUID> recordingMBID;
@@ -113,10 +118,11 @@ namespace lms::metadata
std::vector<std::string> moods;
std::vector<std::string> languages;
Tags userExtraTags;
std::optional<int> year{};
Wt::WDate date;
std::optional<int> originalYear{};
Wt::WDate originalDate;
core::PartialDateTime date;
std::optional<int> originalYear;
core::PartialDateTime originalDate;
std::optional<Advisory> advisory;
core::PartialDateTime encodingTime;
bool hasCover{};
std::optional<core::UUID> acoustID;
std::string copyright;
+86 -9
View File
@@ -17,11 +17,12 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include <map>
#include <vector>
#include <gtest/gtest.h>
#include <Wt/WTime.h>
#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<Track::Advisory> expectedValue) {
const TestTagReader testTags{
{
{ TagType::Advisory, { value } },
}
};
Parser parser;
std::unique_ptr<Track> 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> 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> 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
+1 -4
View File
@@ -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})
@@ -59,7 +59,7 @@ namespace lms::cover
{
setJpegQuality(core::Service<core::IConfig>::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<CandidatePicture> 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;
+1 -4
View File
@@ -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})
+1 -3
View File
@@ -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})
@@ -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})
@@ -32,7 +32,7 @@ namespace lms::recommendation
{
std::filesystem::path getCacheDirectory()
{
return core::Service<core::IConfig>::get()->getPath("working-dir") / "cache" / "features";
return core::Service<core::IConfig>::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<core::IConfig>::get()->getPath("working-dir") / "cache" / "features");
std::filesystem::create_directories(core::Service<core::IConfig>::get()->getPath("working-dir", "/var/lms") / "cache" / "features");
if (!networkToCacheFile(_network, getCacheNetworkFilePath())
|| !objectPositionToCacheFile(_trackPositions, getCacheTrackPositionsFilePath()))
+1 -4
View File
@@ -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})
@@ -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;
@@ -42,6 +42,8 @@ namespace lms::scanner
std::vector<std::string> extraTags;
std::vector<std::string> artistTagDelimiters;
std::vector<std::string> defaultTagDelimiters;
bool skipSingleReleasePlayLists{};
std::vector<MediaLibraryInfo> mediaLibraries;
bool operator==(const ScannerSettings& rhs) const = default;
@@ -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<metadata::Track::Advisory> 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<db::Track>();
track.modify()->setAbsoluteFilePath(_file);
const core::PartialDateTime addedTime{
fileInfo->lastWriteTime.date().year(),
static_cast<unsigned>(fileInfo->lastWriteTime.date().month()),
static_cast<unsigned>(fileInfo->lastWriteTime.date().day()),
static_cast<unsigned>(fileInfo->lastWriteTime.time().hour()),
static_cast<unsigned>(fileInfo->lastWriteTime.time().minute()),
static_cast<unsigned>(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++;
}
}
@@ -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<ImageInfo> _parsedImageInfo;
std::optional<image::ImageProperties> _parsedImageProperties;
};
void ImageFileScanOperation::scan()
{
try
{
std::unique_ptr<image::IRawImage> 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++;
}
}
@@ -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++;
}
}
@@ -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++;
}
}
@@ -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();
}
@@ -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<db::TrackId> trackIds;
std::vector<TrackInfo> tracks;
};
using PlayListFileAssociationContainer = std::deque<PlayListFileAssociation>;
@@ -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<const db::TrackId> trackIds, const db::TrackList::pointer& trackList)
bool isSingleReleasePlayList(std::span<const TrackInfo> 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<const TrackInfo> 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<db::TrackList>(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<db::TrackListEntry>(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;
@@ -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++;
}
@@ -28,7 +28,7 @@
namespace lms::scanner
{
class IFileScanner;
class MediaLibraryInfo;
struct MediaLibraryInfo;
class ScanStepScanFiles : public ScanStepBase
{
@@ -23,7 +23,7 @@
namespace lms::scanner
{
class MediaLibraryInfo;
struct MediaLibraryInfo;
class ScanStepUpdateLibraryFields : public ScanStepBase
{
+1 -3
View File
@@ -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()
+1 -3
View File
@@ -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()
+1 -3
View File
@@ -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()
@@ -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);
@@ -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));
@@ -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<std::size_t>(context.user->getSubsonicDefaultTranscodingOutputBitrate(), maxBitRate);
bitrate = context.user->getSubsonicDefaultTranscodingOutputBitrate();
if (maxBitRate)
bitrate = std::min<std::size_t>(bitrate, maxBitRate);
av::transcoding::OutputParameters& outputParameters{ parameters.outputParameters.emplace() };
@@ -26,7 +26,7 @@
namespace lms::api::subsonic
{
class RequestContext;
struct RequestContext;
Response handleGetLyrics(RequestContext& context);
Response handleGetLyricsBySongId(RequestContext& context);
+14 -1
View File
@@ -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<db::Advisory> 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
+6 -10
View File
@@ -23,20 +23,16 @@
namespace lms::api::subsonic
{
Response::Node createItemDateNode(const Wt::WDate& date, std::optional<int> 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;
}
@@ -19,11 +19,11 @@
#pragma once
#include <Wt/WDate.h>
#include "core/PartialDateTime.hpp"
#include "SubsonicResponse.hpp"
namespace lms::api::subsonic
{
Response::Node createItemDateNode(const Wt::WDate& date, std::optional<int> year);
Response::Node createItemDateNode(const core::PartialDateTime& date);
}

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