Merge branch 'develop' for release v3.20.0

This commit is contained in:
emeric
2020-10-12 13:37:00 +02:00
25 changed files with 744 additions and 399 deletions
+13 -17
View File
@@ -4,42 +4,38 @@
_LMS_ is a self-hosted music streaming software: access your music collection from anywhere using a web interface! _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: A [demo](http://lms.demo.poupon.io) instance is available. Note the administration panel is not available.
- Settings cannot be saved
- No persistent playqueue
- No administration panel
## Main features ## Main features
* Low memory requirements: the demo instance runs on a Raspberry Pi3B+
* Low memory requirement (the demo instance runs on a Raspberry Pi3B+, using less than 10% of total memory even when transcoding)
* User management
* 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, ...
* 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 * ReplayGain support
* Persistent play queue across sessions * 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 * _Systemd_ integration
* _PAM_ authentication backend * User management
* with optional _PAM_ authentication backend
* Subsonic API, with the following additional features: * Subsonic API, with the following additional features:
* Playlists * Playlists
* Starred Album/Artist/Tracks
* Bookmarks * Bookmarks
## Music discovery ## Music discovery
_LMS_ provides several ways to help you find the music you like: _LMS_ provides several ways to help you find the music you like:
* Tag-based filters (ex: _Rock_, _Metal_ and _Aggressive_, _Electronic_ and _Relaxed_, ...) * Tag-based filters (ex: _Rock_, _Metal_ and _Aggressive_, _Electronic_ and _Relaxed_, ...)
* Recommendations for similar artists and albums * Recommendations for similar artists and albums
* Radio mode, based on what is in the current playqueue * Radio mode, based on what is in the current playqueue
* Searches in album, artist and track names (including sort names) * 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 * Recently added _Electronic_ artists
* Random _Metal_ and _Aggressive_ albums * Random _Metal_ and _Aggressive_ albums
* Most played _Relaxed_ tracks * Most played _Relaxed_ tracks
* Starred _Jazz_ albums
* ... * ...
The recommendation engine uses two different sources: The recommendation engine uses two different sources:
@@ -49,7 +45,7 @@ The recommendation engine uses two different sources:
__Notes on the self-organizing map__: __Notes on the self-organizing map__:
* training the map requires significant computation time on large collections (ex: half an hour for 40k tracks) * 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). * 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 ## Subsonic API
The API version implemented is 1.12.0 and has been tested on _Android_ using the official application, _Ultrasonic_ and _DSub_. The API version implemented is 1.12.0 and has been tested on _Android_ using the official application, _Ultrasonic_ and _DSub_.
+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
+257 -88
View File
@@ -35,7 +35,7 @@ namespace Zip
class ZipHeader class ZipHeader
{ {
public: public:
ZipHeader(std::byte* buffer, std::size_t bufferSize) constexpr ZipHeader(std::byte* buffer, SizeType bufferSize)
: _buffer {buffer} : _buffer {buffer}
, _bufferSize {bufferSize} , _bufferSize {bufferSize}
{} {}
@@ -51,36 +51,44 @@ namespace Zip
NoCompression = 0, NoCompression = 0,
}; };
static constexpr std::size_t UnknownCrc32 {0}; static constexpr std::uint32_t UnknownCrc32 {0};
static constexpr std::size_t UnknownFileSize {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: protected:
void write8(std::size_t offset, std::uint8_t value); void write8(SizeType offset, std::uint8_t value);
void write16(std::size_t offset, std::uint16_t value); void write16(SizeType offset, std::uint16_t value);
void write32(std::size_t offset, std::uint32_t value); void write32(SizeType offset, std::uint32_t value);
void writeDateTime(std::size_t offset, const Wt::WDateTime& time); void write64(SizeType offset, std::uint64_t value);
void writeDateTime(SizeType offset, const Wt::WDateTime& time);
private: private:
std::byte* _buffer {}; std::byte* _buffer {};
std::size_t _bufferSize {}; const SizeType _bufferSize {};
}; };
void void
ZipHeader::write8(std::size_t offset, std::uint8_t value) ZipHeader::write8(SizeType offset, std::uint8_t value)
{ {
_buffer[offset] = static_cast<std::byte>(value); _buffer[offset] = static_cast<std::byte>(value);
} }
void void
ZipHeader::write16(std::size_t offset, std::uint16_t value) ZipHeader::write16(SizeType offset, std::uint16_t value)
{ {
_buffer[offset] = static_cast<std::byte>(value & 0xff); _buffer[offset] = static_cast<std::byte>(value & 0xff);
_buffer[offset + 1] = static_cast<std::byte>(value >> 8); _buffer[offset + 1] = static_cast<std::byte>(value >> 8);
} }
void void
ZipHeader::write32(std::size_t offset, std::uint32_t value) ZipHeader::write32(SizeType offset, std::uint32_t value)
{ {
_buffer[offset] = static_cast<std::byte>(value & 0xff); _buffer[offset] = static_cast<std::byte>(value & 0xff);
_buffer[offset + 1] = static_cast<std::byte>((value >> 8) & 0xff); _buffer[offset + 1] = static_cast<std::byte>((value >> 8) & 0xff);
@@ -89,7 +97,20 @@ namespace Zip
} }
void void
ZipHeader::writeDateTime(std::size_t offset, const Wt::WDateTime& dateTime) ZipHeader::write64(SizeType offset, std::uint64_t value)
{
_buffer[offset] = static_cast<std::byte>(value & 0xff);
_buffer[offset + 1] = static_cast<std::byte>((value >> 8) & 0xff);
_buffer[offset + 2] = static_cast<std::byte>((value >> 16) & 0xff);
_buffer[offset + 3] = static_cast<std::byte>((value >> 24) & 0xff);
_buffer[offset + 4] = static_cast<std::byte>((value >> 32) & 0xff);
_buffer[offset + 5] = static_cast<std::byte>((value >> 40) & 0xff);
_buffer[offset + 6] = static_cast<std::byte>((value >> 48) & 0xff);
_buffer[offset + 7] = static_cast<std::byte>(value >> 56);
}
void
ZipHeader::writeDateTime(SizeType offset, const Wt::WDateTime& dateTime)
{ {
std::uint32_t encodedDateTime{}; std::uint32_t encodedDateTime{};
@@ -111,18 +132,41 @@ namespace Zip
public: public:
using ZipHeader::ZipHeader; using ZipHeader::ZipHeader;
// Setters
void setSignature() { write32(0, 0x04034b50); } 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 setGeneralPurposeFlags(std::uint16_t flags) { write16(6, flags); }
void setCompressionMethod(CompressionMethod compressionMethod) { write16(8, compressionMethod); } void setCompressionMethod(CompressionMethod compressionMethod) { write16(8, compressionMethod); }
void setLastModifiedDateTime(const Wt::WDateTime& dateTime) { writeDateTime(10, dateTime); } void setLastModifiedDateTime(const Wt::WDateTime& dateTime) { writeDateTime(10, dateTime); }
void setCrc32UncompressedData(std::uint32_t crc) { write32(14, crc); } void setCrc32UncompressedData(std::uint32_t crc) { write32(14, crc); }
void setCompressedSize(std::size_t size) { write32(18, size); } void setCompressedSize(std::uint32_t size = UINT32_MAX) { write32(18, size); }
void setUncompressedSize(std::size_t size) { write32(22, size); } void setUncompressedSize(std::uint32_t size = UINT32_MAX) { write32(22, size); }
void setFileNameLength(std::size_t size) { write16(26, size); } void setFileNameLength(SizeType size) { write16(26, size); }
void setExtraFieldLength(std::size_t size) { write16(28, size); } void setExtraFieldLength(SizeType size) { write16(28, size); }
static constexpr std::size_t getHeaderSize() { return 30; } 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 class DataDescriptor : public ZipHeader
@@ -132,9 +176,9 @@ namespace Zip
void setSignature() { write32(0, 0x08074b50 ); } void setSignature() { write32(0, 0x08074b50 ); }
void setCrc32UncompressedData(std::uint32_t crc32) { write32(4, crc32); } void setCrc32UncompressedData(std::uint32_t crc32) { write32(4, crc32); }
void setCompressedSize(std::size_t size) { write32(8, size); } void setCompressedSize(SizeType size) { write64(8, size); }
void setUncompressedSize(std::size_t size) { write32(12, size); } void setUncompressedSize(SizeType size) { write64(16, size); }
static constexpr std::size_t getHeaderSize() { return 16; } static constexpr SizeType getHeaderSize() { return 24; }
}; };
class CentralDirectoryHeader : public ZipHeader class CentralDirectoryHeader : public ZipHeader
@@ -143,22 +187,54 @@ namespace Zip
using ZipHeader::ZipHeader; using ZipHeader::ZipHeader;
void setSignature() { write32(0, 0x02014b50); } void setSignature() { write32(0, 0x02014b50); }
void setVersionMadeBy(unsigned major, unsigned minor) { assert(minor < 10); write16(4, major * 10 + minor); } void setVersionMadeBy(Version version) { assert(version.minor < 10); write16(4, version.major * 10 + version.minor); }
void setVersionNeededToExtract(unsigned major, unsigned minor) { assert(minor < 10); write16(6, major*10 + 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 setGeneralPurposeFlags(std::uint16_t flags) { write16(8, flags); }
void setCompressionMethod(CompressionMethod method) { write16(10, method); } void setCompressionMethod(CompressionMethod method) { write16(10, method); }
void setLastModifiedDateTime(const Wt::WDateTime& dateTime) { writeDateTime(12, dateTime); } void setLastModifiedDateTime(const Wt::WDateTime& dateTime) { writeDateTime(12, dateTime); }
void setCrc32UncompressedData(std::uint32_t crc32) { write32(16, crc32); } void setCrc32UncompressedData(std::uint32_t crc32) { write32(16, crc32); }
void setCompressedSize(std::size_t size) { write32(20, size); } void setCompressedSize(SizeType size = UINT32_MAX) { write32(20, size); }
void setUncompressedSize(std::size_t size) { write32(24, size); } void setUncompressedSize(SizeType size = UINT32_MAX) { write32(24, size); }
void setFileNameLength(std::size_t size) { write16(28, size); } void setFileNameLength(SizeType size) { write16(28, size); }
void setExtraFieldLength(std::size_t size) { write16(30, size); } void setExtraFieldLength(SizeType size) { write16(30, size); }
void setFileCommentLength(std::size_t size) { write16(32, size); } void setFileCommentLength(SizeType size) { write16(32, size); }
void setDiskNumber(std::size_t number) { write16(34, number); } void setDiskNumber(SizeType number) { write16(34, number); }
void setInternalFileAttributes(std::uint16_t attributes) { write16(36, attributes); } void setInternalFileAttributes(std::uint16_t attributes) { write16(36, attributes); }
void setExternalFileAttributes(std::uint16_t attributes) { write32(38, attributes); } void setExternalFileAttributes(std::uint16_t attributes) { write32(38, attributes); }
void setRelativeFileHeaderOffset(std::size_t offset) { write32(42, offset); } void setRelativeFileHeaderOffset(SizeType offset = UINT32_MAX) { write32(42, offset); }
static constexpr std::size_t getHeaderSize() { return 46; } 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 class EndOfCentralDirectoryRecord : public ZipHeader
@@ -167,14 +243,14 @@ namespace Zip
using ZipHeader::ZipHeader; using ZipHeader::ZipHeader;
void setSignature() { write32(0, 0x06054b50); } void setSignature() { write32(0, 0x06054b50); }
void setDiskNumber(unsigned number) { write16(4, number); } void setDiskNumber(std::uint16_t number = UINT16_MAX) { write16(4, number); }
void setCentralDirectoryDiskNumber(unsigned number) { write16(6, number); } void setCentralDirectoryDiskNumber(std::uint16_t number = UINT16_MAX) { write16(6, number); }
void setNbDiskCentralDirectoryRecords(unsigned number) { write16(8, number); } void setNbDiskCentralDirectoryRecords(std::uint16_t number = UINT16_MAX) { write16(8, number); }
void setNbCentralDirectoryRecords(unsigned number) { write16(10, number); } void setNbCentralDirectoryRecords(std::uint16_t number = UINT16_MAX) { write16(10, number); }
void setCentralDirectorySize(std::size_t size) { write32(12, size); } void setCentralDirectorySize(std::uint32_t size = UINT32_MAX) { write32(12, size); }
void setCentralDirectoryOffset(std::size_t offset) { write32(16, offset); } void setCentralDirectoryOffset(std::uint32_t offset = UINT32_MAX) { write32(16, offset); }
void setCommentLength(std::size_t length) { write16(20, length); } void setCommentLength(SizeType length) { write16(20, length); }
static constexpr std::size_t getHeaderSize() { return 22; } static constexpr SizeType getHeaderSize() { return 22; }
}; };
Zipper::Zipper(const std::map<std::string, std::filesystem::path>& files, const Wt::WDateTime& lastModifiedTime) Zipper::Zipper(const std::map<std::string, std::filesystem::path>& files, const Wt::WDateTime& lastModifiedTime)
@@ -198,33 +274,32 @@ namespace Zip
_totalZipSize += LocalFileHeader::getHeaderSize(); _totalZipSize += LocalFileHeader::getHeaderSize();
_totalZipSize += filename.size(); _totalZipSize += filename.size();
if (fileContext.fileSize > 0) _totalZipSize += Zip64ExtendedInformationExtraField::getHeaderSize();
{ _totalZipSize += fileContext.fileSize;
_totalZipSize += fileContext.fileSize; _totalZipSize += DataDescriptor::getHeaderSize();
_totalZipSize += DataDescriptor::getHeaderSize(); _totalZipSize += CentralDirectoryHeader::getHeaderSize();
_totalZipSize += CentralDirectoryHeader::getHeaderSize(); _totalZipSize += filename.size();
_totalZipSize += filename.size(); _totalZipSize += Zip64ExtendedInformationExtraField::getHeaderSize(Zip64ExtendedInformationExtraField::WithFileOffset {});
}
} }
_totalZipSize += Zip64EndOfCentralDirectoryRecord::getHeaderSize();
_totalZipSize += Zip64EndOfCentralDirectoryLocator::getHeaderSize();
_totalZipSize += EndOfCentralDirectoryRecord::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); _currentFile = std::begin(_files);
} }
std::size_t SizeType
Zipper::writeSome(std::byte* buffer, std::size_t bufferSize) Zipper::writeSome(std::byte* buffer, SizeType bufferSize)
{ {
// make sure we have some room for the headers // make sure we have some room for the headers
assert(bufferSize >= minOutputBufferSize); assert(bufferSize >= minOutputBufferSize);
std::size_t nbTotalWrittenBytes {}; SizeType nbTotalWrittenBytes {};
while (!isComplete() && (bufferSize >= minOutputBufferSize)) while (!isComplete() && (bufferSize >= minOutputBufferSize))
{ {
std::size_t nbWrittenBytes {}; SizeType nbWrittenBytes {};
switch (_writeState) switch (_writeState)
{ {
@@ -236,6 +311,10 @@ namespace Zip
nbWrittenBytes = writeLocalFileHeaderFileName(buffer, bufferSize); nbWrittenBytes = writeLocalFileHeaderFileName(buffer, bufferSize);
break; break;
case WriteState::LocalFileHeaderExtraFields:
nbWrittenBytes = writeLocalFileHeaderExtraFields(buffer, bufferSize);
break;
case WriteState::FileData: case WriteState::FileData:
nbWrittenBytes = writeFileData(buffer, bufferSize); nbWrittenBytes = writeFileData(buffer, bufferSize);
break; break;
@@ -252,6 +331,18 @@ namespace Zip
nbWrittenBytes = writeCentralDirectoryHeaderFileName(buffer, bufferSize); nbWrittenBytes = writeCentralDirectoryHeaderFileName(buffer, bufferSize);
break; 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: case WriteState::EndOfCentralDirectoryRecord:
nbWrittenBytes = writeEndOfCentralDirectoryRecord(buffer, bufferSize); nbWrittenBytes = writeEndOfCentralDirectoryRecord(buffer, bufferSize);
break; break;
@@ -275,8 +366,8 @@ namespace Zip
return _writeState == WriteState::Complete; return _writeState == WriteState::Complete;
} }
std::size_t SizeType
Zipper::writeLocalFileHeader(std::byte* buffer, std::size_t bufferSize) Zipper::writeLocalFileHeader(std::byte* buffer, SizeType bufferSize)
{ {
static_assert(LocalFileHeader::getHeaderSize() <= minOutputBufferSize); static_assert(LocalFileHeader::getHeaderSize() <= minOutputBufferSize);
@@ -292,15 +383,15 @@ namespace Zip
LocalFileHeader header {buffer, bufferSize}; LocalFileHeader header {buffer, bufferSize};
header.setSignature(); header.setSignature();
header.setVersionNeededToExtract(1, 0); header.setVersionNeededToExtract(ZipHeader::VersionNeededToExtract);
header.setGeneralPurposeFlags(ZipHeader::GeneralPurposeFlag::LanguageEncoding | ZipHeader::GeneralPurposeFlag::UseDataDescriptor); header.setGeneralPurposeFlags(ZipHeader::GeneralPurposeFlag::LanguageEncoding | ZipHeader::GeneralPurposeFlag::UseDataDescriptor);
header.setCompressionMethod(ZipHeader::CompressionMethod::NoCompression); header.setCompressionMethod(ZipHeader::CompressionMethod::NoCompression);
header.setCrc32UncompressedData(ZipHeader::UnknownCrc32); header.setCrc32UncompressedData(ZipHeader::UnknownCrc32);
header.setCompressedSize(ZipHeader::UnknownFileSize); header.setCompressedSize();
header.setUncompressedSize(ZipHeader::UnknownFileSize); header.setUncompressedSize();
header.setLastModifiedDateTime(_currentFile->second.lastModifiedTime); header.setLastModifiedDateTime(_currentFile->second.lastModifiedTime);
header.setFileNameLength(_currentFile->first.size()); header.setFileNameLength(_currentFile->first.size());
header.setExtraFieldLength(0); header.setExtraFieldLength(Zip64ExtendedInformationExtraField::getHeaderSize());
_writeState = WriteState::LocalFileHeaderFileName; _writeState = WriteState::LocalFileHeaderFileName;
_currentFile->second.localFileHeaderOffset = _currentZipOffset; _currentFile->second.localFileHeaderOffset = _currentZipOffset;
@@ -308,8 +399,8 @@ namespace Zip
return header.getHeaderSize(); return header.getHeaderSize();
} }
std::size_t SizeType
Zipper::writeLocalFileHeaderFileName(std::byte* buffer, std::size_t bufferSize) Zipper::writeLocalFileHeaderFileName(std::byte* buffer, SizeType bufferSize)
{ {
assert(_currentFile != std::end(_files)); assert(_currentFile != std::end(_files));
@@ -318,12 +409,12 @@ namespace Zip
assert(_currentOffset <= fileName.size()); assert(_currentOffset <= fileName.size());
if (_currentOffset == fileName.size()) if (_currentOffset == fileName.size())
{ {
_writeState = WriteState::FileData; _writeState = WriteState::LocalFileHeaderExtraFields;
_currentOffset = 0; _currentOffset = 0;
return 0; return 0;
} }
const std::size_t nbBytesToCopy {std::min<std::size_t>(fileName.size() - _currentOffset, bufferSize)}; const SizeType nbBytesToCopy {std::min<std::size_t>(fileName.size() - _currentOffset, bufferSize)};
std::copy(std::next(std::begin(fileName), _currentOffset), std::next(std::begin(fileName), _currentOffset + nbBytesToCopy), reinterpret_cast<unsigned char*>(buffer)); std::copy(std::next(std::begin(fileName), _currentOffset), std::next(std::begin(fileName), _currentOffset + nbBytesToCopy), reinterpret_cast<unsigned char*>(buffer));
@@ -331,8 +422,25 @@ namespace Zip
return nbBytesToCopy; return nbBytesToCopy;
} }
std::size_t SizeType
Zipper::writeFileData(std::byte* buffer, std::size_t bufferSize) 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)); assert(_currentFile != std::end(_files));
@@ -356,7 +464,7 @@ namespace Zip
if (fileSize != _currentFile->second.fileSize) if (fileSize != _currentFile->second.fileSize)
throw ZipperException {"File '" + filePath + "': size mismatch!"}; throw ZipperException {"File '" + filePath + "': size mismatch!"};
const std::size_t nbBytesToRead {std::min(static_cast<std::size_t>(fileSize) - _currentOffset, bufferSize)}; const SizeType nbBytesToRead {std::min(static_cast<std::size_t>(fileSize) - _currentOffset, bufferSize)};
ifs.seekg(_currentOffset, std::ios::beg); ifs.seekg(_currentOffset, std::ios::beg);
ifs.read(reinterpret_cast<char*>(buffer), nbBytesToRead ); ifs.read(reinterpret_cast<char*>(buffer), nbBytesToRead );
@@ -368,8 +476,8 @@ namespace Zip
return actualReadSize; return actualReadSize;
} }
std::size_t SizeType
Zipper::writeDataDescriptor(std::byte* buffer, std::size_t bufferSize) Zipper::writeDataDescriptor(std::byte* buffer, SizeType bufferSize)
{ {
assert(bufferSize >= minOutputBufferSize); assert(bufferSize >= minOutputBufferSize);
static_assert(DataDescriptor::getHeaderSize() <= minOutputBufferSize); static_assert(DataDescriptor::getHeaderSize() <= minOutputBufferSize);
@@ -388,8 +496,8 @@ namespace Zip
return desc.getHeaderSize(); return desc.getHeaderSize();
} }
std::size_t SizeType
Zipper::writeCentralDirectoryHeader(std::byte* buffer, std::size_t bufferSize) Zipper::writeCentralDirectoryHeader(std::byte* buffer, SizeType bufferSize)
{ {
assert(bufferSize >= minOutputBufferSize); assert(bufferSize >= minOutputBufferSize);
static_assert(CentralDirectoryHeader::getHeaderSize() <= minOutputBufferSize); static_assert(CentralDirectoryHeader::getHeaderSize() <= minOutputBufferSize);
@@ -399,28 +507,28 @@ namespace Zip
if (_currentFile == std::end(_files)) if (_currentFile == std::end(_files))
{ {
_writeState = WriteState::EndOfCentralDirectoryRecord; _writeState = WriteState::Zip64EndOfCentralDirectoryRecord;
_currentFile = std::begin(_files); _currentFile = std::begin(_files);
return 0; return 0;
} }
CentralDirectoryHeader header {buffer, bufferSize}; CentralDirectoryHeader header {buffer, bufferSize};
header.setSignature(); header.setSignature();
header.setVersionMadeBy(2, 0); header.setVersionMadeBy(ZipHeader::VersionMadeBy);
header.setVersionNeededToExtract(1, 0); header.setVersionNeededToExtract(ZipHeader::VersionNeededToExtract);
header.setGeneralPurposeFlags(ZipHeader::GeneralPurposeFlag::LanguageEncoding | ZipHeader::GeneralPurposeFlag::UseDataDescriptor); header.setGeneralPurposeFlags(ZipHeader::GeneralPurposeFlag::LanguageEncoding | ZipHeader::GeneralPurposeFlag::UseDataDescriptor);
header.setCompressionMethod(ZipHeader::CompressionMethod::NoCompression); header.setCompressionMethod(ZipHeader::CompressionMethod::NoCompression);
header.setCompressedSize(_currentFile->second.fileSize); header.setCompressedSize();
header.setUncompressedSize(_currentFile->second.fileSize); header.setUncompressedSize();
header.setLastModifiedDateTime(_currentFile->second.lastModifiedTime); header.setLastModifiedDateTime(_currentFile->second.lastModifiedTime);
header.setCrc32UncompressedData(_currentFile->second.fileCrc32.getResult()); header.setCrc32UncompressedData(_currentFile->second.fileCrc32.getResult());
header.setFileNameLength(_currentFile->first.size()); header.setFileNameLength(_currentFile->first.size());
header.setExtraFieldLength(0); header.setExtraFieldLength(Zip64ExtendedInformationExtraField::getHeaderSize(Zip64ExtendedInformationExtraField::WithFileOffset {}));
header.setFileCommentLength(0); header.setFileCommentLength(0);
header.setDiskNumber(0); header.setDiskNumber(0);
header.setInternalFileAttributes(0); header.setInternalFileAttributes(0);
header.setExternalFileAttributes(0); header.setExternalFileAttributes(0);
header.setRelativeFileHeaderOffset(_currentFile->second.localFileHeaderOffset); header.setRelativeFileHeaderOffset();
_writeState = WriteState::CentralDirectoryHeaderFileName; _writeState = WriteState::CentralDirectoryHeaderFileName;
_centralDirectorySize += header.getHeaderSize(); _centralDirectorySize += header.getHeaderSize();
@@ -428,8 +536,8 @@ namespace Zip
return header.getHeaderSize(); return header.getHeaderSize();
} }
std::size_t SizeType
Zipper::writeCentralDirectoryHeaderFileName(std::byte* buffer, std::size_t bufferSize) Zipper::writeCentralDirectoryHeaderFileName(std::byte* buffer, SizeType bufferSize)
{ {
const std::string& fileName {_currentFile->first}; const std::string& fileName {_currentFile->first};
@@ -437,13 +545,12 @@ namespace Zip
if (_currentOffset == fileName.size()) if (_currentOffset == fileName.size())
{ {
_currentOffset = 0; _currentOffset = 0;
++_currentFile; _writeState = WriteState::CentralDirectoryHeaderExtraFields;
_writeState = WriteState::CentralDirectoryHeader;
return 0; return 0;
} }
const std::size_t nbBytesToCopy {std::min<std::size_t>(fileName.size() - _currentOffset, bufferSize)}; const SizeType nbBytesToCopy {std::min<std::size_t>(fileName.size() - _currentOffset, bufferSize)};
std::copy(std::next(std::begin(fileName), _currentOffset), std::next(std::begin(fileName), _currentOffset + nbBytesToCopy), reinterpret_cast<unsigned char*>(buffer)); std::copy(std::next(std::begin(fileName), _currentOffset), std::next(std::begin(fileName), _currentOffset + nbBytesToCopy), reinterpret_cast<unsigned char*>(buffer));
@@ -452,9 +559,71 @@ namespace Zip
return nbBytesToCopy; 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 Zip64ExtendedInformationExtraField header {buffer, bufferSize, Zip64ExtendedInformationExtraField::WithFileOffset {}};
Zipper::writeEndOfCentralDirectoryRecord(std::byte* buffer, std::size_t bufferSize)
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); assert(bufferSize >= minOutputBufferSize);
static_assert(EndOfCentralDirectoryRecord::getHeaderSize() <= minOutputBufferSize); static_assert(EndOfCentralDirectoryRecord::getHeaderSize() <= minOutputBufferSize);
@@ -464,10 +633,10 @@ namespace Zip
record.setSignature(); record.setSignature();
record.setDiskNumber(0); record.setDiskNumber(0);
record.setCentralDirectoryDiskNumber(0); record.setCentralDirectoryDiskNumber(0);
record.setNbDiskCentralDirectoryRecords(_files.size()); record.setNbDiskCentralDirectoryRecords();
record.setNbCentralDirectoryRecords(_files.size()); record.setNbCentralDirectoryRecords();
record.setCentralDirectorySize(_centralDirectorySize); record.setCentralDirectorySize();
record.setCentralDirectoryOffset(_centralDirectoryOffset); record.setCentralDirectoryOffset();
record.setCommentLength(0); record.setCommentLength(0);
_writeState = WriteState::Complete; _writeState = WriteState::Complete;
+28 -21
View File
@@ -19,9 +19,8 @@
#pragma once #pragma once
#include <map>
#include <set>
#include <filesystem> #include <filesystem>
#include <map>
#include <Wt/WDateTime.h> #include <Wt/WDateTime.h>
@@ -30,6 +29,7 @@
namespace Zip namespace Zip
{ {
using SizeType = std::uint64_t;
class ZipperException : public LmsException class ZipperException : public LmsException
{ {
@@ -41,34 +41,36 @@ namespace Zip
{ {
public: public:
using SizeZype = std::uint64_t;
Zipper(const std::map<std::string, std::filesystem::path>& files, const Wt::WDateTime& lastModifiedTime = {}); Zipper(const std::map<std::string, std::filesystem::path>& files, const Wt::WDateTime& lastModifiedTime = {});
static constexpr std::size_t minOutputBufferSize = 64; static constexpr SizeType minOutputBufferSize {64};
std::size_t writeSome(std::byte* buffer, std::size_t bufferSize); SizeType writeSome(std::byte* buffer, SizeType bufferSize);
bool isComplete() const; bool isComplete() const;
SizeZype getTotalZipFile() const { return _totalZipSize; } SizeType getTotalZipFile() const { return _totalZipSize; }
private: private:
void setComplete(); void setComplete();
std::size_t writeLocalFileHeader(std::byte* buffer, std::size_t bufferSize); SizeType writeLocalFileHeader(std::byte* buffer, SizeType bufferSize);
std::size_t writeLocalFileHeaderFileName(std::byte* buffer, std::size_t bufferSize); SizeType writeLocalFileHeaderFileName(std::byte* buffer, SizeType bufferSize);
std::size_t writeFileData(std::byte* buffer, std::size_t bufferSize); SizeType writeLocalFileHeaderExtraFields(std::byte* buffer, SizeType bufferSize);
std::size_t writeDataDescriptor(std::byte* buffer, std::size_t bufferSize); SizeType writeFileData(std::byte* buffer, SizeType bufferSize);
std::size_t writeCentralDirectoryHeader(std::byte* buffer, std::size_t bufferSize); SizeType writeDataDescriptor(std::byte* buffer, SizeType bufferSize);
std::size_t writeCentralDirectoryHeaderFileName(std::byte* buffer, std::size_t bufferSize); SizeType writeCentralDirectoryHeader(std::byte* buffer, SizeType bufferSize);
std::size_t writeEndOfCentralDirectoryRecord(std::byte* buffer, std::size_t 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 struct FileContext
{ {
std::filesystem::path filePath; std::filesystem::path filePath;
std::size_t fileSize; SizeType fileSize;
Wt::WDateTime lastModifiedTime; Wt::WDateTime lastModifiedTime;
Utils::Crc32Calculator fileCrc32; Utils::Crc32Calculator fileCrc32;
std::size_t localFileHeaderOffset {}; SizeType localFileHeaderOffset {};
}; };
using FileContainer = std::map<std::string, FileContext>; using FileContainer = std::map<std::string, FileContext>;
@@ -78,21 +80,26 @@ namespace Zip
{ {
LocalFileHeader, LocalFileHeader,
LocalFileHeaderFileName, LocalFileHeaderFileName,
LocalFileHeaderExtraFields,
FileData, FileData,
DataDescriptor, DataDescriptor,
CentralDirectoryHeader, CentralDirectoryHeader,
CentralDirectoryHeaderFileName, CentralDirectoryHeaderFileName,
CentralDirectoryHeaderExtraFields,
Zip64EndOfCentralDirectoryRecord,
Zip64EndOfCentralDirectoryLocator,
EndOfCentralDirectoryRecord, EndOfCentralDirectoryRecord,
Complete, Complete,
}; };
SizeZype _totalZipSize {}; SizeType _totalZipSize {};
WriteState _writeState {WriteState::LocalFileHeader}; WriteState _writeState {WriteState::LocalFileHeader};
FileContainer::iterator _currentFile; FileContainer::iterator _currentFile;
std::size_t _currentOffset {}; SizeType _currentOffset {};
std::size_t _currentZipOffset {}; SizeType _currentZipOffset {};
std::size_t _centralDirectoryOffset {}; SizeType _centralDirectoryOffset {};
std::size_t _centralDirectorySize {}; SizeType _centralDirectorySize {};
SizeType _zip64EndOfCentralDirectoryRecordOffset {};
}; };
} // namespace Zip } // namespace Zip
+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
+1
View File
@@ -57,6 +57,7 @@ DownloadResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Resp
else else
{ {
zipper = createZipper(); zipper = createZipper();
response.setContentLength(zipper->getTotalZipFile());
response.setMimeType("application/zip"); response.setMimeType("application/zip");
} }
+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;
+6 -1
View File
@@ -61,15 +61,20 @@ int main(int argc, char* argv[])
{ {
Zipper zipper {files}; Zipper zipper {files};
Zip::SizeType nbTotalWrittenBytes {};
while (!zipper.isComplete()) while (!zipper.isComplete())
{ {
//std::array<std::byte, Zipper::minOutputBufferSize> buffer; //std::array<std::byte, Zipper::minOutputBufferSize> buffer;
std::array<std::byte, 65536> buffer; std::array<std::byte, 65536> 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<const char*>(buffer.data()), nbWrittenBytes); ofs.write(reinterpret_cast<const char*>(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; std::cout << "Total zip size = " << zipper.getTotalZipFile() << std::endl;
} }
catch (const ZipperException& e) catch (const ZipperException& e)