From c04f57ceb5a0396e9c9a45461adba1c4fc882434 Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 17 Oct 2020 14:58:41 +0200 Subject: [PATCH 1/8] Made STB the default image manipulation library to reduce memory usage (GraphicsMagick can still be selected). closes #93 --- .travis.yml | 2 +- CMakeLists.txt | 20 +- README.md | 11 +- cmake/modules/FindSTB.cmake | 11 ++ conf/lms.conf | 3 + conf/systemd/default.service | 1 + docker/Dockerfile | 59 ++---- src/libs/auth/CMakeLists.txt | 2 +- src/libs/av/impl/AvInfo.cpp | 2 +- src/libs/cover/CMakeLists.txt | 31 ++- src/libs/cover/impl/CoverArtGrabber.cpp | 178 +++++++++--------- src/libs/cover/impl/CoverArtGrabber.hpp | 44 +++-- .../cover/impl/{Image.hpp => Exception.hpp} | 39 ---- .../impl/{CoverArt.cpp => IRawImage.hpp} | 34 ++-- .../cover/impl/graphicsmagick/JPEGImage.cpp | 55 ++++++ .../cover/impl/graphicsmagick/JPEGImage.hpp | 45 +++++ .../RawImage.cpp} | 115 +++++------ .../cover/impl/graphicsmagick/RawImage.hpp | 54 ++++++ src/libs/cover/impl/stb/JPEGImage.cpp | 61 ++++++ .../impl/{CoverArt.hpp => stb/JPEGImage.hpp} | 26 ++- src/libs/cover/impl/stb/RawImage.cpp | 114 +++++++++++ src/libs/cover/impl/stb/RawImage.hpp | 54 ++++++ .../cover/include/cover/ICoverArtGrabber.hpp | 18 +- .../{ICoverArt.hpp => IEncodedImage.hpp} | 8 +- src/libs/database/CMakeLists.txt | 1 - src/libs/database/impl/Cluster.cpp | 8 +- src/libs/database/impl/Db.cpp | 19 ++ src/libs/database/impl/Session.cpp | 4 +- src/libs/database/impl/SessionPool.cpp | 69 ------- .../database/include/database/Cluster.hpp | 5 +- src/libs/database/include/database/Db.hpp | 7 +- .../database/include/database/Session.hpp | 2 +- .../database/include/database/SessionPool.hpp | 72 ------- src/libs/subsonic/impl/SubsonicResource.cpp | 14 +- .../include/subsonic/SubsonicResource.hpp | 4 +- src/lms/main.cpp | 12 +- src/lms/ui/Auth.cpp | 1 + src/lms/ui/LmsApplication.cpp | 33 ++-- src/lms/ui/LmsApplication.hpp | 7 +- src/lms/ui/MediaPlayer.cpp | 1 + src/lms/ui/PlayQueue.cpp | 1 + src/lms/ui/SettingsView.cpp | 1 + src/lms/ui/admin/DatabaseSettingsView.cpp | 1 + src/lms/ui/admin/InitWizardView.cpp | 2 + src/lms/ui/admin/UserView.cpp | 1 + src/lms/ui/admin/UsersView.cpp | 1 + src/lms/ui/explore/ArtistView.cpp | 1 + src/lms/ui/explore/ArtistsView.cpp | 1 + src/lms/ui/explore/Explore.cpp | 1 + src/lms/ui/explore/Filters.cpp | 1 + src/lms/ui/explore/ReleasePopup.cpp | 1 + src/lms/ui/explore/ReleaseView.cpp | 1 + src/lms/ui/explore/ReleasesView.cpp | 3 +- src/lms/ui/explore/SearchView.cpp | 1 + src/lms/ui/explore/TrackPopup.cpp | 1 + src/lms/ui/explore/TracksView.cpp | 1 + src/lms/ui/resource/AudioFileResource.cpp | 4 +- .../ui/resource/AudioTranscodeResource.cpp | 3 +- src/lms/ui/resource/DownloadResource.cpp | 4 +- src/lms/ui/resource/ImageResource.cpp | 14 +- src/tools/CMakeLists.txt | 4 +- src/tools/cover/CMakeLists.txt | 10 + src/tools/cover/LmsCover.cpp | 107 +++++++++++ 63 files changed, 866 insertions(+), 545 deletions(-) create mode 100644 cmake/modules/FindSTB.cmake rename src/libs/cover/impl/{Image.hpp => Exception.hpp} (57%) rename src/libs/cover/impl/{CoverArt.cpp => IRawImage.hpp} (67%) create mode 100644 src/libs/cover/impl/graphicsmagick/JPEGImage.cpp create mode 100644 src/libs/cover/impl/graphicsmagick/JPEGImage.hpp rename src/libs/cover/impl/{Image.cpp => graphicsmagick/RawImage.cpp} (56%) create mode 100644 src/libs/cover/impl/graphicsmagick/RawImage.hpp create mode 100644 src/libs/cover/impl/stb/JPEGImage.cpp rename src/libs/cover/impl/{CoverArt.hpp => stb/JPEGImage.hpp} (73%) create mode 100644 src/libs/cover/impl/stb/RawImage.cpp create mode 100644 src/libs/cover/impl/stb/RawImage.hpp rename src/libs/cover/include/cover/{ICoverArt.hpp => IEncodedImage.hpp} (87%) delete mode 100644 src/libs/database/impl/SessionPool.cpp delete mode 100644 src/libs/database/include/database/SessionPool.hpp create mode 100644 src/tools/cover/CMakeLists.txt create mode 100644 src/tools/cover/LmsCover.cpp diff --git a/.travis.yml b/.travis.yml index 0635f43f..99c8f4c9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ sudo: required dist: focal before_install: - eval "${MATRIX_EVAL}" -- 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 libpam0g-dev +- sudo apt-get install build-essential cmake libboost-all-dev libconfig++-dev libavcodec-dev libavutil-dev libavformat-dev ffmpeg libstb-dev libpstreams-dev libconfig++-dev libpstreams-dev libtag1-dev libpam0g-dev - git clone https://github.com/emweb/wt.git wt - pushd wt; - git checkout 4.4.0 diff --git a/CMakeLists.txt b/CMakeLists.txt index 6099f285..e2d8e509 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,7 +15,25 @@ find_package(Taglib REQUIRED) find_package(Boost REQUIRED COMPONENTS system program_options) find_package(PStreams REQUIRED) find_package(PAM) -pkg_check_modules(GRAPHICSMAGICKXX REQUIRED GraphicsMagick++) +find_package(STB) +pkg_check_modules(GRAPHICSMAGICKXX GraphicsMagick++) + +if (STB_FOUND) + set(IMAGE_LIBRARY STB CACHE STRING "STB library") +elseif (GRAPHICSMAGICKXX_FOUND) + set(IMAGE_LIBRARY GraphicksMagick++ CACHE STRING "GraphicsMagick++") +else () + message(FATAL_ERROR "Cannot find suitable image library") +endif() +set_property(CACHE IMAGE_LIBRARY PROPERTY STRINGS GraphicksMagick++ STB) + +if (IMAGE_LIBRARY STREQUAL GraphicksMagick++ AND NOT GRAPHICSMAGICKXX_FOUND) + message(FATAL_ERROR "GraphicksMagick++ not found") +elseif (IMAGE_LIBRARY STREQUAL STB AND NOT STB_FOUND) + message(FATAL_ERROR "STB not found") +endif () + +message(STATUS "IMAGE_LIBRARY set to ${IMAGE_LIBRARY}") add_subdirectory(src) diff --git a/README.md b/README.md index 3c17b6be..da137975 100644 --- a/README.md +++ b/README.md @@ -92,10 +92,11 @@ __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 libgraphicsmagick++1-dev libconfig++-dev libpstreams-dev ffmpeg libtag1-dev libpam0g-dev +apt-get install g++ cmake libboost-system-dev libavutil-dev libavformat-dev libstb-dev libconfig++-dev libpstreams-dev ffmpeg libtag1-dev libpam0g-dev ``` - -__Note__: package libpam0g-dev is optional (only for using PAM authentication) +__Notes__: +* libpam0g-dev is optional (only for using PAM authentication) +* libstb-dev can be replaced by libgraphicsmagick++1-dev (the latter will likely use more RAM) You also need _Wt4_, which is not packaged yet on _Debian_. See [installation instructions](https://www.webtoolkit.eu/wt/doc/reference/html/InstallationUnix.html).
No optional requirement is needed, except openSSL if you plan not to deploy behind a reverse proxy (which is not recommended). @@ -110,7 +111,9 @@ mkdir build cd build cmake .. -DCMAKE_BUILD_TYPE=Release ``` -__Note__: in order to customize the installation directory, you can use the _-DCMAKE_INSTALL_PREFIX_ option (defaults to `/usr/local`). +__Notes__: +* you can customize the installation directory using `-DCMAKE_INSTALL_PREFIX=path` (defaults to `/usr/local`). +* you can customize the image library using `-DIMAGE_LIBRARY=` ```sh make diff --git a/cmake/modules/FindSTB.cmake b/cmake/modules/FindSTB.cmake new file mode 100644 index 00000000..0e071117 --- /dev/null +++ b/cmake/modules/FindSTB.cmake @@ -0,0 +1,11 @@ +find_path(STB_INCLUDE_DIR stb/stb.h) + +include(FindPackageHandleStandardArgs) + +FIND_PACKAGE_HANDLE_STANDARD_ARGS( + STB + FOUND_VAR STB_FOUND + REQUIRED_VARS STB_INCLUDE_DIR +) + + diff --git a/conf/lms.conf b/conf/lms.conf index a8bd99a1..1f883e1d 100644 --- a/conf/lms.conf +++ b/conf/lms.conf @@ -48,3 +48,6 @@ cover-max-file-size = 10; # Max cover cache size in MBytes cover-max-cache-size = 30; + +# JPEG quality for covers (range is 1-100) +cover-jpeg-quality = 75; diff --git a/conf/systemd/default.service b/conf/systemd/default.service index 5eb73006..6ff3aa53 100644 --- a/conf/systemd/default.service +++ b/conf/systemd/default.service @@ -10,6 +10,7 @@ WorkingDirectory=/var/lms ExecStart=/usr/bin/lms User=lms Group=lms +Environment=OMP_THREAD_LIMIT=1 [Install] WantedBy=multi-user.target diff --git a/docker/Dockerfile b/docker/Dockerfile index cbd32431..48fae34f 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 GRAPHICSMAGICK_VERSION=1.3.35 +ARG STB_VERSION=b42009b3b9d4ca35bc703f5310eedc74f584be58 ARG PSTREAMS_VERSION=1.0.1 ARG LMS_VERSION=v3.6.3 @@ -22,11 +22,9 @@ ARG BUILD_PACKAGES=" \ libtool \ g++ \ make \ - libjpeg-turbo-dev \ openjpeg-dev \ jasper-dev \ libpng-dev \ - tiff-dev \ nasm \ yasm \ curl \ @@ -43,7 +41,15 @@ ARG BUILD_PACKAGES=" \ RUN apk add --no-cache --update ${BUILD_PACKAGES} -## ffmpeg +# STB +RUN \ + DIR=/tmp/stb && mkdir -p ${DIR} && cd ${DIR} && \ + curl -sLO https://github.com/nothings/stb/archive/${STB_VERSION}.tar.gz && \ + tar -x --strip-components=1 -f ${STB_VERSION}.tar.gz && \ + mkdir -p ${PREFIX}/include/stb && \ + cp *.h ${PREFIX}/include/stb + +# ffmpeg RUN \ DIR=/tmp/ffmpeg && mkdir -p ${DIR} && cd ${DIR} && \ curl -sLO https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.bz2 && \ @@ -73,7 +79,7 @@ RUN \ --enable-libopus \ --enable-libvorbis \ --disable-everything \ - --enable-decoder=aac*,ac3*,alac,als,flac,mp3*,libopus,pcm*,libvorbis,wavpack,wma*,libopenjpg,png,tiff \ + --enable-decoder=aac*,ac3*,alac,als,flac,mp3*,libopus,pcm*,libvorbis,wavpack,wma*,libopenjpg,png \ --enable-encoder=libmp3lame,libopus,libvorbis \ --enable-demuxer=aac,aiff,asf,flac,ipod,ogg,matroska,mov,mp3,mp4,wav,wv,webm \ --enable-muxer=ogg,matroska,mp3,webm \ @@ -96,47 +102,6 @@ RUN \ make && \ make install -# GraphicsMagick++ -RUN \ - 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/graphicsmagick && mkdir -p ${DIR} && cd ${DIR} && \ - ./configure \ - --prefix=${PREFIX} \ - --enable-shared=yes \ - --enable-static=no \ - --disable-openmp \ - --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 - # libpstreams RUN \ DIR=/tmp/libpstreams && mkdir -p ${DIR} && cd ${DIR} && \ @@ -183,11 +148,9 @@ LABEL version=${LMS_VERSION} ARG RUNTIME_PACKAGES=" \ openssl \ - libjpeg-turbo \ openjpeg \ jasper-libs \ libpng \ - tiff \ libogg \ opus \ libvorbis \ diff --git a/src/libs/auth/CMakeLists.txt b/src/libs/auth/CMakeLists.txt index 0bcd31c1..a3c18dc8 100644 --- a/src/libs/auth/CMakeLists.txt +++ b/src/libs/auth/CMakeLists.txt @@ -29,7 +29,7 @@ if (PAM_FOUND) target_sources(lmsauth PRIVATE impl/pam/PAM.cpp) target_include_directories(lmsauth PRIVATE ${PAM_INCLUDE_DIR}) target_link_libraries(lmsauth PRIVATE ${PAM_LIBRARIES}) -endif(PAM_FOUND) +endif (PAM_FOUND) install(TARGETS lmsauth DESTINATION lib) diff --git a/src/libs/av/impl/AvInfo.cpp b/src/libs/av/impl/AvInfo.cpp index 0fdb8277..fa893871 100644 --- a/src/libs/av/impl/AvInfo.cpp +++ b/src/libs/av/impl/AvInfo.cpp @@ -39,7 +39,7 @@ static std::string averror_to_string(int error) std::array buf = {0}; if (av_strerror(error, buf.data(), buf.size()) == 0) - return std::string(&buf[0]); + return &buf[0]; else return "Unknown error"; } diff --git a/src/libs/cover/CMakeLists.txt b/src/libs/cover/CMakeLists.txt index f3a65928..e8c8f09c 100644 --- a/src/libs/cover/CMakeLists.txt +++ b/src/libs/cover/CMakeLists.txt @@ -1,8 +1,6 @@ add_library(lmscover SHARED - impl/CoverArt.cpp impl/CoverArtGrabber.cpp - impl/Image.cpp ) target_include_directories(lmscover INTERFACE @@ -11,22 +9,39 @@ target_include_directories(lmscover INTERFACE target_include_directories(lmscover PRIVATE include - ${GRAPHICSMAGICKXX_INCLUDE_DIRS} - ) - -target_compile_options(lmscover PRIVATE - ${GRAPHICSMAGICKXX_CFLAGS_OTHER} ) target_link_libraries(lmscover PRIVATE lmsav - ${GRAPHICSMAGICKXX_LIBRARIES} ) target_link_libraries(lmscover PUBLIC lmsdatabase + lmsutils std::filesystem ) +if (IMAGE_LIBRARY STREQUAL STB) + target_sources(lmscover PRIVATE + impl/stb/JPEGImage.cpp + impl/stb/RawImage.cpp + ) + target_compile_options(lmscover PRIVATE "-DLMS_SUPPORT_IMAGE_STB") + target_include_directories(lmscover PRIVATE ${STB_INCLUDE_DIR}) +elseif (IMAGE_LIBRARY STREQUAL GraphicksMagick++) + target_sources(lmscover PRIVATE + impl/graphicsmagick/JPEGImage.cpp + impl/graphicsmagick/RawImage.cpp + ) + target_compile_options(lmscover PRIVATE "-DLMS_SUPPORT_IMAGE_GM") + target_include_directories(lmscover PRIVATE ${GRAPHICSMAGICKXX_INCLUDE_DIRS}) + target_compile_options(lmscover PRIVATE ${GRAPHICSMAGICKXX_CFLAGS_OTHER}) + target_link_libraries(lmscover PRIVATE ${GRAPHICSMAGICKXX_LIBRARIES}) +else () + message(FATAL_ERROR "Invalid IMAGE_LIBRARY provided") +endif() + +target_include_directories(lmscover PRIVATE impl) + install(TARGETS lmscover DESTINATION lib) diff --git a/src/libs/cover/impl/CoverArtGrabber.cpp b/src/libs/cover/impl/CoverArtGrabber.cpp index 25051c1b..52cd9729 100644 --- a/src/libs/cover/impl/CoverArtGrabber.cpp +++ b/src/libs/cover/impl/CoverArtGrabber.cpp @@ -25,58 +25,71 @@ #include "database/Session.hpp" #include "database/Track.hpp" +#if LMS_SUPPORT_IMAGE_STB +#include "stb/RawImage.hpp" +using RawImage = CoverArt::STB::RawImage; +#elif LMS_SUPPORT_IMAGE_GM +#include "graphicsmagick/RawImage.hpp" +using RawImage = CoverArt::GraphicsMagick::RawImage; +#endif + #include "utils/Logger.hpp" #include "utils/Random.hpp" +#include "utils/Utils.hpp" +#include "Exception.hpp" -#include "CoverArt.hpp" - -namespace { +namespace CoverArt { +static bool isFileSupported(const std::filesystem::path& file, const std::vector& extensions) { return (std::find(std::cbegin(extensions), std::cend(extensions), file.extension()) != std::cend(extensions)); } -} // namespace - -namespace CoverArt { - -std::unique_ptr createGrabber(const std::filesystem::path& execPath, std::size_t maxCacheSize, std::size_t maxFileSize) +std::unique_ptr +createGrabber(const std::filesystem::path& execPath, + const std::filesystem::path& defaultCoverPath, + std::size_t maxCacheSize, std::size_t maxFileSize, unsigned jpegQuality) { - return std::make_unique(execPath, maxCacheSize, maxFileSize); + return std::make_unique(execPath, defaultCoverPath, maxCacheSize, maxFileSize, jpegQuality); } Grabber::Grabber(const std::filesystem::path& execPath, + const std::filesystem::path& defaultCoverPath, std::size_t maxCacheSize, - std::size_t maxFileSize) - : _maxCacheSize {maxCacheSize} + std::size_t maxFileSize, + unsigned jpegQuality) + : _defaultCoverPath {defaultCoverPath} + , _maxCacheSize {maxCacheSize} , _maxFileSize {maxFileSize} + , _jpegQuality {clamp(jpegQuality, 1, 100)} { + LMS_LOG(COVER, INFO) << "Default cover path = '" << _defaultCoverPath.string() << "'"; LMS_LOG(COVER, INFO) << "Max cache size = " << _maxCacheSize; LMS_LOG(COVER, INFO) << "Max file size = " << _maxFileSize; - init(execPath); -} + LMS_LOG(COVER, INFO) << "JPEG export quality = " << _jpegQuality; + +#if LMS_SUPPORT_IMAGE_GM + GraphicsMagick::init(execPath); +#else + (void)execPath; +#endif -void -Grabber::setDefaultCover(const std::filesystem::path& p) -{ try { - RawImage defaultCover {p}; - _defaultCover = defaultCover.encode(); - LMS_LOG(COVER, INFO) << "Successfully read default cover image!"; + getDefault(512); } catch (const ImageException& e) { - throw LmsException("Cannot read default cover file '" + p.string() + "'"); + throw LmsException("Cannot read default cover file '" + _defaultCoverPath.string() + "': " + e.what()); } } -static std::optional -getFromAvMediaFile(const Av::MediaFile& input, Width width) +std::unique_ptr +Grabber::getFromAvMediaFile(const Av::MediaFile& input, ImageSize width) const { - std::optional image; + std::unique_ptr image; input.visitAttachedPictures([&](const Av::Picture& picture) { @@ -85,12 +98,9 @@ getFromAvMediaFile(const Av::MediaFile& input, Width width) try { - EncodedImage encodedImage {picture.data, picture.dataSize}; - - RawImage rawImage {encodedImage}; - rawImage.scale(width); - - image = rawImage.encode(); + RawImage rawImage {picture.data, picture.dataSize}; + rawImage.resize(width); + image = rawImage.encodeToJPEG(_jpegQuality); } catch (const ImageException& e) { @@ -101,17 +111,16 @@ getFromAvMediaFile(const Av::MediaFile& input, Width width) return image; } -static std::optional -getFromFile(const std::filesystem::path& p, Width width) +std::unique_ptr +Grabber::getFromFile(const std::filesystem::path& p, ImageSize width) const { - std::optional image; + std::unique_ptr image; try { RawImage rawImage {p}; - rawImage.scale(width); - - image = rawImage.encode(); + rawImage.resize(width); + image = rawImage.encodeToJPEG(_jpegQuality); } catch (const ImageException& e) { @@ -121,53 +130,50 @@ getFromFile(const std::filesystem::path& p, Width width) return image; } -EncodedImage -Grabber::getDefault(Width width) +std::shared_ptr +Grabber::getDefault(ImageSize width) { { std::shared_lock lock {_cacheMutex}; - if (auto it {_defaultCache.find(width)}; it != std::cend(_defaultCache)) + if (auto it {_defaultCoverCache.find(width)}; it != std::cend(_defaultCoverCache)) return it->second; } { std::unique_lock lock {_cacheMutex}; - if (auto it {_defaultCache.find(width)}; it != std::cend(_defaultCache)) + if (auto it {_defaultCoverCache.find(width)}; it != std::cend(_defaultCoverCache)) return it->second; - RawImage rawImage {*_defaultCover}; - rawImage.scale(width); - EncodedImage res {rawImage.encode()}; + std::shared_ptr image {getFromFile(_defaultCoverPath, width)}; + _defaultCoverCache[width] = image; + LMS_LOG(COVER, DEBUG) << "Default cache entries = " << _defaultCoverCache.size(); - _defaultCache[width] = res; - LMS_LOG(COVER, DEBUG) << "Default cache entries = " << _defaultCache.size(); - - return res; + return image; } } -std::optional -Grabber::getFromDirectory(const std::filesystem::path& p, std::string_view preferredFileName, Width width) const +std::unique_ptr +Grabber::getFromDirectory(const std::filesystem::path& p, std::string_view preferredFileName, ImageSize width) const { const std::multimap coverPaths {getCoverPaths(p)}; auto tryLoadImageFromFilename = [&](std::string_view fileName) { - std::optional image; + std::unique_ptr image; auto range {coverPaths.equal_range(std::string {fileName})}; for (auto it {range.first}; it != range.second; ++it) { image = getFromFile(it->second, width); - if (!image) - continue; + if (image) + break; } return image; }; - std::optional image; + std::unique_ptr image; if (!preferredFileName.empty()) { @@ -225,10 +231,10 @@ Grabber::getCoverPaths(const std::filesystem::path& directoryPath) const return res; } -std::optional -Grabber::getFromTrack(const std::filesystem::path& p, Width width) const +std::unique_ptr +Grabber::getFromTrack(const std::filesystem::path& p, ImageSize width) const { - std::optional image; + std::unique_ptr image; try { @@ -244,16 +250,16 @@ Grabber::getFromTrack(const std::filesystem::path& p, Width width) const return image; } -EncodedImage -Grabber::getFromTrackInternal(Database::Session& dbSession, Database::IdType trackId, Width width) +std::shared_ptr +Grabber::getFromTrack(Database::Session& dbSession, Database::IdType trackId, ImageSize width) { using namespace Database; const CacheEntryDesc cacheEntryDesc {CacheEntryDesc::Type::Track, trackId, width}; - std::optional cover {loadFromCache(cacheEntryDesc)}; + std::shared_ptr cover {loadFromCache(cacheEntryDesc)}; if (cover) - return *cover; + return cover; bool hasCover {}; bool isMultiDisc {}; @@ -289,19 +295,20 @@ Grabber::getFromTrackInternal(Database::Session& dbSession, Database::IdType tra if (!cover) cover = getDefault(width); - saveToCache(cacheEntryDesc, *cover); + if (cover) + saveToCache(cacheEntryDesc, cover); - return *cover; + return cover; } -EncodedImage -Grabber::getFromReleaseInternal(Database::Session& session, Database::IdType releaseId, Width width) +std::shared_ptr +Grabber::getFromRelease(Database::Session& session, Database::IdType releaseId, ImageSize width) { const CacheEntryDesc cacheEntryDesc {CacheEntryDesc::Type::Release, releaseId, width}; - std::optional cover {loadFromCache(cacheEntryDesc)}; + std::shared_ptr cover {loadFromCache(cacheEntryDesc)}; if (cover) - return *cover; + return cover; std::optional trackId; { @@ -317,16 +324,14 @@ Grabber::getFromReleaseInternal(Database::Session& session, Database::IdType rel } if (trackId) - { - cover = getFromTrackInternal(session, *trackId, width); - } - - if (!cover) + cover = getFromTrack(session, *trackId, width); + else cover = getDefault(width); - saveToCache(cacheEntryDesc, *cover); + if (cover) + saveToCache(cacheEntryDesc, cover); - return *cover; + return cover; } void @@ -341,36 +346,23 @@ Grabber::flushCache() _cache.clear(); } -std::unique_ptr -Grabber::getFromTrack(Database::Session& session, Database::IdType trackId, std::size_t width) -{ - CoverArt toto {getFromTrackInternal(session, trackId, width)}; - return std::make_unique(getFromTrackInternal(session, trackId, width)); -} - -std::unique_ptr -Grabber::getFromRelease(Database::Session& session, Database::IdType releaseId, std::size_t width) -{ - return std::make_unique(getFromReleaseInternal(session, releaseId, width)); -} - void -Grabber::saveToCache(const CacheEntryDesc& entryDesc, const EncodedImage& image) +Grabber::saveToCache(const CacheEntryDesc& entryDesc, std::shared_ptr image) { std::unique_lock lock {_cacheMutex}; - while (_cacheSize + image.getDataSize() > _maxCacheSize && !_cache.empty()) + while (_cacheSize + image->getDataSize() > _maxCacheSize && !_cache.empty()) { - auto it {Random::pickRandom(_cache)}; - _cacheSize -= it->second.getDataSize(); - _cache.erase(it); + auto itRandom {Random::pickRandom(_cache)}; + _cacheSize -= itRandom->second->getDataSize(); + _cache.erase(itRandom); } - _cacheSize += image.getDataSize(); + _cacheSize += image->getDataSize(); _cache[entryDesc] = image; } -std::optional +std::shared_ptr Grabber::loadFromCache(const CacheEntryDesc& entryDesc) { std::shared_lock lock {_cacheMutex}; @@ -379,7 +371,7 @@ Grabber::loadFromCache(const CacheEntryDesc& entryDesc) if (it == std::cend(_cache)) { ++_cacheMisses; - return std::nullopt; + return nullptr; } ++_cacheHits; diff --git a/src/libs/cover/impl/CoverArtGrabber.hpp b/src/libs/cover/impl/CoverArtGrabber.hpp index 021d3e24..87fbbeb9 100644 --- a/src/libs/cover/impl/CoverArtGrabber.hpp +++ b/src/libs/cover/impl/CoverArtGrabber.hpp @@ -21,6 +21,7 @@ #include #include +#include #include #include #include @@ -28,14 +29,19 @@ #include #include "cover/ICoverArtGrabber.hpp" +#include "cover/IEncodedImage.hpp" #include "database/Types.hpp" -#include "Image.hpp" namespace Database { class Session; } +namespace Av +{ + class MediaFile; +} + namespace CoverArt { struct CacheEntryDesc @@ -83,7 +89,11 @@ namespace CoverArt class Grabber : public IGrabber { public: - Grabber(const std::filesystem::path& execPath, std::size_t maxCacheEntries, std::size_t maxFileSize); + Grabber(const std::filesystem::path& execPath, + const std::filesystem::path& defaultCoverPath, + std::size_t maxCacheEntries, + std::size_t maxFileSize, + unsigned jpegQuality); Grabber(const Grabber&) = delete; Grabber& operator=(const Grabber&) = delete; @@ -91,38 +101,34 @@ namespace CoverArt Grabber& operator=(Grabber&&) = delete; private: - - void setDefaultCover(const std::filesystem::path& defaultCoverPath) override; - std::unique_ptr getFromTrack(Database::Session& dbSession, Database::IdType trackId, Width width) override; - std::unique_ptr getFromRelease(Database::Session& dbSession, Database::IdType releaseId, Width width) override; + std::shared_ptr getFromTrack(Database::Session& dbSession, Database::IdType trackId, ImageSize width) override; + std::shared_ptr getFromRelease(Database::Session& dbSession, Database::IdType releaseId, ImageSize width) override; void flushCache() override; - EncodedImage getFromTrackInternal(Database::Session& dbSession, Database::IdType trackId, Width width); - EncodedImage getFromReleaseInternal(Database::Session& dbSession, Database::IdType releaseId, Width width); + std::unique_ptr getFromAvMediaFile(const Av::MediaFile& input, ImageSize width) const; + std::unique_ptr getFromFile(const std::filesystem::path& p, ImageSize width) const; - std::optional getFromTrack(const std::filesystem::path& path, Width width) const; + std::unique_ptr getFromTrack(const std::filesystem::path& path, ImageSize width) const; std::multimap getCoverPaths(const std::filesystem::path& directoryPath) const; - std::optional getFromDirectory(const std::filesystem::path& path, std::string_view preferredFileName, Width width) const; - EncodedImage getDefault(Width width); - - EncodedImage resizeCoverOrFallback(EncodedImage image, Width width) const; - - std::optional _defaultCover; // optional to defer initializing + std::unique_ptr getFromDirectory(const std::filesystem::path& path, std::string_view preferredFileName, ImageSize width) const; + std::shared_ptr getDefault(ImageSize width); std::shared_mutex _cacheMutex; - std::unordered_map _cache; - std::unordered_map _defaultCache; + std::unordered_map> _cache; + std::unordered_map> _defaultCoverCache; std::atomic _cacheMisses {}; std::atomic _cacheHits {}; std::size_t _cacheSize {}; - void saveToCache(const CacheEntryDesc& entryDesc, const EncodedImage& image); - std::optional loadFromCache(const CacheEntryDesc& entryDesc); + void saveToCache(const CacheEntryDesc& entryDesc, std::shared_ptr image); + std::shared_ptr loadFromCache(const CacheEntryDesc& entryDesc); + const std::filesystem::path _defaultCoverPath; const std::size_t _maxCacheSize; static inline const std::vector _fileExtensions {".jpg", ".jpeg", ".png", ".bmp"}; // TODO parametrize const std::size_t _maxFileSize; static inline const std::vector _preferredFileNames {"cover", "front"}; // TODO parametrize + const unsigned _jpegQuality; }; } // namespace CoverArt diff --git a/src/libs/cover/impl/Image.hpp b/src/libs/cover/impl/Exception.hpp similarity index 57% rename from src/libs/cover/impl/Image.hpp rename to src/libs/cover/impl/Exception.hpp index d7091e0b..e6c8c342 100644 --- a/src/libs/cover/impl/Image.hpp +++ b/src/libs/cover/impl/Exception.hpp @@ -19,17 +19,10 @@ #pragma once -#include -#include - -#include - #include "utils/Exception.hpp" namespace CoverArt { - void init(const std::filesystem::path& path); - // internal use only class ImageException : public LmsException { @@ -37,37 +30,5 @@ namespace CoverArt using LmsException::LmsException; }; - class EncodedImage - { - public: - EncodedImage() = default; - EncodedImage(const std::byte* data, std::size_t dataSize); - - const std::byte* getData() const; - std::size_t getDataSize() const; - - private: - friend class RawImage; - EncodedImage(Magick::Blob blob); - - Magick::Blob _blob; - }; - - class RawImage - { - public: - RawImage(const std::filesystem::path& p); - RawImage(const EncodedImage& encodedImage); - - // Operations - void scale(std::size_t width); - - // output - EncodedImage encode() const; - - private: - Magick::Image _image; - }; - } // namespace CoverArt diff --git a/src/libs/cover/impl/CoverArt.cpp b/src/libs/cover/impl/IRawImage.hpp similarity index 67% rename from src/libs/cover/impl/CoverArt.cpp rename to src/libs/cover/impl/IRawImage.hpp index 2852fde9..148d2178 100644 --- a/src/libs/cover/impl/CoverArt.cpp +++ b/src/libs/cover/impl/IRawImage.hpp @@ -17,31 +17,19 @@ * along with LMS. If not, see . */ -#include "CoverArt.hpp" +#pragma once + +#include + +#include "cover/IEncodedImage.hpp" namespace CoverArt { - - CoverArt::CoverArt(EncodedImage image) - : _image {image} - {} - - const std::byte* - CoverArt::getData() const + class IRawImage { - return _image.getData(); - } + public: + virtual void resize(ImageSize width) = 0; + virtual std::unique_ptr encodeToJPEG(unsigned quality) const = 0; + }; +} - std::size_t - CoverArt::getDataSize() const - { - return _image.getDataSize(); - } - - std::string_view - CoverArt::getMimeType() const - { - return "image/jpeg"; - } - -} // namespace CoverArt diff --git a/src/libs/cover/impl/graphicsmagick/JPEGImage.cpp b/src/libs/cover/impl/graphicsmagick/JPEGImage.cpp new file mode 100644 index 00000000..8cf7dfdc --- /dev/null +++ b/src/libs/cover/impl/graphicsmagick/JPEGImage.cpp @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2020 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "JPEGImage.hpp" + +#include "Exception.hpp" +#include "RawImage.hpp" +#include "utils/Logger.hpp" + +namespace CoverArt::GraphicsMagick +{ + JPEGImage::JPEGImage(const RawImage& rawImage, unsigned quality) + { + try + { + Magick::Image image {rawImage.getMagickImage()}; + image.magick("JPEG"); + image.quality(quality); + image.write(&_blob); + } + catch (Magick::Exception& e) + { + LMS_LOG(COVER, ERROR) << "Caught Magick exception: " << e.what(); + throw ImageException {std::string {"Magick read error: "} + e.what()}; + } + } + + const std::byte* + JPEGImage::getData() const + { + return reinterpret_cast(_blob.data()); + } + + std::size_t + JPEGImage::getDataSize() const + { + return _blob.length(); + } +} diff --git a/src/libs/cover/impl/graphicsmagick/JPEGImage.hpp b/src/libs/cover/impl/graphicsmagick/JPEGImage.hpp new file mode 100644 index 00000000..37a5e163 --- /dev/null +++ b/src/libs/cover/impl/graphicsmagick/JPEGImage.hpp @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2020 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#ifndef LMS_SUPPORT_IMAGE_GM +#error "Bad configuration" +#endif + +#include + +#include "cover/IEncodedImage.hpp" + +namespace CoverArt::GraphicsMagick +{ + class RawImage; + class JPEGImage : public IEncodedImage + { + public: + JPEGImage(const RawImage& rawImage, unsigned quality); + + private: + const std::byte* getData() const override; + std::size_t getDataSize() const override; + std::string_view getMimeType() const override { return "image/jpeg"; } + + Magick::Blob _blob; + }; +} diff --git a/src/libs/cover/impl/Image.cpp b/src/libs/cover/impl/graphicsmagick/RawImage.cpp similarity index 56% rename from src/libs/cover/impl/Image.cpp rename to src/libs/cover/impl/graphicsmagick/RawImage.cpp index c628b9fa..3beabc27 100644 --- a/src/libs/cover/impl/Image.cpp +++ b/src/libs/cover/impl/graphicsmagick/RawImage.cpp @@ -17,22 +17,24 @@ * along with LMS. If not, see . */ -#include "Image.hpp" - -#include -#include +#include "RawImage.hpp" #include #include "utils/Logger.hpp" +#include "JPEGImage.hpp" +#include "Exception.hpp" -namespace CoverArt { +namespace CoverArt::GraphicsMagick { void init(const std::filesystem::path& path) { Magick::InitializeMagick(path.string().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"; + if (!MagickLib::SetMagickResourceLimit(MagickLib::ThreadsResource, 1)) LMS_LOG(COVER, ERROR) << "Cannot set Magick thread resource limit to 1!"; @@ -43,26 +45,27 @@ init(const std::filesystem::path& path) LMS_LOG(COVER, INFO) << "Magick Disk resource limit = " << GetMagickResourceLimit(MagickLib::DiskResource); } -EncodedImage::EncodedImage(const std::byte* data, std::size_t dataSize) -: _blob {data, dataSize} +RawImage::RawImage(const std::byte* encodedData, std::size_t encodedDataSize) { -} - -EncodedImage::EncodedImage(Magick::Blob blob) -: _blob {blob} -{ -} - -const std::byte* -EncodedImage::getData() const -{ - return reinterpret_cast(_blob.data()); -} - -std::size_t -EncodedImage::getDataSize() const -{ - return _blob.length(); + try + { + Magick::Blob blob {encodedData, encodedDataSize}; + _image.read(blob); + } + catch (Magick::WarningCoder& e) + { + LMS_LOG(COVER, WARNING) << "Caught Magick WarningCoder: " << e.what(); + } + catch (Magick::Warning& e) + { + LMS_LOG(COVER, WARNING) << "Caught Magick warning: " << e.what(); + throw ImageException {std::string {"Magick read warning: "} + e.what()}; + } + catch (Magick::Exception& e) + { + LMS_LOG(COVER, ERROR) << "Caught Magick exception: " << e.what(); + throw ImageException {std::string {"Magick read error: "} + e.what()}; + } } RawImage::RawImage(const std::filesystem::path& p) @@ -73,79 +76,45 @@ RawImage::RawImage(const std::filesystem::path& p) } catch (Magick::WarningCoder& e) { - LMS_LOG(COVER, WARNING) << "Caught Magick WarningCoder while loading image '" << p.string() << "': " << e.what(); + LMS_LOG(COVER, WARNING) << "Caught Magick WarningCoder: " << e.what(); } catch (Magick::Warning& e) { - LMS_LOG(COVER, WARNING) << "Caught Magick warning while loading raw image '" << p.string() << "': " << e.what(); + LMS_LOG(COVER, WARNING) << "Caught Magick warning: " << e.what(); throw ImageException {std::string {"Magick read warning: "} + e.what()}; } catch (Magick::Exception& e) { - LMS_LOG(COVER, ERROR) << "Caught Magick exception while loading raw image '" << p.string() << "': " << e.what(); - throw ImageException {std::string {"Magick read error: "} + e.what()}; - } -} - -RawImage::RawImage(const EncodedImage& encodedImage) -{ - try - { - _image.read(encodedImage._blob); - } - catch (Magick::WarningCoder& e) - { - LMS_LOG(COVER, WARNING) << "Caught Magick WarningCoder while loading raw image: " << e.what(); - } - catch (Magick::Warning& e) - { - LMS_LOG(COVER, WARNING) << "Caught Magick warning while loading raw image: " << e.what(); - throw ImageException {std::string {"Magick read warning: "} + e.what()}; - } - catch (Magick::Exception& e) - { - LMS_LOG(COVER, ERROR) << "Caught Magick exception while loading raw image: " << e.what(); + LMS_LOG(COVER, ERROR) << "Caught Magick exception: " << e.what(); throw ImageException {std::string {"Magick read error: "} + e.what()}; } } void -RawImage::scale(std::size_t width) +RawImage::resize(ImageSize width) { - if (width == 0) - throw ImageException {"Bad width = 0"}; - try { _image.resize(Magick::Geometry {static_cast(width), static_cast(width)}); } catch (Magick::Exception& e) { - LMS_LOG(COVER, ERROR) << "Caught Magick exception during scale: " << e.what(); + LMS_LOG(COVER, ERROR) << "Caught Magick exception while resizing: " << e.what(); throw ImageException {std::string {"Magick resize error: "} + e.what()}; } } -EncodedImage -RawImage::encode() const +std::unique_ptr +RawImage::encodeToJPEG(unsigned quality) const { - try - { - Magick::Image outputImage {_image}; - - outputImage.magick("JPEG"); - - Magick::Blob blob; - outputImage.write(&blob); - - return EncodedImage {blob}; - } - catch (Magick::Exception& e) - { - LMS_LOG(COVER, ERROR) << "Caught Magick exception while encoding raw image: " << e.what(); - throw ImageException {std::string {"Magick encode error: "} + e.what()}; - } + return std::make_unique(*this, quality); } -} // namespace CoverArt +Magick::Image +RawImage::getMagickImage() const +{ + return _image; +} + +} // namespace CoverArt::GraphicsMagick diff --git a/src/libs/cover/impl/graphicsmagick/RawImage.hpp b/src/libs/cover/impl/graphicsmagick/RawImage.hpp new file mode 100644 index 00000000..354c07c0 --- /dev/null +++ b/src/libs/cover/impl/graphicsmagick/RawImage.hpp @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2020 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#ifndef LMS_SUPPORT_IMAGE_GM +#error "Bad configuration" +#endif + +#include + +#include +#include + +#include "cover/IEncodedImage.hpp" +#include "IRawImage.hpp" + +namespace CoverArt::GraphicsMagick +{ + void init(const std::filesystem::path& path); + + class RawImage : IRawImage + { + public: + RawImage(const std::byte* encodedData, std::size_t encodedDataSize); + RawImage(const std::filesystem::path& path); + + void resize(ImageSize width) override; + std::unique_ptr encodeToJPEG(unsigned quality) const override; + + private: + friend class JPEGImage; + Magick::Image getMagickImage() const; + + Magick::Image _image; + }; +} + diff --git a/src/libs/cover/impl/stb/JPEGImage.cpp b/src/libs/cover/impl/stb/JPEGImage.cpp new file mode 100644 index 00000000..68eafe43 --- /dev/null +++ b/src/libs/cover/impl/stb/JPEGImage.cpp @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2020 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "JPEGImage.hpp" + +#define STB_IMAGE_WRITE_IMPLEMENTATION +#include + +#include "RawImage.hpp" +#include "Exception.hpp" + +namespace CoverArt::STB +{ + JPEGImage::JPEGImage(const RawImage& rawImage, unsigned quality) + { + auto writeCb {[](void* ctx, void* writeData, int writeSize) + { + auto& output {*reinterpret_cast*>(ctx)}; + const std::size_t currentOutputSize {output.size()}; + output.resize(currentOutputSize + writeSize); + std::copy(reinterpret_cast(writeData), reinterpret_cast(writeData) + writeSize, output.data() + currentOutputSize); + }}; + + if (stbi_write_jpg_to_func(writeCb, &_data, rawImage.getWidth(), rawImage.getHeight(), 3, rawImage.getData(), quality) == 0) + { + _data.clear(); + throw ImageException {"Failed to export in jpeg format!"}; + } + } + + const std::byte* + JPEGImage::getData() const + { + if (_data.empty()) + return nullptr; + + return &_data.front(); + } + + std::size_t + JPEGImage::getDataSize() const + { + return _data.size(); + } +} diff --git a/src/libs/cover/impl/CoverArt.hpp b/src/libs/cover/impl/stb/JPEGImage.hpp similarity index 73% rename from src/libs/cover/impl/CoverArt.hpp rename to src/libs/cover/impl/stb/JPEGImage.hpp index b19685b3..2ef7aa76 100644 --- a/src/libs/cover/impl/CoverArt.hpp +++ b/src/libs/cover/impl/stb/JPEGImage.hpp @@ -17,25 +17,23 @@ * along with LMS. If not, see . */ -#pragma once +#include -#include "cover/ICoverArt.hpp" -#include "Image.hpp" +#include "cover/IEncodedImage.hpp" -namespace CoverArt +namespace CoverArt::STB { - - class CoverArt : public ICoverArt + class RawImage; + class JPEGImage : public IEncodedImage { public: - CoverArt(EncodedImage image); - - const std::byte* getData() const override; - std::size_t getDataSize() const override; - std::string_view getMimeType() const override; + JPEGImage(const RawImage& rawImage, unsigned quality); private: - EncodedImage _image; - }; + const std::byte* getData() const override; + std::size_t getDataSize() const override; + std::string_view getMimeType() const override { return "image/jpeg"; } -} // namespace CoverArt + std::vector _data; + }; +} diff --git a/src/libs/cover/impl/stb/RawImage.cpp b/src/libs/cover/impl/stb/RawImage.cpp new file mode 100644 index 00000000..13ff1d75 --- /dev/null +++ b/src/libs/cover/impl/stb/RawImage.cpp @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2020 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "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 + +#include +#include + +#include "JPEGImage.hpp" + +#include "Exception.hpp" + +namespace CoverArt::STB +{ + RawImage::RawImage(const std::byte* encodedData, std::size_t encodedDataSize) + { + int n; + _data = UniquePtrFree {stbi_load_from_memory(reinterpret_cast(encodedData), encodedDataSize, &_width, &_height, &n, 3), std::free}; + if (!_data) + throw ImageException {"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}; + if (!_data) + throw ImageException {"Cannot load image from memory"}; + } + + void + RawImage::resize(ImageSize width) + { + size_t height; + if (_width == _height) + { + height = width; + } + else if (_width > _height) + { + height = (size_t)((float)width/_width*_height); + } + else + { + height = width; + width = (size_t)((float)height/_height*_width); + } + + UniquePtrFree resizedData {reinterpret_cast(malloc(width*height*3)), std::free}; + if (!resizedData) + throw ImageException {"Cannot allocate memory for resized image!"}; + + if (stbir_resize_uint8_srgb(reinterpret_cast(_data.get()), _width, _height, 0, + reinterpret_cast(resizedData.get()), width, height, 0, + 3, STBIR_ALPHA_CHANNEL_NONE, 0) == 0) + { + throw ImageException {"Failed to resize image!"}; + } + + _data = std::move(resizedData); + _height = height; + _width = width; + } + + std::unique_ptr + RawImage::encodeToJPEG(unsigned quality) const + { + return std::make_unique(*this, quality); + } + + ImageSize + RawImage::getWidth() const + { + return _width; + } + + ImageSize + RawImage::getHeight() const + { + return _height; + } + + const std::byte* + RawImage::getData() const + { + if (!_data) + return nullptr; + + return reinterpret_cast(_data.get()); + } +} + diff --git a/src/libs/cover/impl/stb/RawImage.hpp b/src/libs/cover/impl/stb/RawImage.hpp new file mode 100644 index 00000000..d4f36aa1 --- /dev/null +++ b/src/libs/cover/impl/stb/RawImage.hpp @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2020 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#ifndef LMS_SUPPORT_IMAGE_STB +#error "Bad configuration" +#endif + +#include +#include + +#include "cover/IEncodedImage.hpp" +#include "IRawImage.hpp" + +namespace CoverArt::STB +{ + class RawImage : public IRawImage + { + public: + RawImage(const std::byte* encodedData, std::size_t encodedDataSize); + RawImage(const std::filesystem::path& path); + + void resize(ImageSize width) override; + std::unique_ptr encodeToJPEG(unsigned quality) const override; + + ImageSize getWidth() const; + ImageSize getHeight() const; + const std::byte* getData() const; + + private: + int _width; + int _height; + using UniquePtrFree = std::unique_ptr; + UniquePtrFree _data {nullptr, std::free}; + }; +} + diff --git a/src/libs/cover/include/cover/ICoverArtGrabber.hpp b/src/libs/cover/include/cover/ICoverArtGrabber.hpp index 958128ce..8f9671da 100644 --- a/src/libs/cover/include/cover/ICoverArtGrabber.hpp +++ b/src/libs/cover/include/cover/ICoverArtGrabber.hpp @@ -19,13 +19,11 @@ #pragma once -#include #include #include -#include #include "database/Types.hpp" -#include "cover/ICoverArt.hpp" +#include "cover/IEncodedImage.hpp" namespace Database { @@ -34,22 +32,22 @@ namespace Database namespace CoverArt { - using Width = std::size_t; - class IGrabber { public: virtual ~IGrabber() = default; - virtual void setDefaultCover(const std::filesystem::path& defaultCoverPath) = 0; - - virtual std::unique_ptr getFromTrack(Database::Session& dbSession, Database::IdType trackId, Width width) = 0; - virtual std::unique_ptr getFromRelease(Database::Session& dbSession, Database::IdType releaseId, Width width) = 0; + virtual std::shared_ptr getFromTrack(Database::Session& dbSession, Database::IdType trackId, ImageSize width) = 0; + virtual std::shared_ptr getFromRelease(Database::Session& dbSession, Database::IdType releaseId, ImageSize width) = 0; virtual void flushCache() = 0; }; - std::unique_ptr createGrabber(const std::filesystem::path& execPath, std::size_t maxCacheEntries, std::size_t maxFileSize); + std::unique_ptr createGrabber(const std::filesystem::path& execPath, + const std::filesystem::path& defaultCoverPath, + std::size_t maxCacheEntries, + std::size_t maxFileSize, + unsigned jpegQuality); } // namespace CoverArt diff --git a/src/libs/cover/include/cover/ICoverArt.hpp b/src/libs/cover/include/cover/IEncodedImage.hpp similarity index 87% rename from src/libs/cover/include/cover/ICoverArt.hpp rename to src/libs/cover/include/cover/IEncodedImage.hpp index 902dbca1..0fc245be 100644 --- a/src/libs/cover/include/cover/ICoverArt.hpp +++ b/src/libs/cover/include/cover/IEncodedImage.hpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Emeric Poupon + * Copyright (C) 2015 Emeric Poupon * * This file is part of LMS. * @@ -24,11 +24,12 @@ namespace CoverArt { + using ImageSize = std::size_t; - class ICoverArt + class IEncodedImage { public: - virtual ~ICoverArt() = default; + virtual ~IEncodedImage() = default; virtual const std::byte* getData() const = 0; virtual std::size_t getDataSize() const = 0; @@ -36,3 +37,4 @@ namespace CoverArt }; } // namespace CoverArt + diff --git a/src/libs/database/CMakeLists.txt b/src/libs/database/CMakeLists.txt index a7eba7e2..93bcb6f6 100644 --- a/src/libs/database/CMakeLists.txt +++ b/src/libs/database/CMakeLists.txt @@ -9,7 +9,6 @@ add_library(lmsdatabase SHARED impl/Release.cpp impl/ScanSettings.cpp impl/Session.cpp - impl/SessionPool.cpp impl/SqlQuery.cpp impl/Track.cpp impl/TrackBookmark.cpp diff --git a/src/libs/database/impl/Cluster.cpp b/src/libs/database/impl/Cluster.cpp index 66e95c12..26dd4b21 100644 --- a/src/libs/database/impl/Cluster.cpp +++ b/src/libs/database/impl/Cluster.cpp @@ -32,14 +32,14 @@ Cluster::Cluster() { } -Cluster::Cluster(Wt::Dbo::ptr type, std::string name) - : _name(std::string(name, 0, _maxNameLength)), - _clusterType(type) +Cluster::Cluster(Wt::Dbo::ptr type, std::string_view name) + : _name(std::string {name, 0, _maxNameLength}), + _clusterType {type} { } Cluster::pointer -Cluster::create(Session& session, Wt::Dbo::ptr type, std::string name) +Cluster::create(Session& session, Wt::Dbo::ptr type, std::string_view name) { session.checkUniqueLocked(); diff --git a/src/libs/database/impl/Db.cpp b/src/libs/database/impl/Db.cpp index d06eb38b..9854facb 100644 --- a/src/libs/database/impl/Db.cpp +++ b/src/libs/database/impl/Db.cpp @@ -22,6 +22,7 @@ #include #include +#include "database/Session.hpp" #include "database/User.hpp" #include "utils/Logger.hpp" @@ -57,6 +58,24 @@ Db::executeSql(const std::string& sql) connection->executeSql(sql); } +Session& +Db::getTLSSession() +{ + static thread_local Session* tlsSession {}; + + if (!tlsSession) + { + auto newSession {std::make_unique(*this)}; + tlsSession = newSession.get(); + + { + std::scoped_lock lock {_tlsSessionsMutex}; + _tlsSessions.push_back(std::move(newSession)); + } + } + + return *tlsSession; +} Db::ScopedConnection::ScopedConnection(Wt::Dbo::SqlConnectionPool& pool) : _connectionPool {pool} diff --git a/src/libs/database/impl/Session.cpp b/src/libs/database/impl/Session.cpp index ad86ece3..2782be74 100644 --- a/src/libs/database/impl/Session.cpp +++ b/src/libs/database/impl/Session.cpp @@ -362,13 +362,13 @@ Session::checkSharedLocked() UniqueTransaction Session::createUniqueTransaction() { - return UniqueTransaction{_db.getMutex(), _session}; + return UniqueTransaction {_db.getMutex(), _session}; } SharedTransaction Session::createSharedTransaction() { - return SharedTransaction{_db.getMutex(), _session}; + return SharedTransaction {_db.getMutex(), _session}; } void diff --git a/src/libs/database/impl/SessionPool.cpp b/src/libs/database/impl/SessionPool.cpp deleted file mode 100644 index ff6b1111..00000000 --- a/src/libs/database/impl/SessionPool.cpp +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (C) 2019 Emeric Poupon - * - * This file is part of LMS. - * - * LMS is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LMS is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LMS. If not, see . - */ - -#include "database/SessionPool.hpp" - - -#include "database/Session.hpp" -#include "utils/Exception.hpp" -#include "utils/Logger.hpp" - -namespace Database { - -SessionPool::SessionPool(Db& database, std::size_t maxSessionCount) -: _db {database}, -_maxSessionCount {maxSessionCount} -{ -} - -Session& -SessionPool::acquireSession() -{ - std::scoped_lock lock {_mutex}; - - if (_freeSessions.empty()) - { - if (_acquiredSessions.size() == _maxSessionCount) - throw LmsException {"Too many database sessions!"}; - - _freeSessions.emplace_back(std::make_unique(_db)); - } - - std::unique_ptr session {std::move(_freeSessions.back())}; - _freeSessions.pop_back(); - _acquiredSessions.push_back(std::move(session)); - - return *_acquiredSessions.back().get(); -} - -void -SessionPool::releaseSession(Session& sessionToRelease) -{ - std::scoped_lock lock {_mutex}; - - auto it {std::find_if(std::begin(_acquiredSessions), std::end(_acquiredSessions), [&](const std::unique_ptr& session) { return session.get() == &sessionToRelease; })}; - if (it == std::end(_acquiredSessions)) - throw LmsException {"Unknown released Session!"}; - - std::unique_ptr session {std::move(*it)}; - _acquiredSessions.erase(it); - _freeSessions.push_back(std::move(session)); -} - -} // namespace Database diff --git a/src/libs/database/include/database/Cluster.hpp b/src/libs/database/include/database/Cluster.hpp index 3d0642a6..fb0e4518 100644 --- a/src/libs/database/include/database/Cluster.hpp +++ b/src/libs/database/include/database/Cluster.hpp @@ -20,6 +20,7 @@ #pragma once #include +#include #include #include @@ -41,7 +42,7 @@ class Cluster : public Wt::Dbo::Dbo using pointer = Wt::Dbo::ptr; Cluster(); - Cluster(Wt::Dbo::ptr type, std::string name); + Cluster(Wt::Dbo::ptr type, std::string_view name); // Find utility static std::vector getAll(Session& session); @@ -49,7 +50,7 @@ class Cluster : public Wt::Dbo::Dbo static pointer getById(Session& session, IdType id); // Create utility - static pointer create(Session& session, Wt::Dbo::ptr type, std::string name); + static pointer create(Session& session, Wt::Dbo::ptr type, std::string_view name); // Accessors const std::string& getName() const { return _name; } diff --git a/src/libs/database/include/database/Db.hpp b/src/libs/database/include/database/Db.hpp index 565ffcd6..ebece1a6 100644 --- a/src/libs/database/include/database/Db.hpp +++ b/src/libs/database/include/database/Db.hpp @@ -26,7 +26,7 @@ namespace Database { -// Session living class handling the database and the login +class Session; class Db { public: @@ -39,6 +39,8 @@ class Db Db& operator=(const Db&) = delete; Db& operator=(Db&&) = delete; + Session& getTLSSession(); + private: friend class Session; @@ -88,6 +90,9 @@ class Db std::shared_mutex _sharedMutex; std::unique_ptr _connectionPool; + + std::mutex _tlsSessionsMutex; + std::vector> _tlsSessions; }; } // namespace Database diff --git a/src/libs/database/include/database/Session.hpp b/src/libs/database/include/database/Session.hpp index 303501bd..db14ba77 100644 --- a/src/libs/database/include/database/Session.hpp +++ b/src/libs/database/include/database/Session.hpp @@ -84,7 +84,7 @@ class Session void doDatabaseMigrationIfNeeded(); - Db& _db; + Db& _db; Wt::Dbo::Session _session; }; diff --git a/src/libs/database/include/database/SessionPool.hpp b/src/libs/database/include/database/SessionPool.hpp deleted file mode 100644 index f9019c36..00000000 --- a/src/libs/database/include/database/SessionPool.hpp +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (C) 2013 Emeric Poupon - * - * This file is part of LMS. - * - * LMS is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LMS is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LMS. If not, see . - */ - -#pragma once - -#include -#include -#include - -#include "Session.hpp" - -namespace Database { - -class SessionPool -{ - public: - class ScopedSession - { - public: - ScopedSession(SessionPool& pool) : _pool {pool}, _session {_pool.acquireSession()} {} - ~ScopedSession() { _pool.releaseSession(_session); } - - ScopedSession(const ScopedSession&) = delete; - ScopedSession(ScopedSession&&) = delete; - ScopedSession& operator=(const ScopedSession&) = delete; - ScopedSession& operator=(ScopedSession&&) = delete; - - Session& get() { return _session; } - - private: - SessionPool& _pool; - Session& _session; - }; - - SessionPool(Db& database, std::size_t maxSessionCount = 30); - - SessionPool(const SessionPool&) = delete; - SessionPool(SessionPool&&) = delete; - SessionPool& operator=(const SessionPool&) = delete; - SessionPool& operator=(SessionPool&&) = delete; - - private: - friend class ScopedSession; - Session& acquireSession(); - void releaseSession(Session& session); - - std::mutex _mutex; - Db& _db; - std::size_t _maxSessionCount; - std::vector> _freeSessions; - std::vector> _acquiredSessions; -}; - -} // namespace Database - - diff --git a/src/libs/subsonic/impl/SubsonicResource.cpp b/src/libs/subsonic/impl/SubsonicResource.cpp index 220bdbdf..3d93e980 100644 --- a/src/libs/subsonic/impl/SubsonicResource.cpp +++ b/src/libs/subsonic/impl/SubsonicResource.cpp @@ -157,7 +157,7 @@ getClientInfo(const Wt::Http::ParameterMap& parameters) } SubsonicResource::SubsonicResource(Db& db) -: _sessionPool {db} +: _db {db} { } @@ -1746,7 +1746,7 @@ handleGetCoverArt(RequestContext& context, const Wt::Http::Request& /*request*/, std::size_t size {getParameterAs(context.parameters, "size").value_or(256)}; size = clamp(size, std::size_t {32}, std::size_t {1024}); - std::unique_ptr cover; + std::shared_ptr cover; switch (id.type) { case Id::Type::Track: @@ -1911,9 +1911,9 @@ SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Resp clientName = clientInfo.name; - SessionPool::ScopedSession dbSession {_sessionPool}; + Session& dbSession {_db.getTLSSession()}; - switch (Service::get()->checkUserPassword(dbSession.get(), + switch (Service::get()->checkUserPassword(dbSession, boost::asio::ip::address::from_string(request.clientAddress()), clientInfo.user, clientInfo.password)) { @@ -1925,16 +1925,16 @@ SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Resp throw LoginThrottledGenericError {}; } - RequestContext requestContext {parameters, dbSession.get(), clientInfo.user, clientInfo.name}; + RequestContext requestContext {parameters, dbSession, clientInfo.user, clientInfo.name}; auto itEntryPoint {requestEntryPoints.find(requestPath)}; if (itEntryPoint != requestEntryPoints.end()) { if (itEntryPoint->second.mustBeAdmin) { - auto transaction {dbSession.get().createSharedTransaction()}; + auto transaction {dbSession.createSharedTransaction()}; - User::pointer user {User::getByLoginName(dbSession.get(), clientInfo.user)}; + User::pointer user {User::getByLoginName(dbSession, clientInfo.user)}; if (!user || !user->isAdmin()) throw UserNotAuthorizedError {}; } diff --git a/src/libs/subsonic/include/subsonic/SubsonicResource.hpp b/src/libs/subsonic/include/subsonic/SubsonicResource.hpp index 1d8746e7..6d3e14b9 100644 --- a/src/libs/subsonic/include/subsonic/SubsonicResource.hpp +++ b/src/libs/subsonic/include/subsonic/SubsonicResource.hpp @@ -21,8 +21,6 @@ #include #include -#include "database/SessionPool.hpp" - namespace Database { class Db; @@ -41,7 +39,7 @@ class SubsonicResource final : public Wt::WResource void handleRequest(const Wt::Http::Request &request, Wt::Http::Response &response) override; - Database::SessionPool _sessionPool; + Database::Db& _db; }; } // namespace diff --git a/src/lms/main.cpp b/src/lms/main.cpp index fe3ab99d..b52ab3fb 100644 --- a/src/lms/main.cpp +++ b/src/lms/main.cpp @@ -28,6 +28,7 @@ #include "av/AvTranscoder.hpp" #include "cover/ICoverArtGrabber.hpp" #include "database/Db.hpp" +#include "database/Session.hpp" #include "scanner/IMediaScanner.hpp" #include "recommendation/IEngine.hpp" #include "subsonic/SubsonicResource.hpp" @@ -140,7 +141,7 @@ int main(int argc, char* argv[]) std::filesystem::create_directories(config->getPath("working-dir") / "cache"); // Construct WT configuration and get the argc/argv back - std::vector wtServerArgs = generateWtConfig(argv[0]); + const std::vector wtServerArgs {generateWtConfig(argv[0])}; std::vector wtArgv(wtServerArgs.size()); for (std::size_t i = 0; i < wtServerArgs.size(); ++i) @@ -149,8 +150,8 @@ int main(int argc, char* argv[]) wtArgv[i] = wtServerArgs[i].c_str(); } - Wt::WServer server(argv[0]); - server.setServerConfiguration (wtServerArgs.size(), const_cast(&wtArgv[0])); + Wt::WServer server {argv[0]}; + server.setServerConfiguration(wtServerArgs.size(), const_cast(&wtArgv[0])); // lib init Av::Transcoder::init(); @@ -169,9 +170,10 @@ int main(int argc, char* argv[]) Service authTokenService {Auth::createAuthTokenService(config->getULong("login-throttler-max-entriees", 10000))}; Service passwordService {Auth::createPasswordService(config->getULong("login-throttler-max-entriees", 10000))}; Service coverArtService {CoverArt::createGrabber(argv[0], + server.appRoot() + "/images/unknown-cover.jpg", config->getULong("cover-max-cache-size", 30) * 1000 * 1000, - config->getULong("cover-max-file-size", 10) * 1000 * 1000)}; - coverArtService->setDefaultCover(server.appRoot() + "/images/unknown-cover.jpg"); + config->getULong("cover-max-file-size", 10) * 1000 * 1000, + config->getULong("cover-jpeg-quality", 75))}; Service recommendationEngineService {Recommendation::createEngine(database)}; recommendationEngineService->requestLoad(); Service mediaScannerService {Scanner::createMediaScanner(database)}; diff --git a/src/lms/ui/Auth.cpp b/src/lms/ui/Auth.cpp index e3cab312..c871d2a3 100644 --- a/src/lms/ui/Auth.cpp +++ b/src/lms/ui/Auth.cpp @@ -29,6 +29,7 @@ #include "auth/IAuthTokenService.hpp" #include "auth/IPasswordService.hpp" +#include "database/Session.hpp" #include "utils/Logger.hpp" #include "utils/Service.hpp" diff --git a/src/lms/ui/LmsApplication.cpp b/src/lms/ui/LmsApplication.cpp index fa13599b..b836ebab 100644 --- a/src/lms/ui/LmsApplication.cpp +++ b/src/lms/ui/LmsApplication.cpp @@ -32,6 +32,7 @@ #include "database/Cluster.hpp" #include "database/Db.hpp" #include "database/Release.hpp" +#include "database/Session.hpp" #include "database/User.hpp" #include "explore/Explore.hpp" #include "explore/Filters.hpp" @@ -65,19 +66,25 @@ LmsApplication::create(const Wt::WEnvironment& env, Database::Db& db, LmsApplica return std::make_unique(env, db, appGroups); } - LmsApplication* +LmsApplication* LmsApplication::instance() { return reinterpret_cast(Wt::WApplication::instance()); } +Database::Session& +LmsApplication::getDbSession() +{ + return _db.getTLSSession(); +} + Wt::Dbo::ptr LmsApplication::getUser() { if (!_userId) return {}; - return Database::User::getById(_dbSession, *_userId); + return Database::User::getById(getDbSession(), *_userId); } bool @@ -89,7 +96,7 @@ LmsApplication::isUserAuthStrong() const bool LmsApplication::isUserAdmin() { - auto transaction {_dbSession.createSharedTransaction()}; + auto transaction {getDbSession().createSharedTransaction()}; return getUser()->isAdmin(); } @@ -97,7 +104,7 @@ LmsApplication::isUserAdmin() bool LmsApplication::isUserDemo() { - auto transaction {_dbSession.createSharedTransaction()}; + auto transaction {getDbSession().createSharedTransaction()}; return getUser()->isDemo(); } @@ -105,7 +112,7 @@ LmsApplication::isUserDemo() std::string LmsApplication::getUserLoginName() { - auto transaction {_dbSession.createSharedTransaction()}; + auto transaction {getDbSession().createSharedTransaction()}; return getUser()->getLoginName(); } @@ -114,7 +121,7 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env, Database::Db& db, LmsApplicationGroupContainer& appGroups) : Wt::WApplication {env}, - _dbSession {db}, + _db {db}, _appGroups {appGroups} { @@ -155,8 +162,8 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env, // If here is no account in the database, launch the first connection wizard bool firstConnection {}; { - auto transaction {_dbSession.createSharedTransaction()}; - firstConnection = Database::User::getAll(_dbSession).empty(); + auto transaction {getDbSession().createSharedTransaction()}; + firstConnection = Database::User::getAll(getDbSession()).empty(); } LMS_LOG(UI, DEBUG) << "Creating root widget. First connection = " << firstConnection; @@ -174,8 +181,8 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env, Database::User::UITheme theme {Database::User::defaultUITheme}; if (userId) { - auto transaction {_dbSession.createSharedTransaction()}; - const auto user {Database::User::getById(_dbSession, *userId)}; + auto transaction {getDbSession().createSharedTransaction()}; + const auto user {Database::User::getById(getDbSession(), *userId)}; if (user) theme = user->getUITheme(); } @@ -206,8 +213,8 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env, auth->userLoggedIn.connect(this, [this](Database::IdType userId) { { - auto transaction {_dbSession.createSharedTransaction()}; - const auto user {Database::User::getById(_dbSession, userId)}; + auto transaction {getDbSession().createSharedTransaction()}; + const auto user {Database::User::getById(getDbSession(), userId)}; if (user) { LmsTheme* lmsTheme {static_cast(LmsApp->theme().get())}; @@ -402,7 +409,7 @@ LmsApplication::handleUserLoggedOut() LMS_LOG(UI, INFO) << "User '" << getUserLoginName() << " 'logged out"; { - auto transaction {_dbSession.createUniqueTransaction()}; + auto transaction {getDbSession().createUniqueTransaction()}; getUser().modify()->clearAuthTokens(); } diff --git a/src/lms/ui/LmsApplication.hpp b/src/lms/ui/LmsApplication.hpp index 9cf31760..757dc126 100644 --- a/src/lms/ui/LmsApplication.hpp +++ b/src/lms/ui/LmsApplication.hpp @@ -24,8 +24,6 @@ #include #include -#include "database/Db.hpp" -#include "database/Session.hpp" #include "scanner/IMediaScanner.hpp" #include "LmsApplicationGroup.hpp" @@ -35,6 +33,7 @@ namespace Database { class Cluster; class Db; class Release; + class Session; class User; } @@ -83,7 +82,7 @@ class LmsApplication : public Wt::WApplication std::shared_ptr getImageResource() { return _imageResource; } std::shared_ptr getAudioTranscodeResource() { return _audioTranscodeResource; } std::shared_ptr getAudioFileResource() { return _audioFileResource; } - Database::Session& getDbSession() { return _dbSession;} + Database::Session& getDbSession(); // always thread safe Wt::Dbo::ptr getUser(); bool isUserAuthStrong() const; // user must be logged in prior this call @@ -126,8 +125,8 @@ class LmsApplication : public Wt::WApplication void createHome(); + Database::Db& _db; Wt::Signal<> _preQuit; - Database::Session _dbSession; LmsApplicationGroupContainer& _appGroups; Events _events; std::optional _userId; diff --git a/src/lms/ui/MediaPlayer.cpp b/src/lms/ui/MediaPlayer.cpp index bae72d2f..ca96f30f 100644 --- a/src/lms/ui/MediaPlayer.cpp +++ b/src/lms/ui/MediaPlayer.cpp @@ -27,6 +27,7 @@ #include "database/Artist.hpp" #include "database/Release.hpp" +#include "database/Session.hpp" #include "database/Track.hpp" #include "database/TrackList.hpp" #include "database/User.hpp" diff --git a/src/lms/ui/PlayQueue.cpp b/src/lms/ui/PlayQueue.cpp index d427f7ba..0e62b981 100644 --- a/src/lms/ui/PlayQueue.cpp +++ b/src/lms/ui/PlayQueue.cpp @@ -23,6 +23,7 @@ #include #include "database/Cluster.hpp" +#include "database/Session.hpp" #include "database/Track.hpp" #include "database/TrackList.hpp" #include "database/User.hpp" diff --git a/src/lms/ui/SettingsView.cpp b/src/lms/ui/SettingsView.cpp index ebecb3d9..133ae59e 100644 --- a/src/lms/ui/SettingsView.cpp +++ b/src/lms/ui/SettingsView.cpp @@ -33,6 +33,7 @@ #include "common/ValueStringModel.hpp" #include "auth/IPasswordService.hpp" +#include "database/Session.hpp" #include "utils/IConfig.hpp" #include "utils/Logger.hpp" #include "utils/Service.hpp" diff --git a/src/lms/ui/admin/DatabaseSettingsView.cpp b/src/lms/ui/admin/DatabaseSettingsView.cpp index 34ed61a2..def6885f 100644 --- a/src/lms/ui/admin/DatabaseSettingsView.cpp +++ b/src/lms/ui/admin/DatabaseSettingsView.cpp @@ -28,6 +28,7 @@ #include "database/Cluster.hpp" #include "database/ScanSettings.hpp" +#include "database/Session.hpp" #include "utils/Logger.hpp" #include "utils/Service.hpp" #include "utils/String.hpp" diff --git a/src/lms/ui/admin/InitWizardView.cpp b/src/lms/ui/admin/InitWizardView.cpp index bbec809c..8f39e2b0 100644 --- a/src/lms/ui/admin/InitWizardView.cpp +++ b/src/lms/ui/admin/InitWizardView.cpp @@ -25,6 +25,8 @@ #include #include "auth/IPasswordService.hpp" +#include "database/Session.hpp" +#include "database/User.hpp" #include "utils/Exception.hpp" #include "utils/Logger.hpp" #include "utils/Service.hpp" diff --git a/src/lms/ui/admin/UserView.cpp b/src/lms/ui/admin/UserView.cpp index 63c118e3..859519d7 100644 --- a/src/lms/ui/admin/UserView.cpp +++ b/src/lms/ui/admin/UserView.cpp @@ -29,6 +29,7 @@ #include "auth/IPasswordService.hpp" #include "database/User.hpp" +#include "database/Session.hpp" #include "utils/IConfig.hpp" #include "utils/Exception.hpp" #include "utils/Logger.hpp" diff --git a/src/lms/ui/admin/UsersView.cpp b/src/lms/ui/admin/UsersView.cpp index 144a9703..af01413e 100644 --- a/src/lms/ui/admin/UsersView.cpp +++ b/src/lms/ui/admin/UsersView.cpp @@ -24,6 +24,7 @@ #include #include "database/User.hpp" +#include "database/Session.hpp" #include "utils/Logger.hpp" #include "LmsApplication.hpp" diff --git a/src/lms/ui/explore/ArtistView.cpp b/src/lms/ui/explore/ArtistView.cpp index 297250db..73487b64 100644 --- a/src/lms/ui/explore/ArtistView.cpp +++ b/src/lms/ui/explore/ArtistView.cpp @@ -27,6 +27,7 @@ #include "database/Artist.hpp" #include "database/Release.hpp" #include "database/ScanSettings.hpp" +#include "database/Session.hpp" #include "database/User.hpp" #include "recommendation/IEngine.hpp" #include "utils/Logger.hpp" diff --git a/src/lms/ui/explore/ArtistsView.cpp b/src/lms/ui/explore/ArtistsView.cpp index a37025dc..28752654 100644 --- a/src/lms/ui/explore/ArtistsView.cpp +++ b/src/lms/ui/explore/ArtistsView.cpp @@ -25,6 +25,7 @@ #include "common/ValueStringModel.hpp" #include "database/Artist.hpp" +#include "database/Session.hpp" #include "database/User.hpp" #include "database/TrackList.hpp" #include "utils/Logger.hpp" diff --git a/src/lms/ui/explore/Explore.cpp b/src/lms/ui/explore/Explore.cpp index 6e89977d..c7444bbd 100644 --- a/src/lms/ui/explore/Explore.cpp +++ b/src/lms/ui/explore/Explore.cpp @@ -25,6 +25,7 @@ #include "database/Artist.hpp" #include "database/Release.hpp" +#include "database/Session.hpp" #include "database/Track.hpp" #include "utils/Logger.hpp" diff --git a/src/lms/ui/explore/Filters.cpp b/src/lms/ui/explore/Filters.cpp index 62a08100..e893ba40 100644 --- a/src/lms/ui/explore/Filters.cpp +++ b/src/lms/ui/explore/Filters.cpp @@ -25,6 +25,7 @@ #include #include "database/Cluster.hpp" +#include "database/Session.hpp" #include "LmsApplication.hpp" diff --git a/src/lms/ui/explore/ReleasePopup.cpp b/src/lms/ui/explore/ReleasePopup.cpp index 0b57b6f0..79e64497 100644 --- a/src/lms/ui/explore/ReleasePopup.cpp +++ b/src/lms/ui/explore/ReleasePopup.cpp @@ -20,6 +20,7 @@ #include "ReleasePopup.hpp" #include "database/Release.hpp" +#include "database/Session.hpp" #include "database/User.hpp" #include "resource/DownloadResource.hpp" #include "LmsApplication.hpp" diff --git a/src/lms/ui/explore/ReleaseView.cpp b/src/lms/ui/explore/ReleaseView.cpp index 540072c2..5b514636 100644 --- a/src/lms/ui/explore/ReleaseView.cpp +++ b/src/lms/ui/explore/ReleaseView.cpp @@ -27,6 +27,7 @@ #include "database/Release.hpp" #include "database/ScanSettings.hpp" +#include "database/Session.hpp" #include "database/Track.hpp" #include "recommendation/IEngine.hpp" #include "utils/Logger.hpp" diff --git a/src/lms/ui/explore/ReleasesView.cpp b/src/lms/ui/explore/ReleasesView.cpp index 50ca4c02..afc4d3cf 100644 --- a/src/lms/ui/explore/ReleasesView.cpp +++ b/src/lms/ui/explore/ReleasesView.cpp @@ -27,8 +27,9 @@ #include #include "database/Release.hpp" -#include "database/User.hpp" +#include "database/Session.hpp" #include "database/TrackList.hpp" +#include "database/User.hpp" #include "utils/Logger.hpp" #include "utils/String.hpp" diff --git a/src/lms/ui/explore/SearchView.cpp b/src/lms/ui/explore/SearchView.cpp index bd8a8e09..e62c5aff 100644 --- a/src/lms/ui/explore/SearchView.cpp +++ b/src/lms/ui/explore/SearchView.cpp @@ -24,6 +24,7 @@ #include "database/Artist.hpp" #include "database/Release.hpp" +#include "database/Session.hpp" #include "database/Track.hpp" #include "resource/ImageResource.hpp" diff --git a/src/lms/ui/explore/TrackPopup.cpp b/src/lms/ui/explore/TrackPopup.cpp index bb1b92c6..f1943355 100644 --- a/src/lms/ui/explore/TrackPopup.cpp +++ b/src/lms/ui/explore/TrackPopup.cpp @@ -19,6 +19,7 @@ #include "TrackPopup.hpp" +#include "database/Session.hpp" #include "database/Track.hpp" #include "database/User.hpp" #include "resource/DownloadResource.hpp" diff --git a/src/lms/ui/explore/TracksView.cpp b/src/lms/ui/explore/TracksView.cpp index 743c6b38..909b0768 100644 --- a/src/lms/ui/explore/TracksView.cpp +++ b/src/lms/ui/explore/TracksView.cpp @@ -26,6 +26,7 @@ #include "database/Artist.hpp" #include "database/Release.hpp" +#include "database/Session.hpp" #include "database/Track.hpp" #include "database/TrackList.hpp" diff --git a/src/lms/ui/resource/AudioFileResource.cpp b/src/lms/ui/resource/AudioFileResource.cpp index 2a2fa5a6..7aaac02f 100644 --- a/src/lms/ui/resource/AudioFileResource.cpp +++ b/src/lms/ui/resource/AudioFileResource.cpp @@ -23,6 +23,7 @@ #include #include "av/AvInfo.hpp" +#include "database/Session.hpp" #include "database/Track.hpp" #include "utils/FileResourceHandlerCreator.hpp" #include "utils/Logger.hpp" @@ -48,9 +49,6 @@ static std::optional getTrackPathFromTrackId(Database::IdType trackId) { - // DbSession are not thread safe - Wt::WApplication::UpdateLock lock {LmsApp}; - auto transaction {LmsApp->getDbSession().createSharedTransaction()}; const Database::Track::pointer track {Database::Track::getById(LmsApp->getDbSession(), trackId)}; diff --git a/src/lms/ui/resource/AudioTranscodeResource.cpp b/src/lms/ui/resource/AudioTranscodeResource.cpp index 8288247c..16ffbf14 100644 --- a/src/lms/ui/resource/AudioTranscodeResource.cpp +++ b/src/lms/ui/resource/AudioTranscodeResource.cpp @@ -22,6 +22,7 @@ #include #include "av/AvTranscoder.hpp" +#include "database/Session.hpp" #include "database/Track.hpp" #include "database/User.hpp" #include "utils/Logger.hpp" @@ -148,8 +149,6 @@ AudioTranscodeResource::handleRequest(const Wt::Http::Request& request, std::filesystem::path trackPath; { - // DbSession are not thread safe - Wt::WApplication::UpdateLock lock(LmsApp); auto transaction {LmsApp->getDbSession().createSharedTransaction()}; const Database::Track::pointer track {Database::Track::getById(LmsApp->getDbSession(), *trackId)}; diff --git a/src/lms/ui/resource/DownloadResource.cpp b/src/lms/ui/resource/DownloadResource.cpp index cb028f02..6ba483ee 100644 --- a/src/lms/ui/resource/DownloadResource.cpp +++ b/src/lms/ui/resource/DownloadResource.cpp @@ -27,6 +27,7 @@ #include "database/Artist.hpp" #include "database/Release.hpp" +#include "database/Session.hpp" #include "database/Track.hpp" #include "utils/Exception.hpp" #include "utils/Logger.hpp" @@ -182,7 +183,6 @@ DownloadArtistResource::DownloadArtistResource(Database::IdType artistId) std::unique_ptr DownloadArtistResource::createZipper() { - Wt::WApplication::UpdateLock lock {LmsApp}; // DbSession are not thread safe auto transaction {LmsApp->getDbSession().createSharedTransaction()}; const Database::Artist::pointer artist {Database::Artist::getById(LmsApp->getDbSession(), _artistId)}; @@ -209,7 +209,6 @@ DownloadReleaseResource::DownloadReleaseResource(Database::IdType releaseId) std::unique_ptr DownloadReleaseResource::createZipper() { - Wt::WApplication::UpdateLock lock {LmsApp}; // DbSession are not thread safe auto transaction {LmsApp->getDbSession().createSharedTransaction()}; const Database::Release::pointer release {Database::Release::getById(LmsApp->getDbSession(), _releaseId)}; @@ -235,7 +234,6 @@ DownloadTrackResource::DownloadTrackResource(Database::IdType trackId) std::unique_ptr DownloadTrackResource::createZipper() { - Wt::WApplication::UpdateLock lock {LmsApp}; // DbSession are not thread safe auto transaction {LmsApp->getDbSession().createSharedTransaction()}; const Database::Track::pointer track {Database::Track::getById(LmsApp->getDbSession(), _trackId)}; diff --git a/src/lms/ui/resource/ImageResource.cpp b/src/lms/ui/resource/ImageResource.cpp index 06e71610..21edbb67 100644 --- a/src/lms/ui/resource/ImageResource.cpp +++ b/src/lms/ui/resource/ImageResource.cpp @@ -74,7 +74,7 @@ ImageResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Respons return; } - std::unique_ptr cover; + std::shared_ptr cover; if (trackIdStr) { @@ -87,11 +87,7 @@ ImageResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Respons return; } - // DbSession are not thread safe - { - Wt::WApplication::UpdateLock lock {LmsApp}; - cover = Service::get()->getFromTrack(LmsApp->getDbSession(), *trackId, *size); - } + cover = Service::get()->getFromTrack(LmsApp->getDbSession(), *trackId, *size); } else if (releaseIdStr) { @@ -101,11 +97,7 @@ ImageResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Respons if (!releaseId) return; - // DbSession are not thread safe - { - Wt::WApplication::UpdateLock lock {LmsApp}; - cover = Service::get()->getFromRelease(LmsApp->getDbSession(), *releaseId, *size); - } + cover = Service::get()->getFromRelease(LmsApp->getDbSession(), *releaseId, *size); } else { diff --git a/src/tools/CMakeLists.txt b/src/tools/CMakeLists.txt index 5c970f2a..08707a6a 100644 --- a/src/tools/CMakeLists.txt +++ b/src/tools/CMakeLists.txt @@ -1,6 +1,4 @@ - +add_subdirectory(cover) add_subdirectory(metadata) add_subdirectory(recommendation) add_subdirectory(zipper) - - diff --git a/src/tools/cover/CMakeLists.txt b/src/tools/cover/CMakeLists.txt new file mode 100644 index 00000000..141e2983 --- /dev/null +++ b/src/tools/cover/CMakeLists.txt @@ -0,0 +1,10 @@ + +add_executable(lms-cover + LmsCover.cpp + ) + +target_link_libraries(lms-cover PRIVATE + lmscover + Boost::program_options + ) + diff --git a/src/tools/cover/LmsCover.cpp b/src/tools/cover/LmsCover.cpp new file mode 100644 index 00000000..3a1eb30a --- /dev/null +++ b/src/tools/cover/LmsCover.cpp @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2020 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include +#include +#include +#include + +#include + +#include "cover/ICoverArtGrabber.hpp" +#include "database/Db.hpp" +#include "database/Release.hpp" +#include "database/Session.hpp" +#include "database/Track.hpp" +#include "utils/IConfig.hpp" +#include "utils/Logger.hpp" +#include "utils/Service.hpp" +#include "utils/StreamLogger.hpp" + +static +void +dumpTrackCovers(Database::Session& session, CoverArt::ImageSize width) +{ + std::vector trackIds; + { + auto transaction {session.createSharedTransaction()}; + trackIds = Database::Track::getAllIds(session); + } + + for (Database::IdType trackId : trackIds) + { + std::cout << "Getting cover for track id " << trackId << std::endl; + Service::get()->getFromTrack(session, trackId, width); + } +} + + +int main(int argc, char *argv[]) +{ + try + { + namespace po = boost::program_options; + + // log to stdout + Service logger {std::make_unique(std::cout)}; + + + po::options_description desc{"Allowed options"}; + desc.add_options() + ("help,h", "print usage message") + ("conf,c", po::value()->default_value("/etc/lms.conf"), "LMS config file") + ("default-cover,d", po::value(), "Default cover path") + ("tracks,t", "dump covers for tracks") + ("size,s", po::value()->default_value(512), "Requested cover size") + ("quality,q", po::value()->default_value(75), "JPEG quality (1-100)") + ; + + po::variables_map vm; + po::store(po::parse_command_line(argc, argv, desc), vm); + + if (vm.count("help")) + { + std::cout << desc << std::endl; + return EXIT_SUCCESS; + } + + Service config {createConfig(vm["conf"].as())}; + + Service coverArtService {CoverArt::createGrabber(argv[0], + vm["default-cover"].as(), + config->getULong("cover-max-cache-size", 30) * 1000 * 1000, + config->getULong("cover-max-file-size", 10) * 1000 * 1000, + config->getULong("cover-jpeg-quality", vm["quality"].as()) + )}; + + Database::Db db {config->getPath("working-dir") / "lms.db"}; + Database::Session session {db}; + + if (vm.count("tracks")) + dumpTrackCovers(session, vm["size"].as()); + } + catch( std::exception& e) + { + std::cerr << "Caught exception: " << e.what() << std::endl; + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} + From 5a6abdcba901c49df61120e6c4034f1fea27613e Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 17 Oct 2020 22:30:03 +0200 Subject: [PATCH 2/8] Made the number of threads used by the built in http server configurable. Default is as many cpu cores --- conf/lms.conf | 3 +++ src/lms/main.cpp | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/conf/lms.conf b/conf/lms.conf index 1f883e1d..471bdaa2 100644 --- a/conf/lms.conf +++ b/conf/lms.conf @@ -31,6 +31,9 @@ approot = "/usr/share/lms/approot"; # Location for deployment (See README if you want to deploy on a non root path) deploy-path = "/"; +# Number of threads to be used to dispatch http requests (0 means auto detect) +http-server-thread-count = 0; + # Acoustic brainz's root API acousticbrainz-api-url = "https://acousticbrainz.org/api/v1/"; diff --git a/src/lms/main.cpp b/src/lms/main.cpp index b52ab3fb..917fc7f8 100644 --- a/src/lms/main.cpp +++ b/src/lms/main.cpp @@ -17,6 +17,8 @@ * along with LMS. If not, see . */ +#include + #include #include @@ -47,6 +49,7 @@ generateWtConfig(std::string execPath) const std::filesystem::path wtLogFilePath {Service::get()->getPath("log-file", "/var/log/lms.log")}; const std::filesystem::path wtAccessLogFilePath {Service::get()->getPath("access-log-file", "/var/log/lms.access.log")}; const std::filesystem::path wtResourcesPath {Service::get()->getPath("wt-resources", "/usr/share/Wt/resources")}; + const unsigned long configHttpServerThreadCount {Service::get()->getULong("http-server-thread-count", 0)}; args.push_back(execPath); args.push_back("--config=" + wtConfigPath.string()); @@ -81,6 +84,11 @@ generateWtConfig(std::string execPath) pt.put("server.application-settings.log-config", Service::get()->getString("log-config", "* -debug -info:WebRequest")); pt.put("server.application-settings.behind-reverse-proxy", Service::get()->getBool("behind-reverse-proxy", false)); + { + const unsigned long httpServerThreadCount {configHttpServerThreadCount ? configHttpServerThreadCount : std::max(1, std::thread::hardware_concurrency())}; + pt.put("server.application-settings.num-threads", httpServerThreadCount); + } + { boost::property_tree::ptree viewport; viewport.put(".name", "viewport"); From 4bd5344b7d86853934169d677221027f8e8c5c00 Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 17 Oct 2020 22:41:15 +0200 Subject: [PATCH 3/8] Rather use the server args for setting its thread count --- src/lms/main.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lms/main.cpp b/src/lms/main.cpp index 917fc7f8..11649676 100644 --- a/src/lms/main.cpp +++ b/src/lms/main.cpp @@ -76,6 +76,11 @@ generateWtConfig(std::string execPath) if (!wtAccessLogFilePath.empty()) args.push_back("--accesslog=" + wtAccessLogFilePath.string()); + { + const unsigned long httpServerThreadCount {configHttpServerThreadCount ? configHttpServerThreadCount : std::max(1, std::thread::hardware_concurrency())}; + args.push_back("--threads=" + std::to_string(httpServerThreadCount)); + } + // Generate the wt_config.xml file boost::property_tree::ptree pt; @@ -84,11 +89,6 @@ generateWtConfig(std::string execPath) pt.put("server.application-settings.log-config", Service::get()->getString("log-config", "* -debug -info:WebRequest")); pt.put("server.application-settings.behind-reverse-proxy", Service::get()->getBool("behind-reverse-proxy", false)); - { - const unsigned long httpServerThreadCount {configHttpServerThreadCount ? configHttpServerThreadCount : std::max(1, std::thread::hardware_concurrency())}; - pt.put("server.application-settings.num-threads", httpServerThreadCount); - } - { boost::property_tree::ptree viewport; viewport.put(".name", "viewport"); From e4dda516ee3b9ca31cab62a50646976c771ad9c2 Mon Sep 17 00:00:00 2001 From: emeric Date: Mon, 19 Oct 2020 21:42:34 +0200 Subject: [PATCH 4/8] Added virtual destructor --- src/libs/cover/impl/IRawImage.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/cover/impl/IRawImage.hpp b/src/libs/cover/impl/IRawImage.hpp index 148d2178..6ff9e6d1 100644 --- a/src/libs/cover/impl/IRawImage.hpp +++ b/src/libs/cover/impl/IRawImage.hpp @@ -28,6 +28,7 @@ namespace CoverArt class IRawImage { public: + virtual ~IRawImage() = default; virtual void resize(ImageSize width) = 0; virtual std::unique_ptr encodeToJPEG(unsigned quality) const = 0; }; From edc16f6d5e3e9b1d63daf11a9af941fc67bc456c Mon Sep 17 00:00:00 2001 From: Bram Date: Wed, 28 Oct 2020 12:22:53 +0100 Subject: [PATCH 5/8] Fixed several compilation errors --- src/libs/utils/impl/Random.cpp | 2 +- src/libs/utils/include/utils/Random.hpp | 2 +- src/lms/ui/explore/ArtistsView.hpp | 1 + src/lms/ui/explore/ReleasesView.hpp | 1 + src/lms/ui/explore/TracksView.hpp | 1 + src/lms/ui/resource/DownloadResource.cpp | 1 + src/tools/zipper/LmsZipper.cpp | 2 ++ 7 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/libs/utils/impl/Random.cpp b/src/libs/utils/impl/Random.cpp index 5cf67add..12ef2863 100644 --- a/src/libs/utils/impl/Random.cpp +++ b/src/libs/utils/impl/Random.cpp @@ -29,7 +29,7 @@ RandGenerator& getRandGenerator() return randGenerator; } -RandGenerator createSeededGenerator(std::size_t seed) +RandGenerator createSeededGenerator(uint_fast32_t seed) { return RandGenerator {seed}; } diff --git a/src/libs/utils/include/utils/Random.hpp b/src/libs/utils/include/utils/Random.hpp index 5469d1e6..b69505c7 100644 --- a/src/libs/utils/include/utils/Random.hpp +++ b/src/libs/utils/include/utils/Random.hpp @@ -27,7 +27,7 @@ namespace Random { using RandGenerator = std::mt19937; RandGenerator& getRandGenerator(); -RandGenerator createSeededGenerator(std::size_t seed); +RandGenerator createSeededGenerator(uint_fast32_t seed); template T diff --git a/src/lms/ui/explore/ArtistsView.hpp b/src/lms/ui/explore/ArtistsView.hpp index 1a819e77..f1614392 100644 --- a/src/lms/ui/explore/ArtistsView.hpp +++ b/src/lms/ui/explore/ArtistsView.hpp @@ -20,6 +20,7 @@ #pragma once #include +#include #include #include diff --git a/src/lms/ui/explore/ReleasesView.hpp b/src/lms/ui/explore/ReleasesView.hpp index 08aae0d8..21e8e8ea 100644 --- a/src/lms/ui/explore/ReleasesView.hpp +++ b/src/lms/ui/explore/ReleasesView.hpp @@ -20,6 +20,7 @@ #pragma once #include +#include #include #include diff --git a/src/lms/ui/explore/TracksView.hpp b/src/lms/ui/explore/TracksView.hpp index 3da1d4e6..13cce32d 100644 --- a/src/lms/ui/explore/TracksView.hpp +++ b/src/lms/ui/explore/TracksView.hpp @@ -20,6 +20,7 @@ #pragma once #include +#include #include #include diff --git a/src/lms/ui/resource/DownloadResource.cpp b/src/lms/ui/resource/DownloadResource.cpp index 6ba483ee..c2253746 100644 --- a/src/lms/ui/resource/DownloadResource.cpp +++ b/src/lms/ui/resource/DownloadResource.cpp @@ -19,6 +19,7 @@ #include "DownloadResource.hpp" +#include #include #include diff --git a/src/tools/zipper/LmsZipper.cpp b/src/tools/zipper/LmsZipper.cpp index ba37e170..05e3fbc5 100644 --- a/src/tools/zipper/LmsZipper.cpp +++ b/src/tools/zipper/LmsZipper.cpp @@ -18,6 +18,8 @@ */ #include + +#include #include #include From 438e99f5c2598f1a17233ebcf67c841077b17bfb Mon Sep 17 00:00:00 2001 From: emeric Date: Wed, 28 Oct 2020 17:40:15 +0100 Subject: [PATCH 6/8] Loading of the recommendation engine now controlled by scanner. Bonus: better control/reporting --- approot/messages.xml | 5 +- approot/messages_fr.xml | 11 +- src/libs/recommendation/CMakeLists.txt | 2 + .../ClustersClassifierCreator.hpp | 0 src/libs/recommendation/impl/Engine.cpp | 295 +++++++++--------- src/libs/recommendation/impl/Engine.hpp | 84 ++--- .../FeaturesClassifierCreator.hpp | 2 +- .../recommendation => impl}/IClassifier.hpp | 20 +- .../impl/clusters/ClustersClassifier.cpp | 37 ++- .../impl/clusters/ClustersClassifier.hpp | 16 +- .../impl/features/FeaturesClassifier.cpp | 127 ++++---- .../impl/features/FeaturesClassifier.hpp | 26 +- .../include/recommendation/IEngine.hpp | 24 +- src/libs/scanner/CMakeLists.txt | 1 + src/libs/scanner/impl/MediaScanner.cpp | 76 +++-- src/libs/scanner/impl/MediaScanner.hpp | 11 +- src/libs/scanner/impl/MediaScannerStats.cpp | 2 +- .../scanner/include/scanner/IMediaScanner.hpp | 8 +- .../include/scanner/MediaScannerStats.hpp | 7 +- src/libs/subsonic/impl/Scan.cpp | 2 +- src/libs/utils/include/utils/Semaphore.hpp | 58 ---- src/libs/utils/include/utils/Service.hpp | 5 + src/lms/main.cpp | 20 +- src/lms/ui/PlayQueue.cpp | 5 +- src/lms/ui/admin/DatabaseSettingsView.cpp | 31 +- src/lms/ui/admin/ScannerController.cpp | 14 +- src/lms/ui/explore/ArtistView.cpp | 6 +- src/lms/ui/explore/ArtistView.hpp | 3 +- src/lms/ui/explore/ReleaseView.cpp | 8 +- src/lms/ui/explore/ReleaseView.hpp | 3 +- .../recommendation/LmsRecommendation.cpp | 14 +- 31 files changed, 454 insertions(+), 469 deletions(-) rename src/libs/recommendation/{include/recommendation => impl}/ClustersClassifierCreator.hpp (100%) rename src/libs/recommendation/{include/recommendation => impl}/FeaturesClassifierCreator.hpp (95%) rename src/libs/recommendation/{include/recommendation => impl}/IClassifier.hpp (50%) delete mode 100644 src/libs/utils/include/utils/Semaphore.hpp diff --git a/approot/messages.xml b/approot/messages.xml index dad967c1..2bbb4907 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -59,7 +59,7 @@ Cannot read file {1} duplicate files: {1} errors: -Force scan now +Force full rescan now Get report Last scan Not available @@ -73,9 +73,10 @@ Not scheduled Scheduled on {1} Scanning: step {1}/{2} -Checking for missing files... {1}% +Checking files... {1}% Discovering files: {1} files Fetching track features from AcousticBrainz: {1}/{2} tracks ({3}%)... +Reloading similarity engine: {1}%... Scanning files: {1}/{2} files ({3}%)... diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml index 955938ae..0b66924a 100644 --- a/approot/messages_fr.xml +++ b/approot/messages_fr.xml @@ -59,7 +59,7 @@ Impossible de lire le fichier {1} fichiers dupliqués : {1} erreurs : -Lancer un scan forcé +Forcer un rescan complet Rapport Dernier scan Non disponible @@ -73,10 +73,11 @@ Non planifié Planifié le {1} En cours de scan : étape {1}/{2} -Vérification des fichiers supprimés... {1}% -Découverte des fichiers: {1} fichiers -Récupération des métadonnées AcousticBrainz: {1}/{2} fichiers ({3}%)... -Scan des fichiers: {1}/{2} fichiers ({3}%)... +Vérification des fichiers... {1}% +Découverte des fichiers : {1} fichiers +Récupération des métadonnées AcousticBrainz : {1}/{2} fichiers ({3}%)... +Rechargement du moteur de recommandation : {1}%... +Scan des fichiers : {1}/{2} fichiers ({3}%)... Ajouter diff --git a/src/libs/recommendation/CMakeLists.txt b/src/libs/recommendation/CMakeLists.txt index 35243f50..b33e36c3 100644 --- a/src/libs/recommendation/CMakeLists.txt +++ b/src/libs/recommendation/CMakeLists.txt @@ -22,5 +22,7 @@ target_link_libraries(lmsrecommendation PRIVATE wt ) +target_include_directories(lmsrecommendation PRIVATE impl) + install(TARGETS lmsrecommendation DESTINATION lib) diff --git a/src/libs/recommendation/include/recommendation/ClustersClassifierCreator.hpp b/src/libs/recommendation/impl/ClustersClassifierCreator.hpp similarity index 100% rename from src/libs/recommendation/include/recommendation/ClustersClassifierCreator.hpp rename to src/libs/recommendation/impl/ClustersClassifierCreator.hpp diff --git a/src/libs/recommendation/impl/Engine.cpp b/src/libs/recommendation/impl/Engine.cpp index 26caf31b..397a7cda 100644 --- a/src/libs/recommendation/impl/Engine.cpp +++ b/src/libs/recommendation/impl/Engine.cpp @@ -19,16 +19,39 @@ #include "Engine.hpp" -#include "recommendation/ClustersClassifierCreator.hpp" -#include "recommendation/FeaturesClassifierCreator.hpp" +#include +#include +#include "ClustersClassifierCreator.hpp" +#include "FeaturesClassifierCreator.hpp" + +#include "database/Db.hpp" +#include "database/Session.hpp" #include "database/ScanSettings.hpp" -#include "database/TrackList.hpp" #include "utils/Exception.hpp" #include "utils/Logger.hpp" namespace Recommendation { + +static +std::unique_ptr +createClassifier(ClassifierType type) +{ + switch (type) + { + case ClassifierType::Clusters: + return createClustersClassifier(); + break; + + case ClassifierType::Features: + return createFeaturesClassifier(); + break; + } + + return {}; +} + std::unique_ptr createEngine(Database::Db& db) { @@ -36,65 +59,16 @@ createEngine(Database::Db& db) } Engine::Engine(Database::Db& db) -: _dbSession {db} +: _db {db} { - start(); } -Engine::~Engine() -{ - stop(); -} - -void -Engine::start() -{ - assert(!_running); - _running = true; - _ioService.start(); -} - -void -Engine::stop() -{ - assert(_running); - _running = false; - - cancelPendingClassifiers(); - - _ioService.stop(); -} - -void -Engine::requestLoad() -{ - requestReloadInternal(false); -} - -void -Engine::requestReload() -{ - requestReloadInternal(true); -} - -void -Engine::requestReloadInternal(bool databaseChanged) -{ - LMS_LOG(RECOMMENDATION, DEBUG) << "Reload requested..."; - - _ioService.post([=]() - { - reload(databaseChanged); - }); -} - -std::vector +std::unordered_set Engine::getSimilarTracksFromTrackList(Database::Session& session, Database::IdType trackListId, std::size_t maxCount) { + std::unordered_set res; + std::shared_lock lock {_classifiersMutex}; - - std::vector res; - for (const auto& classifierName : _classifierPriorities) { auto itClassifier {_classifiers.find(classifierName)}; @@ -109,23 +83,23 @@ Engine::getSimilarTracksFromTrackList(Database::Session& session, Database::IdTy return res; } -std::vector +std::unordered_set Engine::getSimilarTracks(Database::Session& dbSession, const std::unordered_set& trackIds, std::size_t maxCount) { + std::unordered_set res; + std::shared_lock lock {_classifiersMutex}; - - std::vector res; - - for (const auto& classifierName : _classifierPriorities) + for (ClassifierType classifierType : _classifierPriorities) { - auto itClassifier {_classifiers.find(classifierName)}; + auto itClassifier {_classifiers.find(classifierType)}; if (itClassifier == std::cend(_classifiers)) continue; - res = itClassifier->second->getSimilarTracks(dbSession, trackIds, maxCount); + const IClassifier& classifier {*itClassifier->second}; + res = classifier.getSimilarTracks(dbSession, trackIds, maxCount); if (!res.empty()) { - LMS_LOG(RECOMMENDATION, DEBUG) << "Got " << res.size() << " similar tracks using classifier '" << classifierName << "'"; + LMS_LOG(RECOMMENDATION, DEBUG) << "Got " << res.size() << " similar tracks using classifier '" << classifier.getName() << "'"; break; } } @@ -133,23 +107,23 @@ Engine::getSimilarTracks(Database::Session& dbSession, const std::unordered_set< return res; } -std::vector +std::unordered_set Engine::getSimilarReleases(Database::Session& dbSession, Database::IdType releaseId, std::size_t maxCount) { + std::unordered_set res; + std::shared_lock lock {_classifiersMutex}; - - std::vector res; - - for (const auto& classifierName : _classifierPriorities) + for (ClassifierType classifierType : _classifierPriorities) { - auto itClassifier {_classifiers.find(classifierName)}; + auto itClassifier {_classifiers.find(classifierType)}; if (itClassifier == std::cend(_classifiers)) continue; - res = itClassifier->second->getSimilarReleases(dbSession, releaseId, maxCount); + const IClassifier& classifier {*itClassifier->second}; + res = classifier.getSimilarReleases(dbSession, releaseId, maxCount); if (!res.empty()) { - LMS_LOG(RECOMMENDATION, DEBUG) << "Got " << res.size() << " similar releases using classifier '" << classifierName << "'"; + LMS_LOG(RECOMMENDATION, DEBUG) << "Got " << res.size() << " similar releases using classifier '" << classifier.getName() << "'"; break; } } @@ -157,23 +131,23 @@ Engine::getSimilarReleases(Database::Session& dbSession, Database::IdType releas return res; } -std::vector +std::unordered_set Engine::getSimilarArtists(Database::Session& dbSession, Database::IdType artistId, std::size_t maxCount) { + std::unordered_set res; + std::shared_lock lock {_classifiersMutex}; - - std::vector res; - - for (const auto& classifierName : _classifierPriorities) + for (ClassifierType classifierType : _classifierPriorities) { - auto itClassifier {_classifiers.find(classifierName)}; + auto itClassifier {_classifiers.find(classifierType)}; if (itClassifier == std::cend(_classifiers)) continue; - res = itClassifier->second->getSimilarArtists(dbSession, artistId, maxCount); + const IClassifier& classifier {*itClassifier->second}; + res = classifier.getSimilarArtists(dbSession, artistId, maxCount); if (!res.empty()) { - LMS_LOG(RECOMMENDATION, DEBUG) << "Got " << res.size() << " similar artists using classifier '" << classifierName << "'"; + LMS_LOG(RECOMMENDATION, DEBUG) << "Got " << res.size() << " similar artists using classifier '" << classifier.getName() << "'"; return res; } } @@ -181,107 +155,132 @@ Engine::getSimilarArtists(Database::Session& dbSession, Database::IdType artistI return res; } -void -Engine::reload(bool databaseChanged) +static +Database::ScanSettings::RecommendationEngineType +getRecommendationEngineType(Database::Session& session) { - using namespace Database; + auto transaction {session.createSharedTransaction()}; - LMS_LOG(RECOMMENDATION, INFO) << "Reloading recommendation engines..."; - - const ScanSettings::RecommendationEngineType engineType {[&]() - { - auto transaction {_dbSession.createSharedTransaction()}; - - return ScanSettings::get(_dbSession)->getRecommendationEngineType(); - }()}; - - clearClassifiers(); - - switch (engineType) - { - case ScanSettings::RecommendationEngineType::Features: - { - auto clustersClassifier {createClustersClassifier()}; - auto featuresClassifier {createFeaturesClassifier()}; - - setClassifierPriorities({featuresClassifier->getName(), clustersClassifier->getName()}); - - initAndAddClassifier(std::move(clustersClassifier), databaseChanged); // init first since faster - initAndAddClassifier(std::move(featuresClassifier), databaseChanged); - break; - } - - case ScanSettings::RecommendationEngineType::Clusters: - auto clustersClassifier {createClustersClassifier()}; - - setClassifierPriorities({clustersClassifier->getName()}); - - initAndAddClassifier(std::move(clustersClassifier), databaseChanged); - break; - } - - LMS_LOG(RECOMMENDATION, INFO) << "Recommendation engines reloaded!"; - - _sigReloaded.emit(); + return Database::ScanSettings::get(session)->getRecommendationEngineType(); } void -Engine::setClassifierPriorities(std::initializer_list classifierPriorities) +Engine::load(bool forceReload, const ProgressCallback& progressCallback) +{ + using namespace Database; + + static const std::unordered_map> classifierMappings + { + {ScanSettings::RecommendationEngineType::Features, {ClassifierType::Clusters, ClassifierType::Features}}, + {ScanSettings::RecommendationEngineType::Clusters, {ClassifierType::Clusters}}, + }; + + LMS_LOG(RECOMMENDATION, INFO) << "Reloading recommendation engines..."; + + const ScanSettings::RecommendationEngineType engineType {getRecommendationEngineType(_db.getTLSSession())}; + + assert(_pendingClassifiers.empty()); + clearClassifiers(); + + auto itClassifierTypes {classifierMappings.find(engineType)}; + assert(itClassifierTypes != std::cend(classifierMappings)); + const std::vector& classifierTypes {itClassifierTypes->second}; + + setClassifierPriorities(classifierTypes); + + std::vector> classifiers; + for (ClassifierType type : classifierTypes) + classifiers.emplace_back(createClassifier(type)); + + { + std::scoped_lock lock {_controlMutex}; + + std::transform(std::cbegin(classifiers), std::cend(classifiers), std::inserter(_pendingClassifiers, std::end(_pendingClassifiers)), + [](auto& classifier) { return classifier.get(); }); + } + + for (std::size_t i {}; i < classifiers.size(); ++i) + loadClassifier(std::move(classifiers[i]), classifierTypes[i], forceReload, progressCallback); + + LMS_LOG(RECOMMENDATION, INFO) << "Recommendation engines loaded!"; +} + +void +Engine::setClassifierPriorities(const std::vector& classifierPriorities) { std::unique_lock lock {_classifiersMutex}; - _classifierPriorities.clear(); - std::transform(std::cbegin(classifierPriorities), std::cend(classifierPriorities), std::back_inserter(_classifierPriorities), [](std::string_view name) { return std::string {name}; }); + _classifierPriorities = classifierPriorities; } void Engine::clearClassifiers() { - std::unique_lock lock {_classifiersMutex}; + std::unique_lock lock {_classifiersMutex}; _classifiers.clear(); } void -Engine::initAndAddClassifier(std::unique_ptr classifier, bool databaseChanged) +Engine::loadClassifier(std::unique_ptr classifier, + ClassifierType classifierType, + bool forceReload, + const ProgressCallback& progressCallback) { - PendingClassifierHandler pendingClassifier {*this, *classifier.get()}; + IClassifier* rawClassifier {classifier.get()}; - LMS_LOG(RECOMMENDATION, INFO) << "Initializing classifier '" << classifier->getName() << "'..."; - bool res {classifier->init(_dbSession, databaseChanged)}; - LMS_LOG(RECOMMENDATION, INFO) << "Initializing classifier '" << classifier->getName() << "': " << (res ? "SUCCESS" : "FAILURE"); + bool res {}; + if (!_loadCancelled) + { + LMS_LOG(RECOMMENDATION, INFO) << "Initializing classifier '" << classifier->getName() << "'..."; + + auto progress {[&](IClassifier::Progress progress) + { + progressCallback(Progress {progress.processedElems, progress.totalElems}); + }}; + + res = classifier->load(_db.getTLSSession(), forceReload, progressCallback ? progress : IClassifier::ProgressCallback {}); + + LMS_LOG(RECOMMENDATION, INFO) << "Initializing classifier '" << classifier->getName() << "': " << (res ? "SUCCESS" : "FAILURE"); + } if (res) { - std::unique_lock lock {_classifiersMutex}; + std::unique_lock lock {_classifiersMutex}; - _classifiers.emplace(classifier->getName(), std::move(classifier)); + _classifiers.emplace(classifierType, std::move(classifier)); } + + { + std::scoped_lock lock {_controlMutex}; + + LMS_LOG(RECOMMENDATION, DEBUG) << "About to erase. _pendingClassifiers size = " << _pendingClassifiers.size(); + _pendingClassifiers.erase(rawClassifier); + LMS_LOG(RECOMMENDATION, DEBUG) << "Erased. _pendingClassifiers size = " << _pendingClassifiers.size(); + } + + _pendingClassifiersCondvar.notify_one(); + } void -Engine::cancelPendingClassifiers() +Engine::cancelLoad() { - std::unique_lock lock {_classifiersMutex}; + LMS_LOG(RECOMMENDATION, DEBUG) << "Cancelling loading..."; + + std::unique_lock lock {_controlMutex}; + + LMS_LOG(RECOMMENDATION, DEBUG) << "Still " << _pendingClassifiers.size() << " pending classifiers!"; + + _loadCancelled = true; for (IClassifier* classifier : _pendingClassifiers) - classifier->requestCancelInit(); -} + classifier->requestCancelLoad(); -void -Engine::addPendingClassifier(IClassifier& classifier) -{ - std::unique_lock lock {_classifiersMutex}; + _pendingClassifiersCondvar.wait(lock, [this] {return _pendingClassifiers.empty();}); + _loadCancelled = false; - _pendingClassifiers.insert(&classifier); -} - -void -Engine::removePendingClassifier(IClassifier& classifier) -{ - std::unique_lock lock {_classifiersMutex}; - - _pendingClassifiers.erase(&classifier); + LMS_LOG(RECOMMENDATION, DEBUG) << "Cancelling loading DONE"; } } // ns Similarity diff --git a/src/libs/recommendation/impl/Engine.hpp b/src/libs/recommendation/impl/Engine.hpp index fd2f91fb..dc516863 100644 --- a/src/libs/recommendation/impl/Engine.hpp +++ b/src/libs/recommendation/impl/Engine.hpp @@ -19,23 +19,33 @@ #pragma once -#include +#include #include +#include +#include #include -#include - -#include "database/Session.hpp" #include "recommendation/IEngine.hpp" -#include "recommendation/IClassifier.hpp" +#include "IClassifier.hpp" + +namespace Database +{ + class Db; +} namespace Recommendation { + enum class ClassifierType + { + Clusters, + Features, + }; + class Engine : public IEngine { public: Engine(Database::Db& db); - ~Engine(); + ~Engine() = default; Engine(const Engine&) = delete; Engine(Engine&&) = delete; @@ -43,58 +53,30 @@ namespace Recommendation Engine& operator=(Engine&&) = delete; private: + void load(bool forceReload, const ProgressCallback& progressCallback) override; + void cancelLoad() override; - void start(); - void stop(); + std::unordered_set getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) override; + std::unordered_set getSimilarTracks(Database::Session& session, const std::unordered_set& tracksId, std::size_t maxCount) override; + std::unordered_set getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) override; + std::unordered_set getSimilarArtists(Database::Session& session, Database::IdType artistId, std::size_t maxCount) override; - void requestLoad() override; - void requestReload() override; - Wt::Signal<>& reloaded() override { return _sigReloaded; } - - std::vector getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) override; - std::vector getSimilarTracks(Database::Session& session, const std::unordered_set& tracksId, std::size_t maxCount) override; - std::vector getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) override; - std::vector getSimilarArtists(Database::Session& session, Database::IdType artistId, std::size_t maxCount) override; - - - void requestReloadInternal(bool databaseChanged); - void reload(bool databaseChanged); - - void setClassifierPriorities(std::initializer_list classifierNames); + void setClassifierPriorities(const std::vector& classifierTypes); void clearClassifiers(); - void initAndAddClassifier(std::unique_ptr classifier, bool databaseChanged); + void loadClassifier(std::unique_ptr classifier, ClassifierType classifierType, bool forceReload, const ProgressCallback& progressCallback); - class PendingClassifierHandler - { - public: - PendingClassifierHandler(Engine& engine, IClassifier& classifier) : _engine {engine}, _classifier {classifier} - { - _engine.addPendingClassifier(_classifier); - } + Database::Db& _db; - ~PendingClassifierHandler() - { - _engine.removePendingClassifier(_classifier); - } + std::mutex _controlMutex; + bool _loadCancelled {}; + std::condition_variable _pendingClassifiersCondvar; + std::unordered_set _pendingClassifiers; - private: - Engine& _engine; - IClassifier& _classifier; - }; + std::shared_mutex _classifiersMutex; + using ClassifierContainer = std::unordered_map>; + ClassifierContainer _classifiers; + std::vector _classifierPriorities; // ordered by priority - void cancelPendingClassifiers(); - void addPendingClassifier(IClassifier& classifier); - void removePendingClassifier(IClassifier& classifier); - - bool _running {}; - Wt::WIOService _ioService; - Database::Session _dbSession; - Wt::Signal<> _sigReloaded; - - std::shared_mutex _classifiersMutex; - std::map> _classifiers; - std::vector _classifierPriorities; // ordered by priority - std::unordered_set _pendingClassifiers; }; } // ns Recommendation diff --git a/src/libs/recommendation/include/recommendation/FeaturesClassifierCreator.hpp b/src/libs/recommendation/impl/FeaturesClassifierCreator.hpp similarity index 95% rename from src/libs/recommendation/include/recommendation/FeaturesClassifierCreator.hpp rename to src/libs/recommendation/impl/FeaturesClassifierCreator.hpp index 28a079d7..7f754768 100644 --- a/src/libs/recommendation/include/recommendation/FeaturesClassifierCreator.hpp +++ b/src/libs/recommendation/impl/FeaturesClassifierCreator.hpp @@ -20,7 +20,7 @@ #pragma once #include -#include "recommendation/IClassifier.hpp" +#include "IClassifier.hpp" namespace Recommendation { diff --git a/src/libs/recommendation/include/recommendation/IClassifier.hpp b/src/libs/recommendation/impl/IClassifier.hpp similarity index 50% rename from src/libs/recommendation/include/recommendation/IClassifier.hpp rename to src/libs/recommendation/impl/IClassifier.hpp index 44fa47d5..4c53df48 100644 --- a/src/libs/recommendation/include/recommendation/IClassifier.hpp +++ b/src/libs/recommendation/impl/IClassifier.hpp @@ -19,9 +19,9 @@ #pragma once +#include #include #include -#include #include "database/Types.hpp" @@ -40,13 +40,19 @@ namespace Recommendation virtual std::string_view getName() const = 0; - virtual bool init(Database::Session& session, bool databaseChanged) = 0; - virtual void requestCancelInit() = 0; + struct Progress + { + std::size_t totalElems {}; + std::size_t processedElems {}; + }; + using ProgressCallback = std::function; + virtual bool load(Database::Session& session, bool forceReload, const ProgressCallback& progressCallback) = 0; + virtual void requestCancelLoad() = 0; - virtual std::vector getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) const = 0; - virtual std::vector getSimilarTracks(Database::Session& session, const std::unordered_set& tracksId, std::size_t maxCount) const = 0; - virtual std::vector getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) const = 0; - virtual std::vector getSimilarArtists(Database::Session& session, Database::IdType artistId, std::size_t maxCount) const = 0; + virtual std::unordered_set getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) const = 0; + virtual std::unordered_set getSimilarTracks(Database::Session& session, const std::unordered_set& tracksId, std::size_t maxCount) const = 0; + virtual std::unordered_set getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) const = 0; + virtual std::unordered_set getSimilarArtists(Database::Session& session, Database::IdType artistId, std::size_t maxCount) const = 0; }; } // ns Recommendation diff --git a/src/libs/recommendation/impl/clusters/ClustersClassifier.cpp b/src/libs/recommendation/impl/clusters/ClustersClassifier.cpp index b433d833..c03f517e 100644 --- a/src/libs/recommendation/impl/clusters/ClustersClassifier.cpp +++ b/src/libs/recommendation/impl/clusters/ClustersClassifier.cpp @@ -27,29 +27,29 @@ #include "database/TrackList.hpp" namespace Recommendation { - + std::unique_ptr createClustersClassifier() { return std::make_unique(); } -std::vector +std::unordered_set ClusterClassifier::getSimilarTracks(Database::Session& dbSession, const std::unordered_set& trackIds, std::size_t maxCount) const { auto transaction {dbSession.createSharedTransaction()}; - auto tracks {Database::Track::getSimilarTracks(dbSession, trackIds, 0, maxCount)}; - std::vector res; - res.reserve(tracks.size()); + const auto tracks {Database::Track::getSimilarTracks(dbSession, trackIds, 0, maxCount)}; - std::transform(std::cbegin(tracks), std::cend(tracks), std::back_inserter(res), [](const auto& track) { return track.id(); }); + std::unordered_set res; + std::transform(std::cbegin(tracks), std::cend(tracks), std::inserter(res, std::end(res)), + [](const auto& track) { return track.id(); }); return res; } -std::vector +std::unordered_set ClusterClassifier::getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) const { - std::vector res; + std::unordered_set res; auto transaction {session.createSharedTransaction()}; @@ -57,18 +57,17 @@ ClusterClassifier::getSimilarTracksFromTrackList(Database::Session& session, Dat if (!trackList) return res; - const std::vector tracks {trackList->getSimilarTracks(0, maxCount)}; - res.reserve(tracks.size()); - std::transform(std::cbegin(tracks), std::cend(tracks), std::back_inserter(res), + const auto tracks {trackList->getSimilarTracks(0, maxCount)}; + std::transform(std::cbegin(tracks), std::cend(tracks), std::inserter(res, std::end(res)), [](const Database::Track::pointer& track) { return track.id(); }); return res; } -std::vector +std::unordered_set ClusterClassifier::getSimilarReleases(Database::Session& dbSession, Database::IdType releaseId, std::size_t maxCount) const { - std::vector res; + std::unordered_set res; auto transaction {dbSession.createSharedTransaction()}; @@ -77,16 +76,16 @@ ClusterClassifier::getSimilarReleases(Database::Session& dbSession, Database::Id return res; const auto releases {release->getSimilarReleases(0, maxCount)}; - res.reserve(releases.size()); - std::transform(std::cbegin(releases), std::cend(releases), std::back_inserter(res), [](const auto& release) { return release.id(); }); + std::transform(std::cbegin(releases), std::cend(releases), std::inserter(res, std::end(res)), + [](const auto& release) { return release.id(); }); return res; } -std::vector +std::unordered_set ClusterClassifier::getSimilarArtists(Database::Session& dbSession, Database::IdType artistId, std::size_t maxCount) const { - std::vector res; + std::unordered_set res; auto transaction {dbSession.createSharedTransaction()}; @@ -95,8 +94,8 @@ ClusterClassifier::getSimilarArtists(Database::Session& dbSession, Database::IdT return res; const auto artists {artist->getSimilarArtists(0, maxCount)}; - res.reserve(artists.size()); - std::transform(std::cbegin(artists), std::cend(artists), std::back_inserter(res), [](const auto& artist) { return artist.id(); }); + std::transform(std::cbegin(artists), std::cend(artists), std::inserter(res, std::end(res)), + [](const auto& artist) { return artist.id(); }); return res; } diff --git a/src/libs/recommendation/impl/clusters/ClustersClassifier.hpp b/src/libs/recommendation/impl/clusters/ClustersClassifier.hpp index 53a7bd5f..7f96a828 100644 --- a/src/libs/recommendation/impl/clusters/ClustersClassifier.hpp +++ b/src/libs/recommendation/impl/clusters/ClustersClassifier.hpp @@ -19,8 +19,7 @@ #pragma once -#include "recommendation/IClassifier.hpp" - +#include "IClassifier.hpp" namespace Recommendation { @@ -38,14 +37,13 @@ namespace Recommendation std::string_view getName() const override { return "Clusters"; } - bool init(Database::Session&, bool) override {return true;} - 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 override; - std::vector getSimilarArtists(Database::Session& session, Database::IdType artistId, std::size_t maxCount) const override; + bool load(Database::Session&, bool, const ProgressCallback&) override { return true; } + void requestCancelLoad() override {} + std::unordered_set getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) const override; + std::unordered_set getSimilarTracks(Database::Session& session, const std::unordered_set& tracksId, std::size_t maxCount) const override; + std::unordered_set getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) const override; + std::unordered_set getSimilarArtists(Database::Session& session, Database::IdType artistId, std::size_t maxCount) const override; }; } // namespace Recommendation diff --git a/src/libs/recommendation/impl/features/FeaturesClassifier.cpp b/src/libs/recommendation/impl/features/FeaturesClassifier.cpp index d3b13d5d..119f71de 100644 --- a/src/libs/recommendation/impl/features/FeaturesClassifier.cpp +++ b/src/libs/recommendation/impl/features/FeaturesClassifier.cpp @@ -127,7 +127,7 @@ getInputVectorWeights(const FeatureSettingsMap& featureSettingsMap, std::size_t } bool -FeaturesClassifier::initFromTraining(Database::Session& session, const TrainSettings& trainSettings) +FeaturesClassifier::loadFromTraining(Database::Session& session, const TrainSettings& trainSettings, const ProgressCallback& progressCallback) { LMS_LOG(RECOMMENDATION, INFO) << "Constructing features classifier..."; @@ -158,7 +158,7 @@ FeaturesClassifier::initFromTraining(Database::Session& session, const TrainSett LMS_LOG(RECOMMENDATION, DEBUG) << "Extracting features..."; for (Database::IdType trackId : trackIds) { - if (_initCancelled) + if (_loadCancelled) return false; std::optional featureValuesMap; @@ -201,23 +201,26 @@ FeaturesClassifier::initFromTraining(Database::Session& session, const TrainSett SOM::InputVector weights {getInputVectorWeights(trainSettings.featureSettingsMap, nbDimensions)}; network.setDataWeights(weights); - auto progressIndicator{[](const auto& iter) - { - LMS_LOG(RECOMMENDATION, DEBUG) << "Current pass = " << iter.idIteration << " / " << iter.iterationCount; - }}; + auto somProgressCallback{[&](const SOM::Network::CurrentIteration& iter) + { + LMS_LOG(RECOMMENDATION, DEBUG) << "Current pass = " << iter.idIteration << " / " << iter.iterationCount; + progressCallback(Progress {iter.idIteration, iter.iterationCount}); + }}; LMS_LOG(RECOMMENDATION, DEBUG) << "Training network..."; - network.train(samples, trainSettings.iterationCount, progressIndicator); + network.train(samples, trainSettings.iterationCount, + progressCallback ? somProgressCallback : SOM::Network::ProgressCallback {}, + [this] { return _loadCancelled; }); LMS_LOG(RECOMMENDATION, DEBUG) << "Training network DONE"; - if (_initCancelled) + if (_loadCancelled) return false; LMS_LOG(RECOMMENDATION, DEBUG) << "Classifying tracks..."; ObjectPositions trackPositions; for (std::size_t i {}; i < samples.size(); ++i) { - if (_initCancelled) + if (_loadCancelled) return false; const SOM::Position position {network.getClosestRefVectorPosition(samples[i])}; @@ -227,86 +230,100 @@ FeaturesClassifier::initFromTraining(Database::Session& session, const TrainSett LMS_LOG(RECOMMENDATION, DEBUG) << "Classifying tracks DONE"; - return init(session, std::move(network), std::move(trackPositions)); + return load(session, std::move(network), std::move(trackPositions)); } bool -FeaturesClassifier::initFromCache(Database::Session& session, const FeaturesClassifierCache& cache) +FeaturesClassifier::loadFromCache(Database::Session& session, const FeaturesClassifierCache& cache) { LMS_LOG(RECOMMENDATION, INFO) << "Constructing features classifier from cache..."; - return init(session, std::move(cache._network), cache._trackPositions); + return load(session, std::move(cache._network), cache._trackPositions); } -std::vector +std::unordered_set FeaturesClassifier::getSimilarTracksFromTrackList(Database::Session& session, Database::IdType trackListId, std::size_t maxCount) const { - const std::unordered_set trackIds {[&]() -> std::unordered_set + const std::unordered_set trackIds {[&] { + std::unordered_set res; + auto transaction {session.createSharedTransaction()}; const Database::TrackList::pointer trackList {Database::TrackList::getById(session, trackListId)}; if (trackList) { const std::vector orderedTrackIds {trackList->getTrackIds()}; - return std::unordered_set {std::cbegin(orderedTrackIds), std::cend(orderedTrackIds)}; + res = std::unordered_set(std::cbegin(orderedTrackIds), std::cend(orderedTrackIds)); } - return {}; + return res; }()}; return getSimilarTracks(session, trackIds, maxCount); } -std::vector +std::unordered_set FeaturesClassifier::getSimilarTracks(Database::Session& session, const std::unordered_set& tracksIds, std::size_t maxCount) const { - std::vector similarTrackIds {getSimilarObjects(tracksIds, _tracksMap, _trackPositions, maxCount)}; - + auto similarTrackIds {getSimilarObjects(tracksIds, _tracksMap, _trackPositions, maxCount)}; if (!similarTrackIds.empty()) { // Report only existing ids auto transaction {session.createSharedTransaction()}; - similarTrackIds.erase(std::remove_if(std::begin(similarTrackIds), std::end(similarTrackIds), - [&](Database::IdType trackId) { return Database::Track::getById(session, trackId) == Database::Track::pointer {}; }), - std::cend(similarTrackIds)); + for (auto it {std::begin(similarTrackIds)}; it != std::end(similarTrackIds);) + { + const Database::IdType trackId {*it}; + if (!Database::Track::getById(session, trackId)) + it = similarTrackIds.erase(it); + else + it++; + } } return similarTrackIds; } -std::vector +std::unordered_set FeaturesClassifier::getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) const { - std::vector similarReleaseIds {getSimilarObjects({releaseId}, _releasesMap, _releasePositions, maxCount)}; - + auto similarReleaseIds {getSimilarObjects({releaseId}, _releasesMap, _releasePositions, maxCount)}; if (!similarReleaseIds.empty()) { // Report only existing ids auto transaction {session.createSharedTransaction()}; - similarReleaseIds.erase(std::remove_if(std::begin(similarReleaseIds), std::end(similarReleaseIds), - [&](Database::IdType releaseId) { return Database::Release::getById(session, releaseId) == Database::Release::pointer {}; }), - std::cend(similarReleaseIds)); + for (auto it {std::begin(similarReleaseIds)}; it != std::end(similarReleaseIds);) + { + const Database::IdType releaseId {*it}; + if (!Database::Release::getById(session, releaseId)) + it = similarReleaseIds.erase(it); + else + it++; + } } return similarReleaseIds; } -std::vector +std::unordered_set FeaturesClassifier::getSimilarArtists(Database::Session& session, Database::IdType artistId, std::size_t maxCount) const { - std::vector similarArtistIds {getSimilarObjects({artistId}, _artistsMap, _artistPositions, maxCount)}; - + auto similarArtistIds {getSimilarObjects({artistId}, _artistsMap, _artistPositions, maxCount)}; if (!similarArtistIds.empty()) { // Report only existing ids auto transaction {session.createSharedTransaction()}; - similarArtistIds.erase(std::remove_if(std::begin(similarArtistIds), std::end(similarArtistIds), - [&](Database::IdType artistId) { return Database::Artist::getById(session, artistId) == Database::Artist::pointer {}; }), - std::cend(similarArtistIds)); + for (auto it {std::begin(similarArtistIds)}; it != std::end(similarArtistIds);) + { + const Database::IdType artistId {*it}; + if (!Database::Release::getById(session, artistId)) + it = similarArtistIds.erase(it); + else + it++; + } } return similarArtistIds; @@ -319,22 +336,23 @@ FeaturesClassifier::toCache() const } bool -FeaturesClassifier::init(Database::Session& session, bool databaseChanged) +FeaturesClassifier::load(Database::Session& session, bool forceReload, const ProgressCallback& progressCallback) { - if (databaseChanged) + if (forceReload) { - LMS_LOG(RECOMMENDATION, DEBUG) << "Database changed: invidating cache"; FeaturesClassifierCache::invalidate(); } - - std::optional cache {FeaturesClassifierCache::read()}; - if (cache) - return initFromCache(session, *cache); + else + { + const std::optional cache {FeaturesClassifierCache::read()}; + if (cache) + return loadFromCache(session, *cache); + } TrainSettings trainSettings; trainSettings.featureSettingsMap = getDefaultTrainFeatureSettings(); - bool res {initFromTraining(session, trainSettings)}; + const bool res {loadFromTraining(session, trainSettings, progressCallback)}; if (res) toCache().write(); @@ -342,14 +360,14 @@ FeaturesClassifier::init(Database::Session& session, bool databaseChanged) } void -FeaturesClassifier::requestCancelInit() +FeaturesClassifier::requestCancelLoad() { LMS_LOG(RECOMMENDATION, DEBUG) << "Requesting init cancellation"; - _initCancelled = true; + _loadCancelled = true; } bool -FeaturesClassifier::init(Database::Session& session, +FeaturesClassifier::load(Database::Session& session, SOM::Network network, const ObjectPositions& tracksPosition) { @@ -367,7 +385,7 @@ FeaturesClassifier::init(Database::Session& session, for (auto itTrackCoord : tracksPosition) { - if (_initCancelled) + if (_loadCancelled) return false; auto transaction {session.createSharedTransaction()}; @@ -399,7 +417,7 @@ FeaturesClassifier::init(Database::Session& session, _network = std::make_unique(std::move(network)); - LMS_LOG(RECOMMENDATION, INFO) << "Classifier successfully initialized!"; + LMS_LOG(RECOMMENDATION, INFO) << "Classifier successfully loaded!"; return true; } @@ -439,13 +457,13 @@ FeaturesClassifier::getObjectsIds(const std::unordered_set& posit return res; } -std::vector +std::unordered_set FeaturesClassifier::getSimilarObjects(const std::unordered_set& ids, const MatrixOfObjects& objectsMap, const ObjectPositions& objectPosition, std::size_t maxCount) const { - std::vector res; + std::unordered_set res; std::unordered_set searchedRefVectorsPosition {getMatchingRefVectorsPosition(ids, objectPosition)}; if (searchedRefVectorsPosition.empty()) @@ -459,14 +477,13 @@ FeaturesClassifier::getSimilarObjects(const std::unordered_set for (auto id : ids) closestObjectIds.erase(id); + for (auto it {std::cbegin(closestObjectIds)}; it != std::cend(closestObjectIds); ++it) { - std::vector objectIdsToAdd {std::cbegin(closestObjectIds), std::cend(closestObjectIds)}; - Random::shuffleContainer(objectIdsToAdd ); - std::copy(std::cbegin(objectIdsToAdd), std::cend(objectIdsToAdd), std::back_inserter(res)); - } + if (res.size() == maxCount) + break; - if (res.size() > maxCount) - res.resize(maxCount); + res.insert(*it); + } if (res.size() == maxCount) break; diff --git a/src/libs/recommendation/impl/features/FeaturesClassifier.hpp b/src/libs/recommendation/impl/features/FeaturesClassifier.hpp index f20beec7..2d5b3992 100644 --- a/src/libs/recommendation/impl/features/FeaturesClassifier.hpp +++ b/src/libs/recommendation/impl/features/FeaturesClassifier.hpp @@ -19,15 +19,16 @@ #pragma once +#include #include #include #include -#include "recommendation/IClassifier.hpp" #include "som/DataNormalizer.hpp" #include "som/Network.hpp" #include "FeaturesClassifierCache.hpp" #include "FeaturesDefs.hpp" +#include "IClassifier.hpp" namespace Database { @@ -53,19 +54,20 @@ class FeaturesClassifier : public IClassifier static void setFeaturesFetchFunc(FeaturesFetchFunc func) { _featuresFetchFunc = func; } static const FeatureSettingsMap& getDefaultTrainFeatureSettings(); + private: std::string_view getName() const override { return "Features"; } - bool init(Database::Session& session, bool databaseChanged) override; - void requestCancelInit() override; + bool load(Database::Session& session, bool forceReload, const ProgressCallback& progressCallback) override; + void requestCancelLoad() 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 override; - std::vector getSimilarArtists(Database::Session& session, Database::IdType artistId, std::size_t maxCount) const override; + std::unordered_set getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) const override; + std::unordered_set getSimilarTracks(Database::Session& session, const std::unordered_set& tracksId, std::size_t maxCount) const override; + std::unordered_set getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) const override; + std::unordered_set getSimilarArtists(Database::Session& session, Database::IdType artistId, std::size_t maxCount) const override; - bool initFromCache(Database::Session& session, const FeaturesClassifierCache& cache); + bool loadFromCache(Database::Session& session, const FeaturesClassifierCache& cache); // Use training (may be very slow) struct TrainSettings @@ -74,12 +76,12 @@ class FeaturesClassifier : public IClassifier float sampleCountPerNeuron {4}; FeatureSettingsMap featureSettingsMap; }; - bool initFromTraining(Database::Session& session, const TrainSettings& trainSettings); + bool loadFromTraining(Database::Session& session, const TrainSettings& trainSettings, const ProgressCallback& progressCallback); using ObjectPositions = std::unordered_map>; using MatrixOfObjects = SOM::Matrix>; - bool init(Database::Session& session, + bool load(Database::Session& session, SOM::Network network, const ObjectPositions& tracksPosition); @@ -88,12 +90,12 @@ class FeaturesClassifier : public IClassifier static std::unordered_set getMatchingRefVectorsPosition(const std::unordered_set& ids, const ObjectPositions& objectPositions); static std::unordered_set getObjectsIds(const std::unordered_set& positionSet, const MatrixOfObjects& objectsMap); - std::vector getSimilarObjects(const std::unordered_set& ids, + std::unordered_set getSimilarObjects(const std::unordered_set& ids, const SOM::Matrix>& objectsMap, const ObjectPositions& objectPosition, std::size_t maxCount) const; - bool _initCancelled {}; + bool _loadCancelled {}; std::unique_ptr _network; double _networkRefVectorsDistanceMedian {}; diff --git a/src/libs/recommendation/include/recommendation/IEngine.hpp b/src/libs/recommendation/include/recommendation/IEngine.hpp index 7cf196e8..f1b4ce2b 100644 --- a/src/libs/recommendation/include/recommendation/IEngine.hpp +++ b/src/libs/recommendation/include/recommendation/IEngine.hpp @@ -20,11 +20,8 @@ #pragma once #include -#include #include -#include - #include "database/Types.hpp" namespace Database @@ -40,16 +37,19 @@ namespace Recommendation public: virtual ~IEngine() = default; - virtual void requestLoad() = 0; + struct Progress + { + std::size_t totalElems {}; + std::size_t processedElems {}; + }; + using ProgressCallback = std::function; + virtual void load(bool forceReload, const ProgressCallback& progressCallback = {}) = 0; + virtual void cancelLoad() = 0; - virtual void requestReload() = 0; - virtual Wt::Signal<>& reloaded() = 0; - - // Closest results first - virtual std::vector getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) = 0; - virtual std::vector getSimilarTracks(Database::Session& session, const std::unordered_set& tracksId, std::size_t maxCount) = 0; - virtual std::vector getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) = 0; - virtual std::vector getSimilarArtists(Database::Session& session, Database::IdType artistId, std::size_t maxCount) = 0; + virtual std::unordered_set getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) = 0; + virtual std::unordered_set getSimilarTracks(Database::Session& session, const std::unordered_set& tracksId, std::size_t maxCount) = 0; + virtual std::unordered_set getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) = 0; + virtual std::unordered_set getSimilarArtists(Database::Session& session, Database::IdType artistId, std::size_t maxCount) = 0; }; std::unique_ptr createEngine(Database::Db& db); diff --git a/src/libs/scanner/CMakeLists.txt b/src/libs/scanner/CMakeLists.txt index 2ec7539e..07f6cf1c 100644 --- a/src/libs/scanner/CMakeLists.txt +++ b/src/libs/scanner/CMakeLists.txt @@ -16,6 +16,7 @@ target_include_directories(lmsscanner PRIVATE target_link_libraries(lmsscanner PRIVATE lmsdatabase lmsmetadata + lmsrecommendation lmsutils ) diff --git a/src/libs/scanner/impl/MediaScanner.cpp b/src/libs/scanner/impl/MediaScanner.cpp index 7b7d60cb..65fd3399 100644 --- a/src/libs/scanner/impl/MediaScanner.cpp +++ b/src/libs/scanner/impl/MediaScanner.cpp @@ -30,6 +30,7 @@ #include "database/Track.hpp" #include "database/TrackFeatures.hpp" #include "metadata/TagLibParser.hpp" +#include "recommendation/IEngine.hpp" #include "utils/Exception.hpp" #include "utils/Logger.hpp" #include "utils/Path.hpp" @@ -240,13 +241,14 @@ getOrCreateClusters(Session& session, const MetaData::Clusters& clustersNames) namespace Scanner { std::unique_ptr -createMediaScanner(Database::Db& db) +createMediaScanner(Database::Db& db, Recommendation::IEngine& recommendationEngine) { - return std::make_unique(db); + return std::make_unique(db, recommendationEngine); } -MediaScanner::MediaScanner(Database::Db& db) -: _dbSession {db} +MediaScanner::MediaScanner(Database::Db& db, Recommendation::IEngine& recommendationEngine) +: _recommendationEngine {recommendationEngine} +, _dbSession {db} { // For now, always use TagLib _metadataParser = std::make_unique(); @@ -260,6 +262,7 @@ MediaScanner::MediaScanner(Database::Db& db) MediaScanner::~MediaScanner() { + LMS_LOG(DBUPDATER, INFO) << "Shutting down MediaScanner..."; stop(); } @@ -268,7 +271,14 @@ MediaScanner::start() { std::scoped_lock lock {_controlMutex}; - scheduleNextScan(); + _ioService.post([this] + { + if (_abortScan) + return; + + _recommendationEngine.load(false); + scheduleNextScan(); + }); _ioService.start(); } @@ -279,8 +289,8 @@ MediaScanner::stop() std::scoped_lock lock {_controlMutex}; _abortScan = true; - _scheduleTimer.cancel(); + _recommendationEngine.cancelLoad(); _ioService.stop(); } @@ -294,6 +304,7 @@ MediaScanner::abortScan() _abortScan = true; _scheduleTimer.cancel(); + _recommendationEngine.cancelLoad(); _ioService.stop(); LMS_LOG(DBUPDATER, DEBUG) << "Scan abort done!"; @@ -307,6 +318,9 @@ MediaScanner::requestImmediateScan(bool force) abortScan(); _ioService.post([=]() { + if (_abortScan) + return; + scheduleScan(force); }); } @@ -317,6 +331,9 @@ MediaScanner::requestReload() abortScan(); _ioService.post([=]() { + if (_abortScan) + return; + scheduleNextScan(); }); } @@ -407,12 +424,13 @@ MediaScanner::countAllFiles(ScanStats& stats) if (!ec && isFileSupported(path, _fileExtensions)) { stats.filesScanned++; - stepStats.processedFiles++; + stepStats.processedElems++; notifyInProgressIfNeeded(stepStats); } return true; }); + notifyInProgress(stepStats); } void @@ -476,10 +494,11 @@ MediaScanner::scan(bool forceScan) removeOrphanEntries(); if (!_abortScan) + { checkDuplicatedAudioFiles(stats); - - // Now update all the track features if needed - fetchTrackFeatures(stats); + fetchTrackFeatures(stats); + reloadSimilarityEngine(stats); + } LMS_LOG(DBUPDATER, INFO) << "Scan " << (_abortScan ? "aborted" : "complete") << ". Changes = " << stats.nbChanges() << " (added = " << stats.additions << ", removed = " << stats.deletions << ", updated = " << stats.updates << "), Not changed = " << stats.skips << ", Scanned = " << stats.scans << " (errors = " << stats.errors.size() << "), features fetched = " << stats.featuresFetched << ", duplicates = " << stats.duplicates.size(); @@ -566,7 +585,7 @@ MediaScanner::fetchTrackFeatures(ScanStats& stats) return res; }()}; - stepStats.filesToProcess = tracksToFetch.size(); + stepStats.totalElems = tracksToFetch.size(); notifyInProgress(stepStats); LMS_LOG(DBUPDATER, INFO) << "Found " << tracksToFetch.size() << " track(s) to fetch!"; @@ -579,10 +598,11 @@ MediaScanner::fetchTrackFeatures(ScanStats& stats) if (fetchTrackFeatures(trackToFetch.id, trackToFetch.mbid)) stats.featuresFetched++; - stepStats.processedFiles++; + stepStats.processedElems++; notifyInProgressIfNeeded(stepStats); } + notifyInProgress(stepStats); LMS_LOG(DBUPDATER, INFO) << "Track features fetched!"; } @@ -591,7 +611,7 @@ MediaScanner::refreshScanSettings() { auto transaction {_dbSession.createSharedTransaction()}; - ScanSettings::pointer scanSettings {ScanSettings::get(_dbSession)}; + const ScanSettings::pointer scanSettings {ScanSettings::get(_dbSession)}; LMS_LOG(DBUPDATER, INFO) << "Using scan settings version " << scanSettings->getScanVersion(); @@ -608,7 +628,7 @@ MediaScanner::refreshScanSettings() _mediaDirectory = scanSettings->getMediaDirectory(); _recommendationEngineType = scanSettings->getRecommendationEngineType(); - auto clusterTypes = scanSettings->getClusterTypes(); + const auto clusterTypes = scanSettings->getClusterTypes(); std::set clusterTypeNames; std::transform(std::cbegin(clusterTypes), std::cend(clusterTypes), @@ -616,7 +636,6 @@ MediaScanner::refreshScanSettings() [](ClusterType::pointer clusterType) { return clusterType->getName(); }); _metadataParser->setClusterTypeNames(clusterTypeNames); - } void @@ -800,7 +819,7 @@ void MediaScanner::scanMediaDirectory(const std::filesystem::path& mediaDirectory, bool forceScan, ScanStats& stats) { ScanStepStats stepStats{stats.startTime, ScanProgressStep::ScanningFiles}; - stepStats.filesToProcess = stats.filesScanned; + stepStats.totalElems = stats.filesScanned; notifyInProgress(stepStats); exploreFilesRecursive(mediaDirectory, [&](std::error_code ec, const std::filesystem::path& path) @@ -817,12 +836,14 @@ MediaScanner::scanMediaDirectory(const std::filesystem::path& mediaDirectory, bo { scanAudioFile(path, forceScan, stats ); - stepStats.processedFiles++; + stepStats.processedElems++; notifyInProgressIfNeeded(stepStats); } return true; }); + + notifyInProgress(stepStats); } // Check if a file exists and is still in a media directory @@ -878,7 +899,7 @@ MediaScanner::removeMissingTracks(ScanStats& stats) } LMS_LOG(DBUPDATER, DEBUG) << trackCount << " tracks to be checked..."; - stepStats.filesToProcess = trackCount; + stepStats.totalElems = trackCount; notifyInProgress(stepStats); std::vector> trackPaths; @@ -902,7 +923,7 @@ MediaScanner::removeMissingTracks(ScanStats& stats) if (!checkFile(trackPath, _mediaDirectory, _fileExtensions)) tracksToRemove.push_back(trackId); - stepStats.processedFiles++; + stepStats.processedElems++; } if (!tracksToRemove.empty()) @@ -992,4 +1013,21 @@ MediaScanner::checkDuplicatedAudioFiles(ScanStats& stats) LMS_LOG(DBUPDATER, INFO) << "Checking duplicated audio files done!"; } +void +MediaScanner::reloadSimilarityEngine(ScanStats& stats) +{ + ScanStepStats stepStats {stats.startTime, ScanProgressStep::ReloadingSimilarityEngine}; + + auto progressCallback {[&](const Recommendation::IEngine::Progress& progress) + { + stepStats.totalElems = progress.totalElems; + stepStats.processedElems = progress.processedElems; + notifyInProgressIfNeeded(stepStats); + }}; + + notifyInProgress(stepStats); + _recommendationEngine.load(stats.nbChanges() > 0, progressCallback); + notifyInProgress(stepStats); +} + } // namespace Scanner diff --git a/src/libs/scanner/impl/MediaScanner.hpp b/src/libs/scanner/impl/MediaScanner.hpp index 88cbc27c..6a78b86c 100644 --- a/src/libs/scanner/impl/MediaScanner.hpp +++ b/src/libs/scanner/impl/MediaScanner.hpp @@ -37,12 +37,17 @@ class UUID; +namespace Recommendation +{ + class IEngine; +} + namespace Scanner { class MediaScanner : public IMediaScanner { public: - MediaScanner(Database::Db& db); + MediaScanner(Database::Db& db, Recommendation::IEngine& recommendationEngine); ~MediaScanner(); MediaScanner(const MediaScanner&) = delete; @@ -89,6 +94,9 @@ class MediaScanner : public IMediaScanner Database::IdType doScanAudioFile(const std::filesystem::path& file, ScanStats& stats); void notifyInProgressIfNeeded(const ScanStepStats& stats); void notifyInProgress(const ScanStepStats& stats); + void reloadSimilarityEngine(ScanStats& stats); + + Recommendation::IEngine& _recommendationEngine; std::mutex _controlMutex; std::atomic _abortScan {}; @@ -116,7 +124,6 @@ class MediaScanner : public IMediaScanner std::filesystem::path _mediaDirectory; Database::ScanSettings::RecommendationEngineType _recommendationEngineType; - }; // class MediaScanner } // Scanner diff --git a/src/libs/scanner/impl/MediaScannerStats.cpp b/src/libs/scanner/impl/MediaScannerStats.cpp index 29af4235..17450156 100644 --- a/src/libs/scanner/impl/MediaScannerStats.cpp +++ b/src/libs/scanner/impl/MediaScannerStats.cpp @@ -43,7 +43,7 @@ ScanStats::nbChanges() const unsigned ScanStepStats::progress() const { - return (processedFiles / static_cast(filesToProcess ? filesToProcess : 1)) * 100; + return (processedElems / static_cast(totalElems ? totalElems : 1)) * 100; } } // namespace Scanner diff --git a/src/libs/scanner/include/scanner/IMediaScanner.hpp b/src/libs/scanner/include/scanner/IMediaScanner.hpp index 5a0afbdc..ec8f64d5 100644 --- a/src/libs/scanner/include/scanner/IMediaScanner.hpp +++ b/src/libs/scanner/include/scanner/IMediaScanner.hpp @@ -31,6 +31,11 @@ namespace Database class Db; } +namespace Recommendation +{ + class IEngine; +} + namespace Scanner { class IMediaScanner @@ -73,8 +78,7 @@ class IMediaScanner }; -std::unique_ptr createMediaScanner(Database::Db& db); - +std::unique_ptr createMediaScanner(Database::Db& db, Recommendation::IEngine& recommendationEngine); } // Scanner diff --git a/src/libs/scanner/include/scanner/MediaScannerStats.hpp b/src/libs/scanner/include/scanner/MediaScannerStats.hpp index f5eb67f7..145ea5c3 100644 --- a/src/libs/scanner/include/scanner/MediaScannerStats.hpp +++ b/src/libs/scanner/include/scanner/MediaScannerStats.hpp @@ -62,8 +62,9 @@ namespace Scanner { DiscoveringFiles, ScanningFiles, FetchingTrackFeatures, + ReloadingSimilarityEngine, }; - static inline constexpr unsigned ScanProgressStepCount {4}; + static inline constexpr unsigned ScanProgressStepCount {5}; // reduced scan stats struct ScanStepStats @@ -72,8 +73,8 @@ namespace Scanner { ScanProgressStep currentStep; - std::size_t filesToProcess {}; - std::size_t processedFiles {}; + std::size_t totalElems {}; + std::size_t processedElems {}; unsigned progress() const; }; diff --git a/src/libs/subsonic/impl/Scan.cpp b/src/libs/subsonic/impl/Scan.cpp index 3b9325a6..ff32b960 100644 --- a/src/libs/subsonic/impl/Scan.cpp +++ b/src/libs/subsonic/impl/Scan.cpp @@ -40,7 +40,7 @@ namespace API::Subsonic::Scan std::size_t count{}; if (scanStatus.currentScanStepStats && scanStatus.currentScanStepStats->currentStep == ScanProgressStep::ScanningFiles) - count = scanStatus.currentScanStepStats->processedFiles; + count = scanStatus.currentScanStepStats->processedElems; statusResponse.setAttribute("count", count); } diff --git a/src/libs/utils/include/utils/Semaphore.hpp b/src/libs/utils/include/utils/Semaphore.hpp deleted file mode 100644 index 7b82879d..00000000 --- a/src/libs/utils/include/utils/Semaphore.hpp +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright (C) 2020 Emeric Poupon - * - * This file is part of LMS. - * - * LMS is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LMS is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LMS. If not, see . - */ - -#pragma once - -#include -#include - -class Semaphore -{ - public: - Semaphore() = default; - Semaphore(const Semaphore&) = delete; - Semaphore(Semaphore&&) = delete; - Semaphore& operator=(const Semaphore&) = delete; - Semaphore& operator=(Semaphore&&) = delete; - - void notify() - { - std::unique_lock lock {_mutex}; - - _count++; - _cv.notify_one(); - } - - void wait() - { - std::unique_lock lock(_mutex); - - while (_count == 0) - _cv.wait(lock); - - _count--; - } - - private: - std::mutex _mutex; - std::condition_variable _cv; - unsigned _count {}; -}; - - diff --git a/src/libs/utils/include/utils/Service.hpp b/src/libs/utils/include/utils/Service.hpp index 95a44e37..4c2cf3e7 100644 --- a/src/libs/utils/include/utils/Service.hpp +++ b/src/libs/utils/include/utils/Service.hpp @@ -46,6 +46,11 @@ class Service return Service::get(); } + Class& operator*() const + { + return *Service::get(); + } + static Class* get() { return _service.get(); } private: diff --git a/src/lms/main.cpp b/src/lms/main.cpp index 11649676..c75b9cc1 100644 --- a/src/lms/main.cpp +++ b/src/lms/main.cpp @@ -121,7 +121,7 @@ generateWtConfig(std::string execPath) int main(int argc, char* argv[]) { std::filesystem::path configFilePath {"/etc/lms.conf"}; - int res = EXIT_FAILURE; + int res {EXIT_FAILURE}; assert(argc > 0); assert(argv[0] != NULL); @@ -183,22 +183,10 @@ int main(int argc, char* argv[]) config->getULong("cover-max-file-size", 10) * 1000 * 1000, config->getULong("cover-jpeg-quality", 75))}; Service recommendationEngineService {Recommendation::createEngine(database)}; - recommendationEngineService->requestLoad(); - Service mediaScannerService {Scanner::createMediaScanner(database)}; + Service mediaScannerService {Scanner::createMediaScanner(database, *recommendationEngineService)}; mediaScannerService->scanComplete().connect([&]() { - auto status = mediaScannerService->getStatus(); - - if (status.lastCompleteScanStats->nbChanges() > 0 || status.lastCompleteScanStats->featuresFetched > 0) - { - LMS_LOG(MAIN, INFO) << "Scanner changed some files, reloading the recommendation engine..."; - recommendationEngineService->requestReload(); - } - else - { - LMS_LOG(MAIN, INFO) << "Scanner did not change files, not reloading the recommendation engine..."; - } // Flush cover cache even if no changes: // covers may be external files that changed and we don't keep track of them coverArtService->flushCache(); @@ -224,16 +212,18 @@ int main(int argc, char* argv[]) LMS_LOG(MAIN, INFO) << "Stopping server..."; server.stop(); - LMS_LOG(MAIN, INFO) << "Clean stop!"; + LMS_LOG(MAIN, INFO) << "Quitting..."; res = EXIT_SUCCESS; } catch(Wt::WServer::Exception& e) { std::cerr << "Caught a WServer::Exception: " << e.what() << std::endl; + res = EXIT_FAILURE; } catch(std::exception& e) { std::cerr << "Caught std::exception: " << e.what() << std::endl; + res = EXIT_FAILURE; } return res; diff --git a/src/lms/ui/PlayQueue.cpp b/src/lms/ui/PlayQueue.cpp index 0e62b981..ff0e5db1 100644 --- a/src/lms/ui/PlayQueue.cpp +++ b/src/lms/ui/PlayQueue.cpp @@ -501,7 +501,10 @@ PlayQueue::addSome() void PlayQueue::enqueueRadioTracks() { - const std::vector trackToAddIds {Service::get()->getSimilarTracksFromTrackList(LmsApp->getDbSession(), _tracklistId, 3)}; + const auto similarTrackIds {Service::get()->getSimilarTracksFromTrackList(LmsApp->getDbSession(), _tracklistId, 3)}; + + std::vector trackToAddIds(std::cbegin(similarTrackIds), std::cend(similarTrackIds)); + Random::shuffleContainer(trackToAddIds); enqueueTracks(trackToAddIds); } diff --git a/src/lms/ui/admin/DatabaseSettingsView.cpp b/src/lms/ui/admin/DatabaseSettingsView.cpp index def6885f..fa0fbbc8 100644 --- a/src/lms/ui/admin/DatabaseSettingsView.cpp +++ b/src/lms/ui/admin/DatabaseSettingsView.cpp @@ -47,14 +47,13 @@ class DatabaseSettingsModel : public Wt::WFormModel { public: // Associate each field with a unique string literal. - static const Field MediaDirectoryField; - static const Field UpdatePeriodField; - static const Field UpdateStartTimeField; - static const Field RecommendationEngineTypeField; - static const Field TagsField; + static inline const Field MediaDirectoryField {"media-directory"}; + static inline const Field UpdatePeriodField {"update-period"}; + static inline const Field UpdateStartTimeField {"update-start-time"}; + static inline const Field RecommendationEngineTypeField {"recommendation-engine-type"}; + static inline const Field TagsField {"tags"}; DatabaseSettingsModel() - : Wt::WFormModel() { initializeModels(); @@ -135,7 +134,6 @@ class DatabaseSettingsModel : public Wt::WFormModel } private: - static std::shared_ptr createTagsValidator() { auto v = std::make_shared(); @@ -162,18 +160,11 @@ class DatabaseSettingsModel : public Wt::WFormModel _recommendationEngineTypeModel->add(Wt::WString::tr("Lms.Admin.Database.recommendation-engine-type.features"), ScanSettings::RecommendationEngineType::Features); } - std::shared_ptr> _updatePeriodModel; - std::shared_ptr> _updateStartTimeModel; + std::shared_ptr> _updatePeriodModel; + std::shared_ptr> _updateStartTimeModel; std::shared_ptr> _recommendationEngineTypeModel; - }; -const Wt::WFormModel::Field DatabaseSettingsModel::MediaDirectoryField = "media-directory"; -const Wt::WFormModel::Field DatabaseSettingsModel::UpdatePeriodField = "update-period"; -const Wt::WFormModel::Field DatabaseSettingsModel::UpdateStartTimeField = "update-start-time"; -const Wt::WFormModel::Field DatabaseSettingsModel::RecommendationEngineTypeField = "recommendation-engine-type"; -const Wt::WFormModel::Field DatabaseSettingsModel::TagsField = "tags"; - DatabaseSettingsView::DatabaseSettingsView() { wApp->internalPathChanged().connect(std::bind([=] @@ -223,7 +214,7 @@ DatabaseSettingsView::refreshView() t->bindNew("scanner-controller"); - saveBtn->clicked().connect([=] () + saveBtn->clicked().connect([=] { t->updateModel(model.get()); @@ -231,7 +222,7 @@ DatabaseSettingsView::refreshView() { model->saveData(); - Service::get()->requestReload(); + Service::get()->requestImmediateScan(false); LmsApp->notifyMsg(MsgType::Success, Wt::WString::tr("Lms.Admin.Database.settings-saved")); } @@ -239,14 +230,14 @@ DatabaseSettingsView::refreshView() t->updateView(model.get()); }); - discardBtn->clicked().connect([=] () + discardBtn->clicked().connect([=] { model->loadData(); model->validate(); t->updateView(model.get()); }); - immScanBtn->clicked().connect([=] () + immScanBtn->clicked().connect([=] { Service::get()->requestImmediateScan(false); }); diff --git a/src/lms/ui/admin/ScannerController.cpp b/src/lms/ui/admin/ScannerController.cpp index f6193b98..2e394e61 100644 --- a/src/lms/ui/admin/ScannerController.cpp +++ b/src/lms/ui/admin/ScannerController.cpp @@ -206,20 +206,24 @@ ScannerController::refreshContents() case Scanner::ScanProgressStep::DiscoveringFiles: bindString("step-status", Wt::WString::tr("Lms.Admin.ScannerController.step-discovering-files") - .arg(status.currentScanStepStats->processedFiles)); + .arg(status.currentScanStepStats->processedElems)); break; case Scanner::ScanProgressStep::ScanningFiles: bindString("step-status", Wt::WString::tr("Lms.Admin.ScannerController.step-scanning-files") - .arg(status.currentScanStepStats->processedFiles) - .arg(status.currentScanStepStats->filesToProcess) + .arg(status.currentScanStepStats->processedElems) + .arg(status.currentScanStepStats->totalElems) .arg(status.currentScanStepStats->progress())); break; case Scanner::ScanProgressStep::FetchingTrackFeatures: bindString("step-status", Wt::WString::tr("Lms.Admin.ScannerController.step-fetching-track-features") - .arg(status.currentScanStepStats->processedFiles) - .arg(status.currentScanStepStats->filesToProcess) + .arg(status.currentScanStepStats->processedElems) + .arg(status.currentScanStepStats->totalElems) + .arg(status.currentScanStepStats->progress())); + break; + case Scanner::ScanProgressStep::ReloadingSimilarityEngine: + bindString("step-status", Wt::WString::tr("Lms.Admin.ScannerController.step-reloading-similarity-engine") .arg(status.currentScanStepStats->progress())); break; } diff --git a/src/lms/ui/explore/ArtistView.cpp b/src/lms/ui/explore/ArtistView.cpp index 73487b64..5b16c86f 100644 --- a/src/lms/ui/explore/ArtistView.cpp +++ b/src/lms/ui/explore/ArtistView.cpp @@ -46,7 +46,7 @@ namespace UserInterface { Artist::Artist(Filters* filters) : Wt::WTemplate {Wt::WString::tr("Lms.Explore.Artist.template")} -, _filters(filters) +, _filters {filters} { addFunction("tr", &Wt::WTemplate::Functions::tr); @@ -75,7 +75,7 @@ Artist::refreshView() if (!artistId) throw ArtistNotFoundException {*artistId}; - const std::vector similarArtistIds {Service::get()->getSimilarArtists(LmsApp->getDbSession(), *artistId, 5)}; + const auto similarArtistIds {Service::get()->getSimilarArtists(LmsApp->getDbSession(), *artistId, 5)}; auto transaction {LmsApp->getDbSession().createSharedTransaction()}; @@ -172,7 +172,7 @@ Artist::refreshView() } void -Artist::refreshSimilarArtists(const std::vector& similarArtistsId) +Artist::refreshSimilarArtists(const std::unordered_set& similarArtistsId) { if (similarArtistsId.empty()) return; diff --git a/src/lms/ui/explore/ArtistView.hpp b/src/lms/ui/explore/ArtistView.hpp index 54639412..5b9fe034 100644 --- a/src/lms/ui/explore/ArtistView.hpp +++ b/src/lms/ui/explore/ArtistView.hpp @@ -20,6 +20,7 @@ #pragma once #include +#include #include #include @@ -46,7 +47,7 @@ class Artist : public Wt::WTemplate private: void refreshView(); - void refreshSimilarArtists(const std::vector& similarArtistsId); + void refreshSimilarArtists(const std::unordered_set& similarArtistsId); void refreshLinks(const Wt::Dbo::ptr& artist); std::unique_ptr createRelease(const Wt::Dbo::ptr& artist, const Wt::Dbo::ptr& release); diff --git a/src/lms/ui/explore/ReleaseView.cpp b/src/lms/ui/explore/ReleaseView.cpp index 5b514636..59ed4dc3 100644 --- a/src/lms/ui/explore/ReleaseView.cpp +++ b/src/lms/ui/explore/ReleaseView.cpp @@ -50,11 +50,11 @@ namespace UserInterface { Release::Release(Filters* filters) : Wt::WTemplate {Wt::WString::tr("Lms.Explore.Release.template")} -, _filters(filters) +, _filters {filters} { addFunction("tr", &Wt::WTemplate::Functions::tr); - wApp->internalPathChanged().connect([=]() + wApp->internalPathChanged().connect([=] { refreshView(); }); @@ -79,7 +79,7 @@ Release::refreshView() if (!releaseId) throw ReleaseNotFoundException {*releaseId}; - const std::vector similarReleasesIds {Service::get()->getSimilarReleases(LmsApp->getDbSession(), *releaseId, 6)}; + auto similarReleasesIds {Service::get()->getSimilarReleases(LmsApp->getDbSession(), *releaseId, 6)}; auto transaction {LmsApp->getDbSession().createSharedTransaction()}; @@ -307,7 +307,7 @@ Release::refreshLinks(const Database::Release::pointer& release) } void -Release::refreshSimilarReleases(const std::vector& similarReleasesId) +Release::refreshSimilarReleases(const std::unordered_set& similarReleasesId) { if (similarReleasesId.empty()) return; diff --git a/src/lms/ui/explore/ReleaseView.hpp b/src/lms/ui/explore/ReleaseView.hpp index 7908c405..64b994f8 100644 --- a/src/lms/ui/explore/ReleaseView.hpp +++ b/src/lms/ui/explore/ReleaseView.hpp @@ -19,6 +19,7 @@ #pragma once +#include #include #include "database/Types.hpp" @@ -45,7 +46,7 @@ namespace UserInterface void refreshView(); void refreshCopyright(const Wt::Dbo::ptr& release); void refreshLinks(const Wt::Dbo::ptr& release); - void refreshSimilarReleases(const std::vector& similarReleasesId); + void refreshSimilarReleases(const std::unordered_set& similarReleasesId); Filters* _filters {}; }; diff --git a/src/tools/recommendation/LmsRecommendation.cpp b/src/tools/recommendation/LmsRecommendation.cpp index 9991a596..5c722c7f 100644 --- a/src/tools/recommendation/LmsRecommendation.cpp +++ b/src/tools/recommendation/LmsRecommendation.cpp @@ -31,7 +31,6 @@ #include "database/Session.hpp" #include "database/Track.hpp" #include "utils/IConfig.hpp" -#include "utils/Semaphore.hpp" #include "utils/Service.hpp" #include "utils/StreamLogger.hpp" #include "recommendation/IEngine.hpp" @@ -164,17 +163,8 @@ int main(int argc, char *argv[]) const auto engine {Recommendation::createEngine(db)}; std::cout << "Recommendation engine created!" << std::endl; - Semaphore sem; - - engine->reloaded().connect([&]() - { - sem.notify(); - }); - - engine->requestLoad(); - - std::cout << "Waiting for the recommendation engine to be loaded..." << std::endl; - sem.wait(); + std::cout << "Loading recommendation engine..." << std::endl; + engine->load(false); std::cout << "Recommendation engine loaded!" << std::endl; if (vm.count("tracks")) From 1c17f6a14ad66d13c138a892fc8c5fb32d6d8d8a Mon Sep 17 00:00:00 2001 From: emeric Date: Thu, 29 Oct 2020 13:25:54 +0100 Subject: [PATCH 7/8] Restore correct recommendation classifier priorities --- src/libs/recommendation/impl/Engine.cpp | 45 ++++++++++++++----------- src/libs/scanner/impl/MediaScanner.cpp | 6 +++- 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/src/libs/recommendation/impl/Engine.cpp b/src/libs/recommendation/impl/Engine.cpp index 397a7cda..ca520850 100644 --- a/src/libs/recommendation/impl/Engine.cpp +++ b/src/libs/recommendation/impl/Engine.cpp @@ -169,38 +169,45 @@ Engine::load(bool forceReload, const ProgressCallback& progressCallback) { using namespace Database; - static const std::unordered_map> classifierMappings + LMS_LOG(RECOMMENDATION, INFO) << "Reloading recommendation engines..."; + struct ClassifierWithType { - {ScanSettings::RecommendationEngineType::Features, {ClassifierType::Clusters, ClassifierType::Features}}, - {ScanSettings::RecommendationEngineType::Clusters, {ClassifierType::Clusters}}, + ClassifierType type; + std::unique_ptr classifier; }; - LMS_LOG(RECOMMENDATION, INFO) << "Reloading recommendation engines..."; + std::vector classifiers; + auto addClassifier {[&](ClassifierType type) + { + classifiers.emplace_back(ClassifierWithType {type, createClassifier(type)}); + }}; - const ScanSettings::RecommendationEngineType engineType {getRecommendationEngineType(_db.getTLSSession())}; + switch (getRecommendationEngineType(_db.getTLSSession())) + { + case ScanSettings::RecommendationEngineType::Clusters: + setClassifierPriorities({ClassifierType::Clusters}); + addClassifier(ClassifierType::Clusters); + break; + case ScanSettings::RecommendationEngineType::Features: + setClassifierPriorities({ClassifierType::Features, ClassifierType::Clusters}); + // not same order since clusters is faster to load + addClassifier(ClassifierType::Clusters); + addClassifier(ClassifierType::Features); + break; + } assert(_pendingClassifiers.empty()); clearClassifiers(); - auto itClassifierTypes {classifierMappings.find(engineType)}; - assert(itClassifierTypes != std::cend(classifierMappings)); - const std::vector& classifierTypes {itClassifierTypes->second}; - - setClassifierPriorities(classifierTypes); - - std::vector> classifiers; - for (ClassifierType type : classifierTypes) - classifiers.emplace_back(createClassifier(type)); - { std::scoped_lock lock {_controlMutex}; std::transform(std::cbegin(classifiers), std::cend(classifiers), std::inserter(_pendingClassifiers, std::end(_pendingClassifiers)), - [](auto& classifier) { return classifier.get(); }); + [](auto& classifier) { return classifier.classifier.get(); }); } - for (std::size_t i {}; i < classifiers.size(); ++i) - loadClassifier(std::move(classifiers[i]), classifierTypes[i], forceReload, progressCallback); + for (ClassifierWithType& classifier : classifiers) + loadClassifier(std::move(classifier.classifier), classifier.type, forceReload, progressCallback); LMS_LOG(RECOMMENDATION, INFO) << "Recommendation engines loaded!"; } @@ -254,9 +261,7 @@ Engine::loadClassifier(std::unique_ptr classifier, { std::scoped_lock lock {_controlMutex}; - LMS_LOG(RECOMMENDATION, DEBUG) << "About to erase. _pendingClassifiers size = " << _pendingClassifiers.size(); _pendingClassifiers.erase(rawClassifier); - LMS_LOG(RECOMMENDATION, DEBUG) << "Erased. _pendingClassifiers size = " << _pendingClassifiers.size(); } _pendingClassifiersCondvar.notify_one(); diff --git a/src/libs/scanner/impl/MediaScanner.cpp b/src/libs/scanner/impl/MediaScanner.cpp index 65fd3399..f8759b52 100644 --- a/src/libs/scanner/impl/MediaScanner.cpp +++ b/src/libs/scanner/impl/MediaScanner.cpp @@ -276,7 +276,11 @@ MediaScanner::start() if (_abortScan) return; - _recommendationEngine.load(false); + _recommendationEngine.load(false, + [](const Recommendation::IEngine::Progress& progress) + { + LMS_LOG(DBUPDATER, DEBUG) << "Reloading recommendation : " << progress.processedElems << "/" << progress.totalElems; + }); scheduleNextScan(); }); From 976b94959a303b21eac91a9332c59d9a5489886f Mon Sep 17 00:00:00 2001 From: emeric Date: Thu, 29 Oct 2020 13:26:15 +0100 Subject: [PATCH 8/8] Minor cleanup --- src/libs/som/impl/Network.cpp | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/src/libs/som/impl/Network.cpp b/src/libs/som/impl/Network.cpp index 75e60fce..535052a3 100644 --- a/src/libs/som/impl/Network.cpp +++ b/src/libs/som/impl/Network.cpp @@ -26,6 +26,7 @@ #include #include "utils/Logger.hpp" +#include "utils/Random.hpp" namespace SOM { @@ -78,25 +79,20 @@ defaultNeighbourhoodFunc(Norm norm, const Network::CurrentIteration& iteration) Network::Network(Coordinate width, Coordinate height, std::size_t inputDimCount) : -_inputDimCount(inputDimCount), -_weights(inputDimCount, static_cast(1)), -_refVectors(width, height, _inputDimCount), -_distanceFunc(euclidianSquareDistance), -_learningFactorFunc(defaultLearningFactor), -_neighbourhoodFunc(defaultNeighbourhoodFunc) +_inputDimCount {inputDimCount}, +_weights {inputDimCount, static_cast(1)}, +_refVectors {width, height, _inputDimCount}, +_distanceFunc {euclidianSquareDistance}, +_learningFactorFunc {defaultLearningFactor}, +_neighbourhoodFunc {defaultNeighbourhoodFunc} { - auto now {std::chrono::system_clock::now()}; - std::mt19937 randGenerator {static_cast(std::chrono::duration_cast(now.time_since_epoch()).count())}; - // init each vector with a random normalized value - std::uniform_real_distribution dist{0, 1}; - for (Coordinate y {}; y < _refVectors.getHeight(); ++y) { for (Coordinate x {}; x < _refVectors.getWidth(); ++x) { for (InputVector::value_type& val : _refVectors.get({x,y})) - val = dist(randGenerator); + val = Random::getRealRandom(0, 1); } } } @@ -294,9 +290,6 @@ Network::train(const std::vector& inputData, std::size_t nbIteratio for (const auto& input : inputData) inputDataShuffled.push_back(&input); - auto now {std::chrono::system_clock::now()}; - std::mt19937 randGenerator{static_cast(std::chrono::duration_cast(now.time_since_epoch()).count())}; - for (std::size_t i {}; i < nbIterations; ++i) { CurrentIteration curIter {i, nbIterations}; @@ -304,7 +297,7 @@ Network::train(const std::vector& inputData, std::size_t nbIteratio if (progressCallback) progressCallback(curIter); - std::shuffle(inputDataShuffled.begin(), inputDataShuffled.end(), randGenerator); + Random::shuffleContainer(inputDataShuffled); const LearningFactor learningFactor {_learningFactorFunc(curIter)};