Merge branch 'develop'
This commit is contained in:
@@ -1,19 +1,21 @@
|
|||||||
# LMS - Lightweight Music Server
|
# LMS - Lightweight Music Server
|
||||||
|
|
||||||
LMS is a self-hosted media streaming software, released under the GPLv3 license.
|
LMS is a self-hosted music streaming software, released under the GPLv3 license.
|
||||||
It allows you to access your music using an http(s) web interface.
|
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
|
## Main features
|
||||||
- Responsive design
|
- Responsive design
|
||||||
- Browse your audio collection using tag-based filters
|
- Browse your audio collection using tag-based filters
|
||||||
- Custom tag support
|
|
||||||
- User management
|
- User management
|
||||||
- Persistent play queue
|
- Persistent play queue
|
||||||
- Radio mode
|
- Radio mode
|
||||||
- MusicBrainzID support to handle duplicated artist and release names
|
- MusicBrainzID support to handle duplicated artist and release names
|
||||||
- Audio transcode for maximum interoperability and low bandwith requirements
|
- Audio transcode for maximum interoperability and low bandwith requirements
|
||||||
|
- Custom tag support
|
||||||
LMS is written entirely in C++. Therefore, it is suitable to run on embedded devices, where space and memory are limited.
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
Here are the required packages to build on Debian Stretch:
|
Here are the required packages to build on Debian Stretch:
|
||||||
|
|||||||
@@ -88,12 +88,15 @@
|
|||||||
<message id="Lms.Explore.TracksInfo.most-played">Top tracks</message>
|
<message id="Lms.Explore.TracksInfo.most-played">Top tracks</message>
|
||||||
|
|
||||||
|
|
||||||
<!--Playque-->
|
<!--Playqueue-->
|
||||||
|
<message id="Lms.PlayQueue.clear">Clear</message>
|
||||||
<message id="Lms.PlayQueue.nb-tracks">{1} tracks</message>
|
<message id="Lms.PlayQueue.nb-tracks">{1} tracks</message>
|
||||||
<message id="Lms.PlayQueue.nb-tracks-added">Added {1} tracks</message>
|
<message id="Lms.PlayQueue.nb-tracks-added">Added {1} tracks</message>
|
||||||
<message id="Lms.PlayQueue.nb-tracks-playing">Playing {1} tracks</message>
|
<message id="Lms.PlayQueue.nb-tracks-playing">Playing {1} tracks</message>
|
||||||
<message id="Lms.PlayQueue.playqueue">Play Queue</message>
|
<message id="Lms.PlayQueue.playqueue">Play Queue</message>
|
||||||
<message id="Lms.PlayQueue.radio-mode">Radio mode</message>
|
<message id="Lms.PlayQueue.radio-mode">Radio mode</message>
|
||||||
|
<message id="Lms.PlayQueue.repeat">Repeat</message>
|
||||||
|
<message id="Lms.PlayQueue.shuffle">Shuffle</message>
|
||||||
|
|
||||||
<!--PlayHistory-->
|
<!--PlayHistory-->
|
||||||
<message id="Lms.PlayHistory.playhistory">Play History</message>
|
<message id="Lms.PlayHistory.playhistory">Play History</message>
|
||||||
|
|||||||
+13
-7
@@ -2,20 +2,26 @@
|
|||||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
<messages xmlns:if="Wt.WTemplate.conditions">
|
||||||
<!--FORMS message blocks-->
|
<!--FORMS message blocks-->
|
||||||
|
|
||||||
<message id="Lms.PlayQueue.clear">
|
<message id="Lms.PlayQueue.template.clear-btn">
|
||||||
<i class="fa fa-times-circle Lms-playqueue-btn"></i>
|
<i class="fa fa-times-circle Lms-playqueue-btn"></i>
|
||||||
</message>
|
</message>
|
||||||
|
|
||||||
<message id="Lms.PlayQueue.shuffle">
|
<message id="Lms.PlayQueue.template.shuffle-btn">
|
||||||
<i class="fa fa-random Lms-playqueue-btn"></i>
|
<i class="fa fa-random Lms-playqueue-btn"></i>
|
||||||
</message>
|
</message>
|
||||||
|
|
||||||
|
<message id="Lms.PlayQueue.template.repeat-btn">
|
||||||
|
<i class="fa fa-repeat Lms-playqueue-btn"></i>
|
||||||
|
</message>
|
||||||
|
|
||||||
|
<message id="Lms.PlayQueue.template.radio-btn">
|
||||||
|
<i class="fa fa-rss Lms-playqueue-btn"></i>
|
||||||
|
</message>
|
||||||
|
|
||||||
<message id="Lms.PlayQueue.template">
|
<message id="Lms.PlayQueue.template">
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h2>${tr:Lms.PlayQueue.playqueue}</h2>
|
<h2>${tr:Lms.PlayQueue.playqueue}</h2>
|
||||||
${clear-btn}
|
${clear-btn}${shuffle-btn}${repeat-btn}${radio-btn}
|
||||||
${shuffle-btn}
|
|
||||||
${radio-mode}
|
|
||||||
<h4><small>${nb-tracks}</small></h4>
|
<h4><small>${nb-tracks}</small></h4>
|
||||||
</div>
|
</div>
|
||||||
${entries}
|
${entries}
|
||||||
@@ -26,11 +32,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</message>
|
</message>
|
||||||
|
|
||||||
<message id="Lms.PlayQueue.play">
|
<message id="Lms.PlayQueue.template.play-btn">
|
||||||
<i class="fa fa-play-circle Lms-playqueue-btn"></i>
|
<i class="fa fa-play-circle Lms-playqueue-btn"></i>
|
||||||
</message>
|
</message>
|
||||||
|
|
||||||
<message id="Lms.PlayQueue.delete">
|
<message id="Lms.PlayQueue.template.delete-btn">
|
||||||
<i class="fa fa-times-circle Lms-playqueue-btn"></i>
|
<i class="fa fa-times-circle Lms-playqueue-btn"></i>
|
||||||
</message>
|
</message>
|
||||||
|
|
||||||
|
|||||||
@@ -248,6 +248,10 @@ a:hover {
|
|||||||
color: #337ab7;
|
color: #337ab7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Lms-playqueue-btn-selected {
|
||||||
|
color: #337ab7;
|
||||||
|
}
|
||||||
|
|
||||||
.Lms-playqueue-selected {
|
.Lms-playqueue-selected {
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -41,7 +41,7 @@ lms_SOURCES = \
|
|||||||
$(srcdir)/ui/explore/TracksInfoView.cpp \
|
$(srcdir)/ui/explore/TracksInfoView.cpp \
|
||||||
$(srcdir)/ui/explore/TracksView.cpp \
|
$(srcdir)/ui/explore/TracksView.cpp \
|
||||||
$(srcdir)/ui/resource/ImageResource.cpp \
|
$(srcdir)/ui/resource/ImageResource.cpp \
|
||||||
$(srcdir)/ui/resource/TranscodeResource.cpp \
|
$(srcdir)/ui/resource/AudioResource.cpp \
|
||||||
$(srcdir)/utils/Config.cpp \
|
$(srcdir)/utils/Config.cpp \
|
||||||
$(srcdir)/utils/Logger.cpp \
|
$(srcdir)/utils/Logger.cpp \
|
||||||
$(srcdir)/utils/Path.cpp \
|
$(srcdir)/utils/Path.cpp \
|
||||||
|
|||||||
@@ -42,8 +42,6 @@ enum class Encoding
|
|||||||
};
|
};
|
||||||
|
|
||||||
std::string encodingToMimetype(Encoding encoding);
|
std::string encodingToMimetype(Encoding encoding);
|
||||||
int encodingToInt(Encoding encoding);
|
|
||||||
Encoding encodingFromInt(int encoding);
|
|
||||||
|
|
||||||
struct TranscodeParameters
|
struct TranscodeParameters
|
||||||
{
|
{
|
||||||
|
|||||||
+101
-61
@@ -43,12 +43,15 @@ isFileSupported(const boost::filesystem::path& file, const std::vector<boost::fi
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
namespace CoverArt {
|
namespace CoverArt {
|
||||||
|
|
||||||
Grabber::Grabber()
|
Grabber::Grabber()
|
||||||
{
|
{
|
||||||
|
if (!_defaultCover.load( Wt::WApplication::instance()->docRoot() + "/images/unknown-cover.jpg"))
|
||||||
|
throw LmsException("Cannot read default cover file");
|
||||||
}
|
}
|
||||||
|
|
||||||
Grabber&
|
Grabber&
|
||||||
@@ -58,48 +61,63 @@ Grabber::instance()
|
|||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
static std::vector<Image::Image>
|
Image::Image
|
||||||
getFromAvMediaFile(const Av::MediaFile& input, std::size_t nbMaxCovers)
|
Grabber::getDefaultCover(std::size_t size)
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> 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<Image::Image>
|
||||||
|
getFromAvMediaFile(const Av::MediaFile& input)
|
||||||
{
|
{
|
||||||
std::vector<Image::Image> res;
|
std::vector<Image::Image> res;
|
||||||
|
|
||||||
for (Av::Picture& picture : input.getAttachedPictures(nbMaxCovers))
|
for (auto& picture : input.getAttachedPictures(2))
|
||||||
{
|
{
|
||||||
Image::Image image;
|
Image::Image image;
|
||||||
|
|
||||||
if (image.load(picture.data))
|
if (image.load(picture.data))
|
||||||
res.push_back( image );
|
return image;
|
||||||
else
|
else
|
||||||
LMS_LOG(COVER, ERROR) << "Cannot load embedded cover file in '" << input.getPath().string() << "'";
|
LMS_LOG(COVER, ERROR) << "Cannot load embedded cover file in '" << input.getPath().string() << "'";
|
||||||
}
|
}
|
||||||
|
|
||||||
return res;
|
LMS_LOG(COVER, DEBUG) << "No cover found in media file '" << input.getPath().string() << "'";
|
||||||
|
return boost::none;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Image::Image>
|
boost::optional<Image::Image>
|
||||||
Grabber::getFromDirectory(const boost::filesystem::path& p, std::size_t nbMaxCovers) const
|
Grabber::getFromDirectory(const boost::filesystem::path& p) const
|
||||||
{
|
{
|
||||||
std::vector<Image::Image> res;
|
for (auto coverPath : getCoverPaths(p))
|
||||||
|
|
||||||
std::vector<boost::filesystem::path> coverPathes = getCoverPaths(p, nbMaxCovers);
|
|
||||||
for (auto coverPath : coverPathes)
|
|
||||||
{
|
{
|
||||||
if (res.size() >= nbMaxCovers)
|
|
||||||
break;
|
|
||||||
|
|
||||||
Image::Image image;
|
Image::Image image;
|
||||||
|
|
||||||
if (image.load(coverPath))
|
if (image.load(coverPath))
|
||||||
res.push_back(image);
|
return image;
|
||||||
else
|
else
|
||||||
LMS_LOG(COVER, ERROR) << "Cannot load image in file '" << coverPath.string() << "'";
|
LMS_LOG(COVER, ERROR) << "Cannot load image in file '" << coverPath.string() << "'";
|
||||||
}
|
}
|
||||||
|
|
||||||
return res;
|
LMS_LOG(COVER, DEBUG) << "No cover found in directory '" << p.string() << "'";
|
||||||
|
return boost::none;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<boost::filesystem::path>
|
std::vector<boost::filesystem::path>
|
||||||
Grabber::getCoverPaths(const boost::filesystem::path& directoryPath, std::size_t nbMaxCovers) const
|
Grabber::getCoverPaths(const boost::filesystem::path& directoryPath) const
|
||||||
{
|
{
|
||||||
std::vector<boost::filesystem::path> res;
|
std::vector<boost::filesystem::path> res;
|
||||||
boost::system::error_code ec;
|
boost::system::error_code ec;
|
||||||
@@ -126,86 +144,108 @@ Grabber::getCoverPaths(const boost::filesystem::path& directoryPath, std::size_t
|
|||||||
}
|
}
|
||||||
|
|
||||||
res.push_back(path);
|
res.push_back(path);
|
||||||
if (res.size() >= nbMaxCovers)
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Image::Image>
|
boost::optional<Image::Image>
|
||||||
Grabber::getFromTrack(const boost::filesystem::path& p, std::size_t nbMaxCovers) const
|
Grabber::getFromTrack(const boost::filesystem::path& p) const
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Av::MediaFile input(p);
|
Av::MediaFile input(p);
|
||||||
|
|
||||||
return getFromAvMediaFile(input, nbMaxCovers);
|
return getFromAvMediaFile(input);
|
||||||
}
|
}
|
||||||
catch (Av::MediaFileException& e)
|
catch (Av::MediaFileException& e)
|
||||||
{
|
{
|
||||||
LMS_LOG(COVER, ERROR) << "Cannot get covers from track " << p.string() << ": " << e.what();
|
LMS_LOG(COVER, ERROR) << "Cannot get covers from track " << p.string() << ": " << e.what();
|
||||||
|
return boost::none;
|
||||||
}
|
}
|
||||||
|
|
||||||
return std::vector<Image::Image>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Image::Image>
|
Image::Image
|
||||||
Grabber::getFromTrack(Wt::Dbo::Session& session, Database::IdType trackId, std::size_t nbMaxCovers) const
|
Grabber::getFromTrack(Wt::Dbo::Session& session, Database::IdType trackId, std::size_t size)
|
||||||
{
|
{
|
||||||
using namespace Database;
|
using namespace Database;
|
||||||
|
|
||||||
Wt::Dbo::Transaction transaction(session);
|
boost::optional<Image::Image> cover;
|
||||||
|
|
||||||
Track::pointer track = Track::getById(session, trackId);
|
|
||||||
if (!track)
|
|
||||||
return std::vector<Image::Image>();
|
|
||||||
|
|
||||||
Track::CoverType coverType = track->getCoverType();
|
|
||||||
boost::filesystem::path trackPath = track->getPath();
|
|
||||||
|
|
||||||
transaction.commit();
|
|
||||||
|
|
||||||
switch (coverType)
|
|
||||||
{
|
{
|
||||||
case Track::CoverType::Embedded:
|
Wt::Dbo::Transaction transaction(session);
|
||||||
return Grabber::getFromTrack(trackPath, nbMaxCovers);
|
|
||||||
case Track::CoverType::None:
|
Track::pointer track = Track::getById(session, trackId);
|
||||||
return Grabber::getFromDirectory(trackPath.parent_path(), nbMaxCovers);
|
if (track)
|
||||||
|
{
|
||||||
|
Track::CoverType coverType = track->getCoverType();
|
||||||
|
boost::filesystem::path trackPath = track->getPath();
|
||||||
|
|
||||||
|
transaction.commit();
|
||||||
|
|
||||||
|
if (coverType == Track::CoverType::Embedded)
|
||||||
|
cover = getFromTrack(trackPath);
|
||||||
|
|
||||||
|
if (!cover)
|
||||||
|
cover = getFromDirectory(trackPath.parent_path());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return std::vector<Image::Image>();
|
if (!cover)
|
||||||
|
cover = getDefaultCover(size);
|
||||||
|
else
|
||||||
|
cover->scale(size);
|
||||||
|
|
||||||
|
return *cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
std::vector<Image::Image>
|
Image::Image
|
||||||
Grabber::getFromRelease(Wt::Dbo::Session& session, Database::IdType releaseId, std::size_t nbMaxCovers) const
|
Grabber::getFromRelease(Wt::Dbo::Session& session, Database::IdType releaseId, std::size_t size)
|
||||||
{
|
{
|
||||||
using namespace Database;
|
using namespace Database;
|
||||||
|
|
||||||
Wt::Dbo::Transaction transaction(session);
|
boost::optional<Image::Image> cover;
|
||||||
|
|
||||||
// If the release does not exist, do nothing
|
{
|
||||||
Release::pointer release = Release::getById(session, releaseId);
|
Wt::Dbo::Transaction transaction(session);
|
||||||
if (!release)
|
|
||||||
return std::vector<Image::Image>();
|
|
||||||
|
|
||||||
std::vector<Track::pointer> tracks = release->getTracks();
|
auto release = Release::getById(session, releaseId);
|
||||||
if (tracks.empty())
|
if (release)
|
||||||
return std::vector<Image::Image>();
|
{
|
||||||
|
auto tracks = release->getTracks();
|
||||||
|
if (!tracks.empty())
|
||||||
|
{
|
||||||
|
auto trackId = tracks.front().id();
|
||||||
|
transaction.commit();
|
||||||
|
|
||||||
boost::filesystem::path firstTrackPath = tracks.front()->getPath();
|
return getFromTrack(session, trackId, size);
|
||||||
bool embeddedCover = (tracks.front()->getCoverType() == Track::CoverType::Embedded);
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
transaction.commit();
|
if (!cover)
|
||||||
|
cover = getDefaultCover(size);
|
||||||
|
else
|
||||||
|
cover->scale(size);
|
||||||
|
|
||||||
// First, try to get covers from the directory of the release
|
return *cover;
|
||||||
std::vector<Image::Image> res = getFromDirectory( firstTrackPath.parent_path(), nbMaxCovers);
|
}
|
||||||
|
|
||||||
// Fallback on the embedded cover of the first track
|
std::vector<uint8_t>
|
||||||
if (res.empty() && embeddedCover)
|
Grabber::getFromTrack(Wt::Dbo::Session& session, Database::IdType trackId, Image::Format format, std::size_t size)
|
||||||
res = getFromTrack( firstTrackPath, nbMaxCovers);
|
{
|
||||||
|
Image::Image cover = getFromTrack(session, trackId, size);
|
||||||
|
|
||||||
return res;
|
return cover.save(Image::Format::JPEG);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<uint8_t>
|
||||||
|
Grabber::getFromRelease(Wt::Dbo::Session& session, Database::IdType releaseId, Image::Format format, std::size_t size)
|
||||||
|
{
|
||||||
|
Image::Image cover = getFromRelease(session, releaseId, size);
|
||||||
|
|
||||||
|
return cover.save(Image::Format::JPEG);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace CoverArt
|
} // namespace CoverArt
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <mutex>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "database/Types.hpp"
|
#include "database/Types.hpp"
|
||||||
@@ -35,18 +37,32 @@ class Grabber
|
|||||||
|
|
||||||
static Grabber& instance();
|
static Grabber& instance();
|
||||||
|
|
||||||
std::vector<boost::filesystem::path> getCoverPaths(const boost::filesystem::path& directoryPath, std::size_t nbMaxCovers = 1) const;
|
std::vector<uint8_t> getFromTrack(Wt::Dbo::Session& session, Database::IdType trackId, Image::Format format, std::size_t size);
|
||||||
std::vector<Image::Image> getFromDirectory(const boost::filesystem::path& path, std::size_t nbMaxCovers = 1) const;
|
std::vector<uint8_t> getFromRelease(Wt::Dbo::Session& session, Database::IdType releaseId, Image::Format format, std::size_t size);
|
||||||
std::vector<Image::Image> getFromTrack(const boost::filesystem::path& path, std::size_t nbMaxCovers = 1) const;
|
|
||||||
std::vector<Image::Image> getFromTrack(Wt::Dbo::Session& session, Database::IdType trackId, std::size_t nbMaxCovers = 1) const;
|
|
||||||
std::vector<Image::Image> getFromRelease(Wt::Dbo::Session& session, Database::IdType releaseId, std::size_t nbMaxCovers = 1) const;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
||||||
Grabber();
|
Grabber();
|
||||||
|
|
||||||
|
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<Image::Image> getFromTrack(const boost::filesystem::path& path) const;
|
||||||
|
std::vector<boost::filesystem::path> getCoverPaths(const boost::filesystem::path& directoryPath) const;
|
||||||
|
boost::optional<Image::Image> getFromDirectory(const boost::filesystem::path& path) const;
|
||||||
|
|
||||||
|
Image::Image getDefaultCover(std::size_t size);
|
||||||
|
|
||||||
|
Image::Image _defaultCover;
|
||||||
|
|
||||||
|
std::mutex _mutex;
|
||||||
|
std::map<std::size_t /* size */, Image::Image> _defaultCovers;
|
||||||
|
|
||||||
std::vector<boost::filesystem::path> _fileExtensions
|
std::vector<boost::filesystem::path> _fileExtensions
|
||||||
= {".jpg", ".jpeg", ".png", ".bmp"}; // TODO parametrize
|
= {".jpg", ".jpeg", ".png", ".bmp"}; // TODO parametrize
|
||||||
|
|
||||||
std::size_t _maxFileSize = 5000000;
|
std::size_t _maxFileSize = 5000000;
|
||||||
|
|
||||||
std::vector<boost::filesystem::path> _preferredFileNames
|
std::vector<boost::filesystem::path> _preferredFileNames
|
||||||
= {"cover", "front"}; // TODO parametrize
|
= {"cover", "front"}; // TODO parametrize
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ const std::vector<std::size_t>
|
|||||||
User::audioBitrates =
|
User::audioBitrates =
|
||||||
{
|
{
|
||||||
64000,
|
64000,
|
||||||
|
96000,
|
||||||
128000,
|
128000,
|
||||||
192000,
|
192000,
|
||||||
320000,
|
320000,
|
||||||
|
|||||||
+6
-5
@@ -97,13 +97,13 @@ Image::scale(std::size_t size)
|
|||||||
}
|
}
|
||||||
catch (Magick::Exception& e)
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
std::vector<uint8_t>
|
||||||
Image::save(std::vector<unsigned char>& data, Format format) const
|
Image::save(Format format) const
|
||||||
{
|
{
|
||||||
Magick::Image outputImage(_image);
|
Magick::Image outputImage(_image);
|
||||||
|
|
||||||
@@ -112,8 +112,9 @@ Image::save(std::vector<unsigned char>& data, Format format) const
|
|||||||
Magick::Blob blob;
|
Magick::Blob blob;
|
||||||
outputImage.write(&blob);
|
outputImage.write(&blob);
|
||||||
|
|
||||||
unsigned char *charBuf = (unsigned char*)blob.data();
|
auto begin = static_cast<const uint8_t*>(blob.data());
|
||||||
data.assign( charBuf, charBuf + blob.length() );
|
|
||||||
|
return std::vector<uint8_t>(begin, begin + blob.length());
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Image
|
} // namespace Image
|
||||||
|
|||||||
+1
-1
@@ -49,7 +49,7 @@ class Image
|
|||||||
bool scale(std::size_t size);
|
bool scale(std::size_t size);
|
||||||
|
|
||||||
// output
|
// output
|
||||||
void save(std::vector<unsigned char>& rawData, Format format) const;
|
std::vector<uint8_t> save(Format format) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Magick::Image _image;
|
Magick::Image _image;
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
#include "admin/UsersView.hpp"
|
#include "admin/UsersView.hpp"
|
||||||
|
|
||||||
#include "resource/ImageResource.hpp"
|
#include "resource/ImageResource.hpp"
|
||||||
#include "resource/TranscodeResource.hpp"
|
#include "resource/AudioResource.hpp"
|
||||||
|
|
||||||
namespace UserInterface {
|
namespace UserInterface {
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env, Wt::Dbo::SqlConnecti
|
|||||||
_appGroups(appGroups),
|
_appGroups(appGroups),
|
||||||
_scanner(scanner),
|
_scanner(scanner),
|
||||||
_imageResource(nullptr),
|
_imageResource(nullptr),
|
||||||
_transcodeResource(nullptr)
|
_audioResource(nullptr)
|
||||||
{
|
{
|
||||||
auto bootstrapTheme = std::make_unique<Wt::WBootstrapTheme>();
|
auto bootstrapTheme = std::make_unique<Wt::WBootstrapTheme>();
|
||||||
bootstrapTheme->setVersion(Wt::BootstrapVersion::v3);
|
bootstrapTheme->setVersion(Wt::BootstrapVersion::v3);
|
||||||
@@ -328,8 +328,8 @@ LmsApplication::createHome()
|
|||||||
_isAdmin = LmsApp->getUser()->isAdmin();
|
_isAdmin = LmsApp->getUser()->isAdmin();
|
||||||
}
|
}
|
||||||
|
|
||||||
_imageResource = std::make_shared<ImageResource>(_db);
|
_imageResource = std::make_shared<ImageResource>();
|
||||||
_transcodeResource = std::make_shared<TranscodeResource>(_db);
|
_audioResource = std::make_shared<AudioResource>();
|
||||||
|
|
||||||
setConfirmCloseMessage(Wt::WString::tr("Lms.quit-confirm"));
|
setConfirmCloseMessage(Wt::WString::tr("Lms.quit-confirm"));
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
|
|
||||||
namespace UserInterface {
|
namespace UserInterface {
|
||||||
|
|
||||||
class TranscodeResource;
|
class AudioResource;
|
||||||
class ImageResource;
|
class ImageResource;
|
||||||
|
|
||||||
// Events that can be listen to anywhere in the application
|
// Events that can be listen to anywhere in the application
|
||||||
@@ -75,7 +75,7 @@ class LmsApplication : public Wt::WApplication
|
|||||||
|
|
||||||
// Session application data
|
// Session application data
|
||||||
std::shared_ptr<ImageResource> getImageResource() { return _imageResource; }
|
std::shared_ptr<ImageResource> getImageResource() { return _imageResource; }
|
||||||
std::shared_ptr<TranscodeResource> getTranscodeResource() { return _transcodeResource; }
|
std::shared_ptr<AudioResource> getAudioResource() { return _audioResource; }
|
||||||
Database::Handler& getDb() { return _db;}
|
Database::Handler& getDb() { return _db;}
|
||||||
Wt::Dbo::Session& getDboSession() { return _db.getSession();}
|
Wt::Dbo::Session& getDboSession() { return _db.getSession();}
|
||||||
|
|
||||||
@@ -122,7 +122,7 @@ class LmsApplication : public Wt::WApplication
|
|||||||
Auth* _auth;
|
Auth* _auth;
|
||||||
Scanner::MediaScanner& _scanner;
|
Scanner::MediaScanner& _scanner;
|
||||||
std::shared_ptr<ImageResource> _imageResource;
|
std::shared_ptr<ImageResource> _imageResource;
|
||||||
std::shared_ptr<TranscodeResource> _transcodeResource;
|
std::shared_ptr<AudioResource> _audioResource;
|
||||||
bool _isAdmin = false;
|
bool _isAdmin = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
|
|
||||||
#include "resource/ImageResource.hpp"
|
#include "resource/ImageResource.hpp"
|
||||||
#include "resource/TranscodeResource.hpp"
|
#include "resource/AudioResource.hpp"
|
||||||
|
|
||||||
#include "LmsApplication.hpp"
|
#include "LmsApplication.hpp"
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ MediaPlayer::loadTrack(Database::IdType trackId, bool play)
|
|||||||
{
|
{
|
||||||
Av::MediaFile mediaFile(track->getPath());
|
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);
|
auto imgResource = LmsApp->getImageResource()->getTrackUrl(trackId, 64);
|
||||||
|
|
||||||
std::ostringstream oss;
|
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
|
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()));
|
_title->setText(Wt::WString::fromUTF8(track->getName()));
|
||||||
|
|
||||||
|
|||||||
+39
-19
@@ -38,22 +38,25 @@ PlayQueue::PlayQueue()
|
|||||||
{
|
{
|
||||||
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||||
|
|
||||||
Wt::WText* clearBtn = bindNew<Wt::WText>("clear-btn", Wt::WString::tr("Lms.PlayQueue.clear"), Wt::TextFormat::XHTML);
|
Wt::WText* clearBtn = bindNew<Wt::WText>("clear-btn", Wt::WString::tr("Lms.PlayQueue.template.clear-btn"), Wt::TextFormat::XHTML);
|
||||||
|
clearBtn->setToolTip(Wt::WString::tr("Lms.PlayQueue.clear"));
|
||||||
_entriesContainer = bindNew<Wt::WContainerWidget>("entries");
|
|
||||||
|
|
||||||
_showMore = bindNew<Wt::WPushButton>("show-more", Wt::WString::tr("Lms.Explore.show-more"));
|
|
||||||
_showMore->setHidden(true);
|
|
||||||
|
|
||||||
Wt::WText* shuffleBtn = bindNew<Wt::WText>("shuffle-btn", Wt::WString::tr("Lms.PlayQueue.shuffle"), Wt::TextFormat::XHTML);
|
|
||||||
_radioMode = bindNew<Wt::WCheckBox>("radio-mode", Wt::WString::tr("Lms.PlayQueue.radio-mode"));
|
|
||||||
_nbTracks = bindNew<Wt::WText>("nb-tracks");
|
|
||||||
|
|
||||||
clearBtn->clicked().connect([=]
|
clearBtn->clicked().connect([=]
|
||||||
{
|
{
|
||||||
clearTracks();
|
clearTracks();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
_entriesContainer = bindNew<Wt::WContainerWidget>("entries");
|
||||||
|
|
||||||
|
_showMore = bindNew<Wt::WPushButton>("show-more", Wt::WString::tr("Lms.Explore.show-more"));
|
||||||
|
_showMore->setHidden(true);
|
||||||
|
_showMore->clicked().connect([=]
|
||||||
|
{
|
||||||
|
addSome();
|
||||||
|
updateCurrentTrack(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
Wt::WText* shuffleBtn = bindNew<Wt::WText>("shuffle-btn", Wt::WString::tr("Lms.PlayQueue.template.shuffle-btn"), Wt::TextFormat::XHTML);
|
||||||
|
shuffleBtn->setToolTip(Wt::WString::tr("Lms.PlayQueue.shuffle"));
|
||||||
shuffleBtn->clicked().connect([=]
|
shuffleBtn->clicked().connect([=]
|
||||||
{
|
{
|
||||||
{
|
{
|
||||||
@@ -65,12 +68,24 @@ PlayQueue::PlayQueue()
|
|||||||
addSome();
|
addSome();
|
||||||
});
|
});
|
||||||
|
|
||||||
_showMore->clicked().connect([=]
|
Wt::WText* repeatBtn = bindNew<Wt::WText>("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();
|
_repeatAll = !_repeatAll;
|
||||||
updateCurrentTrack(true);
|
repeatBtn->toggleStyleClass("Lms-playqueue-btn-selected", _repeatAll);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Wt::WText* radioBtn = bindNew<Wt::WText>("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<Wt::WText>("nb-tracks");
|
||||||
|
|
||||||
LmsApp->preQuit().connect([=]
|
LmsApp->preQuit().connect([=]
|
||||||
{
|
{
|
||||||
if (_tracklistId)
|
if (_tracklistId)
|
||||||
@@ -152,12 +167,17 @@ PlayQueue::load(std::size_t pos, bool play)
|
|||||||
// If out of range, stop playing
|
// If out of range, stop playing
|
||||||
if (pos >= tracklist->getCount())
|
if (pos >= tracklist->getCount())
|
||||||
{
|
{
|
||||||
stop();
|
if (!_repeatAll)
|
||||||
return;
|
{
|
||||||
|
stop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pos = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If last and radio mode, fill the next song
|
// 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();
|
addRadioTrack();
|
||||||
|
|
||||||
_trackPos = pos;
|
_trackPos = pos;
|
||||||
@@ -293,7 +313,7 @@ PlayQueue::addSome()
|
|||||||
entry->bindWidget("release-name", LmsApplication::createReleaseAnchor(track->getRelease()));
|
entry->bindWidget("release-name", LmsApplication::createReleaseAnchor(track->getRelease()));
|
||||||
}
|
}
|
||||||
|
|
||||||
Wt::WText* playBtn = entry->bindNew<Wt::WText>("play-btn", Wt::WString::tr("Lms.PlayQueue.play"), Wt::TextFormat::XHTML);
|
Wt::WText* playBtn = entry->bindNew<Wt::WText>("play-btn", Wt::WString::tr("Lms.PlayQueue.template.play-btn"), Wt::TextFormat::XHTML);
|
||||||
playBtn->clicked().connect(std::bind([=]
|
playBtn->clicked().connect(std::bind([=]
|
||||||
{
|
{
|
||||||
auto pos = _entriesContainer->indexOf(entry);
|
auto pos = _entriesContainer->indexOf(entry);
|
||||||
@@ -301,7 +321,7 @@ PlayQueue::addSome()
|
|||||||
load(pos, true);
|
load(pos, true);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
Wt::WText* delBtn = entry->bindNew<Wt::WText>("del-btn", Wt::WString::tr("Lms.PlayQueue.delete"), Wt::TextFormat::XHTML);
|
Wt::WText* delBtn = entry->bindNew<Wt::WText>("del-btn", Wt::WString::tr("Lms.PlayQueue.template.delete-btn"), Wt::TextFormat::XHTML);
|
||||||
delBtn->clicked().connect(std::bind([=]
|
delBtn->clicked().connect(std::bind([=]
|
||||||
{
|
{
|
||||||
// Remove the entry n both the widget tree and the playqueue
|
// Remove the entry n both the widget tree and the playqueue
|
||||||
|
|||||||
@@ -67,8 +67,9 @@ class PlayQueue : public Wt::WTemplate
|
|||||||
void load(std::size_t pos, bool play);
|
void load(std::size_t pos, bool play);
|
||||||
void stop();
|
void stop();
|
||||||
|
|
||||||
|
bool _repeatAll = false;
|
||||||
|
bool _radioMode = false;
|
||||||
boost::optional<Database::IdType> _tracklistId;
|
boost::optional<Database::IdType> _tracklistId;
|
||||||
Wt::WCheckBox* _radioMode;
|
|
||||||
Wt::WContainerWidget* _entriesContainer;
|
Wt::WContainerWidget* _entriesContainer;
|
||||||
Wt::WPushButton* _showMore;
|
Wt::WPushButton* _showMore;
|
||||||
Wt::WText* _nbTracks;
|
Wt::WText* _nbTracks;
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "TranscodeResource.hpp"
|
#include "AudioResource.hpp"
|
||||||
|
|
||||||
#include <Wt/Http/Response.h>
|
#include <Wt/Http/Response.h>
|
||||||
|
|
||||||
@@ -29,28 +29,22 @@
|
|||||||
|
|
||||||
namespace UserInterface {
|
namespace UserInterface {
|
||||||
|
|
||||||
TranscodeResource::TranscodeResource(Database::Handler& db)
|
|
||||||
: _db(db)
|
|
||||||
{
|
|
||||||
LMS_LOG(UI, DEBUG) << "CONSTRUCTING RESOURCE";
|
|
||||||
}
|
|
||||||
|
|
||||||
TranscodeResource:: ~TranscodeResource()
|
AudioResource:: ~AudioResource()
|
||||||
{
|
{
|
||||||
LMS_LOG(UI, DEBUG) << "DESTRUCTING RESOURCE";
|
|
||||||
beingDeleted();
|
beingDeleted();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string
|
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;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
TranscodeResource::handleRequest(const Wt::Http::Request& request,
|
AudioResource::handleRequest(const Wt::Http::Request& request,
|
||||||
Wt::Http::Response& response)
|
Wt::Http::Response& response)
|
||||||
{
|
{
|
||||||
std::shared_ptr<Av::Transcoder> transcoder;
|
std::shared_ptr<Av::Transcoder> transcoder;
|
||||||
@@ -83,14 +77,6 @@ TranscodeResource::handleRequest(const Wt::Http::Request& request,
|
|||||||
auto offsetStr = request.getParameter("offset");
|
auto offsetStr = request.getParameter("offset");
|
||||||
if (offsetStr)
|
if (offsetStr)
|
||||||
parameters.offset = std::chrono::seconds(std::stol(*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)
|
catch (std::exception &e)
|
||||||
{
|
{
|
||||||
@@ -100,12 +86,11 @@ TranscodeResource::handleRequest(const Wt::Http::Request& request,
|
|||||||
|
|
||||||
// transactions are not thread safe
|
// 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(LmsApp->getDboSession(), trackId);
|
||||||
Database::Track::pointer track = Database::Track::getById(_db.getSession(), trackId);
|
|
||||||
|
|
||||||
if (!track)
|
if (!track)
|
||||||
{
|
{
|
||||||
@@ -113,7 +98,23 @@ TranscodeResource::handleRequest(const Wt::Http::Request& request,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
parameters.bitrate = user->getAudioBitrate();
|
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<Av::Transcoder>(track->getPath(), parameters);
|
transcoder = std::make_shared<Av::Transcoder>(track->getPath(), parameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,8 +19,6 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <mutex>
|
|
||||||
|
|
||||||
#include <Wt/WResource.h>
|
#include <Wt/WResource.h>
|
||||||
|
|
||||||
#include "av/AvTranscoder.hpp"
|
#include "av/AvTranscoder.hpp"
|
||||||
@@ -31,21 +29,18 @@
|
|||||||
|
|
||||||
namespace UserInterface {
|
namespace UserInterface {
|
||||||
|
|
||||||
class TranscodeResource : public Wt::WResource
|
class AudioResource : public Wt::WResource
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
TranscodeResource(Database::Handler& db);
|
~AudioResource();
|
||||||
~TranscodeResource();
|
|
||||||
|
|
||||||
std::string getUrl(Database::IdType trackId, Av::Encoding encoding) const;
|
std::string getUrl(Database::IdType trackId) const;
|
||||||
|
|
||||||
void handleRequest(const Wt::Http::Request& request,
|
void handleRequest(const Wt::Http::Request& request,
|
||||||
Wt::Http::Response& response);
|
Wt::Http::Response& response);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
||||||
Database::Handler& _db;
|
|
||||||
|
|
||||||
static const std::size_t _chunkSize = 65536*4;
|
static const std::size_t _chunkSize = 65536*4;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -37,61 +37,11 @@ namespace UserInterface {
|
|||||||
static const std::string unknownCoverPath = "/images/unknown-cover.jpg";
|
static const std::string unknownCoverPath = "/images/unknown-cover.jpg";
|
||||||
static const std::string unknownArtistImagePath = "/images/unknown-artist.jpg";
|
static const std::string unknownArtistImagePath = "/images/unknown-artist.jpg";
|
||||||
|
|
||||||
ImageResource::ImageResource(Database::Handler& db)
|
|
||||||
: _db(db)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
ImageResource::~ImageResource()
|
ImageResource::~ImageResource()
|
||||||
{
|
{
|
||||||
beingDeleted();
|
beingDeleted();
|
||||||
}
|
}
|
||||||
|
|
||||||
Image::Image
|
|
||||||
ImageResource::getDefaultCover(std::size_t size)
|
|
||||||
{
|
|
||||||
std::unique_lock<std::mutex> 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<std::mutex> 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
|
std::string
|
||||||
ImageResource::getReleaseUrl(Database::IdType releaseId, std::size_t size) const
|
ImageResource::getReleaseUrl(Database::IdType releaseId, std::size_t size) const
|
||||||
{
|
{
|
||||||
@@ -104,52 +54,12 @@ ImageResource::getTrackUrl(Database::IdType trackId, std::size_t size) const
|
|||||||
return url() + "&trackid=" + std::to_string(trackId) + "&size=" + std::to_string(size);
|
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<unsigned char> data;
|
|
||||||
|
|
||||||
cover.save(data, Image::Format::JPEG);
|
|
||||||
|
|
||||||
response.setMimeType( Image::format_to_mimeType(Image::Format::JPEG) );
|
|
||||||
response.out().write(reinterpret_cast<const char *>(&data[0]), data.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
ImageResource::putCover(Wt::Http::Response& response, std::vector<Image::Image> 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
|
void
|
||||||
ImageResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response)
|
ImageResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response)
|
||||||
{
|
{
|
||||||
// Retrieve parameters
|
// Retrieve parameters
|
||||||
const std::string *trackIdStr = request.getParameter("trackid");
|
const std::string *trackIdStr = request.getParameter("trackid");
|
||||||
const std::string *releaseIdStr = request.getParameter("releaseid");
|
const std::string *releaseIdStr = request.getParameter("releaseid");
|
||||||
const std::string *artistIdStr = request.getParameter("artistid");
|
|
||||||
const std::string *sizeStr = request.getParameter("size");
|
const std::string *sizeStr = request.getParameter("size");
|
||||||
|
|
||||||
// Mandatory parameter size
|
// Mandatory parameter size
|
||||||
@@ -160,48 +70,19 @@ ImageResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Respons
|
|||||||
if (!size || *size > maxSize)
|
if (!size || *size > maxSize)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
std::vector<uint8_t> cover;
|
||||||
|
|
||||||
if (trackIdStr)
|
if (trackIdStr)
|
||||||
{
|
{
|
||||||
auto trackId = readAs<Database::IdType>(*trackIdStr);
|
auto trackId = readAs<Database::IdType>(*trackIdStr);
|
||||||
if (!trackId)
|
if (!trackId)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
boost::filesystem::path path;
|
// transactions are not thread safe
|
||||||
Database::Track::CoverType coverType = Database::Track::CoverType::None;
|
|
||||||
{
|
{
|
||||||
// transactions are not thread safe
|
Wt::WApplication::UpdateLock lock(LmsApp);
|
||||||
Wt::WApplication::UpdateLock lock(LmsApplication::instance());
|
cover = CoverArt::Grabber::instance().getFromTrack(LmsApp->getDboSession(), *trackId, Image::Format::JPEG, *size);
|
||||||
|
|
||||||
Wt::Dbo::Transaction transaction(_db.getSession());
|
|
||||||
|
|
||||||
Database::Track::pointer track = Database::Track::getById(_db.getSession(), *trackId);
|
|
||||||
if (track)
|
|
||||||
{
|
|
||||||
coverType = track->getCoverType();
|
|
||||||
path = track->getPath();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!path.empty())
|
|
||||||
{
|
|
||||||
std::vector<Image::Image> 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)
|
else if (releaseIdStr)
|
||||||
{
|
{
|
||||||
@@ -209,24 +90,17 @@ ImageResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Respons
|
|||||||
if (!releaseId)
|
if (!releaseId)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
std::vector<Image::Image> covers;
|
|
||||||
|
|
||||||
// transactions are not thread safe
|
// transactions are not thread safe
|
||||||
{
|
{
|
||||||
Wt::WApplication::UpdateLock lock(LmsApplication::instance());
|
Wt::WApplication::UpdateLock lock(LmsApp);
|
||||||
covers = CoverArt::Grabber::instance().getFromRelease(_db.getSession(), *releaseId);
|
cover = CoverArt::Grabber::instance().getFromRelease(LmsApp->getDboSession(), *releaseId, Image::Format::JPEG, *size);
|
||||||
}
|
}
|
||||||
|
|
||||||
putCover(response, covers, *size);
|
|
||||||
}
|
|
||||||
else if (artistIdStr)
|
|
||||||
{
|
|
||||||
putImage(response, getDefaultArtistImage(*size));
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
return;
|
||||||
putImage(response, getDefaultCover(*size));
|
|
||||||
}
|
response.setMimeType( Image::format_to_mimeType(Image::Format::JPEG) );
|
||||||
|
response.out().write(reinterpret_cast<const char *>(&cover[0]), cover.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace UserInterface
|
} // namespace UserInterface
|
||||||
|
|||||||
@@ -37,34 +37,13 @@ class ImageResource : public Wt::WResource
|
|||||||
public:
|
public:
|
||||||
static const std::size_t maxSize = 512;
|
static const std::size_t maxSize = 512;
|
||||||
|
|
||||||
ImageResource(Database::Handler& db);
|
|
||||||
~ImageResource();
|
~ImageResource();
|
||||||
|
|
||||||
std::string getReleaseUrl(Database::IdType releaseId, size_t size) const;
|
std::string getReleaseUrl(Database::IdType releaseId, size_t size) const;
|
||||||
std::string getTrackUrl(Database::IdType trackId, 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);
|
void handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response);
|
||||||
|
|
||||||
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<Image::Image> 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<std::size_t, Image::Image> _defaultCovers;
|
|
||||||
|
|
||||||
// Default artist images for different sizes
|
|
||||||
std::map<std::size_t, Image::Image> _defaultArtistImages;
|
|
||||||
|
|
||||||
// TODO construct a cache for covers?
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace UserInterface
|
} // namespace UserInterface
|
||||||
|
|||||||
Reference in New Issue
Block a user