Merge branch 'develop'

This commit is contained in:
emeric
2018-09-18 13:45:07 +02:00
20 changed files with 249 additions and 308 deletions
+7 -5
View File
@@ -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:
+4 -1
View File
@@ -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
View File
@@ -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>
+4
View File
@@ -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
View File
@@ -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 \
-2
View File
@@ -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
View File
@@ -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
+21 -5
View File
@@ -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
}; };
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+4 -4
View File
@@ -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"));
+3 -3
View File
@@ -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;
}; };
+3 -3
View File
@@ -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
View File
@@ -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
+2 -1
View File
@@ -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;
}; };
+11 -137
View File
@@ -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
-21
View File
@@ -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