diff --git a/README.md b/README.md index 7318abd0..3c17b6be 100644 --- a/README.md +++ b/README.md @@ -4,42 +4,38 @@ _LMS_ is a self-hosted music streaming software: access your music collection from anywhere using a web interface! -A [demo](http://lms.demo.poupon.io) instance is available, with the following limitations: -- Settings cannot be saved -- No persistent playqueue -- No administration panel +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+, using less than 10% of total memory even when transcoding) -* User management +* 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, ... +* Custom tags support: _mood_, _genre_, _albummood_, _albumgrouping_, ... +* Compilation support +* [MusicBrainz Identifier](https://musicbrainz.org/doc/MusicBrainz_Identifier) support to handle duplicated artist and release names +* Disc subtitles support * ReplayGain support * Persistent play queue across sessions -* Compilation support -* Multi-value tags: artists, genres, ... -* Custom tags (ex: _mood_, _genre_, _albummood_, _albumgrouping_, ...) -* MusicBrainzID support to handle duplicated artist and release names -* Disc subtitles support * _Systemd_ integration -* _PAM_ authentication backend +* User management + * with optional _PAM_ authentication backend * Subsonic API, with the following additional features: * Playlists - * Starred Album/Artist/Tracks * Bookmarks ## Music discovery - _LMS_ provides several ways to help you find the music you like: * Tag-based filters (ex: _Rock_, _Metal_ and _Aggressive_, _Electronic_ and _Relaxed_, ...) * Recommendations for similar artists and albums * Radio mode, based on what is in the current playqueue * Searches in album, artist and track names (including sort names) -* Random/Most played/Recently played/Recently added for Artist/Albums/Tracks, allowing you to search for things like: +* Starred Albums/Artists/Tracks +* Random/Starred/Most played/Recently played/Recently added for Artist/Albums/Tracks, allowing you to search for things like: * Recently added _Electronic_ artists * Random _Metal_ and _Aggressive_ albums * Most played _Relaxed_ tracks + * Starred _Jazz_ albums * ... The recommendation engine uses two different sources: @@ -49,7 +45,7 @@ The recommendation engine uses two different sources: __Notes on the self-organizing map__: * training the map requires significant computation time on large collections (ex: half an hour for 40k tracks) * audio acoustic data is pulled from [AcousticBrainz](https://acousticbrainz.org/). Therefore your audio files _must_ contain the [MusicBrainz Identifier](https://musicbrainz.org/doc/MusicBrainz_Identifier). -* to enable the audio similarity source, you have to enable it first in the settings panel. +* to enable the audio similarity source, you have to enable it first in the administration panel. ## Subsonic API The API version implemented is 1.12.0 and has been tested on _Android_ using the official application, _Ultrasonic_ and _DSub_. 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/libs/utils/impl/Zipper.cpp b/src/libs/utils/impl/Zipper.cpp index 3556101e..0da558f2 100644 --- a/src/libs/utils/impl/Zipper.cpp +++ b/src/libs/utils/impl/Zipper.cpp @@ -35,7 +35,7 @@ namespace Zip class ZipHeader { public: - ZipHeader(std::byte* buffer, std::size_t bufferSize) + constexpr ZipHeader(std::byte* buffer, SizeType bufferSize) : _buffer {buffer} , _bufferSize {bufferSize} {} @@ -51,36 +51,44 @@ namespace Zip NoCompression = 0, }; - static constexpr std::size_t UnknownCrc32 {0}; - static constexpr std::size_t UnknownFileSize {0}; + static constexpr std::uint32_t UnknownCrc32 {0}; + static constexpr SizeType UnknownFileSize {0}; + struct Version + { + unsigned major; + unsigned minor; + }; + static constexpr Version VersionMadeBy {4, 5}; + static constexpr Version VersionNeededToExtract {4, 5}; protected: - void write8(std::size_t offset, std::uint8_t value); - void write16(std::size_t offset, std::uint16_t value); - void write32(std::size_t offset, std::uint32_t value); - void writeDateTime(std::size_t offset, const Wt::WDateTime& time); + void write8(SizeType offset, std::uint8_t value); + void write16(SizeType offset, std::uint16_t value); + void write32(SizeType offset, std::uint32_t value); + void write64(SizeType offset, std::uint64_t value); + void writeDateTime(SizeType offset, const Wt::WDateTime& time); private: std::byte* _buffer {}; - std::size_t _bufferSize {}; + const SizeType _bufferSize {}; }; void - ZipHeader::write8(std::size_t offset, std::uint8_t value) + ZipHeader::write8(SizeType offset, std::uint8_t value) { _buffer[offset] = static_cast(value); } void - ZipHeader::write16(std::size_t offset, std::uint16_t value) + ZipHeader::write16(SizeType offset, std::uint16_t value) { _buffer[offset] = static_cast(value & 0xff); _buffer[offset + 1] = static_cast(value >> 8); } void - ZipHeader::write32(std::size_t offset, std::uint32_t value) + ZipHeader::write32(SizeType offset, std::uint32_t value) { _buffer[offset] = static_cast(value & 0xff); _buffer[offset + 1] = static_cast((value >> 8) & 0xff); @@ -89,7 +97,20 @@ namespace Zip } void - ZipHeader::writeDateTime(std::size_t offset, const Wt::WDateTime& dateTime) + ZipHeader::write64(SizeType offset, std::uint64_t value) + { + _buffer[offset] = static_cast(value & 0xff); + _buffer[offset + 1] = static_cast((value >> 8) & 0xff); + _buffer[offset + 2] = static_cast((value >> 16) & 0xff); + _buffer[offset + 3] = static_cast((value >> 24) & 0xff); + _buffer[offset + 4] = static_cast((value >> 32) & 0xff); + _buffer[offset + 5] = static_cast((value >> 40) & 0xff); + _buffer[offset + 6] = static_cast((value >> 48) & 0xff); + _buffer[offset + 7] = static_cast(value >> 56); + } + + void + ZipHeader::writeDateTime(SizeType offset, const Wt::WDateTime& dateTime) { std::uint32_t encodedDateTime{}; @@ -111,18 +132,41 @@ namespace Zip public: using ZipHeader::ZipHeader; - // Setters void setSignature() { write32(0, 0x04034b50); } - void setVersionNeededToExtract(unsigned major, unsigned minor) { assert(minor < 10); write16(4, major*10 + minor); } + void setVersionNeededToExtract(Version version) { assert(version.minor < 10); write16(4, version.major*10 + version.minor); } void setGeneralPurposeFlags(std::uint16_t flags) { write16(6, flags); } void setCompressionMethod(CompressionMethod compressionMethod) { write16(8, compressionMethod); } void setLastModifiedDateTime(const Wt::WDateTime& dateTime) { writeDateTime(10, dateTime); } void setCrc32UncompressedData(std::uint32_t crc) { write32(14, crc); } - void setCompressedSize(std::size_t size) { write32(18, size); } - void setUncompressedSize(std::size_t size) { write32(22, size); } - void setFileNameLength(std::size_t size) { write16(26, size); } - void setExtraFieldLength(std::size_t size) { write16(28, size); } - static constexpr std::size_t getHeaderSize() { return 30; } + void setCompressedSize(std::uint32_t size = UINT32_MAX) { write32(18, size); } + void setUncompressedSize(std::uint32_t size = UINT32_MAX) { write32(22, size); } + void setFileNameLength(SizeType size) { write16(26, size); } + void setExtraFieldLength(SizeType size) { write16(28, size); } + static constexpr SizeType getHeaderSize() { return 30; } + }; + + class Zip64ExtendedInformationExtraField : public ZipHeader + { + public: + using ZipHeader::ZipHeader; + + struct WithFileOffset {}; + constexpr Zip64ExtendedInformationExtraField(std::byte* buffer, SizeType bufferSize, WithFileOffset) + : ZipHeader {buffer, bufferSize} + , _withFileOffset {true} + {} + + void setTag() { write16(0, 0x0001); } + void setSize() { write16(2, (_withFileOffset ? getHeaderSize(WithFileOffset {}) : getHeaderSize()) - 4); } + void setUncompressedSize(SizeType size) { write64(4, size); } + void setCompressedSize(SizeType size) { write64(12, size); } + void setFileOffset(SizeType size) { assert(_withFileOffset); write64(20, size); } + + static constexpr SizeType getHeaderSize() { return 20; } + static constexpr SizeType getHeaderSize(WithFileOffset) { return 28; } + + private: + const bool _withFileOffset {}; }; class DataDescriptor : public ZipHeader @@ -132,9 +176,9 @@ namespace Zip void setSignature() { write32(0, 0x08074b50 ); } void setCrc32UncompressedData(std::uint32_t crc32) { write32(4, crc32); } - void setCompressedSize(std::size_t size) { write32(8, size); } - void setUncompressedSize(std::size_t size) { write32(12, size); } - static constexpr std::size_t getHeaderSize() { return 16; } + void setCompressedSize(SizeType size) { write64(8, size); } + void setUncompressedSize(SizeType size) { write64(16, size); } + static constexpr SizeType getHeaderSize() { return 24; } }; class CentralDirectoryHeader : public ZipHeader @@ -143,22 +187,54 @@ namespace Zip using ZipHeader::ZipHeader; void setSignature() { write32(0, 0x02014b50); } - void setVersionMadeBy(unsigned major, unsigned minor) { assert(minor < 10); write16(4, major * 10 + minor); } - void setVersionNeededToExtract(unsigned major, unsigned minor) { assert(minor < 10); write16(6, major*10 + minor); } + void setVersionMadeBy(Version version) { assert(version.minor < 10); write16(4, version.major * 10 + version.minor); } + void setVersionNeededToExtract(Version version) { assert(version.minor < 10); write16(6, version.major*10 + version.minor); } void setGeneralPurposeFlags(std::uint16_t flags) { write16(8, flags); } void setCompressionMethod(CompressionMethod method) { write16(10, method); } void setLastModifiedDateTime(const Wt::WDateTime& dateTime) { writeDateTime(12, dateTime); } void setCrc32UncompressedData(std::uint32_t crc32) { write32(16, crc32); } - void setCompressedSize(std::size_t size) { write32(20, size); } - void setUncompressedSize(std::size_t size) { write32(24, size); } - void setFileNameLength(std::size_t size) { write16(28, size); } - void setExtraFieldLength(std::size_t size) { write16(30, size); } - void setFileCommentLength(std::size_t size) { write16(32, size); } - void setDiskNumber(std::size_t number) { write16(34, number); } + void setCompressedSize(SizeType size = UINT32_MAX) { write32(20, size); } + void setUncompressedSize(SizeType size = UINT32_MAX) { write32(24, size); } + void setFileNameLength(SizeType size) { write16(28, size); } + void setExtraFieldLength(SizeType size) { write16(30, size); } + void setFileCommentLength(SizeType size) { write16(32, size); } + void setDiskNumber(SizeType number) { write16(34, number); } void setInternalFileAttributes(std::uint16_t attributes) { write16(36, attributes); } void setExternalFileAttributes(std::uint16_t attributes) { write32(38, attributes); } - void setRelativeFileHeaderOffset(std::size_t offset) { write32(42, offset); } - static constexpr std::size_t getHeaderSize() { return 46; } + void setRelativeFileHeaderOffset(SizeType offset = UINT32_MAX) { write32(42, offset); } + static constexpr SizeType getHeaderSize() { return 46; } + }; + + class Zip64EndOfCentralDirectoryRecord : public ZipHeader + { + public: + using ZipHeader::ZipHeader; + + void setSignature() { write32(0, 0x06064b50); } + void setSize() { write64(4, 56 - 12); } + void setVersionMadeBy(Version version) { assert(version.minor < 10); write16(12, version.major * 10 + version.minor); } + void setVersionNeededToExtract(Version version) { assert(version.minor < 10); write16(14, version.major*10 + version.minor); } + void setDiskNumber(SizeType number) { write32(16, number); } + void setCentralDirectoryDiskNumber(unsigned number) { write32(20, number); } + void setNbDiskCentralDirectoryRecords(unsigned number) { write64(24, number); } + void setNbCentralDirectoryRecords(unsigned number) { write64(32, number); } + void setCentralDirectorySize(SizeType size) { write64(40, size); } + void setCentralDirectoryOffset(SizeType offset) { write64(48, offset); } + + static constexpr SizeType getHeaderSize() { return 56; } + }; + + class Zip64EndOfCentralDirectoryLocator : public ZipHeader + { + public: + using ZipHeader::ZipHeader; + + void setSignature() { write32(0, 0x07064b50); } + void setCentralDirectoryDiskNumber(unsigned number) { write32(4, number); } + void setZip64EndOfCentralDirectoryOffset(SizeType offset) { write64(8, offset); } + void setTotalNumberOfDisks(unsigned number) { write32(16, number); }; + + static constexpr SizeType getHeaderSize() { return 20; } }; class EndOfCentralDirectoryRecord : public ZipHeader @@ -167,14 +243,14 @@ namespace Zip using ZipHeader::ZipHeader; void setSignature() { write32(0, 0x06054b50); } - void setDiskNumber(unsigned number) { write16(4, number); } - void setCentralDirectoryDiskNumber(unsigned number) { write16(6, number); } - void setNbDiskCentralDirectoryRecords(unsigned number) { write16(8, number); } - void setNbCentralDirectoryRecords(unsigned number) { write16(10, number); } - void setCentralDirectorySize(std::size_t size) { write32(12, size); } - void setCentralDirectoryOffset(std::size_t offset) { write32(16, offset); } - void setCommentLength(std::size_t length) { write16(20, length); } - static constexpr std::size_t getHeaderSize() { return 22; } + void setDiskNumber(std::uint16_t number = UINT16_MAX) { write16(4, number); } + void setCentralDirectoryDiskNumber(std::uint16_t number = UINT16_MAX) { write16(6, number); } + void setNbDiskCentralDirectoryRecords(std::uint16_t number = UINT16_MAX) { write16(8, number); } + void setNbCentralDirectoryRecords(std::uint16_t number = UINT16_MAX) { write16(10, number); } + void setCentralDirectorySize(std::uint32_t size = UINT32_MAX) { write32(12, size); } + void setCentralDirectoryOffset(std::uint32_t offset = UINT32_MAX) { write32(16, offset); } + void setCommentLength(SizeType length) { write16(20, length); } + static constexpr SizeType getHeaderSize() { return 22; } }; Zipper::Zipper(const std::map& files, const Wt::WDateTime& lastModifiedTime) @@ -198,33 +274,32 @@ namespace Zip _totalZipSize += LocalFileHeader::getHeaderSize(); _totalZipSize += filename.size(); - if (fileContext.fileSize > 0) - { - _totalZipSize += fileContext.fileSize; - _totalZipSize += DataDescriptor::getHeaderSize(); - _totalZipSize += CentralDirectoryHeader::getHeaderSize(); - _totalZipSize += filename.size(); - } + _totalZipSize += Zip64ExtendedInformationExtraField::getHeaderSize(); + _totalZipSize += fileContext.fileSize; + _totalZipSize += DataDescriptor::getHeaderSize(); + _totalZipSize += CentralDirectoryHeader::getHeaderSize(); + _totalZipSize += filename.size(); + _totalZipSize += Zip64ExtendedInformationExtraField::getHeaderSize(Zip64ExtendedInformationExtraField::WithFileOffset {}); } + _totalZipSize += Zip64EndOfCentralDirectoryRecord::getHeaderSize(); + _totalZipSize += Zip64EndOfCentralDirectoryLocator::getHeaderSize(); _totalZipSize += EndOfCentralDirectoryRecord::getHeaderSize(); - if (_totalZipSize > UINT32_MAX) - throw ZipperException {"Cannot create a zip file which is larger than " + std::to_string(UINT32_MAX) + " bytes!"}; _currentFile = std::begin(_files); } - std::size_t - Zipper::writeSome(std::byte* buffer, std::size_t bufferSize) + SizeType + Zipper::writeSome(std::byte* buffer, SizeType bufferSize) { // make sure we have some room for the headers assert(bufferSize >= minOutputBufferSize); - std::size_t nbTotalWrittenBytes {}; + SizeType nbTotalWrittenBytes {}; while (!isComplete() && (bufferSize >= minOutputBufferSize)) { - std::size_t nbWrittenBytes {}; + SizeType nbWrittenBytes {}; switch (_writeState) { @@ -236,6 +311,10 @@ namespace Zip nbWrittenBytes = writeLocalFileHeaderFileName(buffer, bufferSize); break; + case WriteState::LocalFileHeaderExtraFields: + nbWrittenBytes = writeLocalFileHeaderExtraFields(buffer, bufferSize); + break; + case WriteState::FileData: nbWrittenBytes = writeFileData(buffer, bufferSize); break; @@ -252,6 +331,18 @@ namespace Zip nbWrittenBytes = writeCentralDirectoryHeaderFileName(buffer, bufferSize); break; + case WriteState::CentralDirectoryHeaderExtraFields: + nbWrittenBytes = writeCentralDirectoryHeaderExtraFields(buffer, bufferSize); + break; + + case WriteState::Zip64EndOfCentralDirectoryRecord: + nbWrittenBytes = writeZip64EndOfCentralDirectoryRecord(buffer, bufferSize); + break; + + case WriteState::Zip64EndOfCentralDirectoryLocator: + nbWrittenBytes = writeZip64EndOfCentralDirectoryLocator(buffer, bufferSize); + break; + case WriteState::EndOfCentralDirectoryRecord: nbWrittenBytes = writeEndOfCentralDirectoryRecord(buffer, bufferSize); break; @@ -275,8 +366,8 @@ namespace Zip return _writeState == WriteState::Complete; } - std::size_t - Zipper::writeLocalFileHeader(std::byte* buffer, std::size_t bufferSize) + SizeType + Zipper::writeLocalFileHeader(std::byte* buffer, SizeType bufferSize) { static_assert(LocalFileHeader::getHeaderSize() <= minOutputBufferSize); @@ -292,15 +383,15 @@ namespace Zip LocalFileHeader header {buffer, bufferSize}; header.setSignature(); - header.setVersionNeededToExtract(1, 0); + header.setVersionNeededToExtract(ZipHeader::VersionNeededToExtract); header.setGeneralPurposeFlags(ZipHeader::GeneralPurposeFlag::LanguageEncoding | ZipHeader::GeneralPurposeFlag::UseDataDescriptor); header.setCompressionMethod(ZipHeader::CompressionMethod::NoCompression); header.setCrc32UncompressedData(ZipHeader::UnknownCrc32); - header.setCompressedSize(ZipHeader::UnknownFileSize); - header.setUncompressedSize(ZipHeader::UnknownFileSize); + header.setCompressedSize(); + header.setUncompressedSize(); header.setLastModifiedDateTime(_currentFile->second.lastModifiedTime); header.setFileNameLength(_currentFile->first.size()); - header.setExtraFieldLength(0); + header.setExtraFieldLength(Zip64ExtendedInformationExtraField::getHeaderSize()); _writeState = WriteState::LocalFileHeaderFileName; _currentFile->second.localFileHeaderOffset = _currentZipOffset; @@ -308,8 +399,8 @@ namespace Zip return header.getHeaderSize(); } - std::size_t - Zipper::writeLocalFileHeaderFileName(std::byte* buffer, std::size_t bufferSize) + SizeType + Zipper::writeLocalFileHeaderFileName(std::byte* buffer, SizeType bufferSize) { assert(_currentFile != std::end(_files)); @@ -318,12 +409,12 @@ namespace Zip assert(_currentOffset <= fileName.size()); if (_currentOffset == fileName.size()) { - _writeState = WriteState::FileData; + _writeState = WriteState::LocalFileHeaderExtraFields; _currentOffset = 0; return 0; } - const std::size_t nbBytesToCopy {std::min(fileName.size() - _currentOffset, bufferSize)}; + const SizeType nbBytesToCopy {std::min(fileName.size() - _currentOffset, bufferSize)}; std::copy(std::next(std::begin(fileName), _currentOffset), std::next(std::begin(fileName), _currentOffset + nbBytesToCopy), reinterpret_cast(buffer)); @@ -331,8 +422,25 @@ namespace Zip return nbBytesToCopy; } - std::size_t - Zipper::writeFileData(std::byte* buffer, std::size_t bufferSize) + SizeType + Zipper::writeLocalFileHeaderExtraFields(std::byte* buffer, SizeType bufferSize) + { + assert(_currentFile != std::end(_files)); + static_assert(Zip64ExtendedInformationExtraField::getHeaderSize() <= minOutputBufferSize); + + Zip64ExtendedInformationExtraField header {buffer, bufferSize}; + + header.setTag(); + header.setSize(); + header.setUncompressedSize(ZipHeader::UnknownFileSize); + header.setCompressedSize(ZipHeader::UnknownFileSize); + + _writeState = WriteState::FileData; + return header.getHeaderSize(); + } + + SizeType + Zipper::writeFileData(std::byte* buffer, SizeType bufferSize) { assert(_currentFile != std::end(_files)); @@ -356,7 +464,7 @@ namespace Zip if (fileSize != _currentFile->second.fileSize) throw ZipperException {"File '" + filePath + "': size mismatch!"}; - const std::size_t nbBytesToRead {std::min(static_cast(fileSize) - _currentOffset, bufferSize)}; + const SizeType nbBytesToRead {std::min(static_cast(fileSize) - _currentOffset, bufferSize)}; ifs.seekg(_currentOffset, std::ios::beg); ifs.read(reinterpret_cast(buffer), nbBytesToRead ); @@ -368,8 +476,8 @@ namespace Zip return actualReadSize; } - std::size_t - Zipper::writeDataDescriptor(std::byte* buffer, std::size_t bufferSize) + SizeType + Zipper::writeDataDescriptor(std::byte* buffer, SizeType bufferSize) { assert(bufferSize >= minOutputBufferSize); static_assert(DataDescriptor::getHeaderSize() <= minOutputBufferSize); @@ -388,8 +496,8 @@ namespace Zip return desc.getHeaderSize(); } - std::size_t - Zipper::writeCentralDirectoryHeader(std::byte* buffer, std::size_t bufferSize) + SizeType + Zipper::writeCentralDirectoryHeader(std::byte* buffer, SizeType bufferSize) { assert(bufferSize >= minOutputBufferSize); static_assert(CentralDirectoryHeader::getHeaderSize() <= minOutputBufferSize); @@ -399,28 +507,28 @@ namespace Zip if (_currentFile == std::end(_files)) { - _writeState = WriteState::EndOfCentralDirectoryRecord; + _writeState = WriteState::Zip64EndOfCentralDirectoryRecord; _currentFile = std::begin(_files); return 0; } CentralDirectoryHeader header {buffer, bufferSize}; header.setSignature(); - header.setVersionMadeBy(2, 0); - header.setVersionNeededToExtract(1, 0); + header.setVersionMadeBy(ZipHeader::VersionMadeBy); + header.setVersionNeededToExtract(ZipHeader::VersionNeededToExtract); header.setGeneralPurposeFlags(ZipHeader::GeneralPurposeFlag::LanguageEncoding | ZipHeader::GeneralPurposeFlag::UseDataDescriptor); header.setCompressionMethod(ZipHeader::CompressionMethod::NoCompression); - header.setCompressedSize(_currentFile->second.fileSize); - header.setUncompressedSize(_currentFile->second.fileSize); + header.setCompressedSize(); + header.setUncompressedSize(); header.setLastModifiedDateTime(_currentFile->second.lastModifiedTime); header.setCrc32UncompressedData(_currentFile->second.fileCrc32.getResult()); header.setFileNameLength(_currentFile->first.size()); - header.setExtraFieldLength(0); + header.setExtraFieldLength(Zip64ExtendedInformationExtraField::getHeaderSize(Zip64ExtendedInformationExtraField::WithFileOffset {})); header.setFileCommentLength(0); header.setDiskNumber(0); header.setInternalFileAttributes(0); header.setExternalFileAttributes(0); - header.setRelativeFileHeaderOffset(_currentFile->second.localFileHeaderOffset); + header.setRelativeFileHeaderOffset(); _writeState = WriteState::CentralDirectoryHeaderFileName; _centralDirectorySize += header.getHeaderSize(); @@ -428,8 +536,8 @@ namespace Zip return header.getHeaderSize(); } - std::size_t - Zipper::writeCentralDirectoryHeaderFileName(std::byte* buffer, std::size_t bufferSize) + SizeType + Zipper::writeCentralDirectoryHeaderFileName(std::byte* buffer, SizeType bufferSize) { const std::string& fileName {_currentFile->first}; @@ -437,13 +545,12 @@ namespace Zip if (_currentOffset == fileName.size()) { _currentOffset = 0; - ++_currentFile; - _writeState = WriteState::CentralDirectoryHeader; + _writeState = WriteState::CentralDirectoryHeaderExtraFields; return 0; } - const std::size_t nbBytesToCopy {std::min(fileName.size() - _currentOffset, bufferSize)}; + const SizeType nbBytesToCopy {std::min(fileName.size() - _currentOffset, bufferSize)}; std::copy(std::next(std::begin(fileName), _currentOffset), std::next(std::begin(fileName), _currentOffset + nbBytesToCopy), reinterpret_cast(buffer)); @@ -452,9 +559,71 @@ namespace Zip return nbBytesToCopy; } + SizeType + Zipper::writeCentralDirectoryHeaderExtraFields(std::byte* buffer, SizeType bufferSize) + { + assert(bufferSize >= minOutputBufferSize); + assert(_currentFile != std::cend(_files)); + static_assert(Zip64ExtendedInformationExtraField::getHeaderSize(Zip64ExtendedInformationExtraField::WithFileOffset {}) <= minOutputBufferSize); - std::size_t - Zipper::writeEndOfCentralDirectoryRecord(std::byte* buffer, std::size_t bufferSize) + Zip64ExtendedInformationExtraField header {buffer, bufferSize, Zip64ExtendedInformationExtraField::WithFileOffset {}}; + + header.setTag(); + header.setSize(); + header.setUncompressedSize(_currentFile->second.fileSize); + header.setCompressedSize(_currentFile->second.fileSize); + header.setFileOffset(_currentFile->second.localFileHeaderOffset); + + ++_currentFile; + _writeState = WriteState::CentralDirectoryHeader; + _centralDirectorySize += header.getHeaderSize(Zip64ExtendedInformationExtraField::WithFileOffset {}); + + return header.getHeaderSize(Zip64ExtendedInformationExtraField::WithFileOffset {}); + } + + SizeType + Zipper::writeZip64EndOfCentralDirectoryRecord(std::byte* buffer, SizeType bufferSize) + { + assert(bufferSize >= minOutputBufferSize); + static_assert(Zip64EndOfCentralDirectoryRecord::getHeaderSize() <= minOutputBufferSize); + + Zip64EndOfCentralDirectoryRecord record {buffer, bufferSize}; + + record.setSignature(); + record.setSize(); + record.setVersionMadeBy(ZipHeader::VersionNeededToExtract); + record.setVersionNeededToExtract(ZipHeader::VersionNeededToExtract); + record.setDiskNumber(0); + record.setCentralDirectoryDiskNumber(0); + record.setNbDiskCentralDirectoryRecords(_files.size()); + record.setNbCentralDirectoryRecords(_files.size()); + record.setCentralDirectorySize(_centralDirectorySize); + record.setCentralDirectoryOffset(_centralDirectoryOffset); + + _zip64EndOfCentralDirectoryRecordOffset = _currentZipOffset; + _writeState = WriteState::Zip64EndOfCentralDirectoryLocator; + return record.getHeaderSize(); + } + + SizeType + Zipper::writeZip64EndOfCentralDirectoryLocator(std::byte* buffer, SizeType bufferSize) + { + assert(bufferSize >= minOutputBufferSize); + static_assert(Zip64EndOfCentralDirectoryLocator::getHeaderSize() <= minOutputBufferSize); + + Zip64EndOfCentralDirectoryLocator locator {buffer, bufferSize}; + + locator.setSignature(); + locator.setCentralDirectoryDiskNumber(0); + locator.setZip64EndOfCentralDirectoryOffset(_zip64EndOfCentralDirectoryRecordOffset); + locator.setTotalNumberOfDisks(1); + + _writeState = WriteState::EndOfCentralDirectoryRecord; + return locator.getHeaderSize(); + } + + SizeType + Zipper::writeEndOfCentralDirectoryRecord(std::byte* buffer, SizeType bufferSize) { assert(bufferSize >= minOutputBufferSize); static_assert(EndOfCentralDirectoryRecord::getHeaderSize() <= minOutputBufferSize); @@ -464,10 +633,10 @@ namespace Zip record.setSignature(); record.setDiskNumber(0); record.setCentralDirectoryDiskNumber(0); - record.setNbDiskCentralDirectoryRecords(_files.size()); - record.setNbCentralDirectoryRecords(_files.size()); - record.setCentralDirectorySize(_centralDirectorySize); - record.setCentralDirectoryOffset(_centralDirectoryOffset); + record.setNbDiskCentralDirectoryRecords(); + record.setNbCentralDirectoryRecords(); + record.setCentralDirectorySize(); + record.setCentralDirectoryOffset(); record.setCommentLength(0); _writeState = WriteState::Complete; diff --git a/src/libs/utils/include/utils/Zipper.hpp b/src/libs/utils/include/utils/Zipper.hpp index 381e366c..03ff2e95 100644 --- a/src/libs/utils/include/utils/Zipper.hpp +++ b/src/libs/utils/include/utils/Zipper.hpp @@ -19,9 +19,8 @@ #pragma once -#include -#include #include +#include #include @@ -30,6 +29,7 @@ namespace Zip { + using SizeType = std::uint64_t; class ZipperException : public LmsException { @@ -41,34 +41,36 @@ namespace Zip { public: - using SizeZype = std::uint64_t; - Zipper(const std::map& files, const Wt::WDateTime& lastModifiedTime = {}); - static constexpr std::size_t minOutputBufferSize = 64; - std::size_t writeSome(std::byte* buffer, std::size_t bufferSize); + static constexpr SizeType minOutputBufferSize {64}; + SizeType writeSome(std::byte* buffer, SizeType bufferSize); bool isComplete() const; - SizeZype getTotalZipFile() const { return _totalZipSize; } + SizeType getTotalZipFile() const { return _totalZipSize; } private: void setComplete(); - std::size_t writeLocalFileHeader(std::byte* buffer, std::size_t bufferSize); - std::size_t writeLocalFileHeaderFileName(std::byte* buffer, std::size_t bufferSize); - std::size_t writeFileData(std::byte* buffer, std::size_t bufferSize); - std::size_t writeDataDescriptor(std::byte* buffer, std::size_t bufferSize); - std::size_t writeCentralDirectoryHeader(std::byte* buffer, std::size_t bufferSize); - std::size_t writeCentralDirectoryHeaderFileName(std::byte* buffer, std::size_t bufferSize); - std::size_t writeEndOfCentralDirectoryRecord(std::byte* buffer, std::size_t bufferSize); + SizeType writeLocalFileHeader(std::byte* buffer, SizeType bufferSize); + SizeType writeLocalFileHeaderFileName(std::byte* buffer, SizeType bufferSize); + SizeType writeLocalFileHeaderExtraFields(std::byte* buffer, SizeType bufferSize); + SizeType writeFileData(std::byte* buffer, SizeType bufferSize); + SizeType writeDataDescriptor(std::byte* buffer, SizeType bufferSize); + SizeType writeCentralDirectoryHeader(std::byte* buffer, SizeType bufferSize); + SizeType writeCentralDirectoryHeaderFileName(std::byte* buffer, SizeType bufferSize); + SizeType writeCentralDirectoryHeaderExtraFields(std::byte* buffer, SizeType bufferSize); + SizeType writeZip64EndOfCentralDirectoryRecord(std::byte* buffer, SizeType bufferSize); + SizeType writeZip64EndOfCentralDirectoryLocator(std::byte* buffer, SizeType bufferSize); + SizeType writeEndOfCentralDirectoryRecord(std::byte* buffer, SizeType bufferSize); struct FileContext { std::filesystem::path filePath; - std::size_t fileSize; + SizeType fileSize; Wt::WDateTime lastModifiedTime; Utils::Crc32Calculator fileCrc32; - std::size_t localFileHeaderOffset {}; + SizeType localFileHeaderOffset {}; }; using FileContainer = std::map; @@ -78,21 +80,26 @@ namespace Zip { LocalFileHeader, LocalFileHeaderFileName, + LocalFileHeaderExtraFields, FileData, DataDescriptor, CentralDirectoryHeader, CentralDirectoryHeaderFileName, + CentralDirectoryHeaderExtraFields, + Zip64EndOfCentralDirectoryRecord, + Zip64EndOfCentralDirectoryLocator, EndOfCentralDirectoryRecord, Complete, }; - SizeZype _totalZipSize {}; + SizeType _totalZipSize {}; WriteState _writeState {WriteState::LocalFileHeader}; FileContainer::iterator _currentFile; - std::size_t _currentOffset {}; - std::size_t _currentZipOffset {}; - std::size_t _centralDirectoryOffset {}; - std::size_t _centralDirectorySize {}; + SizeType _currentOffset {}; + SizeType _currentZipOffset {}; + SizeType _centralDirectoryOffset {}; + SizeType _centralDirectorySize {}; + SizeType _zip64EndOfCentralDirectoryRecordOffset {}; }; } // namespace Zip 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/DownloadResource.cpp b/src/lms/ui/resource/DownloadResource.cpp index 4cd5f298..cb028f02 100644 --- a/src/lms/ui/resource/DownloadResource.cpp +++ b/src/lms/ui/resource/DownloadResource.cpp @@ -57,6 +57,7 @@ DownloadResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Resp else { zipper = createZipper(); + response.setContentLength(zipper->getTotalZipFile()); response.setMimeType("application/zip"); } 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; diff --git a/src/tools/zipper/LmsZipper.cpp b/src/tools/zipper/LmsZipper.cpp index 7ec889e1..ba37e170 100644 --- a/src/tools/zipper/LmsZipper.cpp +++ b/src/tools/zipper/LmsZipper.cpp @@ -61,15 +61,20 @@ int main(int argc, char* argv[]) { Zipper zipper {files}; + Zip::SizeType nbTotalWrittenBytes {}; while (!zipper.isComplete()) { //std::array buffer; std::array buffer; - std::size_t nbWrittenBytes {zipper.writeSome(buffer.data(), buffer.size())}; + const Zip::SizeType nbWrittenBytes {zipper.writeSome(buffer.data(), buffer.size())}; ofs.write(reinterpret_cast(buffer.data()), nbWrittenBytes); + nbTotalWrittenBytes += nbWrittenBytes; } + if (nbTotalWrittenBytes != zipper.getTotalZipFile()) + std::cerr << "ERROR: actual size mismatch!" << std::endl; + std::cout << "Total zip size = " << zipper.getTotalZipFile() << std::endl; } catch (const ZipperException& e)