From 060379a56b77cc7ed1a28af2908d23e838f0db4c Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 14 Sep 2018 21:03:25 +0200 Subject: [PATCH 1/8] Simplify the covert art resource --- src/cover/CoverArtGrabber.cpp | 129 +++++++++++++------------- src/cover/CoverArtGrabber.hpp | 17 +++- src/ui/LmsApplication.cpp | 4 +- src/ui/resource/ImageResource.cpp | 124 ++----------------------- src/ui/resource/ImageResource.hpp | 14 --- src/ui/resource/TranscodeResource.cpp | 15 +-- src/ui/resource/TranscodeResource.hpp | 3 - 7 files changed, 95 insertions(+), 211 deletions(-) diff --git a/src/cover/CoverArtGrabber.cpp b/src/cover/CoverArtGrabber.cpp index fee2167f..b6485364 100644 --- a/src/cover/CoverArtGrabber.cpp +++ b/src/cover/CoverArtGrabber.cpp @@ -43,12 +43,15 @@ isFileSupported(const boost::filesystem::path& file, const std::vectordocRoot() + "/images/unknown-cover.jpg")) + throw LmsException("Cannot read default cover file"); } Grabber& @@ -58,48 +61,42 @@ Grabber::instance() return instance; } -static std::vector -getFromAvMediaFile(const Av::MediaFile& input, std::size_t nbMaxCovers) +static boost::optional +getFromAvMediaFile(const Av::MediaFile& input) { std::vector res; - for (Av::Picture& picture : input.getAttachedPictures(nbMaxCovers)) + for (auto& picture : input.getAttachedPictures(2)) { Image::Image image; if (image.load(picture.data)) - res.push_back( image ); + return image; else LMS_LOG(COVER, ERROR) << "Cannot load embedded cover file in '" << input.getPath().string() << "'"; } - return res; + return boost::none; } -std::vector -Grabber::getFromDirectory(const boost::filesystem::path& p, std::size_t nbMaxCovers) const +boost::optional +Grabber::getFromDirectory(const boost::filesystem::path& p) const { - std::vector res; - - std::vector coverPathes = getCoverPaths(p, nbMaxCovers); - for (auto coverPath : coverPathes) + for (auto coverPath : getCoverPaths(p)) { - if (res.size() >= nbMaxCovers) - break; - Image::Image image; if (image.load(coverPath)) - res.push_back(image); + return image; else LMS_LOG(COVER, ERROR) << "Cannot load image in file '" << coverPath.string() << "'"; } - return res; + return boost::none; } std::vector -Grabber::getCoverPaths(const boost::filesystem::path& directoryPath, std::size_t nbMaxCovers) const +Grabber::getCoverPaths(const boost::filesystem::path& directoryPath) const { std::vector res; boost::system::error_code ec; @@ -126,86 +123,94 @@ Grabber::getCoverPaths(const boost::filesystem::path& directoryPath, std::size_t } res.push_back(path); - if (res.size() >= nbMaxCovers) - break; } + return res; } -std::vector -Grabber::getFromTrack(const boost::filesystem::path& p, std::size_t nbMaxCovers) const +boost::optional +Grabber::getFromTrack(const boost::filesystem::path& p) const { try { Av::MediaFile input(p); - return getFromAvMediaFile(input, nbMaxCovers); + return getFromAvMediaFile(input); } catch (Av::MediaFileException& e) { LMS_LOG(COVER, ERROR) << "Cannot get covers from track " << p.string() << ": " << e.what(); + return boost::none; } - - return std::vector(); } -std::vector -Grabber::getFromTrack(Wt::Dbo::Session& session, Database::IdType trackId, std::size_t nbMaxCovers) const +Image::Image +Grabber::getFromTrack(Wt::Dbo::Session& session, Database::IdType trackId, std::size_t size) const { using namespace Database; - Wt::Dbo::Transaction transaction(session); + boost::optional cover; - Track::pointer track = Track::getById(session, trackId); - if (!track) - return std::vector(); - - Track::CoverType coverType = track->getCoverType(); - boost::filesystem::path trackPath = track->getPath(); - - transaction.commit(); - - switch (coverType) { - case Track::CoverType::Embedded: - return Grabber::getFromTrack(trackPath, nbMaxCovers); - case Track::CoverType::None: - return Grabber::getFromDirectory(trackPath.parent_path(), nbMaxCovers); + Wt::Dbo::Transaction transaction(session); + + Track::pointer track = Track::getById(session, trackId); + if (track) + { + Track::CoverType coverType = track->getCoverType(); + boost::filesystem::path trackPath = track->getPath(); + + transaction.commit(); + + switch (coverType) + { + case Track::CoverType::Embedded: + cover = getFromTrack(trackPath); + case Track::CoverType::None: + cover = getFromDirectory(trackPath.parent_path()); + } + } } - return std::vector(); + if (!cover) + cover = _defaultCover; + + cover->scale(size); + + return *cover; } -std::vector -Grabber::getFromRelease(Wt::Dbo::Session& session, Database::IdType releaseId, std::size_t nbMaxCovers) const +Image::Image +Grabber::getFromRelease(Wt::Dbo::Session& session, Database::IdType releaseId, std::size_t size) const { using namespace Database; - Wt::Dbo::Transaction transaction(session); + boost::optional cover; - // If the release does not exist, do nothing - Release::pointer release = Release::getById(session, releaseId); - if (!release) - return std::vector(); + { + Wt::Dbo::Transaction transaction(session); - std::vector tracks = release->getTracks(); - if (tracks.empty()) - return std::vector(); + auto release = Release::getById(session, releaseId); + if (release) + { + auto tracks = release->getTracks(); + if (!tracks.empty()) + { + auto trackId = tracks.front().id(); + transaction.commit(); - boost::filesystem::path firstTrackPath = tracks.front()->getPath(); - bool embeddedCover = (tracks.front()->getCoverType() == Track::CoverType::Embedded); + return getFromTrack(session, trackId, size); + } + } + } - transaction.commit(); + if (!cover) + cover = _defaultCover; - // First, try to get covers from the directory of the release - std::vector res = getFromDirectory( firstTrackPath.parent_path(), nbMaxCovers); + cover->scale(size); - // Fallback on the embedded cover of the first track - if (res.empty() && embeddedCover) - res = getFromTrack( firstTrackPath, nbMaxCovers); - - return res; + return *cover; } } // namespace CoverArt diff --git a/src/cover/CoverArtGrabber.hpp b/src/cover/CoverArtGrabber.hpp index 43753c4f..b62c22e0 100644 --- a/src/cover/CoverArtGrabber.hpp +++ b/src/cover/CoverArtGrabber.hpp @@ -35,18 +35,25 @@ class Grabber static Grabber& instance(); - std::vector getCoverPaths(const boost::filesystem::path& directoryPath, std::size_t nbMaxCovers = 1) const; - std::vector getFromDirectory(const boost::filesystem::path& path, std::size_t nbMaxCovers = 1) const; - std::vector getFromTrack(const boost::filesystem::path& path, std::size_t nbMaxCovers = 1) const; - std::vector getFromTrack(Wt::Dbo::Session& session, Database::IdType trackId, std::size_t nbMaxCovers = 1) const; - std::vector getFromRelease(Wt::Dbo::Session& session, Database::IdType releaseId, std::size_t nbMaxCovers = 1) const; + Image::Image getFromTrack(Wt::Dbo::Session& session, Database::IdType trackId, std::size_t size) const; + Image::Image getFromRelease(Wt::Dbo::Session& session, Database::IdType releaseId, std::size_t size) const; private: + Grabber(); + boost::optional getFromTrack(const boost::filesystem::path& path) const; + std::vector getCoverPaths(const boost::filesystem::path& directoryPath) const; + boost::optional getFromDirectory(const boost::filesystem::path& path) const; + + + Image::Image _defaultCover; + std::vector _fileExtensions = {".jpg", ".jpeg", ".png", ".bmp"}; // TODO parametrize + std::size_t _maxFileSize = 5000000; + std::vector _preferredFileNames = {"cover", "front"}; // TODO parametrize }; diff --git a/src/ui/LmsApplication.cpp b/src/ui/LmsApplication.cpp index 354ceeb4..9e6f3b40 100644 --- a/src/ui/LmsApplication.cpp +++ b/src/ui/LmsApplication.cpp @@ -328,8 +328,8 @@ LmsApplication::createHome() _isAdmin = LmsApp->getUser()->isAdmin(); } - _imageResource = std::make_shared(_db); - _transcodeResource = std::make_shared(_db); + _imageResource = std::make_shared(); + _transcodeResource = std::make_shared(); setConfirmCloseMessage(Wt::WString::tr("Lms.quit-confirm")); diff --git a/src/ui/resource/ImageResource.cpp b/src/ui/resource/ImageResource.cpp index dc45b339..a41c9f8d 100644 --- a/src/ui/resource/ImageResource.cpp +++ b/src/ui/resource/ImageResource.cpp @@ -37,61 +37,11 @@ namespace UserInterface { static const std::string unknownCoverPath = "/images/unknown-cover.jpg"; static const std::string unknownArtistImagePath = "/images/unknown-artist.jpg"; -ImageResource::ImageResource(Database::Handler& db) -: _db(db) -{ -} - ImageResource::~ImageResource() { beingDeleted(); } -Image::Image -ImageResource::getDefaultCover(std::size_t size) -{ - std::unique_lock lock(_mutex); - - auto itCover = _defaultCovers.find(size); - if (itCover == _defaultCovers.end()) - { - // Load default cover art for this size - Image::Image image; - - if (!image.load( Wt::WApplication::instance()->docRoot() + unknownCoverPath )) - throw LmsException("Cannot read default cover file"); - - image.scale(size); - - auto res = _defaultCovers.insert(std::make_pair(size, image)); - itCover = res.first; - } - - return itCover->second; -} - -Image::Image -ImageResource::getDefaultArtistImage(std::size_t size) -{ - std::unique_lock lock(_mutex); - - auto itCover = _defaultArtistImages.find(size); - if (itCover == _defaultArtistImages.end()) - { - Image::Image image; - - if (!image.load( Wt::WApplication::instance()->docRoot() + unknownArtistImagePath)) - throw LmsException("Cannot read default artist image file"); - - image.scale(size); - - auto res = _defaultArtistImages.insert(std::make_pair(size, image)); - itCover = res.first; - } - - return itCover->second; -} - std::string ImageResource::getReleaseUrl(Database::IdType releaseId, std::size_t size) const { @@ -127,29 +77,12 @@ ImageResource::putImage(Wt::Http::Response& response, Image::Image cover) response.out().write(reinterpret_cast(&data[0]), data.size()); } -void -ImageResource::putCover(Wt::Http::Response& response, std::vector covers, size_t size) -{ - for (Image::Image cover : covers) - { - if (cover.scale(size)) - { - putImage(response, cover); - return; - } - } - - // If no cover found, just send default one - putImage(response, getDefaultCover(size)); -} - void ImageResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response) { // Retrieve parameters const std::string *trackIdStr = request.getParameter("trackid"); const std::string *releaseIdStr = request.getParameter("releaseid"); - const std::string *artistIdStr = request.getParameter("artistid"); const std::string *sizeStr = request.getParameter("size"); // Mandatory parameter size @@ -160,48 +93,19 @@ ImageResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Respons if (!size || *size > maxSize) return; + Image::Image cover; + if (trackIdStr) { auto trackId = readAs(*trackIdStr); if (!trackId) return; - boost::filesystem::path path; - Database::Track::CoverType coverType = Database::Track::CoverType::None; + // transactions are not thread safe { - // transactions are not thread safe - Wt::WApplication::UpdateLock lock(LmsApplication::instance()); - - Wt::Dbo::Transaction transaction(_db.getSession()); - - Database::Track::pointer track = Database::Track::getById(_db.getSession(), *trackId); - if (track) - { - coverType = track->getCoverType(); - path = track->getPath(); - } + Wt::WApplication::UpdateLock lock(LmsApp); + cover = CoverArt::Grabber::instance().getFromTrack(LmsApp->getDboSession(), *trackId, *size); } - - if (!path.empty()) - { - std::vector covers; - - switch (coverType) - { - case Database::Track::CoverType::Embedded: - covers = CoverArt::Grabber::instance().getFromTrack(path); - break; - - case Database::Track::CoverType::None: - covers = CoverArt::Grabber::instance().getFromDirectory(path.parent_path()); - break; - } - - putCover(response, covers, *size); - return; - } - - putImage(response, getDefaultCover(*size)); } else if (releaseIdStr) { @@ -209,24 +113,16 @@ ImageResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Respons if (!releaseId) return; - std::vector covers; - // transactions are not thread safe { - Wt::WApplication::UpdateLock lock(LmsApplication::instance()); - covers = CoverArt::Grabber::instance().getFromRelease(_db.getSession(), *releaseId); + Wt::WApplication::UpdateLock lock(LmsApp); + cover = CoverArt::Grabber::instance().getFromRelease(LmsApp->getDboSession(), *releaseId, *size); } - - putCover(response, covers, *size); - } - else if (artistIdStr) - { - putImage(response, getDefaultArtistImage(*size)); } else - { - putImage(response, getDefaultCover(*size)); - } + return; + + putImage(response, cover); } } // namespace UserInterface diff --git a/src/ui/resource/ImageResource.hpp b/src/ui/resource/ImageResource.hpp index aa01ce2c..608e4c17 100644 --- a/src/ui/resource/ImageResource.hpp +++ b/src/ui/resource/ImageResource.hpp @@ -37,7 +37,6 @@ class ImageResource : public Wt::WResource public: static const std::size_t maxSize = 512; - ImageResource(Database::Handler& db); ~ImageResource(); std::string getReleaseUrl(Database::IdType releaseId, size_t size) const; @@ -49,22 +48,9 @@ class ImageResource : public Wt::WResource private: - Image::Image getDefaultCover(std::size_t size); - Image::Image getDefaultArtistImage(std::size_t size); void putImage(Wt::Http::Response& response, Image::Image image); void putCover(Wt::Http::Response& response, std::vector covers, std::size_t size); - // Used to protect transactions since they are not thread safe - std::mutex _mutex; - Database::Handler& _db; - - // Default cover for different sizes - std::map _defaultCovers; - - // Default artist images for different sizes - std::map _defaultArtistImages; - - // TODO construct a cache for covers? }; } // namespace UserInterface diff --git a/src/ui/resource/TranscodeResource.cpp b/src/ui/resource/TranscodeResource.cpp index c27e2355..82bda742 100644 --- a/src/ui/resource/TranscodeResource.cpp +++ b/src/ui/resource/TranscodeResource.cpp @@ -29,15 +29,9 @@ namespace UserInterface { -TranscodeResource::TranscodeResource(Database::Handler& db) -: _db(db) -{ - LMS_LOG(UI, DEBUG) << "CONSTRUCTING RESOURCE"; -} TranscodeResource:: ~TranscodeResource() { - LMS_LOG(UI, DEBUG) << "DESTRUCTING RESOURCE"; beingDeleted(); } @@ -100,12 +94,11 @@ TranscodeResource::handleRequest(const Wt::Http::Request& request, // transactions are not thread safe { - Wt::WApplication::UpdateLock lock(LmsApplication::instance()); + Wt::WApplication::UpdateLock lock(LmsApp); - Wt::Dbo::Transaction transaction(_db.getSession()); + Wt::Dbo::Transaction transaction(LmsApp->getDboSession()); - Database::User::pointer user = _db.getCurrentUser(); - Database::Track::pointer track = Database::Track::getById(_db.getSession(), trackId); + Database::Track::pointer track = Database::Track::getById(LmsApp->getDboSession(), trackId); if (!track) { @@ -113,7 +106,7 @@ TranscodeResource::handleRequest(const Wt::Http::Request& request, return; } - parameters.bitrate = user->getAudioBitrate(); + parameters.bitrate = LmsApp->getUser()->getAudioBitrate(); transcoder = std::make_shared(track->getPath(), parameters); } diff --git a/src/ui/resource/TranscodeResource.hpp b/src/ui/resource/TranscodeResource.hpp index ead39acc..6867db91 100644 --- a/src/ui/resource/TranscodeResource.hpp +++ b/src/ui/resource/TranscodeResource.hpp @@ -34,7 +34,6 @@ namespace UserInterface { class TranscodeResource : public Wt::WResource { public: - TranscodeResource(Database::Handler& db); ~TranscodeResource(); std::string getUrl(Database::IdType trackId, Av::Encoding encoding) const; @@ -44,8 +43,6 @@ class TranscodeResource : public Wt::WResource private: - Database::Handler& _db; - static const std::size_t _chunkSize = 65536*4; }; From 843f6ef0fe63e8aadf4d0d1045aff9416c30d1d8 Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 14 Sep 2018 21:23:55 +0200 Subject: [PATCH 2/8] Simplify the covert art resource, step 2 --- src/cover/CoverArtGrabber.cpp | 16 ++++++++++++++++ src/cover/CoverArtGrabber.hpp | 7 +++++-- src/image/Image.cpp | 11 ++++++----- src/image/Image.hpp | 2 +- src/ui/resource/ImageResource.cpp | 32 +++++-------------------------- src/ui/resource/ImageResource.hpp | 7 ------- 6 files changed, 33 insertions(+), 42 deletions(-) diff --git a/src/cover/CoverArtGrabber.cpp b/src/cover/CoverArtGrabber.cpp index b6485364..4f8a3f23 100644 --- a/src/cover/CoverArtGrabber.cpp +++ b/src/cover/CoverArtGrabber.cpp @@ -213,5 +213,21 @@ Grabber::getFromRelease(Wt::Dbo::Session& session, Database::IdType releaseId, s return *cover; } +std::vector +Grabber::getFromTrack(Wt::Dbo::Session& session, Database::IdType trackId, Image::Format format, std::size_t size) const +{ + Image::Image cover = getFromTrack(session, trackId, size); + + return cover.save(Image::Format::JPEG); +} + +std::vector +Grabber::getFromRelease(Wt::Dbo::Session& session, Database::IdType releaseId, Image::Format format, std::size_t size) const +{ + Image::Image cover = getFromRelease(session, releaseId, size); + + return cover.save(Image::Format::JPEG); +} + } // namespace CoverArt diff --git a/src/cover/CoverArtGrabber.hpp b/src/cover/CoverArtGrabber.hpp index b62c22e0..c2d572c2 100644 --- a/src/cover/CoverArtGrabber.hpp +++ b/src/cover/CoverArtGrabber.hpp @@ -35,13 +35,16 @@ class Grabber static Grabber& instance(); - Image::Image getFromTrack(Wt::Dbo::Session& session, Database::IdType trackId, std::size_t size) const; - Image::Image getFromRelease(Wt::Dbo::Session& session, Database::IdType releaseId, std::size_t size) const; + std::vector getFromTrack(Wt::Dbo::Session& session, Database::IdType trackId, Image::Format format, std::size_t size) const; + std::vector getFromRelease(Wt::Dbo::Session& session, Database::IdType releaseId, Image::Format format, std::size_t size) const; private: Grabber(); + Image::Image getFromTrack(Wt::Dbo::Session& session, Database::IdType trackId, std::size_t size) const; + Image::Image getFromRelease(Wt::Dbo::Session& session, Database::IdType releaseId, std::size_t size) const; + boost::optional getFromTrack(const boost::filesystem::path& path) const; std::vector getCoverPaths(const boost::filesystem::path& directoryPath) const; boost::optional getFromDirectory(const boost::filesystem::path& path) const; diff --git a/src/image/Image.cpp b/src/image/Image.cpp index 11c63eda..2822936e 100644 --- a/src/image/Image.cpp +++ b/src/image/Image.cpp @@ -97,13 +97,13 @@ Image::scale(std::size_t size) } catch (Magick::Exception& e) { - LMS_LOG(COVER, ERROR) << "Caught Magick exception: " << e.what(); + LMS_LOG(COVER, ERROR) << "Caught Magick exception during scale: " << e.what(); return false; } } -void -Image::save(std::vector& data, Format format) const +std::vector +Image::save(Format format) const { Magick::Image outputImage(_image); @@ -112,8 +112,9 @@ Image::save(std::vector& data, Format format) const Magick::Blob blob; outputImage.write(&blob); - unsigned char *charBuf = (unsigned char*)blob.data(); - data.assign( charBuf, charBuf + blob.length() ); + auto begin = static_cast(blob.data()); + + return std::vector(begin, begin + blob.length()); } } // namespace Image diff --git a/src/image/Image.hpp b/src/image/Image.hpp index 5520e289..0a12c8cf 100644 --- a/src/image/Image.hpp +++ b/src/image/Image.hpp @@ -49,7 +49,7 @@ class Image bool scale(std::size_t size); // output - void save(std::vector& rawData, Format format) const; + std::vector save(Format format) const; private: Magick::Image _image; diff --git a/src/ui/resource/ImageResource.cpp b/src/ui/resource/ImageResource.cpp index a41c9f8d..ea2863a4 100644 --- a/src/ui/resource/ImageResource.cpp +++ b/src/ui/resource/ImageResource.cpp @@ -54,29 +54,6 @@ ImageResource::getTrackUrl(Database::IdType trackId, std::size_t size) const return url() + "&trackid=" + std::to_string(trackId) + "&size=" + std::to_string(size); } -std::string -ImageResource::getArtistUrl(Database::IdType artistId, std::size_t size) const -{ - return url() + "&artistid=" + std::to_string(artistId) + "&size=" + std::to_string(size); -} - -std::string -ImageResource::getUnknownTrackUrl(size_t size) const -{ - return url() + "&size=" + std::to_string(size); -} - -void -ImageResource::putImage(Wt::Http::Response& response, Image::Image cover) -{ - std::vector data; - - cover.save(data, Image::Format::JPEG); - - response.setMimeType( Image::format_to_mimeType(Image::Format::JPEG) ); - response.out().write(reinterpret_cast(&data[0]), data.size()); -} - void ImageResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response) { @@ -93,7 +70,7 @@ ImageResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Respons if (!size || *size > maxSize) return; - Image::Image cover; + std::vector cover; if (trackIdStr) { @@ -104,7 +81,7 @@ ImageResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Respons // transactions are not thread safe { Wt::WApplication::UpdateLock lock(LmsApp); - cover = CoverArt::Grabber::instance().getFromTrack(LmsApp->getDboSession(), *trackId, *size); + cover = CoverArt::Grabber::instance().getFromTrack(LmsApp->getDboSession(), *trackId, Image::Format::JPEG, *size); } } else if (releaseIdStr) @@ -116,13 +93,14 @@ ImageResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Respons // transactions are not thread safe { Wt::WApplication::UpdateLock lock(LmsApp); - cover = CoverArt::Grabber::instance().getFromRelease(LmsApp->getDboSession(), *releaseId, *size); + cover = CoverArt::Grabber::instance().getFromRelease(LmsApp->getDboSession(), *releaseId, Image::Format::JPEG, *size); } } else return; - putImage(response, cover); + response.setMimeType( Image::format_to_mimeType(Image::Format::JPEG) ); + response.out().write(reinterpret_cast(&cover[0]), cover.size()); } } // namespace UserInterface diff --git a/src/ui/resource/ImageResource.hpp b/src/ui/resource/ImageResource.hpp index 608e4c17..f9ded0fc 100644 --- a/src/ui/resource/ImageResource.hpp +++ b/src/ui/resource/ImageResource.hpp @@ -41,16 +41,9 @@ class ImageResource : public Wt::WResource std::string getReleaseUrl(Database::IdType releaseId, size_t size) const; std::string getTrackUrl(Database::IdType trackId, size_t size) const; - std::string getArtistUrl(Database::IdType artistId, size_t size) const; - std::string getUnknownTrackUrl(size_t size) const; void handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response); - private: - - void putImage(Wt::Http::Response& response, Image::Image image); - void putCover(Wt::Http::Response& response, std::vector covers, std::size_t size); - }; } // namespace UserInterface From fa73b88a2be5f06d3a8b9e773176fe1b5e15ef92 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 16 Sep 2018 20:44:11 +0200 Subject: [PATCH 3/8] Updated REAMDE --- README.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ea2b8707..ddef0295 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,17 @@ # LMS - Lightweight Music Server -LMS is a self-hosted media streaming software, released under the GPLv3 license. -It allows you to access your music using an http(s) web interface. +LMS is a self-hosted music streaming software, released under the GPLv3 license. +It allows you to access your music using a web interface. ## Main features - Responsive design - Browse your audio collection using tag-based filters -- Custom tag support - User management - Persistent play queue - Radio mode - MusicBrainzID support to handle duplicated artist and release names - Audio transcode for maximum interoperability and low bandwith requirements - -LMS is written entirely in C++. Therefore, it is suitable to run on embedded devices, where space and memory are limited. +- Custom tag support ## Installation Here are the required packages to build on Debian Stretch: From 9a0a491dfeceb8aa899908096173b3a8f4871bd9 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 16 Sep 2018 20:44:27 +0200 Subject: [PATCH 4/8] Fixed regression on covers --- src/cover/CoverArtGrabber.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/cover/CoverArtGrabber.cpp b/src/cover/CoverArtGrabber.cpp index 4f8a3f23..b9651b46 100644 --- a/src/cover/CoverArtGrabber.cpp +++ b/src/cover/CoverArtGrabber.cpp @@ -76,6 +76,7 @@ getFromAvMediaFile(const Av::MediaFile& input) LMS_LOG(COVER, ERROR) << "Cannot load embedded cover file in '" << input.getPath().string() << "'"; } + LMS_LOG(COVER, DEBUG) << "No cover found in media file '" << input.getPath().string() << "'"; return boost::none; } @@ -92,6 +93,7 @@ Grabber::getFromDirectory(const boost::filesystem::path& p) const LMS_LOG(COVER, ERROR) << "Cannot load image in file '" << coverPath.string() << "'"; } + LMS_LOG(COVER, DEBUG) << "No cover found in directory '" << p.string() << "'"; return boost::none; } @@ -162,13 +164,11 @@ Grabber::getFromTrack(Wt::Dbo::Session& session, Database::IdType trackId, std:: transaction.commit(); - switch (coverType) - { - case Track::CoverType::Embedded: - cover = getFromTrack(trackPath); - case Track::CoverType::None: - cover = getFromDirectory(trackPath.parent_path()); - } + if (coverType == Track::CoverType::Embedded) + cover = getFromTrack(trackPath); + + if (!cover) + cover = getFromDirectory(trackPath.parent_path()); } } From 8bd94d5493809f4bfcfdf6d1423cd06d7d5af3c7 Mon Sep 17 00:00:00 2001 From: emeric Date: Mon, 17 Sep 2018 13:43:58 +0200 Subject: [PATCH 5/8] add 96kbps + handle the encoding settings from user --- src/Makefile.am | 2 +- src/av/AvTranscoder.hpp | 2 -- src/database/User.cpp | 1 + src/ui/LmsApplication.cpp | 6 ++-- src/ui/LmsApplication.hpp | 6 ++-- src/ui/MediaPlayer.cpp | 6 ++-- ...ranscodeResource.cpp => AudioResource.cpp} | 34 ++++++++++++------- ...ranscodeResource.hpp => AudioResource.hpp} | 8 ++--- 8 files changed, 35 insertions(+), 30 deletions(-) rename src/ui/resource/{TranscodeResource.cpp => AudioResource.cpp} (83%) rename src/ui/resource/{TranscodeResource.hpp => AudioResource.hpp} (86%) diff --git a/src/Makefile.am b/src/Makefile.am index 0e1bfe72..1efba30c 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -41,7 +41,7 @@ lms_SOURCES = \ $(srcdir)/ui/explore/TracksInfoView.cpp \ $(srcdir)/ui/explore/TracksView.cpp \ $(srcdir)/ui/resource/ImageResource.cpp \ - $(srcdir)/ui/resource/TranscodeResource.cpp \ + $(srcdir)/ui/resource/AudioResource.cpp \ $(srcdir)/utils/Config.cpp \ $(srcdir)/utils/Logger.cpp \ $(srcdir)/utils/Path.cpp \ diff --git a/src/av/AvTranscoder.hpp b/src/av/AvTranscoder.hpp index 144ea5d6..40df78fe 100644 --- a/src/av/AvTranscoder.hpp +++ b/src/av/AvTranscoder.hpp @@ -42,8 +42,6 @@ enum class Encoding }; std::string encodingToMimetype(Encoding encoding); -int encodingToInt(Encoding encoding); -Encoding encodingFromInt(int encoding); struct TranscodeParameters { diff --git a/src/database/User.cpp b/src/database/User.cpp index 2bd01e70..d108e103 100644 --- a/src/database/User.cpp +++ b/src/database/User.cpp @@ -28,6 +28,7 @@ const std::vector User::audioBitrates = { 64000, + 96000, 128000, 192000, 320000, diff --git a/src/ui/LmsApplication.cpp b/src/ui/LmsApplication.cpp index 9e6f3b40..9d49104d 100644 --- a/src/ui/LmsApplication.cpp +++ b/src/ui/LmsApplication.cpp @@ -46,7 +46,7 @@ #include "admin/UsersView.hpp" #include "resource/ImageResource.hpp" -#include "resource/TranscodeResource.hpp" +#include "resource/AudioResource.hpp" namespace UserInterface { @@ -78,7 +78,7 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env, Wt::Dbo::SqlConnecti _appGroups(appGroups), _scanner(scanner), _imageResource(nullptr), - _transcodeResource(nullptr) + _audioResource(nullptr) { auto bootstrapTheme = std::make_unique(); bootstrapTheme->setVersion(Wt::BootstrapVersion::v3); @@ -329,7 +329,7 @@ LmsApplication::createHome() } _imageResource = std::make_shared(); - _transcodeResource = std::make_shared(); + _audioResource = std::make_shared(); setConfirmCloseMessage(Wt::WString::tr("Lms.quit-confirm")); diff --git a/src/ui/LmsApplication.hpp b/src/ui/LmsApplication.hpp index 00b5a07d..7d0ca982 100644 --- a/src/ui/LmsApplication.hpp +++ b/src/ui/LmsApplication.hpp @@ -35,7 +35,7 @@ namespace UserInterface { -class TranscodeResource; +class AudioResource; class ImageResource; // Events that can be listen to anywhere in the application @@ -75,7 +75,7 @@ class LmsApplication : public Wt::WApplication // Session application data std::shared_ptr getImageResource() { return _imageResource; } - std::shared_ptr getTranscodeResource() { return _transcodeResource; } + std::shared_ptr getAudioResource() { return _audioResource; } Database::Handler& getDb() { return _db;} Wt::Dbo::Session& getDboSession() { return _db.getSession();} @@ -122,7 +122,7 @@ class LmsApplication : public Wt::WApplication Auth* _auth; Scanner::MediaScanner& _scanner; std::shared_ptr _imageResource; - std::shared_ptr _transcodeResource; + std::shared_ptr _audioResource; bool _isAdmin = false; }; diff --git a/src/ui/MediaPlayer.cpp b/src/ui/MediaPlayer.cpp index 6afa3f8c..6cceaaf1 100644 --- a/src/ui/MediaPlayer.cpp +++ b/src/ui/MediaPlayer.cpp @@ -26,7 +26,7 @@ #include "resource/ImageResource.hpp" -#include "resource/TranscodeResource.hpp" +#include "resource/AudioResource.hpp" #include "LmsApplication.hpp" @@ -65,7 +65,7 @@ MediaPlayer::loadTrack(Database::IdType trackId, bool play) { Av::MediaFile mediaFile(track->getPath()); - auto resource = LmsApp->getTranscodeResource()->getUrl(trackId, Av::Encoding::MP3); + auto resource = LmsApp->getAudioResource()->getUrl(trackId); auto imgResource = LmsApp->getImageResource()->getTrackUrl(trackId, 64); std::ostringstream oss; @@ -77,7 +77,7 @@ MediaPlayer::loadTrack(Database::IdType trackId, bool play) << "};"; oss << "LMS.mediaplayer.loadTrack(params, " << (play ? "true" : "false") << ")"; // true to autoplay - LMS_LOG(UI, DEBUG) << "Runing js = '" << oss.str() << "'"; + LMS_LOG(UI, DEBUG) << "Running js = '" << oss.str() << "'"; _title->setText(Wt::WString::fromUTF8(track->getName())); diff --git a/src/ui/resource/TranscodeResource.cpp b/src/ui/resource/AudioResource.cpp similarity index 83% rename from src/ui/resource/TranscodeResource.cpp rename to src/ui/resource/AudioResource.cpp index 82bda742..4bb294f0 100644 --- a/src/ui/resource/TranscodeResource.cpp +++ b/src/ui/resource/AudioResource.cpp @@ -17,7 +17,7 @@ * along with LMS. If not, see . */ -#include "TranscodeResource.hpp" +#include "AudioResource.hpp" #include @@ -30,21 +30,21 @@ namespace UserInterface { -TranscodeResource:: ~TranscodeResource() +AudioResource:: ~AudioResource() { beingDeleted(); } std::string -TranscodeResource::getUrl(Database::IdType trackId, Av::Encoding encoding) const +AudioResource::getUrl(Database::IdType trackId) const { - std::string res = url()+ "&trackid=" + std::to_string(trackId) + "&encoding=" + std::to_string(Av::encodingToInt(encoding)); + std::string res = url()+ "&trackid=" + std::to_string(trackId); return res; } void -TranscodeResource::handleRequest(const Wt::Http::Request& request, +AudioResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response) { std::shared_ptr transcoder; @@ -77,14 +77,6 @@ TranscodeResource::handleRequest(const Wt::Http::Request& request, auto offsetStr = request.getParameter("offset"); if (offsetStr) parameters.offset = std::chrono::seconds(std::stol(*offsetStr)); - - auto encodingStr = request.getParameter("encoding"); - if (encodingStr) - parameters.encoding = Av::encodingFromInt(std::stol(*encodingStr)); - - auto streamStr = request.getParameter("stream"); - if (streamStr) - parameters.stream = std::stol(*streamStr); } catch (std::exception &e) { @@ -107,6 +99,22 @@ TranscodeResource::handleRequest(const Wt::Http::Request& request, } parameters.bitrate = LmsApp->getUser()->getAudioBitrate(); + + switch (LmsApp->getUser()->getAudioEncoding()) + { + case Database::AudioEncoding::OGA: + parameters.encoding = Av::Encoding::OGA; + break; + case Database::AudioEncoding::WEBMA: + parameters.encoding = Av::Encoding::WEBMA; + break; + case Database::AudioEncoding::MP3: + parameters.encoding = Av::Encoding::MP3; + break; + case Database::AudioEncoding::AUTO: + default: + parameters.encoding = Av::Encoding::MP3; + } transcoder = std::make_shared(track->getPath(), parameters); } diff --git a/src/ui/resource/TranscodeResource.hpp b/src/ui/resource/AudioResource.hpp similarity index 86% rename from src/ui/resource/TranscodeResource.hpp rename to src/ui/resource/AudioResource.hpp index 6867db91..0b8e4226 100644 --- a/src/ui/resource/TranscodeResource.hpp +++ b/src/ui/resource/AudioResource.hpp @@ -19,8 +19,6 @@ #pragma once -#include - #include #include "av/AvTranscoder.hpp" @@ -31,12 +29,12 @@ namespace UserInterface { -class TranscodeResource : public Wt::WResource +class AudioResource : public Wt::WResource { public: - ~TranscodeResource(); + ~AudioResource(); - std::string getUrl(Database::IdType trackId, Av::Encoding encoding) const; + std::string getUrl(Database::IdType trackId) const; void handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response); From 1d55e82e92a8d490ec35b842274f3250d72dac84 Mon Sep 17 00:00:00 2001 From: emeric Date: Mon, 17 Sep 2018 20:05:03 +0200 Subject: [PATCH 6/8] Cache the default cover for various sizes --- src/cover/CoverArtGrabber.cpp | 39 ++++++++++++++++++++++++++--------- src/cover/CoverArtGrabber.hpp | 14 +++++++++---- 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/src/cover/CoverArtGrabber.cpp b/src/cover/CoverArtGrabber.cpp index b9651b46..ce688492 100644 --- a/src/cover/CoverArtGrabber.cpp +++ b/src/cover/CoverArtGrabber.cpp @@ -61,6 +61,25 @@ Grabber::instance() return instance; } +Image::Image +Grabber::getDefaultCover(std::size_t size) +{ + std::unique_lock lock(_mutex); + + auto it = _defaultCovers.find(size); + if (it == _defaultCovers.end()) + { + Image::Image cover = _defaultCover; + + cover.scale(size); + auto res = _defaultCovers.insert(std::make_pair(size, cover)); + assert(res.second); + it = res.first; + } + + return it->second; +} + static boost::optional getFromAvMediaFile(const Av::MediaFile& input) { @@ -147,7 +166,7 @@ Grabber::getFromTrack(const boost::filesystem::path& p) const } Image::Image -Grabber::getFromTrack(Wt::Dbo::Session& session, Database::IdType trackId, std::size_t size) const +Grabber::getFromTrack(Wt::Dbo::Session& session, Database::IdType trackId, std::size_t size) { using namespace Database; @@ -173,16 +192,16 @@ Grabber::getFromTrack(Wt::Dbo::Session& session, Database::IdType trackId, std:: } if (!cover) - cover = _defaultCover; - - cover->scale(size); + cover = getDefaultCover(size); + else + cover->scale(size); return *cover; } Image::Image -Grabber::getFromRelease(Wt::Dbo::Session& session, Database::IdType releaseId, std::size_t size) const +Grabber::getFromRelease(Wt::Dbo::Session& session, Database::IdType releaseId, std::size_t size) { using namespace Database; @@ -206,15 +225,15 @@ Grabber::getFromRelease(Wt::Dbo::Session& session, Database::IdType releaseId, s } if (!cover) - cover = _defaultCover; - - cover->scale(size); + cover = getDefaultCover(size); + else + cover->scale(size); return *cover; } std::vector -Grabber::getFromTrack(Wt::Dbo::Session& session, Database::IdType trackId, Image::Format format, std::size_t size) const +Grabber::getFromTrack(Wt::Dbo::Session& session, Database::IdType trackId, Image::Format format, std::size_t size) { Image::Image cover = getFromTrack(session, trackId, size); @@ -222,7 +241,7 @@ Grabber::getFromTrack(Wt::Dbo::Session& session, Database::IdType trackId, Image } std::vector -Grabber::getFromRelease(Wt::Dbo::Session& session, Database::IdType releaseId, Image::Format format, std::size_t size) const +Grabber::getFromRelease(Wt::Dbo::Session& session, Database::IdType releaseId, Image::Format format, std::size_t size) { Image::Image cover = getFromRelease(session, releaseId, size); diff --git a/src/cover/CoverArtGrabber.hpp b/src/cover/CoverArtGrabber.hpp index c2d572c2..e8d0b6a7 100644 --- a/src/cover/CoverArtGrabber.hpp +++ b/src/cover/CoverArtGrabber.hpp @@ -19,6 +19,8 @@ #pragma once +#include +#include #include #include "database/Types.hpp" @@ -35,23 +37,27 @@ class Grabber static Grabber& instance(); - std::vector getFromTrack(Wt::Dbo::Session& session, Database::IdType trackId, Image::Format format, std::size_t size) const; - std::vector getFromRelease(Wt::Dbo::Session& session, Database::IdType releaseId, Image::Format format, std::size_t size) const; + std::vector getFromTrack(Wt::Dbo::Session& session, Database::IdType trackId, Image::Format format, std::size_t size); + std::vector getFromRelease(Wt::Dbo::Session& session, Database::IdType releaseId, Image::Format format, std::size_t size); private: Grabber(); - Image::Image getFromTrack(Wt::Dbo::Session& session, Database::IdType trackId, std::size_t size) const; - Image::Image getFromRelease(Wt::Dbo::Session& session, Database::IdType releaseId, std::size_t size) const; + Image::Image getFromTrack(Wt::Dbo::Session& session, Database::IdType trackId, std::size_t size); + Image::Image getFromRelease(Wt::Dbo::Session& session, Database::IdType releaseId, std::size_t size); boost::optional getFromTrack(const boost::filesystem::path& path) const; std::vector getCoverPaths(const boost::filesystem::path& directoryPath) const; boost::optional getFromDirectory(const boost::filesystem::path& path) const; + Image::Image getDefaultCover(std::size_t size); Image::Image _defaultCover; + std::mutex _mutex; + std::map _defaultCovers; + std::vector _fileExtensions = {".jpg", ".jpeg", ".png", ".bmp"}; // TODO parametrize From d8db92561bd4f42d386c6910718557b031fd9c8b Mon Sep 17 00:00:00 2001 From: emeric Date: Tue, 18 Sep 2018 13:33:30 +0200 Subject: [PATCH 7/8] Implementing a repeat all funtion in the playqueue --- approot/messages.xml | 5 +++- approot/playqueue.xml | 20 +++++++++----- docroot/css/lms.css | 4 +++ src/ui/PlayQueueView.cpp | 58 +++++++++++++++++++++++++++------------- src/ui/PlayQueueView.hpp | 3 ++- 5 files changed, 62 insertions(+), 28 deletions(-) diff --git a/approot/messages.xml b/approot/messages.xml index e258382c..5a606c24 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -88,12 +88,15 @@ Top tracks - + +Clear {1} tracks Added {1} tracks Playing {1} tracks Play Queue Radio mode +Repeat +Shuffle Play History diff --git a/approot/playqueue.xml b/approot/playqueue.xml index 56eda2a6..b2ba1131 100644 --- a/approot/playqueue.xml +++ b/approot/playqueue.xml @@ -2,20 +2,26 @@ - + - + + + + + + + + + ${entries} @@ -26,11 +32,11 @@ - + - + diff --git a/docroot/css/lms.css b/docroot/css/lms.css index f4049419..7bf20079 100644 --- a/docroot/css/lms.css +++ b/docroot/css/lms.css @@ -248,6 +248,10 @@ a:hover { color: #337ab7; } +.Lms-playqueue-btn-selected { + color: #337ab7; +} + .Lms-playqueue-selected { font-weight: 800; } diff --git a/src/ui/PlayQueueView.cpp b/src/ui/PlayQueueView.cpp index 6011c2ed..3715e8b7 100644 --- a/src/ui/PlayQueueView.cpp +++ b/src/ui/PlayQueueView.cpp @@ -38,22 +38,25 @@ PlayQueue::PlayQueue() { addFunction("tr", &Wt::WTemplate::Functions::tr); - Wt::WText* clearBtn = bindNew("clear-btn", Wt::WString::tr("Lms.PlayQueue.clear"), Wt::TextFormat::XHTML); - - _entriesContainer = bindNew("entries"); - - _showMore = bindNew("show-more", Wt::WString::tr("Lms.Explore.show-more")); - _showMore->setHidden(true); - - Wt::WText* shuffleBtn = bindNew("shuffle-btn", Wt::WString::tr("Lms.PlayQueue.shuffle"), Wt::TextFormat::XHTML); - _radioMode = bindNew("radio-mode", Wt::WString::tr("Lms.PlayQueue.radio-mode")); - _nbTracks = bindNew("nb-tracks"); - + Wt::WText* clearBtn = bindNew("clear-btn", Wt::WString::tr("Lms.PlayQueue.template.clear-btn"), Wt::TextFormat::XHTML); + clearBtn->setToolTip(Wt::WString::tr("Lms.PlayQueue.clear")); clearBtn->clicked().connect([=] { clearTracks(); }); + _entriesContainer = bindNew("entries"); + + _showMore = bindNew("show-more", Wt::WString::tr("Lms.Explore.show-more")); + _showMore->setHidden(true); + _showMore->clicked().connect([=] + { + addSome(); + updateCurrentTrack(true); + }); + + Wt::WText* shuffleBtn = bindNew("shuffle-btn", Wt::WString::tr("Lms.PlayQueue.template.shuffle-btn"), Wt::TextFormat::XHTML); + shuffleBtn->setToolTip(Wt::WString::tr("Lms.PlayQueue.shuffle")); shuffleBtn->clicked().connect([=] { { @@ -65,12 +68,24 @@ PlayQueue::PlayQueue() addSome(); }); - _showMore->clicked().connect([=] + Wt::WText* repeatBtn = bindNew("repeat-btn", Wt::WString::tr("Lms.PlayQueue.template.repeat-btn"), Wt::TextFormat::XHTML); + repeatBtn->setToolTip(Wt::WString::tr("Lms.PlayQueue.repeat")); + repeatBtn->clicked().connect([=] { - addSome(); - updateCurrentTrack(true); + _repeatAll = !_repeatAll; + repeatBtn->toggleStyleClass("Lms-playqueue-btn-selected", _repeatAll); }); + Wt::WText* radioBtn = bindNew("radio-btn", Wt::WString::tr("Lms.PlayQueue.template.radio-btn")); + radioBtn->setToolTip(Wt::WString::tr("Lms.PlayQueue.radio-mode")); + radioBtn->clicked().connect([=] + { + _radioMode = !_radioMode; + radioBtn->toggleStyleClass("Lms-playqueue-btn-selected", _radioMode); + }); + + _nbTracks = bindNew("nb-tracks"); + LmsApp->preQuit().connect([=] { if (_tracklistId) @@ -152,12 +167,17 @@ PlayQueue::load(std::size_t pos, bool play) // If out of range, stop playing if (pos >= tracklist->getCount()) { - stop(); - return; + if (!_repeatAll) + { + stop(); + return; + } + + pos = 0; } // If last and radio mode, fill the next song - if (_radioMode->checkState() == Wt::CheckState::Checked && pos == tracklist->getCount() - 1) + if (_radioMode && pos == tracklist->getCount() - 1) addRadioTrack(); _trackPos = pos; @@ -293,7 +313,7 @@ PlayQueue::addSome() entry->bindWidget("release-name", LmsApplication::createReleaseAnchor(track->getRelease())); } - Wt::WText* playBtn = entry->bindNew("play-btn", Wt::WString::tr("Lms.PlayQueue.play"), Wt::TextFormat::XHTML); + Wt::WText* playBtn = entry->bindNew("play-btn", Wt::WString::tr("Lms.PlayQueue.template.play-btn"), Wt::TextFormat::XHTML); playBtn->clicked().connect(std::bind([=] { auto pos = _entriesContainer->indexOf(entry); @@ -301,7 +321,7 @@ PlayQueue::addSome() load(pos, true); })); - Wt::WText* delBtn = entry->bindNew("del-btn", Wt::WString::tr("Lms.PlayQueue.delete"), Wt::TextFormat::XHTML); + Wt::WText* delBtn = entry->bindNew("del-btn", Wt::WString::tr("Lms.PlayQueue.template.delete-btn"), Wt::TextFormat::XHTML); delBtn->clicked().connect(std::bind([=] { // Remove the entry n both the widget tree and the playqueue diff --git a/src/ui/PlayQueueView.hpp b/src/ui/PlayQueueView.hpp index 596486c8..03d27f55 100644 --- a/src/ui/PlayQueueView.hpp +++ b/src/ui/PlayQueueView.hpp @@ -67,8 +67,9 @@ class PlayQueue : public Wt::WTemplate void load(std::size_t pos, bool play); void stop(); + bool _repeatAll = false; + bool _radioMode = false; boost::optional _tracklistId; - Wt::WCheckBox* _radioMode; Wt::WContainerWidget* _entriesContainer; Wt::WPushButton* _showMore; Wt::WText* _nbTracks; From c4642006444cf3559c8a31daf73e617260339694 Mon Sep 17 00:00:00 2001 From: emeric Date: Tue, 18 Sep 2018 13:41:37 +0200 Subject: [PATCH 8/8] Demo in readme --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index ddef0295..da46d7b6 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,10 @@ LMS is a self-hosted music streaming software, released under the GPLv3 license. It allows you to access your music 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 + ## Main features - Responsive design - Browse your audio collection using tag-based filters