diff --git a/README.md b/README.md index edaf7587..3c17b6be 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ _LMS_ is a self-hosted music streaming software: access your music collection fr A [demo](http://lms.demo.poupon.io) instance is available. Note the administration panel is not available. ## Main features -* Low memory requirement: the demo instance runs on a Raspberry Pi3B+ +* Low memory requirements: the demo instance runs on a Raspberry Pi3B+ * Recommendation engine * Audio transcode for maximum interoperability and low bandwith requirements * Multi-value tags: artists, genres, ... diff --git a/conf/lms.conf b/conf/lms.conf index e1666f60..a8bd99a1 100644 --- a/conf/lms.conf +++ b/conf/lms.conf @@ -40,6 +40,11 @@ api-subsonic = true; # Turn on this option to allow the demo account creation/use demo = false; -# Max entries in the logging throttler (1 entry per client) +# Max entries in the login throttler (1 entry per client) login-throttler-max-entries = 10000; +# Max external cover file size in MBytes +cover-max-file-size = 10; + +# Max cover cache size in MBytes +cover-max-cache-size = 30; diff --git a/docker/Dockerfile b/docker/Dockerfile index 71a27fc9..cbd32431 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -92,7 +92,7 @@ RUN \ RUN \ DIR=/tmp/wt && mkdir -p ${DIR} && cd ${DIR} && \ - cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${PREFIX} && \ + cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${PREFIX} -DBUILD_EXAMPLES=NO && \ make && \ make install @@ -108,6 +108,7 @@ RUN \ --prefix=${PREFIX} \ --enable-shared=yes \ --enable-static=no \ + --disable-openmp \ --with-quantum-depth=8 \ --with-modules=no \ --with-frozenpaths=no \ @@ -197,8 +198,7 @@ ARG RUNTIME_PACKAGES=" \ boost-system \ boost-thread \ libconfig++ \ - taglib \ - libgomp" + taglib" ARG LMS_USER=lms ARG LMS_GROUP=lms diff --git a/src/libs/av/impl/AvInfo.cpp b/src/libs/av/impl/AvInfo.cpp index aca3c045..0fdb8277 100644 --- a/src/libs/av/impl/AvInfo.cpp +++ b/src/libs/av/impl/AvInfo.cpp @@ -45,7 +45,7 @@ static std::string averror_to_string(int error) } MediaFileException::MediaFileException(int avError) -: AvException("MediaFileException: " + averror_to_string(avError)) +: AvException {"MediaFileException: " + averror_to_string(avError)} { } @@ -181,8 +181,8 @@ MediaFile::hasAttachedPictures(void) const return false; } -std::vector -MediaFile::getAttachedPictures(std::size_t nbMaxPictures) const +void +MediaFile::visitAttachedPictures(std::function func) const { static const std::map codecMimeMap = { @@ -194,8 +194,6 @@ MediaFile::getAttachedPictures(std::size_t nbMaxPictures) const { AV_CODEC_ID_PPM, "image/x-portable-pixmap" }, }; - std::vector pictures; - for (std::size_t i = 0; i < _context->nb_streams; ++i) { AVStream *avstream = _context->streams[i]; @@ -225,15 +223,11 @@ MediaFile::getAttachedPictures(std::size_t nbMaxPictures) const const AVPacket& pkt {avstream->attached_pic}; - std::copy(pkt.data, pkt.data + pkt.size, std::back_inserter(picture.data)); + picture.data = reinterpret_cast(pkt.data); + picture.dataSize = pkt.size; - pictures.push_back( picture ); - - if (pictures.size() >= nbMaxPictures) - break; + func(picture); } - - return pictures; } std::optional diff --git a/src/libs/av/include/av/AvInfo.hpp b/src/libs/av/include/av/AvInfo.hpp index d719ae02..60fe0844 100644 --- a/src/libs/av/include/av/AvInfo.hpp +++ b/src/libs/av/include/av/AvInfo.hpp @@ -23,6 +23,7 @@ #include #include +#include #include #include #include @@ -40,7 +41,8 @@ void AvInit(); struct Picture { std::string mimeType; - std::vector data; + const std::byte* data {}; + std::size_t dataSize; }; struct StreamInfo @@ -76,11 +78,11 @@ class MediaFile std::vector getStreamInfo() const; std::optional getBestStream() const; // none if failure/unknown bool hasAttachedPictures(void) const; - std::vector getAttachedPictures(std::size_t nbMaxPictures) const; + void visitAttachedPictures(std::function func) const; private: - std::filesystem::path _p; + const std::filesystem::path _p; AVFormatContext* _context {}; }; diff --git a/src/libs/cover/CMakeLists.txt b/src/libs/cover/CMakeLists.txt index 2e540725..f3a65928 100644 --- a/src/libs/cover/CMakeLists.txt +++ b/src/libs/cover/CMakeLists.txt @@ -1,5 +1,6 @@ add_library(lmscover SHARED + impl/CoverArt.cpp impl/CoverArtGrabber.cpp impl/Image.cpp ) diff --git a/src/libs/cover/impl/CoverArt.cpp b/src/libs/cover/impl/CoverArt.cpp new file mode 100644 index 00000000..2852fde9 --- /dev/null +++ b/src/libs/cover/impl/CoverArt.cpp @@ -0,0 +1,47 @@ +/* + * 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 "CoverArt.hpp" + +namespace CoverArt +{ + + CoverArt::CoverArt(EncodedImage image) + : _image {image} + {} + + const std::byte* + CoverArt::getData() const + { + return _image.getData(); + } + + 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/CoverArt.hpp b/src/libs/cover/impl/CoverArt.hpp new file mode 100644 index 00000000..b19685b3 --- /dev/null +++ b/src/libs/cover/impl/CoverArt.hpp @@ -0,0 +1,41 @@ +/* + * 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 "cover/ICoverArt.hpp" +#include "Image.hpp" + +namespace CoverArt +{ + + class CoverArt : public ICoverArt + { + public: + CoverArt(EncodedImage image); + + const std::byte* getData() const override; + std::size_t getDataSize() const override; + std::string_view getMimeType() const override; + + private: + EncodedImage _image; + }; + +} // namespace CoverArt diff --git a/src/libs/cover/impl/CoverArtGrabber.cpp b/src/libs/cover/impl/CoverArtGrabber.cpp index 64387d5a..25051c1b 100644 --- a/src/libs/cover/impl/CoverArtGrabber.cpp +++ b/src/libs/cover/impl/CoverArtGrabber.cpp @@ -28,6 +28,8 @@ #include "utils/Logger.hpp" #include "utils/Random.hpp" +#include "CoverArt.hpp" + namespace { bool @@ -40,89 +42,156 @@ isFileSupported(const std::filesystem::path& file, const std::vector createGrabber(const std::filesystem::path& execPath) +std::unique_ptr createGrabber(const std::filesystem::path& execPath, std::size_t maxCacheSize, std::size_t maxFileSize) { - return std::make_unique(execPath); + return std::make_unique(execPath, maxCacheSize, maxFileSize); } -Grabber::Grabber(const std::filesystem::path& execPath) +Grabber::Grabber(const std::filesystem::path& execPath, + std::size_t maxCacheSize, + std::size_t maxFileSize) + : _maxCacheSize {maxCacheSize} + , _maxFileSize {maxFileSize} { + LMS_LOG(COVER, INFO) << "Max cache size = " << _maxCacheSize; + LMS_LOG(COVER, INFO) << "Max file size = " << _maxFileSize; init(execPath); } void Grabber::setDefaultCover(const std::filesystem::path& p) { - _defaultCover = std::make_unique(); - if (!_defaultCover->load(p)) + try + { + RawImage defaultCover {p}; + _defaultCover = defaultCover.encode(); + LMS_LOG(COVER, INFO) << "Successfully read default cover image!"; + } + catch (const ImageException& e) + { throw LmsException("Cannot read default cover file '" + p.string() + "'"); + } } -static std::optional -getFromAvMediaFile(const Av::MediaFile& input) +static std::optional +getFromAvMediaFile(const Av::MediaFile& input, Width width) { - std::vector res; + std::optional image; - for (auto& picture : input.getAttachedPictures(2)) + input.visitAttachedPictures([&](const Av::Picture& picture) { - Image image; + if (image) + return; - if (image.load(picture.data)) - return image; - else - LMS_LOG(COVER, ERROR) << "Cannot load embedded cover file in '" << input.getPath().string() << "'"; + try + { + EncodedImage encodedImage {picture.data, picture.dataSize}; + + RawImage rawImage {encodedImage}; + rawImage.scale(width); + + image = rawImage.encode(); + } + catch (const ImageException& e) + { + LMS_LOG(COVER, ERROR) << "Cannot read embedded cover: " << e.what(); + } + }); + + return image; +} + +static std::optional +getFromFile(const std::filesystem::path& p, Width width) +{ + std::optional image; + + try + { + RawImage rawImage {p}; + rawImage.scale(width); + + image = rawImage.encode(); + } + catch (const ImageException& e) + { + LMS_LOG(COVER, ERROR) << "Cannot read cover in file '" << p.string() << "': " << e.what(); } - LMS_LOG(COVER, DEBUG) << "No cover found in media file '" << input.getPath().string() << "'"; - return std::nullopt; + return image; } -std::optional -Grabber::getFromDirectory(const std::filesystem::path& p, std::string_view preferredFileName) const +EncodedImage +Grabber::getDefault(Width width) +{ + { + std::shared_lock lock {_cacheMutex}; + + if (auto it {_defaultCache.find(width)}; it != std::cend(_defaultCache)) + return it->second; + } + + { + std::unique_lock lock {_cacheMutex}; + + if (auto it {_defaultCache.find(width)}; it != std::cend(_defaultCache)) + return it->second; + + RawImage rawImage {*_defaultCover}; + rawImage.scale(width); + EncodedImage res {rawImage.encode()}; + + _defaultCache[width] = res; + LMS_LOG(COVER, DEBUG) << "Default cache entries = " << _defaultCache.size(); + + return res; + } +} + +std::optional +Grabber::getFromDirectory(const std::filesystem::path& p, std::string_view preferredFileName, Width width) const { const std::multimap coverPaths {getCoverPaths(p)}; - auto tryLoadImage = [](const std::filesystem::path& p, Image& image) + auto tryLoadImageFromFilename = [&](std::string_view fileName) { - if (!image.load(p)) - { - LMS_LOG(COVER, ERROR) << "Cannot load image in file '" << p.string() << "'"; - return false; - } + std::optional image; - return true; - }; - - auto tryLoadImageFromFilename = [&](std::string_view fileName, Image& image) - { auto range {coverPaths.equal_range(std::string {fileName})}; for (auto it {range.first}; it != range.second; ++it) { - if (tryLoadImage(it->second, image)) - return true; + image = getFromFile(it->second, width); + if (!image) + continue; } - return false; + return image; }; - Image image; + std::optional image; - if (!preferredFileName.empty() && tryLoadImageFromFilename(preferredFileName, image)) - return image; + if (!preferredFileName.empty()) + { + image = tryLoadImageFromFilename(preferredFileName); + if (image) + return image; + } for (std::string_view filename : _preferredFileNames) { - if (tryLoadImageFromFilename(filename, image)) + image = tryLoadImageFromFilename(filename); + if (image) return image; } // Just pick one for (const auto& [filename, coverPath] : coverPaths) { - if (tryLoadImage(coverPath, image)) + image = getFromFile(coverPath, width); + if (image) return image; } - return std::nullopt; + return image; } std::multimap @@ -156,29 +225,32 @@ Grabber::getCoverPaths(const std::filesystem::path& directoryPath) const return res; } -std::optional -Grabber::getFromTrack(const std::filesystem::path& p) const +std::optional +Grabber::getFromTrack(const std::filesystem::path& p, Width width) const { + std::optional image; + try { - Av::MediaFile input(p); + Av::MediaFile input {p}; - return getFromAvMediaFile(input); + image = getFromAvMediaFile(input, width); } - catch (Av::MediaFileException& e) + catch (Av::AvException& e) { LMS_LOG(COVER, ERROR) << "Cannot get covers from track " << p.string() << ": " << e.what(); - return std::nullopt; } + + return image; } -Image -Grabber::getFromTrack(Database::Session& dbSession, Database::IdType trackId, std::size_t size) +EncodedImage +Grabber::getFromTrackInternal(Database::Session& dbSession, Database::IdType trackId, Width width) { using namespace Database; - const CacheEntryDesc cacheEntryDesc {CacheEntryDesc::Type::Track, trackId, size}; - std::optional cover {loadFromCache(cacheEntryDesc)}; + const CacheEntryDesc cacheEntryDesc {CacheEntryDesc::Type::Track, trackId, width}; + std::optional cover {loadFromCache(cacheEntryDesc)}; if (cover) return *cover; @@ -203,33 +275,30 @@ Grabber::getFromTrack(Database::Session& dbSession, Database::IdType trackId, st } if (hasCover) - cover = getFromTrack(trackPath); + cover = getFromTrack(trackPath, width); if (!cover) - cover = getFromDirectory(trackPath.parent_path(), trackPath.filename().replace_extension("").string()); + cover = getFromDirectory(trackPath.parent_path(), trackPath.filename().replace_extension("").string(), width); if (!cover && isMultiDisc) { if (trackPath.parent_path().has_parent_path()) - cover = getFromDirectory(trackPath.parent_path().parent_path(), {}); + cover = getFromDirectory(trackPath.parent_path().parent_path(), {}, width); } if (!cover) - cover = *_defaultCover; - - cover->scale(Geometry {size, size}); + cover = getDefault(width); saveToCache(cacheEntryDesc, *cover); return *cover; } - -Image -Grabber::getFromRelease(Database::Session& session, Database::IdType releaseId, std::size_t size) +EncodedImage +Grabber::getFromReleaseInternal(Database::Session& session, Database::IdType releaseId, Width width) { - const CacheEntryDesc cacheEntryDesc {CacheEntryDesc::Type::Release, releaseId, size}; - std::optional cover {loadFromCache(cacheEntryDesc)}; + const CacheEntryDesc cacheEntryDesc {CacheEntryDesc::Type::Release, releaseId, width}; + std::optional cover {loadFromCache(cacheEntryDesc)}; if (cover) return *cover; @@ -249,15 +318,11 @@ Grabber::getFromRelease(Database::Session& session, Database::IdType releaseId, if (trackId) { - cover = getFromTrack(session, *trackId, size); + cover = getFromTrackInternal(session, *trackId, width); } - else - { - if (!cover) - cover = *_defaultCover; - cover->scale(Geometry {size, size}); - } + if (!cover) + cover = getDefault(width); saveToCache(cacheEntryDesc, *cover); @@ -269,42 +334,43 @@ Grabber::flushCache() { std::unique_lock lock {_cacheMutex}; - LMS_LOG(COVER, DEBUG) << "Cache stats: hits = " << _cacheHits << ", misses = " << _cacheMisses; + LMS_LOG(COVER, DEBUG) << "Cache stats: hits = " << _cacheHits << ", misses = " << _cacheMisses << ", nb entries = " << _cache.size() << ", size = " << _cacheSize; _cacheHits = 0; _cacheMisses = 0; + _cacheSize = 0; _cache.clear(); } -std::vector -Grabber::getFromTrack(Database::Session& session, Database::IdType trackId, Format format, std::size_t width) +std::unique_ptr +Grabber::getFromTrack(Database::Session& session, Database::IdType trackId, std::size_t width) { - const Image cover {getFromTrack(session, trackId, width)}; - - assert(format == Format::JPEG); - return cover.save(format); + CoverArt toto {getFromTrackInternal(session, trackId, width)}; + return std::make_unique(getFromTrackInternal(session, trackId, width)); } -std::vector -Grabber::getFromRelease(Database::Session& session, Database::IdType releaseId, Format format, std::size_t width) +std::unique_ptr +Grabber::getFromRelease(Database::Session& session, Database::IdType releaseId, std::size_t width) { - const Image cover {getFromRelease(session, releaseId, width)}; - - assert(format == Format::JPEG); - return cover.save(format); + return std::make_unique(getFromReleaseInternal(session, releaseId, width)); } void -Grabber::saveToCache(const CacheEntryDesc& entryDesc, const Image& image) +Grabber::saveToCache(const CacheEntryDesc& entryDesc, const EncodedImage& image) { std::unique_lock lock {_cacheMutex}; - if (_cache.size() >= _maxCacheEntries) - _cache.erase(Random::pickRandom(_cache)); + while (_cacheSize + image.getDataSize() > _maxCacheSize && !_cache.empty()) + { + auto it {Random::pickRandom(_cache)}; + _cacheSize -= it->second.getDataSize(); + _cache.erase(it); + } + _cacheSize += image.getDataSize(); _cache[entryDesc] = image; } -std::optional +std::optional Grabber::loadFromCache(const CacheEntryDesc& entryDesc) { std::shared_lock lock {_cacheMutex}; diff --git a/src/libs/cover/impl/CoverArtGrabber.hpp b/src/libs/cover/impl/CoverArtGrabber.hpp index 25eac8cc..021d3e24 100644 --- a/src/libs/cover/impl/CoverArtGrabber.hpp +++ b/src/libs/cover/impl/CoverArtGrabber.hpp @@ -19,8 +19,8 @@ #pragma once +#include #include -#include #include #include #include @@ -83,7 +83,7 @@ namespace CoverArt class Grabber : public IGrabber { public: - Grabber(const std::filesystem::path& execPath); + Grabber(const std::filesystem::path& execPath, std::size_t maxCacheEntries, std::size_t maxFileSize); Grabber(const Grabber&) = delete; Grabber& operator=(const Grabber&) = delete; @@ -92,33 +92,36 @@ namespace CoverArt private: - void setDefaultCover(const std::filesystem::path& defaultCoverPath) override; + 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; + void flushCache() override; - std::vector getFromTrack(Database::Session& dbSession, Database::IdType trackId, Format format, std::size_t width) override; - std::vector getFromRelease(Database::Session& dbSession, Database::IdType releaseId, Format format, std::size_t 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); - - Image getFromTrack(Database::Session& dbSession, Database::IdType trackId, std::size_t size); - Image getFromRelease(Database::Session& dbSession, Database::IdType releaseId, std::size_t size); - - std::optional getFromTrack(const std::filesystem::path& path) const; + std::optional getFromTrack(const std::filesystem::path& path, Width width) const; std::multimap getCoverPaths(const std::filesystem::path& directoryPath) const; - std::optional getFromDirectory(const std::filesystem::path& path, std::string_view preferredFileName) const; + std::optional getFromDirectory(const std::filesystem::path& path, std::string_view preferredFileName, Width width) const; + EncodedImage getDefault(Width width); - std::unique_ptr _defaultCover; // unique_ptr to defer initializing + EncodedImage resizeCoverOrFallback(EncodedImage image, Width width) const; + + std::optional _defaultCover; // optional to defer initializing std::shared_mutex _cacheMutex; - std::unordered_map _cache; - std::size_t _cacheMisses {}; - std::size_t _cacheHits {}; + std::unordered_map _cache; + std::unordered_map _defaultCache; + std::atomic _cacheMisses {}; + std::atomic _cacheHits {}; + std::size_t _cacheSize {}; - void saveToCache(const CacheEntryDesc& entryDesc, const Image& image); - std::optional loadFromCache(const CacheEntryDesc& entryDesc); + void saveToCache(const CacheEntryDesc& entryDesc, const EncodedImage& image); + std::optional loadFromCache(const CacheEntryDesc& entryDesc); - static inline constexpr std::size_t _maxCacheEntries {1000}; + const std::size_t _maxCacheSize; static inline const std::vector _fileExtensions {".jpg", ".jpeg", ".png", ".bmp"}; // TODO parametrize - static inline constexpr std::size_t _maxFileSize {10000000}; + const std::size_t _maxFileSize; static inline const std::vector _preferredFileNames {"cover", "front"}; // TODO parametrize }; diff --git a/src/libs/cover/impl/Image.cpp b/src/libs/cover/impl/Image.cpp index f48c852f..c628b9fa 100644 --- a/src/libs/cover/impl/Image.cpp +++ b/src/libs/cover/impl/Image.cpp @@ -19,6 +19,11 @@ #include "Image.hpp" +#include +#include + +#include + #include "utils/Logger.hpp" namespace CoverArt { @@ -27,132 +32,118 @@ void init(const std::filesystem::path& path) { Magick::InitializeMagick(path.string().c_str()); + + if (!MagickLib::SetMagickResourceLimit(MagickLib::ThreadsResource, 1)) + LMS_LOG(COVER, ERROR) << "Cannot set Magick thread resource limit to 1!"; + + if (!MagickLib::SetMagickResourceLimit(MagickLib::DiskResource, 0)) + LMS_LOG(COVER, ERROR) << "Cannot set Magick disk resource limit to 0!"; + + LMS_LOG(COVER, INFO) << "Magick threads resource limit = " << GetMagickResourceLimit(MagickLib::ThreadsResource); + LMS_LOG(COVER, INFO) << "Magick Disk resource limit = " << GetMagickResourceLimit(MagickLib::DiskResource); } -static -std::string -formatToMagick(Format format) +EncodedImage::EncodedImage(const std::byte* data, std::size_t dataSize) +: _blob {data, dataSize} { - switch (format) - { - case Format::JPEG: return "JPEG"; - } - - return "JPEG"; } -std::string -formatToMimeType(Format format) +EncodedImage::EncodedImage(Magick::Blob blob) +: _blob {blob} { - switch (format) - { - case Format::JPEG: return "JPEG"; - } - - return "application/octet-stream"; } +const std::byte* +EncodedImage::getData() const +{ + return reinterpret_cast(_blob.data()); +} -bool -Image::load(const std::vector& rawData) +std::size_t +EncodedImage::getDataSize() const +{ + return _blob.length(); +} + +RawImage::RawImage(const std::filesystem::path& p) { try { - Magick::Blob blob {&rawData[0], rawData.size()}; - _image.read(blob); - - return true; + _image.read(p.string().c_str()); } catch (Magick::WarningCoder& e) { - return true; + LMS_LOG(COVER, WARNING) << "Caught Magick WarningCoder while loading image '" << p.string() << "': " << e.what(); + } + catch (Magick::Warning& e) + { + LMS_LOG(COVER, WARNING) << "Caught Magick warning while loading raw image '" << p.string() << "': " << 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(); - return false; + 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(); - return false; + throw ImageException {std::string {"Magick read error: "} + e.what()}; } } -bool -Image::load(const std::filesystem::path& p) +void +RawImage::scale(std::size_t width) { - try - { - _image.read(p.string()); - - return true; - } - catch (Magick::WarningCoder& e) - { - return true; - } - catch (Magick::Warning& e) - { - LMS_LOG(COVER, WARNING) << "Caught Magick warning while loading raw image: " << e.what(); - return false; - } - catch (Magick::Exception& e) - { - LMS_LOG(COVER, ERROR) << "Caught Magick exception while loading image from file '" << p.string() << "': " << e.what(); - return false; - } -} - -Geometry -Image::getSize() const -{ - Magick::Geometry geometry {_image.size()}; - return {geometry.width(), geometry.height()}; -} - -bool -Image::scale(Geometry geometry) -{ - if (geometry.width == 0 || geometry.height == 0) - return false; + if (width == 0) + throw ImageException {"Bad width = 0"}; try { - _image.resize( Magick::Geometry(geometry.width, geometry.height ) ); - - return true; + _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(); - return false; + throw ImageException {std::string {"Magick resize error: "} + e.what()}; } } -std::vector -Image::save(Format format) const +EncodedImage +RawImage::encode() const { - std::vector res; - try { Magick::Image outputImage {_image}; - outputImage.magick(formatToMagick(format)); + outputImage.magick("JPEG"); Magick::Blob blob; outputImage.write(&blob); - auto begin = static_cast(blob.data()); - std::copy(begin, begin + blob.length(), std::back_inserter(res)); - return res; + return EncodedImage {blob}; } catch (Magick::Exception& e) { - LMS_LOG(COVER, ERROR) << "Caught Magick exception during save:" << e.what(); - res.clear(); - return res; + LMS_LOG(COVER, ERROR) << "Caught Magick exception while encoding raw image: " << e.what(); + throw ImageException {std::string {"Magick encode error: "} + e.what()}; } } diff --git a/src/libs/cover/impl/Image.hpp b/src/libs/cover/impl/Image.hpp index 7abcb37c..d7091e0b 100644 --- a/src/libs/cover/impl/Image.hpp +++ b/src/libs/cover/impl/Image.hpp @@ -24,33 +24,50 @@ #include -#include "cover/CoverArt.hpp" +#include "utils/Exception.hpp" namespace CoverArt { - void init(const std::filesystem::path& path); - class Image + // internal use only + class ImageException : public LmsException { public: - - // input - bool load(const std::vector& rawData); - bool load(const std::filesystem::path& p); - - Geometry getSize() const; - - // Operations - bool scale(Geometry geometry); - - // output - std::vector save(Format format) const; - - private: - Magick::Image _image; + 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/include/cover/CoverArt.hpp b/src/libs/cover/include/cover/ICoverArt.hpp similarity index 69% rename from src/libs/cover/include/cover/CoverArt.hpp rename to src/libs/cover/include/cover/ICoverArt.hpp index db392e6f..902dbca1 100644 --- a/src/libs/cover/include/cover/CoverArt.hpp +++ b/src/libs/cover/include/cover/ICoverArt.hpp @@ -1,6 +1,5 @@ - /* - * Copyright (C) 2015 Emeric Poupon + * Copyright (C) 2020 Emeric Poupon * * This file is part of LMS. * @@ -20,21 +19,20 @@ #pragma once -#include +#include +#include namespace CoverArt { - enum class Format + class ICoverArt { - JPEG, - }; - std::string formatToMimeType(Format format); + public: + virtual ~ICoverArt() = default; - struct Geometry - { - std::size_t width; - std::size_t height; + virtual const std::byte* getData() const = 0; + virtual std::size_t getDataSize() const = 0; + virtual std::string_view getMimeType() const = 0; }; -} +} // namespace CoverArt diff --git a/src/libs/cover/include/cover/ICoverArtGrabber.hpp b/src/libs/cover/include/cover/ICoverArtGrabber.hpp index e9cda763..958128ce 100644 --- a/src/libs/cover/include/cover/ICoverArtGrabber.hpp +++ b/src/libs/cover/include/cover/ICoverArtGrabber.hpp @@ -19,32 +19,37 @@ #pragma once +#include #include -#include +#include +#include #include "database/Types.hpp" -#include "cover/CoverArt.hpp" +#include "cover/ICoverArt.hpp" -namespace Database { +namespace Database +{ class Session; } -namespace CoverArt { - -class IGrabber +namespace CoverArt { - public: - virtual ~IGrabber() = default; + using Width = std::size_t; - virtual void setDefaultCover(const std::filesystem::path& defaultCoverPath) = 0; + class IGrabber + { + public: + virtual ~IGrabber() = default; - virtual std::vector getFromTrack(Database::Session& dbSession, Database::IdType trackId, Format format, std::size_t width) = 0; - virtual std::vector getFromRelease(Database::Session& dbSession, Database::IdType releaseId, Format format, std::size_t width) = 0; + virtual void setDefaultCover(const std::filesystem::path& defaultCoverPath) = 0; - virtual void flushCache() = 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; -std::unique_ptr createGrabber(const std::filesystem::path& execPath); + virtual void flushCache() = 0; + }; + + std::unique_ptr createGrabber(const std::filesystem::path& execPath, std::size_t maxCacheEntries, std::size_t maxFileSize); } // namespace CoverArt diff --git a/src/libs/subsonic/impl/ParameterParsing.hpp b/src/libs/subsonic/impl/ParameterParsing.hpp index c5cb15f3..caf7f3c5 100644 --- a/src/libs/subsonic/impl/ParameterParsing.hpp +++ b/src/libs/subsonic/impl/ParameterParsing.hpp @@ -55,7 +55,7 @@ namespace API::Subsonic { std::vector res {getMultiParametersAs(parameterMap, param)}; if (res.empty()) - throw RequiredParameterMissingError {}; + throw RequiredParameterMissingError {param}; return res; } @@ -78,7 +78,7 @@ namespace API::Subsonic { auto res {getParameterAs(parameterMap, param)}; if (!res) - throw RequiredParameterMissingError {}; + throw RequiredParameterMissingError {param}; return *res; } diff --git a/src/libs/subsonic/impl/SubsonicResource.cpp b/src/libs/subsonic/impl/SubsonicResource.cpp index 4b5dddef..220bdbdf 100644 --- a/src/libs/subsonic/impl/SubsonicResource.cpp +++ b/src/libs/subsonic/impl/SubsonicResource.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Emeric Poupon + * copyright (c) 2019 emeric poupon * * This file is part of LMS. * @@ -546,7 +546,7 @@ handleCreatePlaylistRequest(RequestContext& context) throw BadParameterGenericError {"songId"}; if (!name && !id) - throw RequiredParameterMissingError {}; + throw RequiredParameterMissingError {"name or id"}; auto transaction {context.dbSession.createUniqueTransaction()}; @@ -1746,21 +1746,21 @@ 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::vector data; + std::unique_ptr cover; switch (id.type) { case Id::Type::Track: - data = Service::get()->getFromTrack(context.dbSession, id.value, CoverArt::Format::JPEG, size); + cover = Service::get()->getFromTrack(context.dbSession, id.value, size); break; case Id::Type::Release: - data = Service::get()->getFromRelease(context.dbSession, id.value, CoverArt::Format::JPEG, size); + cover = Service::get()->getFromRelease(context.dbSession, id.value, size); break; default: throw BadParameterGenericError {"id"}; } - response.out().write(reinterpret_cast(&data[0]), data.size()); - response.setMimeType(CoverArt::formatToMimeType(CoverArt::Format::JPEG)); + response.out().write(reinterpret_cast(cover->getData()), cover->getDataSize()); + response.setMimeType(std::string {cover->getMimeType()}); } using RequestHandlerFunc = std::function; diff --git a/src/libs/subsonic/impl/SubsonicResponse.hpp b/src/libs/subsonic/impl/SubsonicResponse.hpp index 11cc0ae3..76f05a2d 100644 --- a/src/libs/subsonic/impl/SubsonicResponse.hpp +++ b/src/libs/subsonic/impl/SubsonicResponse.hpp @@ -73,9 +73,14 @@ class GenericError : public Error class RequiredParameterMissingError : public Error { public: - RequiredParameterMissingError() : Error {Code::RequiredParameterMissing} {} + RequiredParameterMissingError(std::string_view param) + : Error {Code::RequiredParameterMissing} + , _param {param} + {} + private: - std::string getMessage() const override { return "Required parameter is missing."; } + std::string getMessage() const override { return "Required parameter '" + _param + "' is missing."; } + std::string _param; }; class ClientMustUpgradeError : public Error diff --git a/src/lms/main.cpp b/src/lms/main.cpp index 657cd868..fe3ab99d 100644 --- a/src/lms/main.cpp +++ b/src/lms/main.cpp @@ -168,7 +168,9 @@ int main(int argc, char* argv[]) // Service initialization order is important 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])}; + Service coverArtService {CoverArt::createGrabber(argv[0], + 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"); Service recommendationEngineService {Recommendation::createEngine(database)}; recommendationEngineService->requestLoad(); diff --git a/src/lms/ui/MediaPlayer.cpp b/src/lms/ui/MediaPlayer.cpp index e0c10cc2..bae72d2f 100644 --- a/src/lms/ui/MediaPlayer.cpp +++ b/src/lms/ui/MediaPlayer.cpp @@ -238,8 +238,6 @@ MediaPlayer::loadTrack(Database::IdType trackId, bool play, float replayGain) if (!track) return; - const std::string imgResourceMimeType {LmsApp->getImageResource()->getMimeType()}; - const std::string transcodeResource {LmsApp->getAudioTranscodeResource()->getUrl(trackId)}; const std::string nativeResource {LmsApp->getAudioFileResource()->getUrl(trackId)}; @@ -255,8 +253,8 @@ MediaPlayer::loadTrack(Database::IdType trackId, bool play, float replayGain) << " artist: \"" << (!artists.empty() ? StringUtils::jsEscape(artists.front()->getName()) : "") << "\"," << " release: \"" << (track->getRelease() ? StringUtils::jsEscape(track->getRelease()->getName()) : "") << "\"," << " artwork: [" - << " { src: \"" << LmsApp->getImageResource()->getTrackUrl(trackId, ImageResource::Size::Small) << "\", sizes: \"128x128\", type: \"" << imgResourceMimeType << "\" }," - << " { src: \"" << LmsApp->getImageResource()->getTrackUrl(trackId, ImageResource::Size::Large) << "\", sizes: \"512x512\", type: \"" << imgResourceMimeType << "\" }," + << " { src: \"" << LmsApp->getImageResource()->getTrackUrl(trackId, ImageResource::Size::Small) << "\", sizes: \"128x128\", type: \"image/jpeg\" }," + << " { src: \"" << LmsApp->getImageResource()->getTrackUrl(trackId, ImageResource::Size::Large) << "\", sizes: \"512x512\", type: \"image/jpeg\" }," << " ]" << "};"; oss << "LMS.mediaplayer.loadTrack(params, " << (play ? "true" : "false") << ")"; // true to autoplay diff --git a/src/lms/ui/resource/ImageResource.cpp b/src/lms/ui/resource/ImageResource.cpp index a99e7317..06e71610 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::vector cover; + std::unique_ptr cover; if (trackIdStr) { @@ -90,7 +90,7 @@ ImageResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Respons // DbSession are not thread safe { Wt::WApplication::UpdateLock lock {LmsApp}; - cover = Service::get()->getFromTrack(LmsApp->getDbSession(), *trackId, CoverArt::Format::JPEG, *size); + cover = Service::get()->getFromTrack(LmsApp->getDbSession(), *trackId, *size); } } else if (releaseIdStr) @@ -104,7 +104,7 @@ ImageResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Respons // DbSession are not thread safe { Wt::WApplication::UpdateLock lock {LmsApp}; - cover = Service::get()->getFromRelease(LmsApp->getDbSession(), *releaseId, CoverArt::Format::JPEG, *size); + cover = Service::get()->getFromRelease(LmsApp->getDbSession(), *releaseId, *size); } } else @@ -113,15 +113,9 @@ ImageResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Respons return; } - response.setMimeType(getMimeType()); + response.setMimeType(std::string {cover->getMimeType()}); - response.out().write(reinterpret_cast(&cover[0]), cover.size()); -} - -std::string -ImageResource::getMimeType() -{ - return CoverArt::formatToMimeType(CoverArt::Format::JPEG); + response.out().write(reinterpret_cast(cover->getData()), cover->getDataSize()); } } // namespace UserInterface diff --git a/src/lms/ui/resource/ImageResource.hpp b/src/lms/ui/resource/ImageResource.hpp index dc8756d2..ea6adbf1 100644 --- a/src/lms/ui/resource/ImageResource.hpp +++ b/src/lms/ui/resource/ImageResource.hpp @@ -45,8 +45,6 @@ class ImageResource : public Wt::WResource std::string getReleaseUrl(Database::IdType releaseId, Size size) const; std::string getTrackUrl(Database::IdType trackId, Size size) const; - static std::string getMimeType(); - private: void handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override;