diff --git a/.travis.yml b/.travis.yml index f2f798b8..19156a46 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,17 +9,17 @@ matrix: sources: - ubuntu-toolchain-r-test packages: - - g++-8 + - g++-9 env: - - MATRIX_EVAL="CC=gcc-8 && CXX=g++-8" + - MATRIX_EVAL="CC=gcc-9 && CXX=g++-9" compiler: - gcc before_install: - eval "${MATRIX_EVAL}" -- sudo apt-get install build-essential cmake libboost-all-dev libconfig++-dev libavcodec-dev libavutil-dev libavformat-dev ffmpeg libmagick++-dev libpstreams-dev libconfig++-dev libpstreams-dev libtag1-dev +- sudo apt-get install build-essential cmake libboost-all-dev libconfig++-dev libavcodec-dev libavutil-dev libavformat-dev ffmpeg libgraphicsmagick++1-dev libpstreams-dev libconfig++-dev libpstreams-dev libtag1-dev - git clone https://github.com/emweb/wt.git wt - pushd wt; -- git checkout 4.1.0 +- git checkout 4.2.1 - cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr && sudo make install - popd script: cmake -DCMAKE_BUILD_TYPE=Release . && make && make test diff --git a/CMakeLists.txt b/CMakeLists.txt index 516595ee..69b4c9b6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,12 +2,16 @@ cmake_minimum_required(VERSION 3.10) project(lms) + +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/modules/") + set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED True) include(CTest) -find_package(PkgConfig) -pkg_check_modules(IMAGEMAGICKXX REQUIRED ImageMagick++) +find_package(PkgConfig REQUIRED) +find_package(Filesystem REQUIRED) +pkg_check_modules(GRAPHICSMAGICKXX REQUIRED GraphicsMagick++) add_subdirectory(src) diff --git a/README.md b/README.md index 25b903a4..2738d002 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ __Notes__: * a C++17 compiler is needed * ffmpeg version 4 minimum is required ```sh -apt-get install g++ cmake libboost-system-dev libavutil-dev libavformat-dev libmagick++-dev libconfig++-dev libpstreams-dev ffmpeg libtag1-dev +apt-get install g++ cmake libboost-system-dev libavutil-dev libavformat-dev libgraphicsmagick++1-dev libconfig++-dev libpstreams-dev ffmpeg libtag1-dev ``` You also need _Wt4_, which is not packaged yet on _Debian_. See [installation instructions](https://www.webtoolkit.eu/wt/doc/reference/html/InstallationUnix.html).
@@ -157,7 +157,7 @@ systemctl restart lms __Note__: don't forget to give the _lms_ user read access to the music directory you want to scan. ### Configuration -_LMS_ uses a configuration file, installed by default in `/etc/lms.conf`. It is recommended to edit this file and change relevant settings (listen address, listen port, working directory, Subsonic API activation, ...). +_LMS_ uses a configuration file, installed by default in `/etc/lms.conf`. It is recommended to edit this file and change relevant settings (listen address, listen port, working directory, Subsonic API activation, deployment path, ...). All other settings are set using the web interface (user management, scan settings, transcode settings, ...). @@ -208,6 +208,6 @@ To connect to _LMS_, just open your favorite browser and go to http://localhost: * Wt (http://www.webtoolkit.eu/) * bootstrap3 (http://getbootstrap.com/) * ffmpeg project (https://ffmpeg.org/) -* Magick++ (http://www.imagemagick.org/Magick++/) +* GraphicsMagick++ (http://www.graphicsmagick.org/) * MetaBrainz (https://metabrainz.org/) * Bootstrap Notify: https://github.com/mouse0270/bootstrap-notify diff --git a/cmake/modules/FindFilesystem.cmake b/cmake/modules/FindFilesystem.cmake new file mode 100755 index 00000000..221130f8 --- /dev/null +++ b/cmake/modules/FindFilesystem.cmake @@ -0,0 +1,233 @@ +# Distributed under the OSI-approved BSD 3-Clause License. See accompanying +# file Copyright.txt or https://cmake.org/licensing for details. + +#[=======================================================================[.rst: + +FindFilesystem +############## + +This module supports the C++17 standard library's filesystem utilities. Use the +:imp-target:`std::filesystem` imported target to + +Options +******* + +The ``COMPONENTS`` argument to this module supports the following values: + +.. find-component:: Experimental + :name: fs.Experimental + + Allows the module to find the "experimental" Filesystem TS version of the + Filesystem library. This is the library that should be used with the + ``std::experimental::filesystem`` namespace. + +.. find-component:: Final + :name: fs.Final + + Finds the final C++17 standard version of the filesystem library. + +If no components are provided, behaves as if the +:find-component:`fs.Final` component was specified. + +If both :find-component:`fs.Experimental` and :find-component:`fs.Final` are +provided, first looks for ``Final``, and falls back to ``Experimental`` in case +of failure. If ``Final`` is found, :imp-target:`std::filesystem` and all +:ref:`variables ` will refer to the ``Final`` version. + + +Imported Targets +**************** + +.. imp-target:: std::filesystem + + The ``std::filesystem`` imported target is defined when any requested + version of the C++ filesystem library has been found, whether it is + *Experimental* or *Final*. + + If no version of the filesystem library is available, this target will not + be defined. + + .. note:: + This target has ``cxx_std_17`` as an ``INTERFACE`` + :ref:`compile language standard feature `. Linking + to this target will automatically enable C++17 if no later standard + version is already required on the linking target. + + +.. _fs.variables: + +Variables +********* + +.. variable:: CXX_FILESYSTEM_IS_EXPERIMENTAL + + Set to ``TRUE`` when the :find-component:`fs.Experimental` version of C++ + filesystem library was found, otherwise ``FALSE``. + +.. variable:: CXX_FILESYSTEM_HAVE_FS + + Set to ``TRUE`` when a filesystem header was found. + +.. variable:: CXX_FILESYSTEM_HEADER + + Set to either ``filesystem`` or ``experimental/filesystem`` depending on + whether :find-component:`fs.Final` or :find-component:`fs.Experimental` was + found. + +.. variable:: CXX_FILESYSTEM_NAMESPACE + + Set to either ``std::filesystem`` or ``std::experimental::filesystem`` + depending on whether :find-component:`fs.Final` or + :find-component:`fs.Experimental` was found. + + +Examples +******** + +Using `find_package(Filesystem)` with no component arguments: + +.. code-block:: cmake + + find_package(Filesystem REQUIRED) + + add_executable(my-program main.cpp) + target_link_libraries(my-program PRIVATE std::filesystem) + + +#]=======================================================================] + + +if(TARGET std::filesystem) + # This module has already been processed. Don't do it again. + return() +endif() + +include(CMakePushCheckState) +include(CheckIncludeFileCXX) +include(CheckCXXSourceCompiles) + +cmake_push_check_state() + +set(CMAKE_REQUIRED_QUIET ${Filesystem_FIND_QUIETLY}) + +# All of our tests required C++17 or later +set(CMAKE_CXX_STANDARD 17) + +# Normalize and check the component list we were given +set(want_components ${Filesystem_FIND_COMPONENTS}) +if(Filesystem_FIND_COMPONENTS STREQUAL "") + set(want_components Final) +endif() + +# Warn on any unrecognized components +set(extra_components ${want_components}) +list(REMOVE_ITEM extra_components Final Experimental) +foreach(component IN LISTS extra_components) + message(WARNING "Extraneous find_package component for Filesystem: ${component}") +endforeach() + +# Detect which of Experimental and Final we should look for +set(find_experimental TRUE) +set(find_final TRUE) +if(NOT "Final" IN_LIST want_components) + set(find_final FALSE) +endif() +if(NOT "Experimental" IN_LIST want_components) + set(find_experimental FALSE) +endif() + +if(find_final) + check_include_file_cxx("filesystem" _CXX_FILESYSTEM_HAVE_HEADER) + mark_as_advanced(_CXX_FILESYSTEM_HAVE_HEADER) + if(_CXX_FILESYSTEM_HAVE_HEADER) + # We found the non-experimental header. Don't bother looking for the + # experimental one. + set(find_experimental FALSE) + endif() +else() + set(_CXX_FILESYSTEM_HAVE_HEADER FALSE) +endif() + +if(find_experimental) + check_include_file_cxx("experimental/filesystem" _CXX_FILESYSTEM_HAVE_EXPERIMENTAL_HEADER) + mark_as_advanced(_CXX_FILESYSTEM_HAVE_EXPERIMENTAL_HEADER) +else() + set(_CXX_FILESYSTEM_HAVE_EXPERIMENTAL_HEADER FALSE) +endif() + +if(_CXX_FILESYSTEM_HAVE_HEADER) + set(_have_fs TRUE) + set(_fs_header filesystem) + set(_fs_namespace std::filesystem) +elseif(_CXX_FILESYSTEM_HAVE_EXPERIMENTAL_HEADER) + set(_have_fs TRUE) + set(_fs_header experimental/filesystem) + set(_fs_namespace std::experimental::filesystem) +else() + set(_have_fs FALSE) +endif() + +set(CXX_FILESYSTEM_HAVE_FS ${_have_fs} CACHE BOOL "TRUE if we have the C++ filesystem headers") +set(CXX_FILESYSTEM_HEADER ${_fs_header} CACHE STRING "The header that should be included to obtain the filesystem APIs") +set(CXX_FILESYSTEM_NAMESPACE ${_fs_namespace} CACHE STRING "The C++ namespace that contains the filesystem APIs") + +set(_found FALSE) + +if(CXX_FILESYSTEM_HAVE_FS) + # We have some filesystem library available. Do link checks + string(CONFIGURE [[ + #include <@CXX_FILESYSTEM_HEADER@> + + int main() { + auto cwd = @CXX_FILESYSTEM_NAMESPACE@::current_path(); + return static_cast(cwd.string().size()); + } + ]] code @ONLY) + + # Try to compile a simple filesystem program without any linker flags + check_cxx_source_compiles("${code}" CXX_FILESYSTEM_NO_LINK_NEEDED) + + set(can_link ${CXX_FILESYSTEM_NO_LINK_NEEDED}) + + if(NOT CXX_FILESYSTEM_NO_LINK_NEEDED) + set(prev_libraries ${CMAKE_REQUIRED_LIBRARIES}) + # Try to compile a simple filesystem program with the libstdc++ flag + set(CMAKE_REQUIRED_LIBRARIES ${prev_libraries} -lstdc++fs) + check_cxx_source_compiles("${code}" CXX_FILESYSTEM_STDCPPFS_NEEDED) + set(can_link ${CXX_FILESYSTEM_STDCPPFS_NEEDED}) + if(NOT CXX_FILESYSTEM_STDCPPFS_NEEDED) + # Try to compile a simple filesystem program with the libc++ flag + set(CMAKE_REQUIRED_LIBRARIES ${prev_libraries} -lc++fs) + check_cxx_source_compiles("${code}" CXX_FILESYSTEM_CPPFS_NEEDED) + set(can_link ${CXX_FILESYSTEM_CPPFS_NEEDED}) + if(NOT CXX_FILESYSTEM_CPPFS_NEEDED) + # Try to compile a simple filesystem program without any linker flags + check_cxx_source_compiles("${code}" CXX_FILESYSTEM_NO_LINK_NEEDED) + set(can_link ${CXX_FILESYSTEM_NO_LINK_NEEDED}) + endif() + endif() + + endif() + + if(can_link) + add_library(std::filesystem INTERFACE IMPORTED) + target_compile_features(std::filesystem INTERFACE cxx_std_17) + set(_found TRUE) + + if(CXX_FILESYSTEM_NO_LINK_NEEDED) + # Nothing to add... + elseif(CXX_FILESYSTEM_STDCPPFS_NEEDED) + target_link_libraries(std::filesystem INTERFACE -lstdc++fs) + elseif(CXX_FILESYSTEM_CPPFS_NEEDED) + target_link_libraries(std::filesystem INTERFACE -lc++fs) + endif() + endif() +endif() + +cmake_pop_check_state() + +set(Filesystem_FOUND ${_found} CACHE BOOL "TRUE if we can compile and link a program using std::filesystem" FORCE) + +if(Filesystem_FIND_REQUIRED AND NOT Filesystem_FOUND) + message(FATAL_ERROR "Cannot Compile simple program using std::filesystem") +endif() diff --git a/conf/lms.conf b/conf/lms.conf index 2c186cb3..04d48d54 100644 --- a/conf/lms.conf +++ b/conf/lms.conf @@ -18,6 +18,9 @@ listen-port = 5082; listen-addr = "0.0.0.0"; behind-reverse-proxy = false; +# Location for deployment +deploy-path = "/"; + # If enabled, these files have to exist and have correct permissions tls-enable = false; tls-cert = "/var/lms/cert.pem"; diff --git a/docker/Dockerfile b/docker/Dockerfile index 4ac83cd4..e3ed986c 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -5,7 +5,7 @@ WORKDIR /tmp/workdir ARG MAKEFLAGS="-j2" ARG FFMPEG_VERSION=4.1.4 ARG WT_VERSION=4.2.0 -ARG IMAGEMAGICK6_VERSION=6.9.10-71 +ARG GRAPHICSMAGICK_VERSION=1.3.35 ARG PSTREAMS_VERSION=1.0.1 ARG LMS_VERSION=v3.6.3 @@ -24,6 +24,7 @@ ARG BUILD_PACKAGES=" \ make \ libjpeg-turbo-dev \ openjpeg-dev \ + jasper-dev \ libpng-dev \ tiff-dev \ nasm \ @@ -95,50 +96,42 @@ RUN \ make && \ make install -# ImageMagick +# GraphicsMagick++ RUN \ - DIR=/tmp/imagemagick && mkdir -p ${DIR} && cd ${DIR} && \ - curl -sLO https://github.com/ImageMagick/ImageMagick6/archive/${IMAGEMAGICK6_VERSION}.tar.gz && \ - tar -x --strip-components=1 -f ${IMAGEMAGICK6_VERSION}.tar.gz + DIR=/tmp/graphicsmagick && mkdir -p ${DIR} && cd ${DIR} && \ + curl -sL ftp://ftp.graphicsmagick.org/pub/GraphicsMagick/1.3/GraphicsMagick-${GRAPHICSMAGICK_VERSION}.tar.gz -o ${GRAPHICSMAGICK_VERSION}.tar.gz && \ + tar -x --strip-components=1 -f ${GRAPHICSMAGICK_VERSION}.tar.gz RUN \ - DIR=/tmp/imagemagick && mkdir -p ${DIR} && cd ${DIR} && \ + DIR=/tmp/graphicsmagick && mkdir -p ${DIR} && cd ${DIR} && \ ./configure \ --prefix=${PREFIX} \ --enable-shared=yes \ - --disable-static \ - --disable-docs \ - --with-magick-plus-plus \ - --without-zstd \ - --without-dps \ - --without-autotrace \ - --without-dps \ - --without-fftw \ - --without-flif \ - --without-fpx \ - --without-djvu \ - --without-fontconfig \ - --without-freetype \ - --without-raqm \ - --without-gslib \ - --without-gvc \ - --without-heic \ - --without-jbig \ - --with-jpeg \ - --without-jxl \ - --without-lcms \ - --with-openjp2 \ - --without-lqr \ - --with-lzma \ - --without-openexr \ - --without-pango \ - --with-png \ - --without-raw \ - --without-rsvg \ - --with-tiff \ - --without-webp \ - --without-wmf \ - --without-xml && \ + --enable-static=no \ + --with-quantum-depth=8 \ + --with-modules=no \ + --with-frozenpaths=no \ + --with-magick-plus-plus=yes \ + --with-perl=no \ + --with-bzlib=no \ + --with-dps=no \ + --with-fpx=no \ + --with-jbig=no \ + --with-webp=no \ + --with-jp2=yes \ + --with-jpeg=yes \ + --with-lcms2=no \ + --with-lzma=no \ + --with-png=yes \ + --with-tiff=yes \ + --with-trio=no \ + --with-ttf=no \ + --with-tcmalloc=no \ + --with-wmf=no \ + --with-xml=no \ + --with-zlib=yes \ + --with-zstd=no \ + --with-x=no && \ make && \ make install && \ make distclean @@ -158,8 +151,9 @@ RUN \ RUN \ DIR=/tmp/lms && mkdir -p ${DIR} && cd ${DIR} && \ - PKG_CONFIG_PATH=/tmp/install/lib/pkgconfig CXXFLAGS="-I${PREFIX}/include -L${PREFIX}/lib -Wl,--rpath-link=${PREFIX}/lib" cmake . -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${PREFIX} && \ + PKG_CONFIG_PATH=/tmp/install/lib/pkgconfig CXXFLAGS="-I${PREFIX}/include" LDFLAGS="-L${PREFIX}/lib -Wl,--rpath-link=${PREFIX}/lib" cmake . -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${PREFIX} && \ make && \ + LD_LIBRARY_PATH=${PREFIX}/lib make test && \ make install && \ mkdir -p ${PREFIX}/etc/ && \ cp conf/lms.conf ${PREFIX}/etc @@ -178,7 +172,8 @@ RUN \ done && \ cp -r ${PREFIX}/lib /tmp/fakeroot/lib && \ cp -r ${PREFIX}/share /tmp/fakeroot/share && \ - LD_LIBRARY_PATH=/tmp/fakeroot/lib /tmp/fakeroot/bin/ffmpeg -buildconf + rm -rf /tmp/fakeroot/share/doc && \ + rm -rf /tmp/fakeroot/share/man ## Release Stage FROM alpine:3.10 AS release @@ -188,6 +183,7 @@ ARG RUNTIME_PACKAGES=" \ openssl \ libjpeg-turbo \ openjpeg \ + jasper-libs \ libpng \ tiff \ libogg \ diff --git a/src/libs/av/CMakeLists.txt b/src/libs/av/CMakeLists.txt index adb37766..d79974ac 100644 --- a/src/libs/av/CMakeLists.txt +++ b/src/libs/av/CMakeLists.txt @@ -19,6 +19,7 @@ target_link_libraries(lmsav PUBLIC lmsutils avformat avutil + std::filesystem wt ) diff --git a/src/libs/cover/CMakeLists.txt b/src/libs/cover/CMakeLists.txt index 62eb5d43..2e540725 100644 --- a/src/libs/cover/CMakeLists.txt +++ b/src/libs/cover/CMakeLists.txt @@ -10,17 +10,21 @@ target_include_directories(lmscover INTERFACE target_include_directories(lmscover PRIVATE include - ${IMAGEMAGICKXX_INCLUDE_DIRS} + ${GRAPHICSMAGICKXX_INCLUDE_DIRS} ) target_compile_options(lmscover PRIVATE - ${IMAGEMAGICKXX_CFLAGS_OTHER} + ${GRAPHICSMAGICKXX_CFLAGS_OTHER} ) target_link_libraries(lmscover PRIVATE lmsav + ${GRAPHICSMAGICKXX_LIBRARIES} + ) + +target_link_libraries(lmscover PUBLIC lmsdatabase - ${IMAGEMAGICKXX_LIBRARIES} + std::filesystem ) install(TARGETS lmscover DESTINATION lib) diff --git a/src/libs/cover/impl/CoverArtGrabber.cpp b/src/libs/cover/impl/CoverArtGrabber.cpp index 110b80d1..38781cbc 100644 --- a/src/libs/cover/impl/CoverArtGrabber.cpp +++ b/src/libs/cover/impl/CoverArtGrabber.cpp @@ -49,15 +49,13 @@ Grabber::Grabber(const std::filesystem::path& execPath) init(execPath); } -Grabber::~Grabber() -{ - deinit(); -} - void Grabber::setDefaultCover(const std::filesystem::path& p) { - if (!_defaultCover.load(p)) + std::unique_lock lock {_mutex}; + + _defaultCover = std::make_unique(); + if (!_defaultCover->load(p)) throw LmsException("Cannot read default cover file '" + p.string() + "'"); } @@ -65,12 +63,12 @@ Image Grabber::getDefaultCover(std::size_t size) { LMS_LOG(COVER, DEBUG) << "Getting a default cover using size = " << size; - std::unique_lock lock(_mutex); + std::unique_lock lock {_mutex}; auto it = _defaultCovers.find(size); if (it == _defaultCovers.end()) { - Image cover = _defaultCover; + Image cover = *_defaultCover; LMS_LOG(COVER, DEBUG) << "default cover size = " << cover.getSize().width << " x " << cover.getSize().height; diff --git a/src/libs/cover/impl/CoverArtGrabber.hpp b/src/libs/cover/impl/CoverArtGrabber.hpp index bc921355..df932315 100644 --- a/src/libs/cover/impl/CoverArtGrabber.hpp +++ b/src/libs/cover/impl/CoverArtGrabber.hpp @@ -41,7 +41,6 @@ namespace CoverArt { public: Grabber(const std::filesystem::path& execPath); - ~Grabber(); Grabber(const Grabber&) = delete; Grabber& operator=(const Grabber&) = delete; @@ -63,9 +62,8 @@ namespace CoverArt std::optional getFromDirectory(const std::filesystem::path& path) const; Image getDefaultCover(std::size_t size); - Image _defaultCover; - std::mutex _mutex; + std::unique_ptr _defaultCover; std::map _defaultCovers; static inline const std::vector _fileExtensions {".jpg", ".jpeg", ".png", ".bmp"}; // TODO parametrize diff --git a/src/libs/cover/impl/Image.cpp b/src/libs/cover/impl/Image.cpp index a39c0061..f05fcc73 100644 --- a/src/libs/cover/impl/Image.cpp +++ b/src/libs/cover/impl/Image.cpp @@ -29,12 +29,6 @@ init(const std::filesystem::path& path) Magick::InitializeMagick(path.string().c_str()); } -void -deinit() -{ - MagickCore::MagickCoreTerminus(); -} - static std::string formatToMagick(Format format) diff --git a/src/libs/cover/impl/Image.hpp b/src/libs/cover/impl/Image.hpp index 6932825f..7abcb37c 100644 --- a/src/libs/cover/impl/Image.hpp +++ b/src/libs/cover/impl/Image.hpp @@ -30,7 +30,6 @@ namespace CoverArt { void init(const std::filesystem::path& path); - void deinit(); class Image { diff --git a/src/libs/database/CMakeLists.txt b/src/libs/database/CMakeLists.txt index 638fb2e2..a7eba7e2 100644 --- a/src/libs/database/CMakeLists.txt +++ b/src/libs/database/CMakeLists.txt @@ -30,6 +30,7 @@ target_link_libraries(lmsdatabase PRIVATE target_link_libraries(lmsdatabase PUBLIC lmsutils + std::filesystem wtdbo ) diff --git a/src/libs/database/impl/Session.cpp b/src/libs/database/impl/Session.cpp index 35eeb7df..63586f17 100644 --- a/src/libs/database/impl/Session.cpp +++ b/src/libs/database/impl/Session.cpp @@ -40,7 +40,7 @@ namespace Database { -#define LMS_DATABASE_VERSION 13 +#define LMS_DATABASE_VERSION 14 using Version = std::size_t; @@ -153,7 +153,13 @@ CREATE TABLE IF NOT EXISTS "track_bookmark" ( } else if (version == 12) { - // Artist and release that have a baddly parsed name but a MBID had no chance to updat the name + // Artist and release that have a badly parsed name but a MBID had no chance to updat the name + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + ScanSettings::get(*this).modify()->incScanVersion(); + } + else if (version == 13) + { + // Always store UUID in lower case + better WMA parsing // Just increment the scan version of the settings to make the next scheduled scan rescan everything ScanSettings::get(*this).modify()->incScanVersion(); } diff --git a/src/libs/metadata/CMakeLists.txt b/src/libs/metadata/CMakeLists.txt index cc6a1dbb..0fc6da2d 100644 --- a/src/libs/metadata/CMakeLists.txt +++ b/src/libs/metadata/CMakeLists.txt @@ -19,6 +19,7 @@ target_link_libraries(lmsmetadata PRIVATE target_link_libraries(lmsmetadata PUBLIC lmsutils + std::filesystem ) install(TARGETS lmsmetadata DESTINATION lib) diff --git a/src/libs/metadata/impl/TagLibParser.cpp b/src/libs/metadata/impl/TagLibParser.cpp index 78949ce8..fee48f25 100644 --- a/src/libs/metadata/impl/TagLibParser.cpp +++ b/src/libs/metadata/impl/TagLibParser.cpp @@ -154,12 +154,118 @@ getAlbum(const TagLib::PropertyMap& properties) return Album {std::move(albumName.front()), albumMBID.front()}; } +void +TagLibParser::processTag(Track& track, const std::string& tag, const TagLib::StringList& values, bool debug) +{ + + // TODO validate MBID format + if (debug) + { + std::vector strs; + std::transform(values.begin(), values.end(), std::back_inserter(strs), [](const auto& value) { return value.to8Bit(true); }); + + std::cout << "[" << tag << "] = " << StringUtils::joinStrings(strs, "*SEP*") << std::endl; + } + + if (tag.empty() || values.isEmpty() || values.front().isEmpty()) + return; + + std::string value {StringUtils::stringTrim(values.front().to8Bit(true))}; + + if (tag == "TITLE") + track.title = value; + else if (tag == "MUSICBRAINZ_RELEASETRACKID" + || tag == "MUSICBRAINZ RELEASE TRACK ID") + { + track.musicBrainzTrackID = UUID::fromString(value); + } + else if (tag == "MUSICBRAINZ_TRACKID" + || tag == "MUSICBRAINZ TRACK ID") + track.musicBrainzRecordID = UUID::fromString(value); + else if (tag == "ACOUSTID_ID") + track.acoustID = UUID::fromString(value); + else if (tag == "TRACKTOTAL") + { + auto totalTrack = StringUtils::readAs(value); + if (totalTrack) + track.totalTrack = totalTrack; + } + else if (tag == "TRACKNUMBER") + { + // Expecting 'Number/Total' + std::vector strings {splitAndTrimString(value, "/")}; + + if (!strings.empty()) + { + track.trackNumber = StringUtils::readAs(strings[0]); + + // Lower priority than TRACKTOTAL + if (strings.size() > 1 && !track.totalTrack) + track.totalTrack = StringUtils::readAs(strings[1]); + } + } + else if (tag == "DISCTOTAL") + { + auto totalDisc = StringUtils::readAs(value); + if (totalDisc) + track.totalDisc = totalDisc; + } + else if (tag == "DISCNUMBER") + { + // Expecting 'Number/Total' + std::vector strings {StringUtils::splitString(value, "/")}; + + if (!strings.empty()) + { + track.discNumber = StringUtils::readAs(strings[0]); + + // Lower priority than DISCTOTAL + if (strings.size() > 1 && !track.totalDisc) + track.totalDisc = StringUtils::readAs(strings[1]); + } + } + else if (tag == "DATE") + track.year = StringUtils::readAs(value); + else if (tag == "ORIGINALDATE" && !track.originalYear) + { + // Lower priority than ORIGINALYEAR + track.originalYear = StringUtils::readAs(value); + } + else if (tag == "ORIGINALYEAR") + { + // Higher priority than ORIGINALDATE + auto originalYear = StringUtils::readAs(value); + if (originalYear) + track.originalYear = originalYear; + } + else if (tag == "METADATA_BLOCK_PICTURE") + track.hasCover = true; + else if (tag == "COPYRIGHT") + track.copyright = value; + else if (tag == "COPYRIGHTURL") + track.copyrightURL = value; + else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end()) + { + std::set clusterNames; + for (const auto& valueList : values) + { + auto values = splitAndTrimString(valueList.to8Bit(true), "/,;"); + + for (const auto& value : values) + clusterNames.insert(value); + } + + if (!clusterNames.empty()) + track.clusters[tag] = clusterNames; + } +} + std::optional TagLibParser::parse(const std::filesystem::path& p, bool debug) { TagLib::FileRef f {p.string().c_str(), - true, // read audio properties - TagLib::AudioProperties::Fast}; + true, // read audio properties + TagLib::AudioProperties::Fast}; if (f.isNull()) { @@ -184,14 +290,40 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug) track.audioStreams = {std::move(audioStream)}; } + TagLib::PropertyMap properties {f.file()->properties()}; + // Not that good embedded pictures handling // WMA if (TagLib::ASF::File* asfFile {dynamic_cast(f.file())}) { const TagLib::ASF::Tag* tag {asfFile->tag()}; - if (tag && tag->attributeListMap().contains("WM/Picture")) - track.hasCover = true; + if (tag) + { + if (tag->attributeListMap().contains("WM/Picture")) + track.hasCover = true; + + for (const auto& [name, attributeList] : tag->attributeListMap()) + { + if (name.to8Bit().find("WM/") == 0 || properties.contains(name)) + continue; + + TagLib::StringList stringAttributeList; + for (const auto& attribute : attributeList) + { + if (attribute.type() == TagLib::ASF::Attribute::AttributeTypes::UnicodeType) + stringAttributeList.append(attribute.toString()); + } + + if (!stringAttributeList.isEmpty()) + { + if (debug) + std::cout << "Property: '" << name << "'" << std::endl; + + properties.insert(name, stringAttributeList); + } + } + } } // MP3 else if (TagLib::MPEG::File* mp3File {dynamic_cast(f.file())}) @@ -209,124 +341,18 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug) track.hasCover = true; } - if (f.tag()) + for(const auto& property : properties) { - MetaData::Clusters clusters; - const TagLib::PropertyMap& properties {f.file()->properties()}; - - for(const auto& property : properties) - { - const std::string tag {property.first.upper().to8Bit(true)}; - const TagLib::StringList& values {property.second}; - - // TODO validate MBID format - if (debug) - { - std::vector strs; - std::transform(values.begin(), values.end(), std::back_inserter(strs), [](const auto& value) { return value.to8Bit(true); }); - - std::cout << "[" << tag << "] = " << StringUtils::joinStrings(strs, "*SEP*") << std::endl; - } - - if (tag.empty() || values.isEmpty() || values.front().isEmpty()) - continue; - - std::string value {StringUtils::stringTrim(values.front().to8Bit(true))}; - - if (tag == "TITLE") - track.title = value; - else if (tag == "MUSICBRAINZ_RELEASETRACKID" - || tag == "MUSICBRAINZ RELEASE TRACK ID") - { - track.musicBrainzTrackID = UUID::fromString(value); - } - else if (tag == "MUSICBRAINZ_TRACKID" - || tag == "MUSICBRAINZ TRACK ID") - track.musicBrainzRecordID = UUID::fromString(value); - else if (tag == "ACOUSTID_ID") - track.acoustID = UUID::fromString(value); - else if (tag == "TRACKTOTAL") - { - auto totalTrack = StringUtils::readAs(value); - if (totalTrack) - track.totalTrack = totalTrack; - } - else if (tag == "TRACKNUMBER") - { - // Expecting 'Number/Total' - std::vector strings {splitAndTrimString(value, "/")}; - - if (!strings.empty()) - { - track.trackNumber = StringUtils::readAs(strings[0]); - - // Lower priority than TRACKTOTAL - if (strings.size() > 1 && !track.totalTrack) - track.totalTrack = StringUtils::readAs(strings[1]); - } - } - else if (tag == "DISCTOTAL") - { - auto totalDisc = StringUtils::readAs(value); - if (totalDisc) - track.totalDisc = totalDisc; - } - else if (tag == "DISCNUMBER") - { - // Expecting 'Number/Total' - std::vector strings {StringUtils::splitString(value, "/")}; - - if (!strings.empty()) - { - track.discNumber = StringUtils::readAs(strings[0]); - - // Lower priority than DISCTOTAL - if (strings.size() > 1 && !track.totalDisc) - track.totalDisc = StringUtils::readAs(strings[1]); - } - } - else if (tag == "DATE") - track.year = StringUtils::readAs(value); - else if (tag == "ORIGINALDATE" && !track.originalYear) - { - // Lower priority than ORIGINALYEAR - track.originalYear = StringUtils::readAs(value); - } - else if (tag == "ORIGINALYEAR") - { - // Higher priority than ORIGINALDATE - auto originalYear = StringUtils::readAs(value); - if (originalYear) - track.originalYear = originalYear; - } - else if (tag == "METADATA_BLOCK_PICTURE") - track.hasCover = true; - else if (tag == "COPYRIGHT") - track.copyright = value; - else if (tag == "COPYRIGHTURL") - track.copyrightURL = value; - else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end()) - { - std::set clusterNames; - for (const auto& valueList : values) - { - auto values = splitAndTrimString(valueList.to8Bit(true), "/,;"); - - for (const auto& value : values) - clusterNames.insert(value); - } - - if (!clusterNames.empty()) - track.clusters[tag] = clusterNames; - } - } - - track.artists = getArtists(properties); - track.albumArtists = getAlbumArtists(properties); - track.album = getAlbum(properties); + const std::string tag {property.first.upper().to8Bit(true)}; + const TagLib::StringList& values {property.second}; + processTag(track, tag, values, debug); } + track.artists = getArtists(properties); + track.albumArtists = getAlbumArtists(properties); + track.album = getAlbum(properties); + return track; } diff --git a/src/libs/metadata/include/metadata/TagLibParser.hpp b/src/libs/metadata/include/metadata/TagLibParser.hpp index 1c055b76..ee298f8d 100644 --- a/src/libs/metadata/include/metadata/TagLibParser.hpp +++ b/src/libs/metadata/include/metadata/TagLibParser.hpp @@ -21,14 +21,21 @@ #include "metadata/IParser.hpp" +namespace TagLib +{ + class StringList; +} + namespace MetaData { // Parse that makes use of AvFormat class TagLibParser : public IParser { - public: + private: std::optional parse(const std::filesystem::path& p, bool debug = false) override; + + void processTag(Track& track, const std::string& tag, const TagLib::StringList& values, bool debug); }; } // namespace MetaData diff --git a/src/libs/recommendation/CMakeLists.txt b/src/libs/recommendation/CMakeLists.txt index b7a52f12..35243f50 100644 --- a/src/libs/recommendation/CMakeLists.txt +++ b/src/libs/recommendation/CMakeLists.txt @@ -18,6 +18,7 @@ target_include_directories(lmsrecommendation PRIVATE target_link_libraries(lmsrecommendation PRIVATE lmsdatabase lmssom + std::filesystem wt ) diff --git a/src/libs/recommendation/impl/clusters/ClustersClassifier.hpp b/src/libs/recommendation/impl/clusters/ClustersClassifier.hpp index 4619d5dc..53a7bd5f 100644 --- a/src/libs/recommendation/impl/clusters/ClustersClassifier.hpp +++ b/src/libs/recommendation/impl/clusters/ClustersClassifier.hpp @@ -36,7 +36,7 @@ namespace Recommendation private: - std::string_view getName() const { return "Clusters"; } + std::string_view getName() const override { return "Clusters"; } bool init(Database::Session&, bool) override {return true;} void requestCancelInit() override {} diff --git a/src/libs/recommendation/impl/features/FeaturesClassifier.hpp b/src/libs/recommendation/impl/features/FeaturesClassifier.hpp index ecf3b947..f20beec7 100644 --- a/src/libs/recommendation/impl/features/FeaturesClassifier.hpp +++ b/src/libs/recommendation/impl/features/FeaturesClassifier.hpp @@ -55,15 +55,15 @@ class FeaturesClassifier : public IClassifier static const FeatureSettingsMap& getDefaultTrainFeatureSettings(); private: - std::string_view getName() const { return "Features"; } + std::string_view getName() const override { return "Features"; } bool init(Database::Session& session, bool databaseChanged) override; void requestCancelInit() override; std::vector getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) const override; std::vector getSimilarTracks(Database::Session& session, const std::unordered_set& tracksId, std::size_t maxCount) const override; - std::vector getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) const; - std::vector getSimilarArtists(Database::Session& session, Database::IdType artistId, std::size_t maxCount) const; + std::vector getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) const override; + std::vector getSimilarArtists(Database::Session& session, Database::IdType artistId, std::size_t maxCount) const override; bool initFromCache(Database::Session& session, const FeaturesClassifierCache& cache); diff --git a/src/libs/scanner/CMakeLists.txt b/src/libs/scanner/CMakeLists.txt index 57fae63a..2ec7539e 100644 --- a/src/libs/scanner/CMakeLists.txt +++ b/src/libs/scanner/CMakeLists.txt @@ -20,6 +20,7 @@ target_link_libraries(lmsscanner PRIVATE ) target_link_libraries(lmsscanner PUBLIC + std::filesystem wt ) diff --git a/src/libs/subsonic/CMakeLists.txt b/src/libs/subsonic/CMakeLists.txt index e5743a37..34d3d68d 100644 --- a/src/libs/subsonic/CMakeLists.txt +++ b/src/libs/subsonic/CMakeLists.txt @@ -21,6 +21,7 @@ target_link_libraries(lmssubsonic PRIVATE lmscover lmsrecommendation lmsutils + std::filesystem ) target_link_libraries(lmssubsonic PUBLIC diff --git a/src/libs/subsonic/impl/ParameterParsing.cpp b/src/libs/subsonic/impl/ParameterParsing.cpp index dbfd0656..48dc09e4 100644 --- a/src/libs/subsonic/impl/ParameterParsing.cpp +++ b/src/libs/subsonic/impl/ParameterParsing.cpp @@ -23,14 +23,14 @@ namespace StringUtils { template<> std::optional - StringUtils::readAs(const std::string& str) + readAs(const std::string& str) { return API::Subsonic::IdFromString(str); } template<> std::optional - StringUtils::readAs(const std::string& str) + readAs(const std::string& str) { if (str == "true") return true; diff --git a/src/libs/subsonic/impl/ParameterParsing.hpp b/src/libs/subsonic/impl/ParameterParsing.hpp index 4b503238..c5cb15f3 100644 --- a/src/libs/subsonic/impl/ParameterParsing.hpp +++ b/src/libs/subsonic/impl/ParameterParsing.hpp @@ -90,10 +90,10 @@ namespace StringUtils { template<> std::optional - StringUtils::readAs(const std::string& str); + readAs(const std::string& str); template<> std::optional - StringUtils::readAs(const std::string& str); + readAs(const std::string& str); } diff --git a/src/libs/subsonic/impl/SubsonicResource.cpp b/src/libs/subsonic/impl/SubsonicResource.cpp index c09a4c98..11391e36 100644 --- a/src/libs/subsonic/impl/SubsonicResource.cpp +++ b/src/libs/subsonic/impl/SubsonicResource.cpp @@ -19,6 +19,7 @@ #include "subsonic/SubsonicResource.hpp" #include +#include #include @@ -65,7 +66,7 @@ namespace StringUtils { template<> std::optional - StringUtils::readAs(const std::string& str) + readAs(const std::string& str) { // Expects "X.Y.Z" const auto numbers {StringUtils::splitString(str, ".")}; diff --git a/src/libs/subsonic/include/subsonic/SubsonicResource.hpp b/src/libs/subsonic/include/subsonic/SubsonicResource.hpp index af9bc53c..1d8746e7 100644 --- a/src/libs/subsonic/include/subsonic/SubsonicResource.hpp +++ b/src/libs/subsonic/include/subsonic/SubsonicResource.hpp @@ -36,7 +36,7 @@ class SubsonicResource final : public Wt::WResource public: SubsonicResource(Database::Db& db); - static std::string getPath() { return "/rest/"; } + static std::string getPath() { return "rest/"; } private: void handleRequest(const Wt::Http::Request &request, Wt::Http::Response &response) override; diff --git a/src/libs/utils/CMakeLists.txt b/src/libs/utils/CMakeLists.txt index e3cd2e9e..2dbf4d2c 100644 --- a/src/libs/utils/CMakeLists.txt +++ b/src/libs/utils/CMakeLists.txt @@ -26,7 +26,7 @@ target_link_libraries(lmsutils PRIVATE target_link_libraries(lmsutils PUBLIC boost_system - stdc++fs + std::filesystem wt ) diff --git a/src/libs/utils/impl/String.cpp b/src/libs/utils/impl/String.cpp index be6a79a1..dbcb82d2 100644 --- a/src/libs/utils/impl/String.cpp +++ b/src/libs/utils/impl/String.cpp @@ -20,6 +20,7 @@ #include "utils/String.hpp" #include +#include #include #include @@ -98,15 +99,25 @@ stringTrimEnd(const std::string& str, const std::string& whitespace) } std::string -stringToLower(const std::string& str) +stringToLower(std::string_view str) { - return boost::algorithm::to_lower_copy(str); + std::string res; + res.reserve(str.size()); + + std::transform(std::cbegin(str), std::cend(str), std::back_inserter(res), [](char c) { return std::tolower(c);}); + + return res; } std::string stringToUpper(const std::string& str) { - return boost::to_upper_copy(str); + std::string res; + res.reserve(str.size()); + + std::transform(std::cbegin(str), std::cend(str), std::back_inserter(res), [](char c) { return std::toupper(c);}); + + return res; } std::string diff --git a/src/libs/utils/impl/UUID.cpp b/src/libs/utils/impl/UUID.cpp index f95209e2..6b9b402a 100644 --- a/src/libs/utils/impl/UUID.cpp +++ b/src/libs/utils/impl/UUID.cpp @@ -21,6 +21,8 @@ #include +#include "utils/String.hpp" + namespace StringUtils { template <> @@ -40,6 +42,10 @@ stringIsUUID(std::string_view str) return std::regex_match(std::cbegin(str), std::cend(str), re); } +UUID::UUID(std::string_view str) + : _value {StringUtils::stringToLower(str)} +{ +} std::optional UUID::fromString(std::string_view str) diff --git a/src/libs/utils/include/utils/String.hpp b/src/libs/utils/include/utils/String.hpp index e62edb91..62d6fba4 100644 --- a/src/libs/utils/include/utils/String.hpp +++ b/src/libs/utils/include/utils/String.hpp @@ -21,6 +21,7 @@ #include #include +#include #include #include @@ -40,7 +41,7 @@ std::string stringTrimEnd(const std::string& str, const std::string& whitespaces = " \t"); std::string -stringToLower(const std::string& str); +stringToLower(std::string_view str); std::string stringToUpper(const std::string& str); diff --git a/src/libs/utils/include/utils/UUID.hpp b/src/libs/utils/include/utils/UUID.hpp index daf561d0..38814d1f 100644 --- a/src/libs/utils/include/utils/UUID.hpp +++ b/src/libs/utils/include/utils/UUID.hpp @@ -34,7 +34,7 @@ class UUID std::string_view getAsString() const { return _value; } private: - UUID(std::string_view value) : _value {value} {} + UUID(std::string_view value); std::string _value; }; diff --git a/src/lms/main.cpp b/src/lms/main.cpp index 54fe076c..18dcd7f2 100644 --- a/src/lms/main.cpp +++ b/src/lms/main.cpp @@ -48,6 +48,7 @@ std::vector generateWtConfig(std::string execPath) args.push_back("--config=" + wtConfigPath.string()); args.push_back("--docroot=" + ServiceProvider::get()->getString("docroot")); args.push_back("--approot=" + ServiceProvider::get()->getString("approot")); + args.push_back("--deploy-path=" + ServiceProvider::get()->getString("deploy-path", "/")); args.push_back("--resources-dir=" + ServiceProvider::get()->getString("wt-resources")); if (ServiceProvider::get()->getBool("tls-enable", false)) @@ -197,6 +198,8 @@ int main(int argc, char* argv[]) LMS_LOG(MAIN, INFO) << "Stopping recommendation engine..."; recommendationEngine.stop(); + ServiceProvider::clear(); + LMS_LOG(MAIN, INFO) << "Clean stop!"; res = EXIT_SUCCESS; } diff --git a/src/tools/recommendation/LmsRecommendation.cpp b/src/tools/recommendation/LmsRecommendation.cpp index 954af8c0..31af307d 100644 --- a/src/tools/recommendation/LmsRecommendation.cpp +++ b/src/tools/recommendation/LmsRecommendation.cpp @@ -88,7 +88,7 @@ dumpRecommendation(Database::Session session, Recommendation::IEngine& engine) }; std::cout << "Processing release '" << releaseToString(releaseId) << "'" << std::endl; - for (Database::IdType similarReleaseId : engine.getSimilarReleases(session, {releaseId}, 3)) + for (Database::IdType similarReleaseId : engine.getSimilarReleases(session, releaseId, 3)) std::cout << "\t- Similar release '" << releaseToString(similarReleaseId) << "'" << std::endl; } @@ -110,7 +110,7 @@ dumpRecommendation(Database::Session session, Recommendation::IEngine& engine) }; std::cout << "Processing artist '" << artistToString(artistId) << "'" << std::endl; - for (Database::IdType similarArtistId : engine.getSimilarArtists(session, {artistId}, 3)) + for (Database::IdType similarArtistId : engine.getSimilarArtists(session, artistId, 3)) std::cout << "\t- Similar artist '" << artistToString(similarArtistId) << "'" << std::endl; }