Merge branch 'develop' for release v3.63.0
This commit is contained in:
+11
-15
@@ -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=" \
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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";
|
||||
|
||||
@@ -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,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})
|
||||
|
||||
@@ -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 "";
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 };
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace lms::core
|
||||
{
|
||||
try
|
||||
{
|
||||
_config.readFile(p.string().c_str());
|
||||
_config.readFile(p.c_str());
|
||||
}
|
||||
catch (libconfig::FileIOException& e)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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 {};
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -3,6 +3,7 @@ include(GoogleTest)
|
||||
add_executable(test-core
|
||||
EnumSet.cpp
|
||||
LiteralString.cpp
|
||||
PartialDateTime.cpp
|
||||
Path.cpp
|
||||
RecursiveSharedMutex.cpp
|
||||
Service.cpp
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <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
|
||||
@@ -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,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()
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -94,6 +94,15 @@ namespace lms::db
|
||||
}
|
||||
}
|
||||
|
||||
void ScanSettings::setSkipSingleReleasePlayLists(bool value)
|
||||
{
|
||||
if (_skipSingleReleasePlayLists != value)
|
||||
{
|
||||
_skipSingleReleasePlayLists = value;
|
||||
incScanVersion();
|
||||
}
|
||||
}
|
||||
|
||||
void ScanSettings::incScanVersion()
|
||||
{
|
||||
_scanVersion += 1;
|
||||
|
||||
@@ -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)");
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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})
|
||||
|
||||
|
||||
@@ -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" };
|
||||
|
||||
|
||||
@@ -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,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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -22,4 +22,10 @@
|
||||
namespace lms::image
|
||||
{
|
||||
using ImageSize = std::size_t;
|
||||
}
|
||||
|
||||
struct ImageProperties
|
||||
{
|
||||
ImageSize width{};
|
||||
ImageSize height{};
|
||||
};
|
||||
} // namespace lms::image
|
||||
@@ -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})
|
||||
|
||||
|
||||
@@ -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" } },
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"))
|
||||
{
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
*/
|
||||
|
||||
#include "Utils.hpp"
|
||||
|
||||
#include <ctime>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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,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,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 +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,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,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,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);
|
||||
|
||||
@@ -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
|
||||
@@ -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
Reference in New Issue
Block a user