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!
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_.
+6 -1
View File
@@ -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;
+3 -3
View File
@@ -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
+6 -12
View File
@@ -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<Picture>
MediaFile::getAttachedPictures(std::size_t nbMaxPictures) const
void
MediaFile::visitAttachedPictures(std::function<void(const Picture&)> func) const
{
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" },
};
std::vector<Picture> 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<const std::byte*>(pkt.data);
picture.dataSize = pkt.size;
pictures.push_back( picture );
if (pictures.size() >= nbMaxPictures)
break;
func(picture);
}
return pictures;
}
std::optional<MediaFileFormat>
+5 -3
View File
@@ -23,6 +23,7 @@
#include <chrono>
#include <filesystem>
#include <functional>
#include <map>
#include <optional>
#include <string>
@@ -40,7 +41,8 @@ void AvInit();
struct Picture
{
std::string mimeType;
std::vector<uint8_t> data;
const std::byte* data {};
std::size_t dataSize;
};
struct StreamInfo
@@ -76,11 +78,11 @@ class MediaFile
std::vector<StreamInfo> getStreamInfo() const;
std::optional<std::size_t> getBestStream() const; // none if failure/unknown
bool hasAttachedPictures(void) const;
std::vector<Picture> getAttachedPictures(std::size_t nbMaxPictures) const;
void visitAttachedPictures(std::function<void(const Picture&)> func) const;
private:
std::filesystem::path _p;
const std::filesystem::path _p;
AVFormatContext* _context {};
};
+1
View File
@@ -1,5 +1,6 @@
add_library(lmscover SHARED
impl/CoverArt.cpp
impl/CoverArtGrabber.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/Random.hpp"
#include "CoverArt.hpp"
namespace {
bool
@@ -40,89 +42,156 @@ isFileSupported(const std::filesystem::path& file, const std::vector<std::filesy
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);
}
void
Grabber::setDefaultCover(const std::filesystem::path& p)
{
_defaultCover = std::make_unique<Image>();
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<Image>
getFromAvMediaFile(const Av::MediaFile& input)
static std::optional<EncodedImage>
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))
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<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 std::nullopt;
return image;
}
std::optional<Image>
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<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)};
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<EncodedImage> 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<EncodedImage> 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<std::string, std::filesystem::path>
@@ -156,29 +225,32 @@ Grabber::getCoverPaths(const std::filesystem::path& directoryPath) const
return res;
}
std::optional<Image>
Grabber::getFromTrack(const std::filesystem::path& p) const
std::optional<EncodedImage>
Grabber::getFromTrack(const std::filesystem::path& p, Width width) const
{
std::optional<EncodedImage> 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<Image> cover {loadFromCache(cacheEntryDesc)};
const CacheEntryDesc cacheEntryDesc {CacheEntryDesc::Type::Track, trackId, width};
std::optional<EncodedImage> 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<Image> cover {loadFromCache(cacheEntryDesc)};
const CacheEntryDesc cacheEntryDesc {CacheEntryDesc::Type::Release, releaseId, width};
std::optional<EncodedImage> 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<uint8_t>
Grabber::getFromTrack(Database::Session& session, Database::IdType trackId, Format format, std::size_t width)
std::unique_ptr<ICoverArt>
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<CoverArt>(getFromTrackInternal(session, trackId, width));
}
std::vector<uint8_t>
Grabber::getFromRelease(Database::Session& session, Database::IdType releaseId, Format format, std::size_t width)
std::unique_ptr<ICoverArt>
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<CoverArt>(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<Image>
std::optional<EncodedImage>
Grabber::loadFromCache(const CacheEntryDesc& entryDesc)
{
std::shared_lock lock {_cacheMutex};
+23 -20
View File
@@ -19,8 +19,8 @@
#pragma once
#include <atomic>
#include <filesystem>
#include <map>
#include <optional>
#include <shared_mutex>
#include <string_view>
@@ -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<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;
std::vector<uint8_t> 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<Image> getFromTrack(const std::filesystem::path& path) const;
std::optional<EncodedImage> getFromTrack(const std::filesystem::path& path, Width width) 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::unordered_map<CacheEntryDesc, Image> _cache;
std::size_t _cacheMisses {};
std::size_t _cacheHits {};
std::unordered_map<CacheEntryDesc, EncodedImage> _cache;
std::unordered_map<Width, EncodedImage> _defaultCache;
std::atomic<std::size_t> _cacheMisses {};
std::atomic<std::size_t> _cacheHits {};
std::size_t _cacheSize {};
void saveToCache(const CacheEntryDesc& entryDesc, const Image& image);
std::optional<Image> loadFromCache(const CacheEntryDesc& entryDesc);
void saveToCache(const CacheEntryDesc& entryDesc, const EncodedImage& image);
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 constexpr std::size_t _maxFileSize {10000000};
const std::size_t _maxFileSize;
static inline const std::vector<std::string> _preferredFileNames {"cover", "front"}; // TODO parametrize
};
+68 -77
View File
@@ -19,6 +19,11 @@
#include "Image.hpp"
#include <atomic>
#include <fstream>
#include <magick/resource.h>
#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<const std::byte*>(_blob.data());
}
bool
Image::load(const std::vector<unsigned char>& 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<unsigned int>(width), static_cast<unsigned int>(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<uint8_t>
Image::save(Format format) const
EncodedImage
RawImage::encode() const
{
std::vector<uint8_t> res;
try
{
Magick::Image outputImage {_image};
outputImage.magick(formatToMagick(format));
outputImage.magick("JPEG");
Magick::Blob blob;
outputImage.write(&blob);
auto begin = static_cast<const uint8_t*>(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()};
}
}
+35 -18
View File
@@ -24,33 +24,50 @@
#include <Magick++.h>
#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<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;
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
@@ -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 <string>
#include <cstddef>
#include <string_view>
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
@@ -19,32 +19,37 @@
#pragma once
#include <cstddef>
#include <filesystem>
#include <vector>
#include <memory>
#include <string_view>
#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<uint8_t> getFromTrack(Database::Session& dbSession, Database::IdType trackId, Format format, std::size_t width) = 0;
virtual std::vector<uint8_t> 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<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
+2 -2
View File
@@ -55,7 +55,7 @@ namespace API::Subsonic
{
std::vector<T> res {getMultiParametersAs<T>(parameterMap, param)};
if (res.empty())
throw RequiredParameterMissingError {};
throw RequiredParameterMissingError {param};
return res;
}
@@ -78,7 +78,7 @@ namespace API::Subsonic
{
auto res {getParameterAs<T>(parameterMap, param)};
if (!res)
throw RequiredParameterMissingError {};
throw RequiredParameterMissingError {param};
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.
*
@@ -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<std::size_t>(context.parameters, "size").value_or(256)};
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)
{
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;
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;
default:
throw BadParameterGenericError {"id"};
}
response.out().write(reinterpret_cast<const char*>(&data[0]), data.size());
response.setMimeType(CoverArt::formatToMimeType(CoverArt::Format::JPEG));
response.out().write(reinterpret_cast<const char*>(cover->getData()), cover->getDataSize());
response.setMimeType(std::string {cover->getMimeType()});
}
using RequestHandlerFunc = std::function<Response(RequestContext& context)>;
+7 -2
View File
@@ -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
+257 -88
View File
@@ -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<std::byte>(value);
}
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 + 1] = static_cast<std::byte>(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<std::byte>(value & 0xff);
_buffer[offset + 1] = static_cast<std::byte>((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<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{};
@@ -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<std::string, std::filesystem::path>& 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<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));
@@ -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<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.read(reinterpret_cast<char*>(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<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));
@@ -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;
+28 -21
View File
@@ -19,9 +19,8 @@
#pragma once
#include <map>
#include <set>
#include <filesystem>
#include <map>
#include <Wt/WDateTime.h>
@@ -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<std::string, std::filesystem::path>& 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<std::string, FileContext>;
@@ -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
+3 -1
View File
@@ -168,7 +168,9 @@ int main(int argc, char* argv[])
// Service initialization order is important
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<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");
Service<Recommendation::IEngine> recommendationEngineService {Recommendation::createEngine(database)};
recommendationEngineService->requestLoad();
+2 -4
View File
@@ -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
+1
View File
@@ -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");
}
+5 -11
View File
@@ -74,7 +74,7 @@ ImageResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Respons
return;
}
std::vector<uint8_t> cover;
std::unique_ptr<CoverArt::ICoverArt> 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<CoverArt::IGrabber>::get()->getFromTrack(LmsApp->getDbSession(), *trackId, CoverArt::Format::JPEG, *size);
cover = Service<CoverArt::IGrabber>::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<CoverArt::IGrabber>::get()->getFromRelease(LmsApp->getDbSession(), *releaseId, CoverArt::Format::JPEG, *size);
cover = Service<CoverArt::IGrabber>::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<const char *>(&cover[0]), cover.size());
}
std::string
ImageResource::getMimeType()
{
return CoverArt::formatToMimeType(CoverArt::Format::JPEG);
response.out().write(reinterpret_cast<const char *>(cover->getData()), cover->getDataSize());
}
} // 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 getTrackUrl(Database::IdType trackId, Size size) const;
static std::string getMimeType();
private:
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};
Zip::SizeType nbTotalWrittenBytes {};
while (!zipper.isComplete())
{
//std::array<std::byte, Zipper::minOutputBufferSize> 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);
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)