diff --git a/README.md b/README.md
index ea2b8707..da46d7b6 100644
--- a/README.md
+++ b/README.md
@@ -1,19 +1,21 @@
# LMS - Lightweight Music Server
-LMS is a self-hosted media streaming software, released under the GPLv3 license.
-It allows you to access your music using an http(s) web interface.
+LMS is a self-hosted music streaming software, released under the GPLv3 license.
+It allows you to access your music using a web interface.
+
+A [demo](http://lms.demo.poupon.io) instance is available, with the following limitations:
+- Settings cannot be saved
+- No persistent playqueue
## Main features
- Responsive design
- Browse your audio collection using tag-based filters
-- Custom tag support
- User management
- Persistent play queue
- Radio mode
- MusicBrainzID support to handle duplicated artist and release names
- Audio transcode for maximum interoperability and low bandwith requirements
-
-LMS is written entirely in C++. Therefore, it is suitable to run on embedded devices, where space and memory are limited.
+- Custom tag support
## Installation
Here are the required packages to build on Debian Stretch:
diff --git a/approot/messages.xml b/approot/messages.xml
index e258382c..5a606c24 100644
--- a/approot/messages.xml
+++ b/approot/messages.xml
@@ -88,12 +88,15 @@
Top tracks
-
+
+Clear
{1} tracks
Added {1} tracks
Playing {1} tracks
Play Queue
Radio mode
+Repeat
+Shuffle
Play History
diff --git a/approot/playqueue.xml b/approot/playqueue.xml
index 56eda2a6..b2ba1131 100644
--- a/approot/playqueue.xml
+++ b/approot/playqueue.xml
@@ -2,20 +2,26 @@
-
+
-
+
+
+
+
+
+
+
+
+
${entries}
@@ -26,11 +32,11 @@
-
+
-
+
diff --git a/docroot/css/lms.css b/docroot/css/lms.css
index f4049419..7bf20079 100644
--- a/docroot/css/lms.css
+++ b/docroot/css/lms.css
@@ -248,6 +248,10 @@ a:hover {
color: #337ab7;
}
+.Lms-playqueue-btn-selected {
+ color: #337ab7;
+}
+
.Lms-playqueue-selected {
font-weight: 800;
}
diff --git a/src/Makefile.am b/src/Makefile.am
index 0e1bfe72..1efba30c 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -41,7 +41,7 @@ lms_SOURCES = \
$(srcdir)/ui/explore/TracksInfoView.cpp \
$(srcdir)/ui/explore/TracksView.cpp \
$(srcdir)/ui/resource/ImageResource.cpp \
- $(srcdir)/ui/resource/TranscodeResource.cpp \
+ $(srcdir)/ui/resource/AudioResource.cpp \
$(srcdir)/utils/Config.cpp \
$(srcdir)/utils/Logger.cpp \
$(srcdir)/utils/Path.cpp \
diff --git a/src/av/AvTranscoder.hpp b/src/av/AvTranscoder.hpp
index 144ea5d6..40df78fe 100644
--- a/src/av/AvTranscoder.hpp
+++ b/src/av/AvTranscoder.hpp
@@ -42,8 +42,6 @@ enum class Encoding
};
std::string encodingToMimetype(Encoding encoding);
-int encodingToInt(Encoding encoding);
-Encoding encodingFromInt(int encoding);
struct TranscodeParameters
{
diff --git a/src/cover/CoverArtGrabber.cpp b/src/cover/CoverArtGrabber.cpp
index fee2167f..ce688492 100644
--- a/src/cover/CoverArtGrabber.cpp
+++ b/src/cover/CoverArtGrabber.cpp
@@ -43,12 +43,15 @@ isFileSupported(const boost::filesystem::path& file, const std::vectordocRoot() + "/images/unknown-cover.jpg"))
+ throw LmsException("Cannot read default cover file");
}
Grabber&
@@ -58,48 +61,63 @@ Grabber::instance()
return instance;
}
-static std::vector
-getFromAvMediaFile(const Av::MediaFile& input, std::size_t nbMaxCovers)
+Image::Image
+Grabber::getDefaultCover(std::size_t size)
+{
+ std::unique_lock lock(_mutex);
+
+ auto it = _defaultCovers.find(size);
+ if (it == _defaultCovers.end())
+ {
+ Image::Image cover = _defaultCover;
+
+ cover.scale(size);
+ auto res = _defaultCovers.insert(std::make_pair(size, cover));
+ assert(res.second);
+ it = res.first;
+ }
+
+ return it->second;
+}
+
+static boost::optional
+getFromAvMediaFile(const Av::MediaFile& input)
{
std::vector res;
- for (Av::Picture& picture : input.getAttachedPictures(nbMaxCovers))
+ for (auto& picture : input.getAttachedPictures(2))
{
Image::Image image;
if (image.load(picture.data))
- res.push_back( image );
+ return image;
else
LMS_LOG(COVER, ERROR) << "Cannot load embedded cover file in '" << input.getPath().string() << "'";
}
- return res;
+ LMS_LOG(COVER, DEBUG) << "No cover found in media file '" << input.getPath().string() << "'";
+ return boost::none;
}
-std::vector
-Grabber::getFromDirectory(const boost::filesystem::path& p, std::size_t nbMaxCovers) const
+boost::optional
+Grabber::getFromDirectory(const boost::filesystem::path& p) const
{
- std::vector res;
-
- std::vector coverPathes = getCoverPaths(p, nbMaxCovers);
- for (auto coverPath : coverPathes)
+ for (auto coverPath : getCoverPaths(p))
{
- if (res.size() >= nbMaxCovers)
- break;
-
Image::Image image;
if (image.load(coverPath))
- res.push_back(image);
+ return image;
else
LMS_LOG(COVER, ERROR) << "Cannot load image in file '" << coverPath.string() << "'";
}
- return res;
+ LMS_LOG(COVER, DEBUG) << "No cover found in directory '" << p.string() << "'";
+ return boost::none;
}
std::vector
-Grabber::getCoverPaths(const boost::filesystem::path& directoryPath, std::size_t nbMaxCovers) const
+Grabber::getCoverPaths(const boost::filesystem::path& directoryPath) const
{
std::vector res;
boost::system::error_code ec;
@@ -126,86 +144,108 @@ Grabber::getCoverPaths(const boost::filesystem::path& directoryPath, std::size_t
}
res.push_back(path);
- if (res.size() >= nbMaxCovers)
- break;
}
+
return res;
}
-std::vector
-Grabber::getFromTrack(const boost::filesystem::path& p, std::size_t nbMaxCovers) const
+boost::optional
+Grabber::getFromTrack(const boost::filesystem::path& p) const
{
try
{
Av::MediaFile input(p);
- return getFromAvMediaFile(input, nbMaxCovers);
+ return getFromAvMediaFile(input);
}
catch (Av::MediaFileException& e)
{
LMS_LOG(COVER, ERROR) << "Cannot get covers from track " << p.string() << ": " << e.what();
+ return boost::none;
}
-
- return std::vector();
}
-std::vector
-Grabber::getFromTrack(Wt::Dbo::Session& session, Database::IdType trackId, std::size_t nbMaxCovers) const
+Image::Image
+Grabber::getFromTrack(Wt::Dbo::Session& session, Database::IdType trackId, std::size_t size)
{
using namespace Database;
- Wt::Dbo::Transaction transaction(session);
+ boost::optional cover;
- Track::pointer track = Track::getById(session, trackId);
- if (!track)
- return std::vector();
-
- Track::CoverType coverType = track->getCoverType();
- boost::filesystem::path trackPath = track->getPath();
-
- transaction.commit();
-
- switch (coverType)
{
- case Track::CoverType::Embedded:
- return Grabber::getFromTrack(trackPath, nbMaxCovers);
- case Track::CoverType::None:
- return Grabber::getFromDirectory(trackPath.parent_path(), nbMaxCovers);
+ Wt::Dbo::Transaction transaction(session);
+
+ Track::pointer track = Track::getById(session, trackId);
+ if (track)
+ {
+ Track::CoverType coverType = track->getCoverType();
+ boost::filesystem::path trackPath = track->getPath();
+
+ transaction.commit();
+
+ if (coverType == Track::CoverType::Embedded)
+ cover = getFromTrack(trackPath);
+
+ if (!cover)
+ cover = getFromDirectory(trackPath.parent_path());
+ }
}
- return std::vector();
+ if (!cover)
+ cover = getDefaultCover(size);
+ else
+ cover->scale(size);
+
+ return *cover;
}
-std::vector
-Grabber::getFromRelease(Wt::Dbo::Session& session, Database::IdType releaseId, std::size_t nbMaxCovers) const
+Image::Image
+Grabber::getFromRelease(Wt::Dbo::Session& session, Database::IdType releaseId, std::size_t size)
{
using namespace Database;
- Wt::Dbo::Transaction transaction(session);
+ boost::optional cover;
- // If the release does not exist, do nothing
- Release::pointer release = Release::getById(session, releaseId);
- if (!release)
- return std::vector();
+ {
+ Wt::Dbo::Transaction transaction(session);
- std::vector tracks = release->getTracks();
- if (tracks.empty())
- return std::vector();
+ auto release = Release::getById(session, releaseId);
+ if (release)
+ {
+ auto tracks = release->getTracks();
+ if (!tracks.empty())
+ {
+ auto trackId = tracks.front().id();
+ transaction.commit();
- boost::filesystem::path firstTrackPath = tracks.front()->getPath();
- bool embeddedCover = (tracks.front()->getCoverType() == Track::CoverType::Embedded);
+ return getFromTrack(session, trackId, size);
+ }
+ }
+ }
- transaction.commit();
+ if (!cover)
+ cover = getDefaultCover(size);
+ else
+ cover->scale(size);
- // First, try to get covers from the directory of the release
- std::vector res = getFromDirectory( firstTrackPath.parent_path(), nbMaxCovers);
+ return *cover;
+}
- // Fallback on the embedded cover of the first track
- if (res.empty() && embeddedCover)
- res = getFromTrack( firstTrackPath, nbMaxCovers);
+std::vector
+Grabber::getFromTrack(Wt::Dbo::Session& session, Database::IdType trackId, Image::Format format, std::size_t size)
+{
+ Image::Image cover = getFromTrack(session, trackId, size);
- return res;
+ return cover.save(Image::Format::JPEG);
+}
+
+std::vector
+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
diff --git a/src/cover/CoverArtGrabber.hpp b/src/cover/CoverArtGrabber.hpp
index 43753c4f..e8d0b6a7 100644
--- a/src/cover/CoverArtGrabber.hpp
+++ b/src/cover/CoverArtGrabber.hpp
@@ -19,6 +19,8 @@
#pragma once
+#include