From 75c700df2a2d166e3b76b19da21097f4409b7493 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 28 Jun 2015 19:49:58 +0200 Subject: [PATCH] [Cover] Adding external cover file support (lookup in the same directory) --- TODO | 3 +- conf/lms.conf.sample | 8 +- src/av/InputFormatContext.cpp | 11 +- src/av/InputFormatContext.hpp | 2 +- src/config/ConfigReader.cpp | 11 + src/config/ConfigReader.hpp | 4 + src/cover/CoverArtGrabber.cpp | 194 ++++++++++++++---- src/cover/CoverArtGrabber.hpp | 32 ++- src/database-updater/DatabaseUpdater.cpp | 27 ++- src/database/Track.cpp | 2 +- src/database/Track.hpp | 16 +- src/logger/Logger.hpp | 4 +- src/main/main.cpp | 7 + .../server/AudioCollectionRequestHandler.cpp | 22 +- src/rest-api/Resource.cpp | 136 ++++++++++++ src/rest-api/Resource.hpp | 44 ++++ src/transcode/InputMediaFile.cpp | 2 - src/transcode/InputMediaFile.hpp | 7 - src/ui/audio/desktop/AudioMediaPlayer.cpp | 2 +- src/ui/audio/desktop/PlayQueue.cpp | 5 +- src/ui/resource/CoverResource.cpp | 25 ++- src/ui/resource/CoverResource.hpp | 2 +- 22 files changed, 470 insertions(+), 96 deletions(-) create mode 100644 src/rest-api/Resource.cpp create mode 100644 src/rest-api/Resource.hpp diff --git a/TODO b/TODO index 76224fbd..ea4d5187 100644 --- a/TODO +++ b/TODO @@ -12,7 +12,8 @@ [Cover] - Scaling: find something more "reliable" than GIL and its custom extensions (adobe work, io_new)? -- Get the cover from image files in the same folder ("cover.jpg", etc.) +- Handle several file formats (not only jpg) +- Handle preferred cover file names [Database] - Optim, use SQL query to get the "genre" orphans diff --git a/conf/lms.conf.sample b/conf/lms.conf.sample index 45adfb85..9174d9d7 100644 --- a/conf/lms.conf.sample +++ b/conf/lms.conf.sample @@ -11,7 +11,13 @@ main = { path = "/var/lms/lms.db"; audio_extensions = "mp3 ogg oga aac m4a flac wav wma aif aiff ape mpc shn"; - video_extensions = "flv avi mpg mpeg mp4 m4v mkv mov wmv ogv divx m2ts" + video_extensions = "flv avi mpg mpeg mp4 m4v mkv mov wmv ogv divx m2ts"; + } + + cover = { + file_extensions = "jpg jpeg"; + file_max_size = 500000; + file_preferred_names = "cover front"; } } diff --git a/src/av/InputFormatContext.cpp b/src/av/InputFormatContext.cpp index e9432923..e455d9e7 100644 --- a/src/av/InputFormatContext.cpp +++ b/src/av/InputFormatContext.cpp @@ -127,8 +127,8 @@ InputFormatContext::getNbPictures(void) const return res; } -void -InputFormatContext::getPictures(std::vector& pictures) const +std::vector +InputFormatContext::getPictures(std::size_t nbMaxPictures) const { static const std::map codecMimeMap = { @@ -140,6 +140,8 @@ InputFormatContext::getPictures(std::vector& pictures) const { AV_CODEC_ID_PPM, "image/x-portable-pixmap" }, }; + std::vector pictures; + for (std::size_t i = 0; i < native()->nb_streams; ++i) { Stream stream(native()->streams[i]); @@ -159,8 +161,13 @@ InputFormatContext::getPictures(std::vector& pictures) const std::copy(pkt.data, pkt.data + pkt.size, std::back_inserter(picture.data)); pictures.push_back( picture ); + + if (pictures.size() >= nbMaxPictures) + break; } } + + return pictures; } } //namespace Av diff --git a/src/av/InputFormatContext.hpp b/src/av/InputFormatContext.hpp index 7844ea97..9385fdcc 100644 --- a/src/av/InputFormatContext.hpp +++ b/src/av/InputFormatContext.hpp @@ -52,7 +52,7 @@ class InputFormatContext : public FormatContext // Get attached pictures std::size_t getNbPictures(void) const; - void getPictures(std::vector& pictures) const; + std::vector getPictures(std::size_t nbMaxPictures) const; // Get the streams std::vector getStreams(void); diff --git a/src/config/ConfigReader.cpp b/src/config/ConfigReader.cpp index c9d18cf0..efe00fe2 100644 --- a/src/config/ConfigReader.cpp +++ b/src/config/ConfigReader.cpp @@ -47,6 +47,17 @@ ConfigReader::getLoggerConfig(Logger::Config& config) config.minSeverity = static_cast((int)_config.lookup("main.logger.level")); } +void +ConfigReader::getCoverGrabberConfig(CoverArt::Grabber::Config& config) +{ + std::string extensions = _config.lookup("main.cover.file_extensions"); + config.maxFileSize = static_cast(_config.lookup("main.cover.file_max_size")); + std::string filenames = _config.lookup("main.cover.file_preferred_names"); + + splitStrings(extensions, config.fileExtensions); + splitStrings(filenames, config.preferredFileNames); +} + void ConfigReader::getUserInterfaceConfig(Service::UserInterfaceService::Config& config) { diff --git a/src/config/ConfigReader.hpp b/src/config/ConfigReader.hpp index 23c835de..5701a3b5 100644 --- a/src/config/ConfigReader.hpp +++ b/src/config/ConfigReader.hpp @@ -23,6 +23,7 @@ #include #include +#include "cover/CoverArtGrabber.hpp" #include "logger/Logger.hpp" #include "service/UserInterfaceService.hpp" @@ -38,6 +39,9 @@ class ConfigReader // Logger configuration void getLoggerConfig(Logger::Config& config); + // Covers + void getCoverGrabberConfig(CoverArt::Grabber::Config& config); + // Service configurations void getUserInterfaceConfig(Service::UserInterfaceService::Config& config); void getRemoteServerConfig(Service::RemoteServerService::Config& config); diff --git a/src/cover/CoverArtGrabber.cpp b/src/cover/CoverArtGrabber.cpp index bed0ef5b..deab6d46 100644 --- a/src/cover/CoverArtGrabber.cpp +++ b/src/cover/CoverArtGrabber.cpp @@ -26,19 +26,54 @@ #include "CoverArtGrabber.hpp" +namespace { + +bool +isFileSupported(const boost::filesystem::path& file, const std::vector extensions) +{ + boost::filesystem::path fileExtension = file.extension(); + + for (auto extension : extensions) + { + if (extension == fileExtension) + return true; + } + + return false; +} +} // namespace namespace CoverArt { +Grabber::Grabber() +: _maxFileSize(0) +{ +} + +Grabber& +Grabber::instance() +{ + static Grabber instance; + return instance; +} + +void +Grabber::init(const Config& config) +{ + for (auto extension : config.fileExtensions) + _fileExtensions.push_back("." + extension); + + _maxFileSize = config.maxFileSize; +} std::vector -Grabber::getFromInputFormatContext(const Av::InputFormatContext& input) +Grabber::getFromInputFormatContext(const Av::InputFormatContext& input, std::size_t nbMaxCovers) const { std::vector res; try { - std::vector pictures; - input.getPictures(pictures); + std::vector pictures = input.getPictures(nbMaxCovers); BOOST_FOREACH(const Av::Picture& picture, pictures) res.push_back( CoverArt(picture.mimeType, picture.data) ); @@ -53,7 +88,65 @@ Grabber::getFromInputFormatContext(const Av::InputFormatContext& input) } std::vector -Grabber::getFromTrack(const boost::filesystem::path& p) +Grabber::getFromDirectory(const boost::filesystem::path& p, std::size_t nbMaxCovers) const +{ + std::vector res; + + std::vector coverPathes = getCoverPaths(p, nbMaxCovers); + for (auto coverPath : coverPathes) + { + if (res.size() >= nbMaxCovers) + break; + + std::vector data; + std::ifstream file(coverPath.string(), std::ios::binary); + char c; + while (file.get(c)) + data.push_back(c); + + // TODO handle other formats + res.push_back(CoverArt("image/jpeg", data)); + } + + return res; +} + +std::vector +Grabber::getCoverPaths(const boost::filesystem::path& directoryPath, std::size_t nbMaxCovers) const +{ + std::vector res; + + // TODO handle preferred file names + + boost::filesystem::directory_iterator itPath(directoryPath); + boost::filesystem::directory_iterator itEnd; + while (itPath != itEnd) + { + boost::filesystem::path path = *itPath; + itPath++; + + if (!boost::filesystem::is_regular(path)) + continue; + + if (!isFileSupported(path, _fileExtensions)) + continue; + + if (boost::filesystem::file_size(path) > _maxFileSize) + { + LMS_LOG(MOD_COVER, SEV_INFO) << "Cover file '" << path << " is too big (" << boost::filesystem::file_size(path) << "), limit is " << _maxFileSize; + continue; + } + + 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 { std::vector res; @@ -61,55 +154,80 @@ Grabber::getFromTrack(const boost::filesystem::path& p) { Av::InputFormatContext input(p); - return getFromInputFormatContext(input); + res = getFromInputFormatContext(input, nbMaxCovers); } catch(std::exception& e) { - LMS_LOG(MOD_COVER, SEV_ERROR) << "Cannot get pictures: " << e.what(); + LMS_LOG(MOD_COVER, SEV_ERROR) << "Cannot get covers from file " << p << ": " << e.what(); } return res; } std::vector -Grabber::getFromTrack(Database::Track::pointer track) -{ - std::vector res; - - if (!track || !track->hasCover()) - return std::vector(); - - try - { - Av::InputFormatContext input(track->getPath()); - - return getFromInputFormatContext(input); - } - catch(std::exception& e) - { - LMS_LOG(MOD_COVER, SEV_ERROR) << "Cannot get pictures: " << e.what(); - } - - return res; -} - -std::vector -Grabber::getFromRelease(Wt::Dbo::Session& session, std::string releaseName) +Grabber::getFromTrack(Wt::Dbo::Session& session, Database::Track::id_type trackId, std::size_t nbMaxCovers) const { using namespace Database; - // For now, just return the embedded cover of the first track - SearchFilter filter; - filter.exactMatch[SearchFilter::Field::Release].push_back(releaseName); + Wt::Dbo::Transaction transaction(session); - std::vector tracks - = Track::getAll(session, filter, -1, 1 /* limit result size */); - - if (!tracks.empty()) - return getFromTrack( tracks.front() ); - else + 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::ExternalFile: + return Grabber::getFromDirectory(trackPath.parent_path(), nbMaxCovers); + case Track::CoverType::None: + return std::vector(); + } + + return std::vector(); +} + + +std::vector +Grabber::getFromRelease(Wt::Dbo::Session& session, std::string releaseName, std::size_t nbMaxCovers) const +{ + using namespace Database; + + boost::filesystem::path firstTrackPath; + bool embeddedCover = false; + + // Get the first track of the release + { + SearchFilter filter; + filter.exactMatch[SearchFilter::Field::Release].push_back(releaseName); + + Wt::Dbo::Transaction transaction(session); + + std::vector tracks + = Track::getAll(session, filter, -1, 1 /* limit result size */); + + if (tracks.empty()) + return std::vector(); + + firstTrackPath = tracks.front()->getPath(); + embeddedCover = (tracks.front()->getCoverType() == Track::CoverType::Embedded); + } + + // First, try to get covers from the directory of the release + std::vector res = getFromDirectory( firstTrackPath.parent_path(), nbMaxCovers); + + // Fallback on the embedded cover of the first track + if (res.empty() && embeddedCover) + res = getFromTrack( firstTrackPath, nbMaxCovers); + + return res; } } // namespace CoverArt diff --git a/src/cover/CoverArtGrabber.hpp b/src/cover/CoverArtGrabber.hpp index 43555b7b..a4768be5 100644 --- a/src/cover/CoverArtGrabber.hpp +++ b/src/cover/CoverArtGrabber.hpp @@ -33,11 +33,35 @@ namespace CoverArt { class Grabber { public: + Grabber(const Grabber&) = delete; + Grabber& operator=(const Grabber&) = delete; + + static Grabber& instance(); + + struct Config + { + std::vector fileExtensions; + std::size_t maxFileSize; + std::vector preferredFileNames; + }; + + void init(const Config& config); + + 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 getFromInputFormatContext(const Av::InputFormatContext& input, 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::Track::id_type trackId, std::size_t nbMaxCovers = 1) const; + std::vector getFromRelease(Wt::Dbo::Session& session, std::string releaseName, std::size_t nbMaxCovers = 1) const; + + private: + Grabber(); + + std::vector _fileExtensions; + std::size_t _maxFileSize; + std::vector _preferredFileNames; + - static std::vector getFromInputFormatContext(const Av::InputFormatContext& input); - static std::vector getFromTrack(Database::Track::pointer track); - static std::vector getFromTrack(const boost::filesystem::path& path); - static std::vector getFromRelease(Wt::Dbo::Session& session, std::string releaseName); }; } // namespace CoverArt diff --git a/src/database-updater/DatabaseUpdater.cpp b/src/database-updater/DatabaseUpdater.cpp index 7984f29e..c69c273c 100644 --- a/src/database-updater/DatabaseUpdater.cpp +++ b/src/database-updater/DatabaseUpdater.cpp @@ -29,6 +29,7 @@ #include "Checksum.hpp" #include "DatabaseUpdater.hpp" +#include "cover/CoverArtGrabber.hpp" namespace { @@ -272,9 +273,23 @@ Updater::processAudioFile( const boost::filesystem::path& file, Stats& stats) // Skip file if last write is the same Wt::Dbo::ptr track = Track::getByPath(_db.getSession(), file); - if (track && track->getLastWriteTime() == lastWriteTime) + + // if the file is the same and embeds covers, no need to update + if (track && track->getLastWriteTime() == lastWriteTime + && (track->getCoverType() == Database::Track::CoverType::Embedded)) return; + // Check for external covers + std::vector externalCovers = CoverArt::Grabber::instance().getCoverPaths(file.parent_path()); + if (track && track->getLastWriteTime() == lastWriteTime) + { + // no change since last time we updated + // Skip only if no external covers has to be set + if (track->getCoverType() == Database::Track::CoverType::None && externalCovers.empty() + || track->getCoverType() == Database::Track::CoverType::ExternalFile && !externalCovers.empty()) + return; + } + MetaData::Items items; _metadataParser.parse(file, items); @@ -400,11 +415,17 @@ Updater::processAudioFile( const boost::filesystem::path& file, Stats& stats) if (items.find(MetaData::Type::HasCover) != items.end()) { - track.modify()->setHasCover( boost::any_cast(items[MetaData::Type::HasCover])); + bool hasCover = boost::any_cast(items[MetaData::Type::HasCover]); + + if (hasCover) + track.modify()->setCoverType( Track::CoverType::Embedded ); + else if (!externalCovers.empty()) + track.modify()->setCoverType( Track::CoverType::ExternalFile ); + else + track.modify()->setCoverType( Track::CoverType::None); } transaction.commit(); - } catch( std::exception& e ) { LMS_LOG(MOD_DBUPDATER, SEV_ERROR) << "Exception while parsing audio file : '" << file << "': '" << e.what() << "' => skipping!"; diff --git a/src/database/Track.cpp b/src/database/Track.cpp index 5c7b3bd0..930887ae 100644 --- a/src/database/Track.cpp +++ b/src/database/Track.cpp @@ -132,7 +132,7 @@ Track::Track(const boost::filesystem::path& p) _trackNumber(0), _discNumber(0), _filePath( p.string() ), -_hasCover(false) +_coverType(CoverType::None) { } diff --git a/src/database/Track.hpp b/src/database/Track.hpp index 8b32d1cb..9bd59df1 100644 --- a/src/database/Track.hpp +++ b/src/database/Track.hpp @@ -104,6 +104,12 @@ class Track Track() {} Track(const boost::filesystem::path& p); + enum class CoverType + { + Embedded, // Contains embedded cover + ExternalFile, // Cover is in an external file + None, // No local cover available + }; // Find utility functions static pointer getByPath(Wt::Dbo::Session& session, const boost::filesystem::path& p); @@ -141,14 +147,14 @@ class Track void setOriginalDate(const boost::posix_time::ptime& date) { _originalDate = date; } void setGenres(const std::string& genreList) { _genreList = genreList; } void setGenres(std::vector genres); - void setHasCover(bool hasCover) { _hasCover = hasCover; } + void setCoverType(CoverType coverType) { _coverType = coverType; } int getTrackNumber(void) const { return _trackNumber; } int getDiscNumber(void) const { return _discNumber; } std::string getName(void) const { return _name; } std::string getArtistName(void) const { return _artistName; } std::string getReleaseName(void) const { return _releaseName; } - const std::string& getPath(void) const { return _filePath; } + boost::filesystem::path getPath(void) const { return _filePath; } boost::posix_time::time_duration getDuration(void) const { return _duration; } boost::posix_time::ptime getDate(void) const { return _date; } boost::posix_time::ptime getOriginalDate(void) const { return _originalDate; } @@ -157,7 +163,7 @@ class Track boost::posix_time::ptime getLastWriteTime(void) const { return _fileLastWrite; } const std::vector& getChecksum(void) const { return _fileChecksum; } - bool hasCover(void) const { return _hasCover; } + CoverType getCoverType(void) const { return _coverType; } template void persist(Action& a) @@ -174,7 +180,7 @@ class Track Wt::Dbo::field(a, _filePath, "path"); Wt::Dbo::field(a, _fileLastWrite, "last_write"); Wt::Dbo::field(a, _fileChecksum, "checksum"); - Wt::Dbo::field(a, _hasCover, "has_cover"); + Wt::Dbo::field(a, _coverType, "cover_type"); Wt::Dbo::hasMany(a, _genres, Wt::Dbo::ManyToMany, "track_genre", "", Wt::Dbo::OnDeleteCascade); Wt::Dbo::hasMany(a, _playlistEntries, Wt::Dbo::ManyToOne, "track"); } @@ -199,7 +205,7 @@ class Track std::string _filePath; std::vector _fileChecksum; boost::posix_time::ptime _fileLastWrite; - bool _hasCover; + CoverType _coverType; Wt::Dbo::collection< Genre::pointer > _genres; // Genres that are related to this track Wt::Dbo::collection< Wt::Dbo::ptr > _playlistEntries; diff --git a/src/logger/Logger.hpp b/src/logger/Logger.hpp index c1f0be57..ca540f65 100644 --- a/src/logger/Logger.hpp +++ b/src/logger/Logger.hpp @@ -60,6 +60,9 @@ class Logger { public: + Logger(const Logger&) = delete; + Logger& operator=(const Logger&) = delete; + static Logger& instance(); struct Config { @@ -76,7 +79,6 @@ class Logger get(Module module); private: - Logger(); std::map > _loggers; diff --git a/src/main/main.cpp b/src/main/main.cpp index ff595ccc..afd404cc 100644 --- a/src/main/main.cpp +++ b/src/main/main.cpp @@ -67,6 +67,12 @@ int main(int argc, char* argv[]) Logger::instance().init(loggerConfig); } + { + CoverArt::Grabber::Config config; + configReader.getCoverGrabberConfig(config); + CoverArt::Grabber::instance().init(config); + } + LMS_LOG(MOD_MAIN, SEV_INFO) << "Reading service configurations..."; Service::DatabaseUpdateService::Config dbUpdateConfig; @@ -103,6 +109,7 @@ int main(int argc, char* argv[]) res = EXIT_SUCCESS; } + // TODO catch setting not found exception catch( libconfig::ParseException& e) { std::cerr << "Caught libconfig::ParseException! error='" << e.getError() << "', file = '" << e.getFile() << "', line = " << e.getLine() << std::endl; diff --git a/src/remote/server/AudioCollectionRequestHandler.cpp b/src/remote/server/AudioCollectionRequestHandler.cpp index bd3cf912..009997ee 100644 --- a/src/remote/server/AudioCollectionRequestHandler.cpp +++ b/src/remote/server/AudioCollectionRequestHandler.cpp @@ -294,23 +294,14 @@ AudioCollectionRequestHandler::processGetCoverArt(const AudioCollectionRequest:: response.set_type(AudioCollectionResponse::TypeCoverArt); - Wt::Dbo::Transaction transaction( _db.getSession() ); - Database::Track::pointer track; + std::vector coverArts; switch(request.type()) { case AudioCollectionRequest::GetCoverArt::TypeGetCoverArtRelease: if (request.has_release()) { - SearchFilter filter; - filter.exactMatch[SearchFilter::Field::Release].push_back(request.release()); - - std::vector tracks - = Database::Track::getAll(_db.getSession(), filter, -1, 1 /* limit reuslt size */); - - if (!tracks.empty()) - track = tracks.front(); - + coverArts = CoverArt::Grabber::instance().getFromRelease( _db.getSession(), request.release()); res = true; } break; @@ -318,19 +309,12 @@ AudioCollectionRequestHandler::processGetCoverArt(const AudioCollectionRequest:: case AudioCollectionRequest::GetCoverArt::TypeGetCoverArtTrack: if (request.has_track_id()) { - // Get the request release - track = Database::Track::getById( _db.getSession(), request.track_id()); - + coverArts = CoverArt::Grabber::instance().getFromTrack(_db.getSession(), request.track_id()); res = true; } break; } - if (!res) - return false; - - std::vector coverArts = CoverArt::Grabber::getFromTrack(track); - BOOST_FOREACH(CoverArt::CoverArt& coverArt, coverArts) { AudioCollectionResponse_CoverArt* cover_art = response.add_cover_art(); diff --git a/src/rest-api/Resource.cpp b/src/rest-api/Resource.cpp new file mode 100644 index 00000000..46f347a6 --- /dev/null +++ b/src/rest-api/Resource.cpp @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2015 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include +#include + +#include "logger/Logger.hpp" +#include "database/DatabaseHandler.hpp" +#include "transcode/Format.hpp" +#include "transcode/InputMediaFile.hpp" +#include "transcode/Parameters.hpp" +#include "transcode/AvConvTranscoder.hpp" + +#include "Resource.hpp" + + +namespace RestAPI { + +Resource::Resource(boost::filesystem::path dbPath) +: _dbPath(dbPath) +{ + +} + +void +Resource::handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response) +{ + static const std::size_t _bufferSize = 65536; + LMS_LOG(MOD_REST_API, SEV_DEBUG) << "Handle request..."; + + // see if this request is for a continuation: + Wt::Http::ResponseContinuation *continuation = request.continuation(); + + LMS_LOG(MOD_UI, SEV_DEBUG) << "Handling request. Continuation = " << std::boolalpha << continuation; + + std::shared_ptr transcoder; + if (continuation) + { + transcoder = boost::any_cast >(continuation->data()); + } + else + { + const Wt::Http::ParameterMap& parameters = request.getParameterMap(); + + for (auto itParameter : parameters) + { + LMS_LOG(MOD_REST_API, SEV_DEBUG) << "Param name: '" << itParameter.first << "'"; + + for (auto value : itParameter.second) + { + LMS_LOG(MOD_REST_API, SEV_DEBUG) << "\tvalue: '" << value << "'"; + } + } + + auto itParamMediaId = parameters.find("mediaid"); + if (itParamMediaId == parameters.end()) + { + LMS_LOG(MOD_REST_API, SEV_DEBUG) << "Cannot find parameter mediaid"; + return; + } + + std::string mediaId = itParamMediaId->second.front(); + LMS_LOG(MOD_REST_API, SEV_DEBUG) << "MediaId = " << mediaId; + + try + { + Database::Handler db(_dbPath); + + Wt::Dbo::Transaction transaction(db.getSession()); + + Database::Track::pointer track = Database::Track::getById(db.getSession(), std::stol(mediaId)); + + LMS_LOG(MOD_UI, SEV_DEBUG) << "Launching transcoder"; + Transcode::InputMediaFile input(track->getPath()); + Transcode::Parameters parameters(input, Transcode::Format::get(Transcode::Format::OGA)); + + transcoder = std::make_shared(parameters); + + LMS_LOG(MOD_UI, SEV_DEBUG) << "Mime type set to '" << parameters.getOutputFormat().getMimeType() << "'"; + response.setMimeType(parameters.getOutputFormat().getMimeType()); + + } + catch(std::exception &e) + { + LMS_LOG(MOD_UI, SEV_DEBUG) << "Caught exception: " << e.what(); + return; + } + } + + if (!transcoder) + { + LMS_LOG(MOD_UI, SEV_ERROR) << "No transcoder ?!"; + return; + } + + if (!transcoder->isComplete()) + { + std::vector data; + data.reserve(_bufferSize); + + transcoder->process(data, _bufferSize); + + // Give the client all the output data + response.out().write(reinterpret_cast(&data[0]), data.size()); + + LMS_LOG(MOD_UI, SEV_DEBUG) << "Written " << data.size() << " bytes! complete = " << std::boolalpha << transcoder->isComplete() << ", produced bytes = " << transcoder->getOutputBytes(); + + if (!response.out()) + LMS_LOG(MOD_UI, SEV_ERROR) << "Write failed!"; + } + + if (!transcoder->isComplete() && response.out()) { + continuation = response.createContinuation(); + continuation->setData(transcoder); + } + else + LMS_LOG(MOD_UI, SEV_DEBUG) << "No more data!"; +} + +} // namespace API diff --git a/src/rest-api/Resource.hpp b/src/rest-api/Resource.hpp new file mode 100644 index 00000000..39ebc99e --- /dev/null +++ b/src/rest-api/Resource.hpp @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2015 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 . + */ + +#ifndef API_RESOURCE_HPP +#define API_RESOURCE_HPP + +#include + +#include + +namespace RestAPI { + +class Resource : public Wt::WResource +{ + public: + Resource(boost::filesystem::path dbPath); + + void handleRequest(const Wt::Http::Request& request, + Wt::Http::Response& response); + + private: + + boost::filesystem::path _dbPath; +}; + +} // namespace API + +#endif diff --git a/src/transcode/InputMediaFile.cpp b/src/transcode/InputMediaFile.cpp index dfc6690b..18045aa4 100644 --- a/src/transcode/InputMediaFile.cpp +++ b/src/transcode/InputMediaFile.cpp @@ -96,8 +96,6 @@ InputMediaFile::InputMediaFile(const boost::filesystem::path& p) else LMS_LOG(MOD_TRANSCODE, SEV_WARNING) << "Cannot find best stream for type " << type; } - - _covers = CoverArt::Grabber::getFromInputFormatContext(input); } std::vector diff --git a/src/transcode/InputMediaFile.hpp b/src/transcode/InputMediaFile.hpp index 702434e0..138f85f4 100644 --- a/src/transcode/InputMediaFile.hpp +++ b/src/transcode/InputMediaFile.hpp @@ -34,8 +34,6 @@ namespace Transcode { - - class InputMediaFile { public: @@ -53,9 +51,6 @@ class InputMediaFile boost::filesystem::path getPath(void) const {return _path;} boost::posix_time::time_duration getDuration(void) const {return _duration;} - // Pictures - const std::vector< CoverArt::CoverArt >& getCovers(void) const { return _covers; } - // Stream handling const Stream& getStream(Stream::Id id) const; std::vector getStreams(Stream::Type type) const; @@ -68,8 +63,6 @@ class InputMediaFile std::vector _streams; std::map _bestStreams; - - std::vector< CoverArt::CoverArt > _covers; }; diff --git a/src/ui/audio/desktop/AudioMediaPlayer.cpp b/src/ui/audio/desktop/AudioMediaPlayer.cpp index 8f88ea75..d5b939f5 100644 --- a/src/ui/audio/desktop/AudioMediaPlayer.cpp +++ b/src/ui/audio/desktop/AudioMediaPlayer.cpp @@ -71,7 +71,7 @@ AudioMediaPlayer::AudioMediaPlayer(Wt::WContainerWidget *parent) mainLayout->addLayout(currentMediaLayout, 1); currentMediaLayout->addWidget( _mediaCover = new Wt::WImage()); - _mediaCover->setImageLink( LmsApplication::instance()->getCoverResource()->getUnkownTrackUrl(72)); + _mediaCover->setImageLink( LmsApplication::instance()->getCoverResource()->getUnknownTrackUrl(72)); _mediaCover->setStyleClass("mediaplayer-current-cover"); Wt::WVBoxLayout* mediaInfoLayout = new Wt::WVBoxLayout(); diff --git a/src/ui/audio/desktop/PlayQueue.cpp b/src/ui/audio/desktop/PlayQueue.cpp index 7ba53fcf..567e85c5 100644 --- a/src/ui/audio/desktop/PlayQueue.cpp +++ b/src/ui/audio/desktop/PlayQueue.cpp @@ -382,10 +382,11 @@ PlayQueue::addTracks(const std::vector& trackIds) _model->setData(dataRow, COLUMN_ID_TRACK_ID, track.id(), Wt::UserRole); std::string coverUrl; - if (track->hasCover()) + if (track->getCoverType() != Database::Track::CoverType::None) coverUrl = LmsApplication::instance()->getCoverResource()->getTrackUrl(track.id(), 64); else - coverUrl = LmsApplication::instance()->getCoverResource()->getUnkownTrackUrl(64); + coverUrl = LmsApplication::instance()->getCoverResource()->getUnknownTrackUrl(64); + _model->setData(dataRow, COLUMN_ID_COVER, coverUrl, Wt::DecorationRole); _model->setData(dataRow, COLUMN_ID_COVER, "playqueue-cover", Wt::StyleClassRole); diff --git a/src/ui/resource/CoverResource.cpp b/src/ui/resource/CoverResource.cpp index 3219f639..0224ab83 100644 --- a/src/ui/resource/CoverResource.cpp +++ b/src/ui/resource/CoverResource.cpp @@ -85,7 +85,7 @@ CoverResource::getTrackUrl(Database::Track::id_type trackId, std::size_t size) c } std::string -CoverResource::getUnkownTrackUrl(size_t size) const +CoverResource::getUnknownTrackUrl(size_t size) const { return url() + "&size=" + std::to_string(size); } @@ -120,8 +120,8 @@ CoverResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Respons if (trackIdStr) { Database::Track::id_type trackId = std::stol(*trackIdStr); - std::string path; - bool hasCover = false; + boost::filesystem::path path; + Database::Track::CoverType coverType = Database::Track::CoverType::None; { // transactions are not thread safe @@ -132,13 +132,24 @@ CoverResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Respons Database::Track::pointer track = Database::Track::getById(_db.getSession(), trackId); if (track) { - hasCover = track->hasCover(); + coverType = track->getCoverType(); path = track->getPath(); } } - if (hasCover) - covers = CoverArt::Grabber::getFromTrack(path); + switch (coverType) + { + case Database::Track::CoverType::Embedded: + covers = CoverArt::Grabber::instance().getFromTrack(path); + break; + + case Database::Track::CoverType::ExternalFile: + covers = CoverArt::Grabber::instance().getFromDirectory(path.parent_path()); + break; + + case Database::Track::CoverType::None: + break; + } } else if (releaseStr) { @@ -146,7 +157,7 @@ CoverResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Respons std::unique_lock lock(_mutex); Wt::Dbo::Transaction transaction(_db.getSession()); - covers = CoverArt::Grabber::getFromRelease(_db.getSession(), *releaseStr); + covers = CoverArt::Grabber::instance().getFromRelease(_db.getSession(), *releaseStr); } BOOST_FOREACH(CoverArt::CoverArt& cover, covers) diff --git a/src/ui/resource/CoverResource.hpp b/src/ui/resource/CoverResource.hpp index bdfeaeea..9b2610e6 100644 --- a/src/ui/resource/CoverResource.hpp +++ b/src/ui/resource/CoverResource.hpp @@ -41,7 +41,7 @@ class CoverResource : public Wt::WResource std::string getReleaseUrl(std::string releaseName, size_t size) const; std::string getTrackUrl(Database::Track::id_type trackId, size_t size) const; - std::string getUnkownTrackUrl(size_t size) const; + std::string getUnknownTrackUrl(size_t size) const; void handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response);