Cover art handling: significantly reduce memory usage. fixes #93

This commit is contained in:
emeric
2020-10-12 13:30:34 +02:00
parent f6736bb919
commit e79dae48d1
21 changed files with 440 additions and 273 deletions
+1 -1
View File
@@ -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. A [demo](http://lms.demo.poupon.io) instance is available. Note the administration panel is not available.
## Main features ## 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 * Recommendation engine
* Audio transcode for maximum interoperability and low bandwith requirements * Audio transcode for maximum interoperability and low bandwith requirements
* Multi-value tags: artists, genres, ... * Multi-value tags: artists, genres, ...
+6 -1
View File
@@ -40,6 +40,11 @@ api-subsonic = true;
# Turn on this option to allow the demo account creation/use # Turn on this option to allow the demo account creation/use
demo = false; 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; 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;
+3 -3
View File
@@ -92,7 +92,7 @@ RUN \
RUN \ RUN \
DIR=/tmp/wt && mkdir -p ${DIR} && cd ${DIR} && \ 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 && \
make install make install
@@ -108,6 +108,7 @@ RUN \
--prefix=${PREFIX} \ --prefix=${PREFIX} \
--enable-shared=yes \ --enable-shared=yes \
--enable-static=no \ --enable-static=no \
--disable-openmp \
--with-quantum-depth=8 \ --with-quantum-depth=8 \
--with-modules=no \ --with-modules=no \
--with-frozenpaths=no \ --with-frozenpaths=no \
@@ -197,8 +198,7 @@ ARG RUNTIME_PACKAGES=" \
boost-system \ boost-system \
boost-thread \ boost-thread \
libconfig++ \ libconfig++ \
taglib \ taglib"
libgomp"
ARG LMS_USER=lms ARG LMS_USER=lms
ARG LMS_GROUP=lms ARG LMS_GROUP=lms
+6 -12
View File
@@ -45,7 +45,7 @@ static std::string averror_to_string(int error)
} }
MediaFileException::MediaFileException(int avError) 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; return false;
} }
std::vector<Picture> void
MediaFile::getAttachedPictures(std::size_t nbMaxPictures) const MediaFile::visitAttachedPictures(std::function<void(const Picture&)> func) const
{ {
static const std::map<int, std::string> codecMimeMap = static const std::map<int, std::string> codecMimeMap =
{ {
@@ -194,8 +194,6 @@ MediaFile::getAttachedPictures(std::size_t nbMaxPictures) const
{ AV_CODEC_ID_PPM, "image/x-portable-pixmap" }, { AV_CODEC_ID_PPM, "image/x-portable-pixmap" },
}; };
std::vector<Picture> pictures;
for (std::size_t i = 0; i < _context->nb_streams; ++i) for (std::size_t i = 0; i < _context->nb_streams; ++i)
{ {
AVStream *avstream = _context->streams[i]; AVStream *avstream = _context->streams[i];
@@ -225,15 +223,11 @@ MediaFile::getAttachedPictures(std::size_t nbMaxPictures) const
const AVPacket& pkt {avstream->attached_pic}; const AVPacket& pkt {avstream->attached_pic};
std::copy(pkt.data, pkt.data + pkt.size, std::back_inserter(picture.data)); picture.data = reinterpret_cast<const std::byte*>(pkt.data);
picture.dataSize = pkt.size;
pictures.push_back( picture ); func(picture);
if (pictures.size() >= nbMaxPictures)
break;
} }
return pictures;
} }
std::optional<MediaFileFormat> std::optional<MediaFileFormat>
+5 -3
View File
@@ -23,6 +23,7 @@
#include <chrono> #include <chrono>
#include <filesystem> #include <filesystem>
#include <functional>
#include <map> #include <map>
#include <optional> #include <optional>
#include <string> #include <string>
@@ -40,7 +41,8 @@ void AvInit();
struct Picture struct Picture
{ {
std::string mimeType; std::string mimeType;
std::vector<uint8_t> data; const std::byte* data {};
std::size_t dataSize;
}; };
struct StreamInfo struct StreamInfo
@@ -76,11 +78,11 @@ class MediaFile
std::vector<StreamInfo> getStreamInfo() const; std::vector<StreamInfo> getStreamInfo() const;
std::optional<std::size_t> getBestStream() const; // none if failure/unknown std::optional<std::size_t> getBestStream() const; // none if failure/unknown
bool hasAttachedPictures(void) const; bool hasAttachedPictures(void) const;
std::vector<Picture> getAttachedPictures(std::size_t nbMaxPictures) const; void visitAttachedPictures(std::function<void(const Picture&)> func) const;
private: private:
std::filesystem::path _p; const std::filesystem::path _p;
AVFormatContext* _context {}; AVFormatContext* _context {};
}; };
+1
View File
@@ -1,5 +1,6 @@
add_library(lmscover SHARED add_library(lmscover SHARED
impl/CoverArt.cpp
impl/CoverArtGrabber.cpp impl/CoverArtGrabber.cpp
impl/Image.cpp impl/Image.cpp
) )
+47
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#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
+41
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#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
+149 -83
View File
@@ -28,6 +28,8 @@
#include "utils/Logger.hpp" #include "utils/Logger.hpp"
#include "utils/Random.hpp" #include "utils/Random.hpp"
#include "CoverArt.hpp"
namespace { namespace {
bool bool
@@ -40,89 +42,156 @@ isFileSupported(const std::filesystem::path& file, const std::vector<std::filesy
namespace CoverArt { namespace CoverArt {
std::unique_ptr<IGrabber> createGrabber(const std::filesystem::path& execPath) std::unique_ptr<IGrabber> createGrabber(const std::filesystem::path& execPath, std::size_t maxCacheSize, std::size_t maxFileSize)
{ {
return std::make_unique<Grabber>(execPath); return std::make_unique<Grabber>(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); init(execPath);
} }
void void
Grabber::setDefaultCover(const std::filesystem::path& p) Grabber::setDefaultCover(const std::filesystem::path& p)
{ {
_defaultCover = std::make_unique<Image>(); try
if (!_defaultCover->load(p)) {
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() + "'"); throw LmsException("Cannot read default cover file '" + p.string() + "'");
}
} }
static std::optional<Image> static std::optional<EncodedImage>
getFromAvMediaFile(const Av::MediaFile& input) getFromAvMediaFile(const Av::MediaFile& input, Width width)
{ {
std::vector<Image> res; std::optional<EncodedImage> image;
for (auto& picture : input.getAttachedPictures(2)) input.visitAttachedPictures([&](const Av::Picture& picture)
{ {
Image image; if (image)
return;
if (image.load(picture.data)) try
return image; {
else EncodedImage encodedImage {picture.data, picture.dataSize};
LMS_LOG(COVER, ERROR) << "Cannot load embedded cover file in '" << input.getPath().string() << "'";
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<EncodedImage>
getFromFile(const std::filesystem::path& p, Width width)
{
std::optional<EncodedImage> 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 image;
return std::nullopt;
} }
std::optional<Image> EncodedImage
Grabber::getFromDirectory(const std::filesystem::path& p, std::string_view preferredFileName) const 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<EncodedImage>
Grabber::getFromDirectory(const std::filesystem::path& p, std::string_view preferredFileName, Width width) const
{ {
const std::multimap<std::string, std::filesystem::path> coverPaths {getCoverPaths(p)}; const std::multimap<std::string, std::filesystem::path> coverPaths {getCoverPaths(p)};
auto tryLoadImage = [](const std::filesystem::path& p, Image& image) auto tryLoadImageFromFilename = [&](std::string_view fileName)
{ {
if (!image.load(p)) std::optional<EncodedImage> image;
{
LMS_LOG(COVER, ERROR) << "Cannot load image in file '" << p.string() << "'";
return false;
}
return true;
};
auto tryLoadImageFromFilename = [&](std::string_view fileName, Image& image)
{
auto range {coverPaths.equal_range(std::string {fileName})}; auto range {coverPaths.equal_range(std::string {fileName})};
for (auto it {range.first}; it != range.second; ++it) for (auto it {range.first}; it != range.second; ++it)
{ {
if (tryLoadImage(it->second, image)) image = getFromFile(it->second, width);
return true; if (!image)
continue;
} }
return false; return image;
}; };
Image image; std::optional<EncodedImage> image;
if (!preferredFileName.empty() && tryLoadImageFromFilename(preferredFileName, image)) if (!preferredFileName.empty())
return image; {
image = tryLoadImageFromFilename(preferredFileName);
if (image)
return image;
}
for (std::string_view filename : _preferredFileNames) for (std::string_view filename : _preferredFileNames)
{ {
if (tryLoadImageFromFilename(filename, image)) image = tryLoadImageFromFilename(filename);
if (image)
return image; return image;
} }
// Just pick one // Just pick one
for (const auto& [filename, coverPath] : coverPaths) for (const auto& [filename, coverPath] : coverPaths)
{ {
if (tryLoadImage(coverPath, image)) image = getFromFile(coverPath, width);
if (image)
return image; return image;
} }
return std::nullopt; return image;
} }
std::multimap<std::string, std::filesystem::path> std::multimap<std::string, std::filesystem::path>
@@ -156,29 +225,32 @@ Grabber::getCoverPaths(const std::filesystem::path& directoryPath) const
return res; return res;
} }
std::optional<Image> std::optional<EncodedImage>
Grabber::getFromTrack(const std::filesystem::path& p) const Grabber::getFromTrack(const std::filesystem::path& p, Width width) const
{ {
std::optional<EncodedImage> image;
try 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(); LMS_LOG(COVER, ERROR) << "Cannot get covers from track " << p.string() << ": " << e.what();
return std::nullopt;
} }
return image;
} }
Image EncodedImage
Grabber::getFromTrack(Database::Session& dbSession, Database::IdType trackId, std::size_t size) Grabber::getFromTrackInternal(Database::Session& dbSession, Database::IdType trackId, Width width)
{ {
using namespace Database; using namespace Database;
const CacheEntryDesc cacheEntryDesc {CacheEntryDesc::Type::Track, trackId, size}; const CacheEntryDesc cacheEntryDesc {CacheEntryDesc::Type::Track, trackId, width};
std::optional<Image> cover {loadFromCache(cacheEntryDesc)}; std::optional<EncodedImage> cover {loadFromCache(cacheEntryDesc)};
if (cover) if (cover)
return *cover; return *cover;
@@ -203,33 +275,30 @@ Grabber::getFromTrack(Database::Session& dbSession, Database::IdType trackId, st
} }
if (hasCover) if (hasCover)
cover = getFromTrack(trackPath); cover = getFromTrack(trackPath, width);
if (!cover) 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 (!cover && isMultiDisc)
{ {
if (trackPath.parent_path().has_parent_path()) if (trackPath.parent_path().has_parent_path())
cover = getFromDirectory(trackPath.parent_path().parent_path(), {}); cover = getFromDirectory(trackPath.parent_path().parent_path(), {}, width);
} }
if (!cover) if (!cover)
cover = *_defaultCover; cover = getDefault(width);
cover->scale(Geometry {size, size});
saveToCache(cacheEntryDesc, *cover); saveToCache(cacheEntryDesc, *cover);
return *cover; return *cover;
} }
EncodedImage
Image Grabber::getFromReleaseInternal(Database::Session& session, Database::IdType releaseId, Width width)
Grabber::getFromRelease(Database::Session& session, Database::IdType releaseId, std::size_t size)
{ {
const CacheEntryDesc cacheEntryDesc {CacheEntryDesc::Type::Release, releaseId, size}; const CacheEntryDesc cacheEntryDesc {CacheEntryDesc::Type::Release, releaseId, width};
std::optional<Image> cover {loadFromCache(cacheEntryDesc)}; std::optional<EncodedImage> cover {loadFromCache(cacheEntryDesc)};
if (cover) if (cover)
return *cover; return *cover;
@@ -249,15 +318,11 @@ Grabber::getFromRelease(Database::Session& session, Database::IdType releaseId,
if (trackId) 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); saveToCache(cacheEntryDesc, *cover);
@@ -269,42 +334,43 @@ Grabber::flushCache()
{ {
std::unique_lock lock {_cacheMutex}; 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; _cacheHits = 0;
_cacheMisses = 0; _cacheMisses = 0;
_cacheSize = 0;
_cache.clear(); _cache.clear();
} }
std::vector<uint8_t> std::unique_ptr<ICoverArt>
Grabber::getFromTrack(Database::Session& session, Database::IdType trackId, Format format, std::size_t width) Grabber::getFromTrack(Database::Session& session, Database::IdType trackId, std::size_t width)
{ {
const Image cover {getFromTrack(session, trackId, width)}; CoverArt toto {getFromTrackInternal(session, trackId, width)};
return std::make_unique<CoverArt>(getFromTrackInternal(session, trackId, width));
assert(format == Format::JPEG);
return cover.save(format);
} }
std::vector<uint8_t> std::unique_ptr<ICoverArt>
Grabber::getFromRelease(Database::Session& session, Database::IdType releaseId, Format format, std::size_t width) Grabber::getFromRelease(Database::Session& session, Database::IdType releaseId, std::size_t width)
{ {
const Image cover {getFromRelease(session, releaseId, width)}; return std::make_unique<CoverArt>(getFromReleaseInternal(session, releaseId, width));
assert(format == Format::JPEG);
return cover.save(format);
} }
void void
Grabber::saveToCache(const CacheEntryDesc& entryDesc, const Image& image) Grabber::saveToCache(const CacheEntryDesc& entryDesc, const EncodedImage& image)
{ {
std::unique_lock lock {_cacheMutex}; std::unique_lock lock {_cacheMutex};
if (_cache.size() >= _maxCacheEntries) while (_cacheSize + image.getDataSize() > _maxCacheSize && !_cache.empty())
_cache.erase(Random::pickRandom(_cache)); {
auto it {Random::pickRandom(_cache)};
_cacheSize -= it->second.getDataSize();
_cache.erase(it);
}
_cacheSize += image.getDataSize();
_cache[entryDesc] = image; _cache[entryDesc] = image;
} }
std::optional<Image> std::optional<EncodedImage>
Grabber::loadFromCache(const CacheEntryDesc& entryDesc) Grabber::loadFromCache(const CacheEntryDesc& entryDesc)
{ {
std::shared_lock lock {_cacheMutex}; std::shared_lock lock {_cacheMutex};
+23 -20
View File
@@ -19,8 +19,8 @@
#pragma once #pragma once
#include <atomic>
#include <filesystem> #include <filesystem>
#include <map>
#include <optional> #include <optional>
#include <shared_mutex> #include <shared_mutex>
#include <string_view> #include <string_view>
@@ -83,7 +83,7 @@ namespace CoverArt
class Grabber : public IGrabber class Grabber : public IGrabber
{ {
public: 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(const Grabber&) = delete;
Grabber& operator=(const Grabber&) = delete; Grabber& operator=(const Grabber&) = delete;
@@ -92,33 +92,36 @@ namespace CoverArt
private: private:
void setDefaultCover(const std::filesystem::path& defaultCoverPath) override; void setDefaultCover(const std::filesystem::path& defaultCoverPath) override;
std::unique_ptr<ICoverArt> getFromTrack(Database::Session& dbSession, Database::IdType trackId, Width width) override;
std::unique_ptr<ICoverArt> getFromRelease(Database::Session& dbSession, Database::IdType releaseId, Width width) override;
void flushCache() override;
std::vector<uint8_t> getFromTrack(Database::Session& dbSession, Database::IdType trackId, Format format, std::size_t width) override; EncodedImage getFromTrackInternal(Database::Session& dbSession, Database::IdType trackId, Width width);
std::vector<uint8_t> getFromRelease(Database::Session& dbSession, Database::IdType releaseId, Format format, std::size_t width) override; EncodedImage getFromReleaseInternal(Database::Session& dbSession, Database::IdType releaseId, Width width);
void flushCache() override;
std::optional<EncodedImage> getFromTrack(const std::filesystem::path& path, Width width) const;
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<Image> getFromTrack(const std::filesystem::path& path) const;
std::multimap<std::string, std::filesystem::path> getCoverPaths(const std::filesystem::path& directoryPath) const; std::multimap<std::string, std::filesystem::path> getCoverPaths(const std::filesystem::path& directoryPath) const;
std::optional<Image> getFromDirectory(const std::filesystem::path& path, std::string_view preferredFileName) const; std::optional<EncodedImage> getFromDirectory(const std::filesystem::path& path, std::string_view preferredFileName, Width width) const;
EncodedImage getDefault(Width width);
std::unique_ptr<Image> _defaultCover; // unique_ptr to defer initializing EncodedImage resizeCoverOrFallback(EncodedImage image, Width width) const;
std::optional<EncodedImage> _defaultCover; // optional to defer initializing
std::shared_mutex _cacheMutex; std::shared_mutex _cacheMutex;
std::unordered_map<CacheEntryDesc, Image> _cache; std::unordered_map<CacheEntryDesc, EncodedImage> _cache;
std::size_t _cacheMisses {}; std::unordered_map<Width, EncodedImage> _defaultCache;
std::size_t _cacheHits {}; std::atomic<std::size_t> _cacheMisses {};
std::atomic<std::size_t> _cacheHits {};
std::size_t _cacheSize {};
void saveToCache(const CacheEntryDesc& entryDesc, const Image& image); void saveToCache(const CacheEntryDesc& entryDesc, const EncodedImage& image);
std::optional<Image> loadFromCache(const CacheEntryDesc& entryDesc); std::optional<EncodedImage> loadFromCache(const CacheEntryDesc& entryDesc);
static inline constexpr std::size_t _maxCacheEntries {1000}; const std::size_t _maxCacheSize;
static inline const std::vector<std::filesystem::path> _fileExtensions {".jpg", ".jpeg", ".png", ".bmp"}; // TODO parametrize static inline const std::vector<std::filesystem::path> _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<std::string> _preferredFileNames {"cover", "front"}; // TODO parametrize static inline const std::vector<std::string> _preferredFileNames {"cover", "front"}; // TODO parametrize
}; };
+68 -77
View File
@@ -19,6 +19,11 @@
#include "Image.hpp" #include "Image.hpp"
#include <atomic>
#include <fstream>
#include <magick/resource.h>
#include "utils/Logger.hpp" #include "utils/Logger.hpp"
namespace CoverArt { namespace CoverArt {
@@ -27,132 +32,118 @@ void
init(const std::filesystem::path& path) init(const std::filesystem::path& path)
{ {
Magick::InitializeMagick(path.string().c_str()); 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 EncodedImage::EncodedImage(const std::byte* data, std::size_t dataSize)
std::string : _blob {data, dataSize}
formatToMagick(Format format)
{ {
switch (format)
{
case Format::JPEG: return "JPEG";
}
return "JPEG";
} }
std::string EncodedImage::EncodedImage(Magick::Blob blob)
formatToMimeType(Format format) : _blob {blob}
{ {
switch (format)
{
case Format::JPEG: return "JPEG";
}
return "application/octet-stream";
} }
const std::byte*
EncodedImage::getData() const
{
return reinterpret_cast<const std::byte*>(_blob.data());
}
bool std::size_t
Image::load(const std::vector<unsigned char>& rawData) EncodedImage::getDataSize() const
{
return _blob.length();
}
RawImage::RawImage(const std::filesystem::path& p)
{ {
try try
{ {
Magick::Blob blob {&rawData[0], rawData.size()}; _image.read(p.string().c_str());
_image.read(blob);
return true;
} }
catch (Magick::WarningCoder& e) 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) catch (Magick::Warning& e)
{ {
LMS_LOG(COVER, WARNING) << "Caught Magick warning while loading raw image: " << e.what(); 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) catch (Magick::Exception& e)
{ {
LMS_LOG(COVER, ERROR) << "Caught Magick exception while loading raw image: " << e.what(); 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 void
Image::load(const std::filesystem::path& p) RawImage::scale(std::size_t width)
{ {
try if (width == 0)
{ throw ImageException {"Bad width = 0"};
_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;
try try
{ {
_image.resize( Magick::Geometry(geometry.width, geometry.height ) ); _image.resize(Magick::Geometry {static_cast<unsigned int>(width), static_cast<unsigned int>(width)});
return true;
} }
catch (Magick::Exception& e) catch (Magick::Exception& e)
{ {
LMS_LOG(COVER, ERROR) << "Caught Magick exception during scale: " << e.what(); LMS_LOG(COVER, ERROR) << "Caught Magick exception during scale: " << e.what();
return false; throw ImageException {std::string {"Magick resize error: "} + e.what()};
} }
} }
std::vector<uint8_t> EncodedImage
Image::save(Format format) const RawImage::encode() const
{ {
std::vector<uint8_t> res;
try try
{ {
Magick::Image outputImage {_image}; Magick::Image outputImage {_image};
outputImage.magick(formatToMagick(format)); outputImage.magick("JPEG");
Magick::Blob blob; Magick::Blob blob;
outputImage.write(&blob); outputImage.write(&blob);
auto begin = static_cast<const uint8_t*>(blob.data()); return EncodedImage {blob};
std::copy(begin, begin + blob.length(), std::back_inserter(res));
return res;
} }
catch (Magick::Exception& e) catch (Magick::Exception& e)
{ {
LMS_LOG(COVER, ERROR) << "Caught Magick exception during save:" << e.what(); LMS_LOG(COVER, ERROR) << "Caught Magick exception while encoding raw image: " << e.what();
res.clear(); throw ImageException {std::string {"Magick encode error: "} + e.what()};
return res;
} }
} }
+35 -18
View File
@@ -24,33 +24,50 @@
#include <Magick++.h> #include <Magick++.h>
#include "cover/CoverArt.hpp" #include "utils/Exception.hpp"
namespace CoverArt namespace CoverArt
{ {
void init(const std::filesystem::path& path); void init(const std::filesystem::path& path);
class Image // internal use only
class ImageException : public LmsException
{ {
public: public:
using LmsException::LmsException;
// input
bool load(const std::vector<unsigned char>& rawData);
bool load(const std::filesystem::path& p);
Geometry getSize() const;
// Operations
bool scale(Geometry geometry);
// output
std::vector<uint8_t> save(Format format) const;
private:
Magick::Image _image;
}; };
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 } // namespace CoverArt
@@ -1,6 +1,5 @@
/* /*
* Copyright (C) 2015 Emeric Poupon * Copyright (C) 2020 Emeric Poupon
* *
* This file is part of LMS. * This file is part of LMS.
* *
@@ -20,21 +19,20 @@
#pragma once #pragma once
#include <string> #include <cstddef>
#include <string_view>
namespace CoverArt namespace CoverArt
{ {
enum class Format class ICoverArt
{ {
JPEG, public:
}; virtual ~ICoverArt() = default;
std::string formatToMimeType(Format format);
struct Geometry virtual const std::byte* getData() const = 0;
{ virtual std::size_t getDataSize() const = 0;
std::size_t width; virtual std::string_view getMimeType() const = 0;
std::size_t height;
}; };
} } // namespace CoverArt
@@ -19,32 +19,37 @@
#pragma once #pragma once
#include <cstddef>
#include <filesystem> #include <filesystem>
#include <vector> #include <memory>
#include <string_view>
#include "database/Types.hpp" #include "database/Types.hpp"
#include "cover/CoverArt.hpp" #include "cover/ICoverArt.hpp"
namespace Database { namespace Database
{
class Session; class Session;
} }
namespace CoverArt { namespace CoverArt
class IGrabber
{ {
public: using Width = std::size_t;
virtual ~IGrabber() = default;
virtual void setDefaultCover(const std::filesystem::path& defaultCoverPath) = 0; class IGrabber
{
public:
virtual ~IGrabber() = default;
virtual std::vector<uint8_t> getFromTrack(Database::Session& dbSession, Database::IdType trackId, Format format, std::size_t width) = 0; virtual void setDefaultCover(const std::filesystem::path& defaultCoverPath) = 0;
virtual std::vector<uint8_t> getFromRelease(Database::Session& dbSession, Database::IdType releaseId, Format format, std::size_t width) = 0;
virtual void flushCache() = 0; virtual std::unique_ptr<ICoverArt> getFromTrack(Database::Session& dbSession, Database::IdType trackId, Width width) = 0;
}; virtual std::unique_ptr<ICoverArt> getFromRelease(Database::Session& dbSession, Database::IdType releaseId, Width width) = 0;
std::unique_ptr<IGrabber> createGrabber(const std::filesystem::path& execPath); virtual void flushCache() = 0;
};
std::unique_ptr<IGrabber> createGrabber(const std::filesystem::path& execPath, std::size_t maxCacheEntries, std::size_t maxFileSize);
} // namespace CoverArt } // namespace CoverArt
+2 -2
View File
@@ -55,7 +55,7 @@ namespace API::Subsonic
{ {
std::vector<T> res {getMultiParametersAs<T>(parameterMap, param)}; std::vector<T> res {getMultiParametersAs<T>(parameterMap, param)};
if (res.empty()) if (res.empty())
throw RequiredParameterMissingError {}; throw RequiredParameterMissingError {param};
return res; return res;
} }
@@ -78,7 +78,7 @@ namespace API::Subsonic
{ {
auto res {getParameterAs<T>(parameterMap, param)}; auto res {getParameterAs<T>(parameterMap, param)};
if (!res) if (!res)
throw RequiredParameterMissingError {}; throw RequiredParameterMissingError {param};
return *res; return *res;
} }
+7 -7
View File
@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2019 Emeric Poupon * copyright (c) 2019 emeric poupon
* *
* This file is part of LMS. * This file is part of LMS.
* *
@@ -546,7 +546,7 @@ handleCreatePlaylistRequest(RequestContext& context)
throw BadParameterGenericError {"songId"}; throw BadParameterGenericError {"songId"};
if (!name && !id) if (!name && !id)
throw RequiredParameterMissingError {}; throw RequiredParameterMissingError {"name or id"};
auto transaction {context.dbSession.createUniqueTransaction()}; auto transaction {context.dbSession.createUniqueTransaction()};
@@ -1746,21 +1746,21 @@ handleGetCoverArt(RequestContext& context, const Wt::Http::Request& /*request*/,
std::size_t size {getParameterAs<std::size_t>(context.parameters, "size").value_or(256)}; std::size_t size {getParameterAs<std::size_t>(context.parameters, "size").value_or(256)};
size = clamp(size, std::size_t {32}, std::size_t {1024}); size = clamp(size, std::size_t {32}, std::size_t {1024});
std::vector<unsigned char> data; std::unique_ptr<CoverArt::ICoverArt> cover;
switch (id.type) switch (id.type)
{ {
case Id::Type::Track: case Id::Type::Track:
data = Service<CoverArt::IGrabber>::get()->getFromTrack(context.dbSession, id.value, CoverArt::Format::JPEG, size); cover = Service<CoverArt::IGrabber>::get()->getFromTrack(context.dbSession, id.value, size);
break; break;
case Id::Type::Release: case Id::Type::Release:
data = Service<CoverArt::IGrabber>::get()->getFromRelease(context.dbSession, id.value, CoverArt::Format::JPEG, size); cover = Service<CoverArt::IGrabber>::get()->getFromRelease(context.dbSession, id.value, size);
break; break;
default: default:
throw BadParameterGenericError {"id"}; throw BadParameterGenericError {"id"};
} }
response.out().write(reinterpret_cast<const char*>(&data[0]), data.size()); response.out().write(reinterpret_cast<const char*>(cover->getData()), cover->getDataSize());
response.setMimeType(CoverArt::formatToMimeType(CoverArt::Format::JPEG)); response.setMimeType(std::string {cover->getMimeType()});
} }
using RequestHandlerFunc = std::function<Response(RequestContext& context)>; using RequestHandlerFunc = std::function<Response(RequestContext& context)>;
+7 -2
View File
@@ -73,9 +73,14 @@ class GenericError : public Error
class RequiredParameterMissingError : public Error class RequiredParameterMissingError : public Error
{ {
public: public:
RequiredParameterMissingError() : Error {Code::RequiredParameterMissing} {} RequiredParameterMissingError(std::string_view param)
: Error {Code::RequiredParameterMissing}
, _param {param}
{}
private: 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 class ClientMustUpgradeError : public Error
+3 -1
View File
@@ -168,7 +168,9 @@ int main(int argc, char* argv[])
// Service initialization order is important // Service initialization order is important
Service<Auth::IAuthTokenService> authTokenService {Auth::createAuthTokenService(config->getULong("login-throttler-max-entriees", 10000))}; Service<Auth::IAuthTokenService> authTokenService {Auth::createAuthTokenService(config->getULong("login-throttler-max-entriees", 10000))};
Service<Auth::IPasswordService> passwordService {Auth::createPasswordService(config->getULong("login-throttler-max-entriees", 10000))}; Service<Auth::IPasswordService> passwordService {Auth::createPasswordService(config->getULong("login-throttler-max-entriees", 10000))};
Service<CoverArt::IGrabber> coverArtService {CoverArt::createGrabber(argv[0])}; Service<CoverArt::IGrabber> 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"); coverArtService->setDefaultCover(server.appRoot() + "/images/unknown-cover.jpg");
Service<Recommendation::IEngine> recommendationEngineService {Recommendation::createEngine(database)}; Service<Recommendation::IEngine> recommendationEngineService {Recommendation::createEngine(database)};
recommendationEngineService->requestLoad(); recommendationEngineService->requestLoad();
+2 -4
View File
@@ -238,8 +238,6 @@ MediaPlayer::loadTrack(Database::IdType trackId, bool play, float replayGain)
if (!track) if (!track)
return; return;
const std::string imgResourceMimeType {LmsApp->getImageResource()->getMimeType()};
const std::string transcodeResource {LmsApp->getAudioTranscodeResource()->getUrl(trackId)}; const std::string transcodeResource {LmsApp->getAudioTranscodeResource()->getUrl(trackId)};
const std::string nativeResource {LmsApp->getAudioFileResource()->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()) : "") << "\"," << " artist: \"" << (!artists.empty() ? StringUtils::jsEscape(artists.front()->getName()) : "") << "\","
<< " release: \"" << (track->getRelease() ? StringUtils::jsEscape(track->getRelease()->getName()) : "") << "\"," << " release: \"" << (track->getRelease() ? StringUtils::jsEscape(track->getRelease()->getName()) : "") << "\","
<< " artwork: [" << " artwork: ["
<< " { src: \"" << LmsApp->getImageResource()->getTrackUrl(trackId, ImageResource::Size::Small) << "\", sizes: \"128x128\", 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: \"" << imgResourceMimeType << "\" }," << " { src: \"" << LmsApp->getImageResource()->getTrackUrl(trackId, ImageResource::Size::Large) << "\", sizes: \"512x512\", type: \"image/jpeg\" },"
<< " ]" << " ]"
<< "};"; << "};";
oss << "LMS.mediaplayer.loadTrack(params, " << (play ? "true" : "false") << ")"; // true to autoplay oss << "LMS.mediaplayer.loadTrack(params, " << (play ? "true" : "false") << ")"; // true to autoplay
+5 -11
View File
@@ -74,7 +74,7 @@ ImageResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Respons
return; return;
} }
std::vector<uint8_t> cover; std::unique_ptr<CoverArt::ICoverArt> cover;
if (trackIdStr) if (trackIdStr)
{ {
@@ -90,7 +90,7 @@ ImageResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Respons
// DbSession are not thread safe // DbSession are not thread safe
{ {
Wt::WApplication::UpdateLock lock {LmsApp}; Wt::WApplication::UpdateLock lock {LmsApp};
cover = Service<CoverArt::IGrabber>::get()->getFromTrack(LmsApp->getDbSession(), *trackId, CoverArt::Format::JPEG, *size); cover = Service<CoverArt::IGrabber>::get()->getFromTrack(LmsApp->getDbSession(), *trackId, *size);
} }
} }
else if (releaseIdStr) else if (releaseIdStr)
@@ -104,7 +104,7 @@ ImageResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Respons
// DbSession are not thread safe // DbSession are not thread safe
{ {
Wt::WApplication::UpdateLock lock {LmsApp}; Wt::WApplication::UpdateLock lock {LmsApp};
cover = Service<CoverArt::IGrabber>::get()->getFromRelease(LmsApp->getDbSession(), *releaseId, CoverArt::Format::JPEG, *size); cover = Service<CoverArt::IGrabber>::get()->getFromRelease(LmsApp->getDbSession(), *releaseId, *size);
} }
} }
else else
@@ -113,15 +113,9 @@ ImageResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Respons
return; return;
} }
response.setMimeType(getMimeType()); response.setMimeType(std::string {cover->getMimeType()});
response.out().write(reinterpret_cast<const char *>(&cover[0]), cover.size()); response.out().write(reinterpret_cast<const char *>(cover->getData()), cover->getDataSize());
}
std::string
ImageResource::getMimeType()
{
return CoverArt::formatToMimeType(CoverArt::Format::JPEG);
} }
} // namespace UserInterface } // namespace UserInterface
-2
View File
@@ -45,8 +45,6 @@ class ImageResource : public Wt::WResource
std::string getReleaseUrl(Database::IdType releaseId, Size size) const; std::string getReleaseUrl(Database::IdType releaseId, Size size) const;
std::string getTrackUrl(Database::IdType trackId, Size size) const; std::string getTrackUrl(Database::IdType trackId, Size size) const;
static std::string getMimeType();
private: private:
void handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override; void handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override;