Merge branch 'develop'
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -127,8 +127,8 @@ InputFormatContext::getNbPictures(void) const
|
||||
return res;
|
||||
}
|
||||
|
||||
void
|
||||
InputFormatContext::getPictures(std::vector<Picture>& pictures) const
|
||||
std::vector<Picture>
|
||||
InputFormatContext::getPictures(std::size_t nbMaxPictures) const
|
||||
{
|
||||
static const std::map<int, std::string> codecMimeMap =
|
||||
{
|
||||
@@ -140,6 +140,8 @@ InputFormatContext::getPictures(std::vector<Picture>& pictures) const
|
||||
{ AV_CODEC_ID_PPM, "image/x-portable-pixmap" },
|
||||
};
|
||||
|
||||
std::vector<Picture> 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<Picture>& 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
|
||||
|
||||
@@ -52,7 +52,7 @@ class InputFormatContext : public FormatContext
|
||||
|
||||
// Get attached pictures
|
||||
std::size_t getNbPictures(void) const;
|
||||
void getPictures(std::vector<Picture>& pictures) const;
|
||||
std::vector<Picture> getPictures(std::size_t nbMaxPictures) const;
|
||||
|
||||
// Get the streams
|
||||
std::vector<Stream> getStreams(void);
|
||||
|
||||
@@ -47,6 +47,17 @@ ConfigReader::getLoggerConfig(Logger::Config& config)
|
||||
config.minSeverity = static_cast<Severity>((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<unsigned int>(_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)
|
||||
{
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <libconfig.h++>
|
||||
|
||||
#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);
|
||||
|
||||
+156
-38
@@ -26,19 +26,54 @@
|
||||
#include "CoverArtGrabber.hpp"
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
bool
|
||||
isFileSupported(const boost::filesystem::path& file, const std::vector<boost::filesystem::path> 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<CoverArt>
|
||||
Grabber::getFromInputFormatContext(const Av::InputFormatContext& input)
|
||||
Grabber::getFromInputFormatContext(const Av::InputFormatContext& input, std::size_t nbMaxCovers) const
|
||||
{
|
||||
std::vector<CoverArt> res;
|
||||
|
||||
try
|
||||
{
|
||||
std::vector<Av::Picture> pictures;
|
||||
input.getPictures(pictures);
|
||||
std::vector<Av::Picture> 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<CoverArt>
|
||||
Grabber::getFromTrack(const boost::filesystem::path& p)
|
||||
Grabber::getFromDirectory(const boost::filesystem::path& p, std::size_t nbMaxCovers) const
|
||||
{
|
||||
std::vector<CoverArt> res;
|
||||
|
||||
std::vector<boost::filesystem::path> coverPathes = getCoverPaths(p, nbMaxCovers);
|
||||
for (auto coverPath : coverPathes)
|
||||
{
|
||||
if (res.size() >= nbMaxCovers)
|
||||
break;
|
||||
|
||||
std::vector<unsigned char> 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<boost::filesystem::path>
|
||||
Grabber::getCoverPaths(const boost::filesystem::path& directoryPath, std::size_t nbMaxCovers) const
|
||||
{
|
||||
std::vector<boost::filesystem::path> 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<CoverArt>
|
||||
Grabber::getFromTrack(const boost::filesystem::path& p, std::size_t nbMaxCovers) const
|
||||
{
|
||||
std::vector<CoverArt> 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<CoverArt>
|
||||
Grabber::getFromTrack(Database::Track::pointer track)
|
||||
{
|
||||
std::vector<CoverArt> res;
|
||||
|
||||
if (!track || !track->hasCover())
|
||||
return std::vector<CoverArt>();
|
||||
|
||||
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<CoverArt>
|
||||
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<Track::pointer> 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<CoverArt>();
|
||||
|
||||
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<CoverArt>();
|
||||
}
|
||||
|
||||
return std::vector<CoverArt>();
|
||||
}
|
||||
|
||||
|
||||
std::vector<CoverArt>
|
||||
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<Track::pointer> tracks
|
||||
= Track::getAll(session, filter, -1, 1 /* limit result size */);
|
||||
|
||||
if (tracks.empty())
|
||||
return std::vector<CoverArt>();
|
||||
|
||||
firstTrackPath = tracks.front()->getPath();
|
||||
embeddedCover = (tracks.front()->getCoverType() == Track::CoverType::Embedded);
|
||||
}
|
||||
|
||||
// First, try to get covers from the directory of the release
|
||||
std::vector<CoverArt> 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
|
||||
|
||||
@@ -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<std::string> fileExtensions;
|
||||
std::size_t maxFileSize;
|
||||
std::vector<std::string> preferredFileNames;
|
||||
};
|
||||
|
||||
void init(const Config& config);
|
||||
|
||||
std::vector<boost::filesystem::path> getCoverPaths(const boost::filesystem::path& directoryPath, std::size_t nbMaxCovers = 1) const;
|
||||
std::vector<CoverArt> getFromDirectory(const boost::filesystem::path& path, std::size_t nbMaxCovers = 1) const;
|
||||
std::vector<CoverArt> getFromInputFormatContext(const Av::InputFormatContext& input, std::size_t nbMaxCovers = 1) const;
|
||||
std::vector<CoverArt> getFromTrack(const boost::filesystem::path& path, std::size_t nbMaxCovers = 1) const;
|
||||
std::vector<CoverArt> getFromTrack(Wt::Dbo::Session& session, Database::Track::id_type trackId, std::size_t nbMaxCovers = 1) const;
|
||||
std::vector<CoverArt> getFromRelease(Wt::Dbo::Session& session, std::string releaseName, std::size_t nbMaxCovers = 1) const;
|
||||
|
||||
private:
|
||||
Grabber();
|
||||
|
||||
std::vector<boost::filesystem::path> _fileExtensions;
|
||||
std::size_t _maxFileSize;
|
||||
std::vector<boost::filesystem::path> _preferredFileNames;
|
||||
|
||||
|
||||
static std::vector<CoverArt> getFromInputFormatContext(const Av::InputFormatContext& input);
|
||||
static std::vector<CoverArt> getFromTrack(Database::Track::pointer track);
|
||||
static std::vector<CoverArt> getFromTrack(const boost::filesystem::path& path);
|
||||
static std::vector<CoverArt> getFromRelease(Wt::Dbo::Session& session, std::string releaseName);
|
||||
};
|
||||
|
||||
} // namespace CoverArt
|
||||
|
||||
@@ -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 = 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<boost::filesystem::path> 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<bool>(items[MetaData::Type::HasCover]));
|
||||
bool hasCover = boost::any_cast<bool>(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!";
|
||||
|
||||
@@ -132,7 +132,7 @@ Track::Track(const boost::filesystem::path& p)
|
||||
_trackNumber(0),
|
||||
_discNumber(0),
|
||||
_filePath( p.string() ),
|
||||
_hasCover(false)
|
||||
_coverType(CoverType::None)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
+11
-5
@@ -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<Genre::pointer> 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<unsigned char>& getChecksum(void) const { return _fileChecksum; }
|
||||
bool hasCover(void) const { return _hasCover; }
|
||||
CoverType getCoverType(void) const { return _coverType; }
|
||||
|
||||
template<class Action>
|
||||
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<unsigned char> _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<PlaylistEntry> > _playlistEntries;
|
||||
|
||||
@@ -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<Module, boost::log::sources::severity_logger< Severity > > _loggers;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<CoverArt::CoverArt> 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<Database::Track::pointer> 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<CoverArt::CoverArt> coverArts = CoverArt::Grabber::getFromTrack(track);
|
||||
|
||||
BOOST_FOREACH(CoverArt::CoverArt& coverArt, coverArts)
|
||||
{
|
||||
AudioCollectionResponse_CoverArt* cover_art = response.add_cover_art();
|
||||
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <Wt/Http/Request>
|
||||
#include <Wt/Http/Response>
|
||||
|
||||
#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<Transcode::AvConvTranscoder> transcoder;
|
||||
if (continuation)
|
||||
{
|
||||
transcoder = boost::any_cast<std::shared_ptr<Transcode::AvConvTranscoder> >(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<Transcode::AvConvTranscoder>(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<unsigned char> data;
|
||||
data.reserve(_bufferSize);
|
||||
|
||||
transcoder->process(data, _bufferSize);
|
||||
|
||||
// Give the client all the output data
|
||||
response.out().write(reinterpret_cast<char*>(&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
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef API_RESOURCE_HPP
|
||||
#define API_RESOURCE_HPP
|
||||
|
||||
#include <Wt/WResource>
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
|
||||
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
|
||||
@@ -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<Stream>
|
||||
|
||||
@@ -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<Stream> getStreams(Stream::Type type) const;
|
||||
@@ -68,8 +63,6 @@ class InputMediaFile
|
||||
|
||||
std::vector<Stream> _streams;
|
||||
std::map<Stream::Type, Stream::Id> _bestStreams;
|
||||
|
||||
std::vector< CoverArt::CoverArt > _covers;
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -382,10 +382,11 @@ PlayQueue::addTracks(const std::vector<Database::Track::id_type>& 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);
|
||||
|
||||
|
||||
@@ -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<std::mutex> 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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user