From c04f57ceb5a0396e9c9a45461adba1c4fc882434 Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 17 Oct 2020 14:58:41 +0200 Subject: [PATCH] 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; +} +