-
-
-
+
+
+
+
+
${cover}
+
${name} ${artist}
+
+
+
+
+
+
+
+
+
+
+
+
+ ${title}
+
+ ${show-more}
+
+
+
+
+
+
+
+
+
+ ${title}
+ ${release-container}
+ ${show-more}
+
+
+
+
+
+
+
+ ${cover}
+
+
${release-name} ${artist-name}${} - ${year}${ }${} (${orig-year})${ }
+
+
+
+
+
+
+
+
+
+
+ ${audio}
+ ${prev}${play-pause}${next}
+
+ ${volume}
+
+
+ ${cover}
+
+
+ ${shuffle}${repeat}
+
+
diff --git a/docroot/css/lms.css b/docroot/css/lms.css
index 1bd8c8f4..6c0bf27d 100644
--- a/docroot/css/lms.css
+++ b/docroot/css/lms.css
@@ -1,5 +1,6 @@
.main-nav {
margin-bottom: 0px;
+ box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.4);
}
.Wt-hrh2 {
@@ -10,13 +11,10 @@
background-color: white;
}
-div.contents {
- padding: 0px 12px 6px;
-}
-
.playqueue {
background-color: #EEE;
border-radius: 10px;
+ box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.4);
}
.playqueue-playing {
@@ -39,32 +37,98 @@ div.contents {
height: 64px;
}
-.mediaplayer {
- background-color: #CCC;
- border-radius: 10px;
- min-width: 360px;
-}
-
-.mediaplayer-btn-controls {
- font-weight: bold;
-}
-
-.mediaplayer-current-cover {
- width: 72px;
- height: 72px;
- border-radius: 8px;
- box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.5);
-}
-
-.mediaplayer-current-track {
+.mediaplayer-track {
font-weight: bold;
font-size: 120%;
}
-.mediaplayer-current-artist {
+.mediaplayer-artist {
font-style: italic;
}
+.mediaplayer {
+ background-color: #CCC;
+ border-radius: 10px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ height: 72px;
+ box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.4);
+}
+
+.mediaplayer-cover {
+ margin: 8px;
+ width: 64px;
+ min-width: 64px;
+ height: 64px;
+ min-height: 64px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.mediaplayer-cover img {
+ border-radius: 8px;
+ border-radius: 8px;
+ box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.4);
+}
+
+.mediaplayer-btn {
+ margin: 8px;
+}
+
+.mediaplayer-btn i {
+ cursor: pointer;
+ text-shadow: 0px 2px 3px rgba(0, 0, 0, 0.4);
+}
+
+.mediaplayer-btn-active {
+ color: #428bca;
+}
+
+.mediaplayer-info-container {
+ width: 100%;
+ min-width: 100px;
+ margin: 8px;
+}
+
+.mediaplayer-track-info {
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+}
+
+.mediaplayer-progress-container {
+ display: flex;
+ align-items: center;
+}
+
+.mediaplayer-badge {
+ min-width: initial;
+}
+
+.mediaplayer-current-duration {
+ margin-right: 8px;
+}
+.mediaplayer-total-duration {
+ margin-left: 8px;
+}
+
+.mediaplayer-seekbar {
+}
+
+.mediaplayer-volume-container {
+ width: 24px;
+ height: 64px;
+ margin: 8px;
+}
+
+.mediaplayer-volume {
+ height: 64px;
+ -webkit-appearance: slider-vertical;
+ writing-mode: bt-lr;
+}
+
.mobile-search-title {
font-weight: bold;
height: 32px;
@@ -117,3 +181,20 @@ div.contents {
height: 48px;
}
+.release_res_shadow {
+ box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.5)
+}
+
+.release_truncated-name {
+ white-space: nowrap;
+ overflow: hidden;
+ overflow-x: hidden;
+ overflow-y: hidden;
+ text-overflow: ellipsis;
+}
+
+.release_img-responsive {
+ display: block;
+ max-width: 100%;
+ height: auto;
+}
diff --git a/docroot/images/unknown-cover.jpg b/docroot/images/unknown-cover.jpg
old mode 100755
new mode 100644
diff --git a/src/Makefile.am b/src/Makefile.am
index ce6a0c12..e6b0f919 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -20,13 +20,12 @@ lms_SOURCES = \
$(srcdir)/image/Image.cpp \
$(srcdir)/logger/Logger.cpp \
$(srcdir)/metadata/AvFormat.cpp \
- $(srcdir)/metadata/Utils.cpp \
$(srcdir)/service/ServiceManager.cpp \
$(srcdir)/service/DatabaseUpdateService.cpp \
$(srcdir)/ui/LmsApplication.cpp \
$(srcdir)/ui/auth/LmsAuth.cpp \
+ $(srcdir)/ui/audio/AudioPlayer.cpp \
$(srcdir)/ui/audio/desktop/DesktopAudio.cpp \
- $(srcdir)/ui/audio/desktop/AudioMediaPlayer.cpp \
$(srcdir)/ui/audio/desktop/FilterChain.cpp \
$(srcdir)/ui/audio/desktop/KeywordSearchFilter.cpp \
$(srcdir)/ui/audio/desktop/PlayQueue.cpp \
@@ -34,13 +33,15 @@ lms_SOURCES = \
$(srcdir)/ui/audio/desktop/TrackView.cpp \
$(srcdir)/ui/audio/mobile/ArtistSearch.cpp \
$(srcdir)/ui/audio/mobile/MobileAudio.cpp \
- $(srcdir)/ui/audio/mobile/MobileAudioMediaPlayer.cpp \
$(srcdir)/ui/audio/mobile/ReleaseSearch.cpp \
+ $(srcdir)/ui/audio/mobile/TrackReleaseView.cpp \
$(srcdir)/ui/audio/mobile/TrackSearch.cpp \
$(srcdir)/ui/common/DirectoryValidator.cpp \
+ $(srcdir)/ui/common/InputRange.cpp \
$(srcdir)/ui/common/LineEdit.cpp \
$(srcdir)/ui/resource/AvConvTranscodeStreamResource.cpp \
$(srcdir)/ui/resource/CoverResource.cpp \
+ $(srcdir)/ui/resource/TranscodeResource.cpp \
$(srcdir)/ui/settings/Settings.cpp \
$(srcdir)/ui/settings/SettingsAccountFormView.cpp \
$(srcdir)/ui/settings/SettingsAudioFormView.cpp \
@@ -48,8 +49,9 @@ lms_SOURCES = \
$(srcdir)/ui/settings/SettingsFirstConnectionFormView.cpp \
$(srcdir)/ui/settings/SettingsMediaDirectories.cpp \
$(srcdir)/ui/settings/SettingsMediaDirectoryFormView.cpp \
- $(srcdir)/ui/settings/SettingsUserFormView.cpp \
- $(srcdir)/ui/settings/SettingsUsers.cpp
+ $(srcdir)/ui/settings/SettingsUserFormView.cpp \
+ $(srcdir)/ui/settings/SettingsUsers.cpp \
+ $(srcdir)/utils/Utils.cpp
if VIDEO
lms_SOURCES += \
@@ -60,5 +62,4 @@ lms_SOURCES += \
endif
lms_CXXFLAGS=-std=c++11 -Wall -I$(top_srcdir)/third-party -I$(srcdir)/ui $(MAGICKXX_CFLAGS)
-
lms_LDADD=$(MAGICKXX_LIBS)
diff --git a/src/av/AvTranscoder.cpp b/src/av/AvTranscoder.cpp
index a9d8171a..67ee92b6 100644
--- a/src/av/AvTranscoder.cpp
+++ b/src/av/AvTranscoder.cpp
@@ -29,22 +29,55 @@ namespace Av {
#define LMS_LOG_TRANSCODE(sev) LMS_LOG(TRANSCODE, INFO) << "[" << _id << "] - "
+struct EncodingInfo
+{
+ Encoding encoding;
+ std::string mimetype;
+ int id;
+};
+
+static std::vector
encodingInfos =
+{
+ {Encoding::MP3, "audio/mp3", 0},
+ {Encoding::OGA, "audio/ogg", 1},
+ {Encoding::OGV, "video/ogg", 2},
+ {Encoding::WEBMA, "audio/webm", 3},
+ {Encoding::WEBMV, "video/webm", 4},
+ {Encoding::M4A, "audio/mp4", 5},
+ {Encoding::M4V, "video/mp4", 6},
+};
+
std::string encoding_to_mimetype(Encoding encoding)
{
- switch(encoding)
+ for (auto encodingInfo : encodingInfos)
{
- case Encoding::MP3: return "audio/mp3";
- case Encoding::OGA: return "audio/ogg";
- case Encoding::OGV: return "video/ogg";
- case Encoding::WEBMA: return "audio/webm";
- case Encoding::WEBMV: return "video/webm";
- case Encoding::FLA: return "audio/x-flv";
- case Encoding::FLV: return "video/x-flv";
- case Encoding::M4A: return "audio/mp4";
- case Encoding::M4V: return "video/mp4";
+ if (encodingInfo.encoding == encoding)
+ return encodingInfo.mimetype;
}
- return "";
+ throw std::logic_error("encoding_to_mimetype failed!");
+}
+
+int encoding_to_int(Encoding encoding)
+{
+ for (auto encodingInfo : encodingInfos)
+ {
+ if (encodingInfo.encoding == encoding)
+ return encodingInfo.id;
+ }
+
+ throw std::logic_error("encoding_to_int failed!");
+}
+
+Encoding encoding_from_int(int encodingId)
+{
+ for (auto encodingInfo : encodingInfos)
+ {
+ if (encodingInfo.id == encodingId)
+ return encodingInfo.encoding;
+ }
+
+ throw std::logic_error("encoding_from_int failed!");
}
// TODO, parametrize?
@@ -129,6 +162,7 @@ Transcoder::start()
// in order not to block the whole forked process
args.push_back("-loglevel");
args.push_back("quiet");
+ args.push_back("-nostdin");
// input Offset
if (_parameters.getOffset().total_seconds() > 0)
@@ -230,25 +264,6 @@ Transcoder::start()
args.push_back("m4v");
break;
- case Encoding::FLV:
- args.push_back("-acodec");
- args.push_back("libmp3lame");
- args.push_back("-ac");
- args.push_back("2");
- args.push_back("-ar");
- args.push_back("44100");
- args.push_back("-vcodec");
- args.push_back("libx264");
- args.push_back("-f");
- args.push_back("flv");
- break;
-
- case Encoding::FLA:
- args.push_back("-acodec");
- args.push_back("libmp3lame");
- args.push_back("-f");
- args.push_back("flv");
- break;
default:
return false;
}
@@ -265,8 +280,8 @@ Transcoder::start()
_child = std::make_shared();
- const redi::pstreams::pmode mode = redi::pstreams::pstdout; // | redi::pstreams::pstderr;
- _child->open(avConvPath.string(), args, mode);
+ // Caution: stdin must have been closed before
+ _child->open(avConvPath.string(), args);
if (!_child->is_open())
{
LMS_LOG_TRANSCODE(DEBUG) << "Exec failed!";
@@ -279,8 +294,8 @@ Transcoder::start()
return false;
}
- LMS_LOG_TRANSCODE(DEBUG) << "Stream opened!";
}
+ LMS_LOG_TRANSCODE(DEBUG) << "Stream opened!";
return true;
}
@@ -291,14 +306,31 @@ Transcoder::process(std::vector& output, std::size_t maxSize)
if (!_child || _isComplete)
return;
+ if (_child->out().fail())
+ {
+ LMS_LOG_TRANSCODE(DEBUG) << "Stdout FAILED 2";
+ }
+
+ if (_child->out().eof())
+ {
+ LMS_LOG_TRANSCODE(DEBUG) << "Stdout ENDED 2";
+ }
+
output.resize(maxSize);
+ LMS_LOG_TRANSCODE(DEBUG) << "Reading up to " << output.size() << " bytes";
+
//Read on the output stream
_child->out().read(reinterpret_cast(&output[0]), maxSize);
output.resize(_child->out().gcount());
LMS_LOG_TRANSCODE(DEBUG) << "Read " << output.size() << " bytes";
+ if (_child->out().fail())
+ {
+ LMS_LOG_TRANSCODE(DEBUG) << "Stdout FAILED";
+ }
+
if (_child->out().eof())
{
LMS_LOG_TRANSCODE(DEBUG) << "Stdout EOF!";
diff --git a/src/av/AvTranscoder.hpp b/src/av/AvTranscoder.hpp
index 3f7a71f7..7e671fb8 100644
--- a/src/av/AvTranscoder.hpp
+++ b/src/av/AvTranscoder.hpp
@@ -40,13 +40,13 @@ enum class Encoding
MP3,
WEBMA,
WEBMV,
- FLA,
- FLV,
M4A,
M4V,
};
std::string encoding_to_mimetype(Encoding encoding);
+int encoding_to_int(Encoding encoding);
+Encoding encoding_from_int(int encoding);
class TranscodeParameters
{
@@ -91,6 +91,8 @@ class Transcoder
void process(std::vector& output, std::size_t maxSize);
bool isComplete(void) { return _isComplete; }
+ const TranscodeParameters& getParameters() const { return _parameters; }
+
private:
Transcoder();
diff --git a/src/cover/CoverArtGrabber.cpp b/src/cover/CoverArtGrabber.cpp
index 738d04f3..1cfe38ef 100644
--- a/src/cover/CoverArtGrabber.cpp
+++ b/src/cover/CoverArtGrabber.cpp
@@ -170,23 +170,25 @@ Grabber::getFromRelease(Wt::Dbo::Session& session, Database::Release::id_type re
{
using namespace Database;
- boost::filesystem::path firstTrackPath;
- bool embeddedCover = false;
+ Wt::Dbo::Transaction transaction(session);
+
+ // If the release does not exist or is the special release "None", do nothing
+ Release::pointer release = Release::getById(session, releaseId);
+ if (!release || release->isNone())
+ return std::vector();
// Get the first track of the release
- {
- Wt::Dbo::Transaction transaction(session);
+ std::vector tracks = Track::getByFilter(session,
+ SearchFilter::ById(SearchFilter::Field::Release, releaseId),
+ -1, 1 /* limit result size */);
- std::vector tracks = Track::getByFilter(session,
- SearchFilter::IdMatch({{SearchFilter::Field::Release, {releaseId}}}),
- -1, 1 /* limit result size */);
+ if (tracks.empty())
+ return std::vector();
- if (tracks.empty())
- return std::vector();
+ boost::filesystem::path firstTrackPath = tracks.front()->getPath();
+ bool embeddedCover = (tracks.front()->getCoverType() == Track::CoverType::Embedded);
- firstTrackPath = tracks.front()->getPath();
- embeddedCover = (tracks.front()->getCoverType() == Track::CoverType::Embedded);
- }
+ transaction.commit();
// First, try to get covers from the directory of the release
std::vector res = getFromDirectory( firstTrackPath.parent_path(), nbMaxCovers);
diff --git a/src/database-updater/DatabaseUpdater.cpp b/src/database-updater/DatabaseUpdater.cpp
index eba1e54b..33afd506 100644
--- a/src/database-updater/DatabaseUpdater.cpp
+++ b/src/database-updater/DatabaseUpdater.cpp
@@ -526,9 +526,15 @@ Updater::processAudioFile( const boost::filesystem::path& file, Stats& stats)
if (items.find(MetaData::Type::TrackNumber) != items.end())
track.modify()->setTrackNumber( boost::any_cast(items[MetaData::Type::TrackNumber]) );
+ if (items.find(MetaData::Type::TotalTrack) != items.end())
+ track.modify()->setTotalTrackNumber( boost::any_cast(items[MetaData::Type::TotalTrack]) );
+
if (items.find(MetaData::Type::DiscNumber) != items.end())
track.modify()->setDiscNumber( boost::any_cast(items[MetaData::Type::DiscNumber]) );
+ if (items.find(MetaData::Type::TotalDisc) != items.end())
+ track.modify()->setTotalDiscNumber( boost::any_cast(items[MetaData::Type::TotalDisc]) );
+
if (items.find(MetaData::Type::Date) != items.end())
track.modify()->setDate( boost::any_cast(items[MetaData::Type::Date]) );
diff --git a/src/database/Release.cpp b/src/database/Release.cpp
index 5ef5415a..ba4f88d2 100644
--- a/src/database/Release.cpp
+++ b/src/database/Release.cpp
@@ -44,6 +44,11 @@ Release::getByMBID(Wt::Dbo::Session& session, const std::string& mbid)
return session.find().where("mbid = ?").bind(mbid);
}
+Release::pointer
+Release::getById(Wt::Dbo::Session& session, Release::id_type id)
+{
+ return session.find().where("id = ?").bind(id);
+}
Release::pointer
Release::create(Wt::Dbo::Session& session, const std::string& name, const std::string& MBID)
@@ -51,6 +56,12 @@ Release::create(Wt::Dbo::Session& session, const std::string& name, const std::s
return session.add(new Release(name, MBID));
}
+bool
+Release::isNone() const
+{
+ return _name == "";
+}
+
Release::pointer
Release::getNone(Wt::Dbo::Session& session)
{
@@ -129,16 +140,28 @@ Release::getByFilter(Wt::Dbo::Session& session, SearchFilter filter, int offset,
return std::vector(res.begin(), res.end());
}
-std::vector< Wt::Dbo::ptr >
-Release::getArtists() const
+int
+Release::getReleaseYear(bool original) const
{
- assert(self());
- assert(self()->id() != Wt::Dbo::dbo_traits::invalidId() );
assert(session());
- Wt::Dbo::collection< Wt::Dbo::ptr > res = session()->query >("SELECT a FROM artist a INNER JOIN release r ON r.id = t.release_id INNER JOIN track t ON t.release_id = r.id").where("r.id = ?").bind(id());
+ // TODO something better
+ auto tracks = Track::getByFilter(*session(), SearchFilter::ById(SearchFilter::Field::Release, this->id()), -1, 1);
- return std::vector< Wt::Dbo::ptr > (res.begin(), res.end());
+ if (tracks.empty())
+ return 0;
+
+ boost::gregorian::date date;
+
+ if (original)
+ date = tracks.front()->getOriginalDate().date();
+ else
+ date = tracks.front()->getDate().date();
+
+ if (date.is_special())
+ return 0;
+
+ return date.year();
}
} // namespace Database
diff --git a/src/database/Release.hpp b/src/database/Release.hpp
index 0bb779d0..2ace3867 100644
--- a/src/database/Release.hpp
+++ b/src/database/Release.hpp
@@ -53,6 +53,9 @@ class Release : public Wt::Dbo::Dbo
// Create
static pointer create(Wt::Dbo::Session& session, const std::string& name, const std::string& MBID = "");
+ // Utility functions
+ int getReleaseYear(bool originalDate = false) const; // 0 if unknown or various
+
// MVC models for the user interface
// ID, Release name, year, track counts
typedef boost::tuple UIQueryResult;
@@ -65,8 +68,6 @@ class Release : public Wt::Dbo::Dbo
std::string getMBID() const { return _MBID; }
bool isNone(void) const;
boost::posix_time::time_duration getDuration(void) const;
- std::vector > getArtists() const; // Get the artists of this release
- std::vector > getTracks() const; // Get the tracks of this release
void setMBID(std::string mbid) { _MBID = mbid; }
diff --git a/src/database/SearchFilter.cpp b/src/database/SearchFilter.cpp
index 44c227cb..f6dbaa1c 100644
--- a/src/database/SearchFilter.cpp
+++ b/src/database/SearchFilter.cpp
@@ -17,6 +17,8 @@
* along with LMS. If not, see .
*/
+#include "logger/Logger.hpp"
+
#include "SearchFilter.hpp"
namespace Database
diff --git a/src/database/SearchFilter.hpp b/src/database/SearchFilter.hpp
index 2d0d397c..ad8716d8 100644
--- a/src/database/SearchFilter.hpp
+++ b/src/database/SearchFilter.hpp
@@ -58,12 +58,29 @@ class SearchFilter
return SearchFilter(_nameLikeMatch);
}
- // Single ID match
+ // Single Field ID match
static SearchFilter ById(Field field, Wt::Dbo::dbo_default_traits::IdType id)
{
return SearchFilter({{field, {id} }});
}
+ // Single Field Name match OR
+ static SearchFilter ByNameOr(Field field, std::vector keywords)
+ {
+ return NameLikeMatch({{{field, keywords}}});
+ }
+
+ // Single Field Name match AND
+ static SearchFilter ByNameAnd(Field field, std::vector keywords)
+ {
+ NameLikeMatchType nameLikeMatch;
+
+ for (auto keyword : keywords)
+ nameLikeMatch.push_back({{{field, {keyword}}}});
+
+ return NameLikeMatch(nameLikeMatch);
+ }
+
// The filter is a AND of the following conditions:
// ((Field1.name LIKE STR1-1 OR Field1.name LIKE STR1-2 ...) OR (Field2.name LIKE STR2-1 OR Field2.name LIKE STR2-2 ...) ...
diff --git a/src/database/Track.cpp b/src/database/Track.cpp
index cf78eac0..75b226ba 100644
--- a/src/database/Track.cpp
+++ b/src/database/Track.cpp
@@ -30,7 +30,9 @@ namespace Database {
Track::Track(const boost::filesystem::path& p)
:
_trackNumber(0),
+_totalTrackNumber(0),
_discNumber(0),
+_totalDiscNumber(0),
_filePath( p.string() ),
_coverType(CoverType::None)
{
diff --git a/src/database/Track.hpp b/src/database/Track.hpp
index 17d4f809..cd96cf17 100644
--- a/src/database/Track.hpp
+++ b/src/database/Track.hpp
@@ -136,7 +136,9 @@ class Track
// Accessors
void setTrackNumber(int num) { _trackNumber = num; }
+ void setTotalTrackNumber(int num) { _totalTrackNumber = num; }
void setDiscNumber(int num) { _discNumber = num; }
+ void setTotalDiscNumber(int num) { _totalDiscNumber = num; }
void setName(const std::string& name) { _name = std::string(name, 0, _maxNameLength); }
void setDuration(boost::posix_time::time_duration duration) { _duration = duration; }
void setLastWriteTime(boost::posix_time::ptime time) { _fileLastWrite = time; }
@@ -146,13 +148,15 @@ class Track
void setOriginalDate(const boost::posix_time::ptime& date) { _originalDate = date; }
void setGenres(const std::string& genreList) { _genreList = genreList; }
void setCoverType(CoverType coverType) { _coverType = coverType; }
- void setMBID(const std::string& MBID) { _MBID = MBID; }
+ void setMBID(const std::string& MBID) { _MBID = MBID; }
void setArtist(Wt::Dbo::ptr artist) { _artist = artist; }
void setRelease(Wt::Dbo::ptr release) { _release = release; }
void setGenres(std::vector genres);
int getTrackNumber(void) const { return _trackNumber; }
+ int getTotalTrackNumber(void) const { return _totalTrackNumber; }
int getDiscNumber(void) const { return _discNumber; }
+ int getTotalDiscNumber(void) const { return _totalDiscNumber; }
std::string getName(void) const { return _name; }
boost::filesystem::path getPath(void) const { return _filePath; }
boost::posix_time::time_duration getDuration(void) const { return _duration; }
@@ -172,7 +176,9 @@ class Track
void persist(Action& a)
{
Wt::Dbo::field(a, _trackNumber, "track_number");
+ Wt::Dbo::field(a, _totalTrackNumber, "total_track_number");
Wt::Dbo::field(a, _discNumber, "disc_number");
+ Wt::Dbo::field(a, _totalDiscNumber, "total_disc_number");
Wt::Dbo::field(a, _name, "name");
Wt::Dbo::field(a, _duration, "duration");
Wt::Dbo::field(a, _date, "date");
@@ -197,7 +203,9 @@ class Track
static const std::size_t _maxNameLength = 128;
int _trackNumber;
+ int _totalTrackNumber;
int _discNumber;
+ int _totalDiscNumber;
std::string _name;
std::string _artistName;
std::string _releaseName;
diff --git a/src/database/User.cpp b/src/database/User.cpp
index 21aa8310..d25410c3 100644
--- a/src/database/User.cpp
+++ b/src/database/User.cpp
@@ -55,7 +55,6 @@ User::audioEncodings =
AudioEncoding::MP3,
AudioEncoding::OGA,
AudioEncoding::WEBMA,
- AudioEncoding::FLA,
};
const std::vector
diff --git a/src/database/User.hpp b/src/database/User.hpp
index 2450aa1f..0bf1138a 100644
--- a/src/database/User.hpp
+++ b/src/database/User.hpp
@@ -39,7 +39,6 @@ enum class AudioEncoding
MP3,
OGA,
WEBMA,
- FLA,
};
enum class VideoEncoding
diff --git a/src/logger/Logger.hpp b/src/logger/Logger.hpp
index 1f08c04a..b3b7b7a4 100644
--- a/src/logger/Logger.hpp
+++ b/src/logger/Logger.hpp
@@ -17,8 +17,7 @@
* along with LMS. If not, see .
*/
-#ifndef LOGGER_HPP__
-#define LOGGER_HPP__
+#pragma once
#include
#include
@@ -55,4 +54,3 @@ std::string getSeverityName(Severity sev);
#define LMS_LOG(module, level) Wt::log(getSeverityName(Severity::level)) << Wt::WLogger::sep << "[" << getModuleName(Module::module) << "]" << Wt::WLogger::sep
-#endif
diff --git a/src/main/main.cpp b/src/main/main.cpp
index 149d7cbc..228678ff 100644
--- a/src/main/main.cpp
+++ b/src/main/main.cpp
@@ -41,6 +41,8 @@ int main(int argc, char* argv[])
try
{
+ // Make pstream work with ffmpeg
+ close(STDIN_FILENO);
Wt::WServer server(argv[0]);
server.setServerConfiguration (argc, argv);
diff --git a/src/metadata/AvFormat.cpp b/src/metadata/AvFormat.cpp
index 18f305bc..07778457 100644
--- a/src/metadata/AvFormat.cpp
+++ b/src/metadata/AvFormat.cpp
@@ -19,15 +19,12 @@
#include "AvFormat.hpp"
-#include
+#include "logger/Logger.hpp"
+#include "utils/Utils.hpp"
#include
-#include
#include "av/AvInfo.hpp"
-#include "logger/Logger.hpp"
-
-#include "Utils.hpp"
namespace MetaData
{
@@ -112,21 +109,48 @@ AvFormat::parse(const boost::filesystem::path& p, Items& items)
for (it = metadata.begin(); it != metadata.end(); ++it)
{
if (boost::iequals(it->first, "artist"))
- items.insert( std::make_pair(MetaData::Type::Artist, string_trim( string_to_utf8(it->second)) ));
+ items.insert( std::make_pair(MetaData::Type::Artist, stringTrim( stringToUTF8(it->second)) ));
else if (boost::iequals(it->first, "album"))
- items.insert( std::make_pair(MetaData::Type::Album, string_trim( string_to_utf8(it->second)) ));
+ items.insert( std::make_pair(MetaData::Type::Album, stringTrim( stringToUTF8(it->second)) ));
else if (boost::iequals(it->first, "title"))
- items.insert( std::make_pair(MetaData::Type::Title, string_trim( string_to_utf8(it->second)) ));
- else if (boost::iequals(it->first, "track")) {
- std::size_t number;
- if (readAs(it->second, number))
- items.insert( std::make_pair(MetaData::Type::TrackNumber, number ));
+ items.insert( std::make_pair(MetaData::Type::Title, stringTrim( stringToUTF8(it->second)) ));
+ else if (boost::iequals(it->first, "track"))
+ {
+ // Expecting 'Number/Total'
+ auto strings = splitString(it->second, "/");
+
+ if (strings.size() > 0)
+ {
+ std::size_t number;
+ if (readAs(strings[0], number))
+ items.insert( std::make_pair(MetaData::Type::TrackNumber, number ));
+
+ if (strings.size() > 1)
+ {
+ std::size_t totalNumber;
+ if (readAs(strings[1], totalNumber))
+ items.insert( std::make_pair(MetaData::Type::TotalTrack, totalNumber ));
+ }
+ }
}
else if (boost::iequals(it->first, "disc"))
{
- std::size_t number;
- if (readAs(it->second, number))
- items.insert( std::make_pair(MetaData::Type::DiscNumber, number ));
+ // Expecting 'Number/Total'
+ auto strings = splitString(it->second, "/");
+
+ if (strings.size() > 0)
+ {
+ std::size_t number;
+ if (readAs(strings[0], number))
+ items.insert( std::make_pair(MetaData::Type::DiscNumber, number ));
+
+ if (strings.size() > 1)
+ {
+ std::size_t totalNumber;
+ if (readAs(strings[1], totalNumber))
+ items.insert( std::make_pair(MetaData::Type::TotalDisc, totalNumber ));
+ }
+ }
}
else if (boost::iequals(it->first, "date")
|| boost::iequals(it->first, "year")
@@ -145,18 +169,21 @@ AvFormat::parse(const boost::filesystem::path& p, Items& items)
}
else if (boost::iequals(it->first, "genre"))
{
+ // TODO use splitStrings
std::list genres;
if (readList(it->second, ";,", genres))
items.insert( std::make_pair(MetaData::Type::Genres, genres));
}
- else if (boost::iequals(it->first, "MusicBrainz Artist Id"))
+ else if (boost::iequals(it->first, "MusicBrainz Artist Id")
+ || boost::iequals(it->first, "MUSICBRAINZ_ARTISTID"))
{
- items.insert( std::make_pair(MetaData::Type::MusicBrainzArtistID, string_trim( string_to_utf8(it->second)) ));
+ items.insert( std::make_pair(MetaData::Type::MusicBrainzArtistID, stringTrim( stringToUTF8(it->second)) ));
}
- else if (boost::iequals(it->first, "MusicBrainz Album Id"))
+ else if (boost::iequals(it->first, "MusicBrainz Album Id")
+ || boost::iequals(it->first, "MUSICBRAINZ_ALBUMID"))
{
- items.insert( std::make_pair(MetaData::Type::MusicBrainzAlbumID, string_trim( string_to_utf8(it->second)) ));
+ items.insert( std::make_pair(MetaData::Type::MusicBrainzAlbumID, stringTrim( stringToUTF8(it->second)) ));
}
}
diff --git a/src/metadata/AvFormat.hpp b/src/metadata/AvFormat.hpp
index da5981b7..4c08d943 100644
--- a/src/metadata/AvFormat.hpp
+++ b/src/metadata/AvFormat.hpp
@@ -25,7 +25,7 @@
namespace MetaData
{
-// Implements AVFORMAT library
+// Parse that makes use of AvFormat
class AvFormat : public Parser
{
public:
diff --git a/src/metadata/MetaData.hpp b/src/metadata/MetaData.hpp
index 7548bb68..fdb65d04 100644
--- a/src/metadata/MetaData.hpp
+++ b/src/metadata/MetaData.hpp
@@ -37,6 +37,8 @@ namespace MetaData
Duration, // boost::posix_time::time_duration
TrackNumber, // size_t
DiscNumber, // size_t
+ TotalTrack, // size_t
+ TotalDisc, // size_t
Date, // boost::posix_time::ptime
OriginalDate, // boost::posix_time::ptime
HasCover, // bool
diff --git a/src/ui/LmsApplication.cpp b/src/ui/LmsApplication.cpp
index 0011ed78..be39ef1f 100644
--- a/src/ui/LmsApplication.cpp
+++ b/src/ui/LmsApplication.cpp
@@ -23,7 +23,6 @@
#include
#include
#include
-#include
#include
#include
#include
@@ -100,6 +99,7 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env, Wt::Dbo::SqlConnecti
setTheme(bootstrapTheme);
useStyleSheet("css/lms.css");
+ useStyleSheet("resources/font-awesome/css/font-awesome.min.css");
// Add a resource bundle
messageResourceBundle().use(appRoot() + "templates");
@@ -142,6 +142,16 @@ Database::User::pointer CurrentUser()
return DbHandler().getCurrentUser();
}
+CoverResource* SessionCoverResource()
+{
+ return LmsApplication::instance()->getCoverResource();
+}
+
+TranscodeResource* SessionTranscodeResource()
+{
+ return LmsApplication::instance()->getTranscodeResource();
+}
+
void
LmsApplication::createFirstConnectionUI()
{
@@ -155,6 +165,8 @@ void
LmsApplication::createLmsUI()
{
_coverResource = new CoverResource(_db, root());
+ _transcodeResource = new TranscodeResource(_db, root());
+
DbHandler().getLogin().changed().connect(this, &LmsApplication::handleAuthEvent);
LmsAuth *authWidget = new LmsAuth();
@@ -191,9 +203,15 @@ LmsApplication::handleAuthEvent(void)
contentsStack->setOverflow(Wt::WContainerWidget::OverflowAuto);
contentsStack->addStyleClass("contents");
- // Setup a Left-aligned menu.
- Wt::WMenu *leftMenu = new Wt::WMenu(contentsStack);
- navigation->addMenu(leftMenu);
+ Wt::WMenu *menu = new Wt::WMenu(contentsStack);
+ navigation->addMenu(menu, Wt::AlignRight);
+
+ LineEdit *searchEdit = new LineEdit(500);
+ navigation->bindWidget("search", searchEdit);
+ searchEdit->setEmptyText("Search...");
+ searchEdit->addStyleClass("navbar-form navbar-nav");
+ searchEdit->setWidth(150);
+ // TODO add a span with a search icon
Audio *audio;
@@ -202,23 +220,18 @@ LmsApplication::handleAuthEvent(void)
else
audio = new Desktop::Audio();
- leftMenu->addItem("Audio", audio);
+ menu->addItem("Audio", audio);
#if defined HAVE_VIDEO
- leftMenu->addItem("Video", new VideoWidget());
+ menu->addItem("Video", new VideoWidget());
#endif
- leftMenu->addItem("Settings", new Settings::Settings());
-
- // Setup a Right-aligned menu.
- Wt::WMenu *rightMenu = new Wt::WMenu();
-
- navigation->addMenu(rightMenu, Wt::AlignRight);
+ menu->addItem("Settings", new Settings::Settings());
Wt::WPopupMenu *popup = new Wt::WPopupMenu();
popup->addItem("Logout");
Wt::WMenuItem *item = new Wt::WMenuItem( CurrentAuthUser().identity(Wt::Auth::Identity::LoginName) );
- item->setMenu(popup);
- rightMenu->addItem(item);
+ item->setMenu(popup);
+ menu->addItem(item);
popup->itemSelected().connect(std::bind([=] (Wt::WMenuItem* item)
{
@@ -226,17 +239,12 @@ LmsApplication::handleAuthEvent(void)
DbHandler().getLogin().logout();
}, std::placeholders::_1));
- // Add a Search control.
- LineEdit *searchEdit = new LineEdit(500);
- searchEdit->setEmptyText("Search...");
-
searchEdit->timedChanged().connect(std::bind([=] ()
{
// TODO: check which view is activated and search into it
audio->search(searchEdit->text().toUTF8());
}));
- navigation->addSearch(searchEdit, Wt::AlignLeft);
layout->addWidget(navigation);
layout->addWidget(contentsStack, 1);
diff --git a/src/ui/LmsApplication.hpp b/src/ui/LmsApplication.hpp
index b2946991..447c1db3 100644
--- a/src/ui/LmsApplication.hpp
+++ b/src/ui/LmsApplication.hpp
@@ -27,6 +27,7 @@
#include "database/DatabaseHandler.hpp"
#include "resource/CoverResource.hpp"
+#include "resource/TranscodeResource.hpp"
namespace UserInterface {
@@ -40,6 +41,7 @@ class LmsApplication : public Wt::WApplication
// Session application data
CoverResource* getCoverResource() { return _coverResource; }
+ TranscodeResource* getTranscodeResource() { return _transcodeResource; }
Database::Handler& getDbHandler() { return _db;}
private:
@@ -50,6 +52,7 @@ class LmsApplication : public Wt::WApplication
Database::Handler _db;
CoverResource* _coverResource;
+ TranscodeResource* _transcodeResource;
};
// Helpers to get session data
@@ -59,6 +62,9 @@ Wt::Dbo::Session& DboSession();
const Wt::Auth::User& CurrentAuthUser();
Database::User::pointer CurrentUser();
+CoverResource *SessionCoverResource();
+TranscodeResource *SessionTranscodeResource();
+
} // namespace UserInterface
#endif
diff --git a/src/ui/audio/AudioPlayer.cpp b/src/ui/audio/AudioPlayer.cpp
new file mode 100644
index 00000000..1d166635
--- /dev/null
+++ b/src/ui/audio/AudioPlayer.cpp
@@ -0,0 +1,338 @@
+/*
+ * Copyright (C) 2015 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+
+#include
+#include
+#include
+#include
+#include
+
+#include "common/InputRange.hpp"
+#include "logger/Logger.hpp"
+#include "utils/Utils.hpp"
+
+#include "LmsApplication.hpp"
+
+#include "AudioPlayer.hpp"
+
+namespace UserInterface {
+
+
+Av::Encoding
+AudioPlayer::getBestEncoding() const
+{
+ // TODO get the supported formats of the player and pick one
+ return Av::Encoding::MP3;
+}
+
+
+bool
+AudioPlayer::loadTrack(Database::Track::id_type trackId)
+{
+ // TODO reduce this scope
+ Wt::Dbo::Transaction transaction(DboSession());
+
+ Database::Track::pointer track = Database::Track::getById(DboSession(), trackId);
+ if (!track)
+ {
+ LMS_LOG(UI, INFO) << "No track found for id " << trackId;
+ return false;
+ }
+
+ Database::User::pointer user = CurrentUser();
+ if (!user)
+ {
+ LMS_LOG(UI, ERROR) << "No user found!";
+ return false;
+ }
+
+ Av::Encoding encoding;
+ switch (user->getAudioEncoding())
+ {
+ case Database::AudioEncoding::MP3: encoding = Av::Encoding::MP3; break;
+ case Database::AudioEncoding::OGA: encoding = Av::Encoding::OGA; break;
+ case Database::AudioEncoding::WEBMA: encoding = Av::Encoding::WEBMA; break;
+ case Database::AudioEncoding::AUTO:
+ default:
+ encoding = getBestEncoding();
+ break;
+ }
+
+ bindString("track", Wt::WString::fromUTF8(track->getName()));
+ bindString("artist", Wt::WString::fromUTF8(track->getArtist()->getName()));
+ _cover->setImageLink(SessionCoverResource()->getTrackUrl(trackId, 64));
+
+ std::string durationFormat = track->getDuration().total_seconds() < 3600 ? "%M:%S" : "%H:%M:%S";
+
+ _trackDuration->setText(durationToString(track->getDuration(), durationFormat));
+ _trackCurTime->setText(durationToString(boost::posix_time::seconds(0), durationFormat));
+
+ // Analyse track, select the best media stream
+ Av::MediaFile mediaFile(track->getPath());
+
+ if (!mediaFile.open() || !mediaFile.scan())
+ {
+ LMS_LOG(UI, ERROR) << "Cannot open file '" << track->getPath();
+ return false;
+ }
+
+
+ int audioBestStreamId = mediaFile.getBestStreamId(Av::Stream::Type::Audio);
+ std::vector streams;
+ if (audioBestStreamId != -1)
+ streams.push_back(audioBestStreamId);
+
+ this->doJavaScript("\
+ document.lms.audio.state = \"loaded\";\
+ document.lms.audio.seekbar.min = " + std::to_string(0) + ";\
+ document.lms.audio.seekbar.max = " + std::to_string(track->getDuration().total_seconds()) + ";\
+ document.lms.audio.seekbar.value = 0;\
+ document.lms.audio.seekbar.disabled = false;\
+ document.lms.audio.offset = 0;\
+ document.lms.audio.curTime = 0;\
+ ");
+
+ LMS_LOG(UI, DEBUG) << "Loading, URL = '" << SessionTranscodeResource()->getUrl(trackId, encoding, 0, streams) << "'";
+
+ //TODO, try to load everything in JS in order to prevent the WriteError bug?
+ _audio->pause();
+ _audio->clearSources();
+ _audio->addSource(SessionTranscodeResource()->getUrl(trackId, encoding, 0, streams));
+ _audio->setPreloadMode(Wt::WAudio::PreloadNone);
+ _audio->play();
+
+ return true;
+}
+
+AudioPlayer::AudioPlayer(Wt::WContainerWidget *parent)
+: Wt::WTemplate(parent)
+{
+ setTemplateText(Wt::WString::tr("wa-audio-player"));
+ addStyleClass("mediaplayer");
+
+ // TODO potential leak here
+ _audio = new Wt::WAudio();
+ _audio->setOptions(Wt::WAudio::Autoplay);
+ _audio->setPreloadMode(Wt::WAudio::PreloadNone);
+ bindWidget("audio", _audio);
+
+ _audio->ended().connect(std::bind([=] ()
+ {
+ _playbackEnded.emit();
+ }));
+
+ _cover = new Wt::WImage();
+ bindWidget("cover", _cover);
+ _cover->setImageLink(SessionCoverResource()->getUnknownTrackUrl(64));
+
+ InputRange *seekbar = new InputRange();
+ seekbar->addStyleClass("mediaplayer-seekbar");
+ bindWidget("seekbar", seekbar);
+
+ bindString("track", "Track");
+ bindString("artist", "Artist");
+
+ InputRange *volumeSlider = new InputRange();
+ volumeSlider->addStyleClass("mediaplayer-volume");
+ volumeSlider->setAttributeValue("orient", "vertical"); // firefox
+ bindWidget("volume", volumeSlider);
+
+ Wt::WText *repeatBtn = new Wt::WText(" ", Wt::XHTMLText);
+ repeatBtn->addStyleClass("mediaplayer-btn");
+ bindWidget("repeat", repeatBtn);
+ repeatBtn->clicked().connect(std::bind([=] ()
+ {
+ if (repeatBtn->hasStyleClass("mediaplayer-btn-active"))
+ {
+ repeatBtn->removeStyleClass("mediaplayer-btn-active");
+ _loop.emit(false);
+ }
+ else
+ {
+ repeatBtn->addStyleClass("mediaplayer-btn-active");
+ _loop.emit(true);
+ }
+ }));
+
+ Wt::WText *shuffleBtn = new Wt::WText(" ", Wt::XHTMLText);
+ shuffleBtn->addStyleClass("mediaplayer-btn");
+ bindWidget("shuffle", shuffleBtn);
+ shuffleBtn->clicked().connect(std::bind([=] ()
+ {
+ if (shuffleBtn->hasStyleClass("mediaplayer-btn-active"))
+ {
+ shuffleBtn->removeStyleClass("mediaplayer-btn-active");
+ _shuffle.emit(false);
+ }
+ else
+ {
+ shuffleBtn->addStyleClass("mediaplayer-btn-active");
+ _shuffle.emit(true);
+ }
+ }));
+
+ Wt::WText *prevBtn = new Wt::WText(" ", Wt::XHTMLText);
+ prevBtn->addStyleClass("mediaplayer-btn hidden-xs");
+ bindWidget("prev", prevBtn);
+ prevBtn->clicked().connect(std::bind([=] ()
+ {
+ _playPrevious.emit();
+ }));
+
+ Wt::WText *nextBtn = new Wt::WText(" ", Wt::XHTMLText);
+ nextBtn->addStyleClass("mediaplayer-btn");
+ bindWidget("next", nextBtn);
+ nextBtn->clicked().connect(std::bind([=] ()
+ {
+ _playNext.emit();
+ }));
+
+ Wt::WText *playPauseBtn = new Wt::WText(" ", Wt::XHTMLText);
+ playPauseBtn->addStyleClass("mediaplayer-btn");
+ bindWidget("play-pause", playPauseBtn);
+
+ _trackCurTime = new Wt::WText("--:--");
+ _trackCurTime->addStyleClass("hidden-xs badge mediaplayer-badge mediaplayer-current-duration");
+ bindWidget("curtime", _trackCurTime);
+
+ _trackDuration = new Wt::WText("--:--");
+ _trackDuration->addStyleClass("hidden-xs badge mediaplayer-badge mediaplayer-total-duration");
+ bindWidget("duration", _trackDuration);
+
+ this->doJavaScript(
+ "\
+ document.lms = {};\
+ document.lms.audio = {};\
+ document.lms.audio.audio = " + _audio->jsRef() + ";\
+ document.lms.audio.seekbar = " + seekbar->jsRef() +";\
+ document.lms.audio.volumeSlider = " + volumeSlider->jsRef() + ";\
+ document.lms.audio.curTimeText = " + _trackCurTime->jsRef() + ";\
+ document.lms.audio.playPause = " + playPauseBtn->jsRef() + ";\
+ \
+ document.lms.audio.offset = 0;\
+ document.lms.audio.curTime = 0;\
+ document.lms.audio.state = \"init\";\
+ document.lms.audio.volume = 1;\
+ \
+ document.lms.audio.seekbar.value = 0;\
+ document.lms.audio.seekbar.disabled = true;\
+ \
+ document.lms.audio.volumeSlider.min = 0;\
+ document.lms.audio.volumeSlider.max = 100;\
+ document.lms.audio.volumeSlider.value = 100;\
+ \
+ function updateUIPlaying() { \
+ var icon = document.lms.audio.playPause.getElementsByTagName(\"i\")[0]; \
+ icon.className = \"fa fa-pause fa-3x fa-fw\"; \
+ } \
+ \
+ function updateUIStopped() { \
+ var icon = document.lms.audio.playPause.getElementsByTagName(\"i\")[0]; \
+ icon.className = \"fa fa-play fa-3x fa-fw\"; \
+ } \
+ \
+ function durationToString(duration, displayHours) { \
+ var hours = parseInt( duration / 3600 ) % 24; \
+ var minutes = parseInt( duration / 60) % 60; \
+ var seconds = duration % 60; \
+ \
+ var res = \"\"; \
+ if (displayHours) \
+ res = (hours < 10 ? \"0\" + hours : hours) + \":\"; \
+ \
+ res += (minutes < 10 ? \"0\" + minutes : minutes) + \":\"; \
+ res += (seconds < 10 ? \"0\" + seconds : seconds); \
+ return res; \
+ } \
+ \
+ function updateUI() {\
+ document.lms.audio.curTimeText.innerHTML = durationToString(document.lms.audio.curTime, document.lms.audio.seekbar.max > 3600); \
+ document.lms.audio.seekbar.value = document.lms.audio.curTime;\
+ }\
+ \
+ var mouseDown = 0;\
+ function seekMouseDown(e) {\
+ ++mouseDown;\
+ }\
+ function seekMouseUp(e) {\
+ --mouseDown;\
+ }\
+ \
+ function seeking(e) {\
+ if (document.lms.audio.state == \"init\")\
+ return;\
+ \
+ document.lms.audio.curTimeText.innerHTML = durationToString(document.lms.audio.seekbar.value, document.lms.audio.seekbar.max > 3600); \
+ }\
+ \
+ function seek(e) {\
+ if (document.lms.audio.state == \"init\")\
+ return;\
+ \
+ document.lms.audio.audio.pause(); \
+ document.lms.audio.offset = parseInt(document.lms.audio.seekbar.value);\
+ document.lms.audio.curTime = document.lms.audio.seekbar.value;\
+ var audioSource = document.lms.audio.audio.getElementsByTagName(\"source\")[0];\
+ var src = audioSource.src;\
+ src = src.slice(0, src.lastIndexOf(\"=\") + 1);\
+ audioSource.src = src + document.lms.audio.seekbar.value;\
+ document.lms.audio.audio.load(); \
+ document.lms.audio.audio.play(); \
+ document.lms.audio.curTimeText.innerHTML = durationToString(document.lms.audio.curTime, document.lms.audio.seekbar.max > 3600); \
+ }\
+ \
+ function volumeChanged() {\
+ document.lms.audio.audio.volume = document.lms.audio.volumeSlider.value / 100;\
+ }\
+ \
+ function updateCurTime() {\
+ document.lms.audio.curTime = document.lms.audio.offset + ~~document.lms.audio.audio.currentTime; \
+ if (mouseDown == 0)\
+ updateUI();\
+ } \
+ \
+ function playPause() { \
+ if (document.lms.audio.state == \"init\") \
+ return;\
+ \
+ if (document.lms.audio.audio.paused) \
+ document.lms.audio.audio.play(); \
+ else \
+ document.lms.audio.audio.pause();\
+ \
+ }\
+ \
+ document.lms.audio.audio.addEventListener('timeupdate', updateCurTime); \
+ document.lms.audio.audio.addEventListener('playing', updateUIPlaying); \
+ document.lms.audio.audio.addEventListener('play', updateUIPlaying); \
+ document.lms.audio.audio.addEventListener('pause', updateUIStopped); \
+ document.lms.audio.audio.addEventListener('ended', updateUIStopped); \
+ document.lms.audio.seekbar.addEventListener('change', seek);\
+ document.lms.audio.seekbar.addEventListener('input', seeking);\
+ document.lms.audio.seekbar.addEventListener('mousedown', seekMouseDown);\
+ document.lms.audio.seekbar.addEventListener('mouseup', seekMouseUp);\
+ document.lms.audio.volumeSlider.addEventListener('input', volumeChanged);\
+ document.lms.audio.playPause.addEventListener('click', playPause);\
+ "
+ );
+}
+
+
+} // namespaceUserInterface
diff --git a/src/ui/audio/AudioPlayer.hpp b/src/ui/audio/AudioPlayer.hpp
new file mode 100644
index 00000000..371fe2b3
--- /dev/null
+++ b/src/ui/audio/AudioPlayer.hpp
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2015 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#pragma once
+
+#include
+#include
+
+#include "common/InputRange.hpp"
+#include "database/Types.hpp"
+#include "av/AvTranscoder.hpp"
+
+namespace UserInterface {
+
+class AudioPlayer : public Wt::WTemplate
+{
+ public:
+
+ AudioPlayer(Wt::WContainerWidget *parent = 0);
+
+ Av::Encoding getBestEncoding() const;
+ bool loadTrack(Database::Track::id_type trackId);
+
+ // Signals
+ Wt::Signal& playbackEnded() {return _playbackEnded;}
+ Wt::Signal& playNext() {return _playNext;}
+ Wt::Signal& playPrevious() {return _playPrevious;}
+ Wt::Signal& shuffle() {return _shuffle;}
+ Wt::Signal& loop() {return _loop;}
+
+ private:
+
+ // Signals
+ Wt::Signal _playbackEnded;
+ Wt::Signal _playNext;
+ Wt::Signal _playPrevious;
+ Wt::Signal _shuffle;
+ Wt::Signal _loop;
+
+ Wt::WAudio* _audio;
+ Wt::WText* _trackCurTime;
+ Wt::WText* _trackDuration;
+ Wt::WText* _trackName;
+ Wt::WImage* _cover;
+};
+
+} // namespace UserInterface
diff --git a/src/ui/audio/desktop/AudioMediaPlayer.cpp b/src/ui/audio/desktop/AudioMediaPlayer.cpp
deleted file mode 100644
index 39462fdb..00000000
--- a/src/ui/audio/desktop/AudioMediaPlayer.cpp
+++ /dev/null
@@ -1,292 +0,0 @@
-/*
- * Copyright (C) 2013 Emeric Poupon
- *
- * This file is part of LMS.
- *
- * LMS is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * LMS is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with LMS. If not, see .
- */
-
-#include
-#include
-#include
-#include
-#include
-#include
-
-#include "logger/Logger.hpp"
-
-#include "LmsApplication.hpp"
-
-#include "AudioMediaPlayer.hpp"
-
-namespace UserInterface {
-namespace Desktop {
-
-Wt::WMediaPlayer::Encoding
-AudioMediaPlayer::getBestEncoding()
-{
- // MP3 seems to be better supported everywhere
- return Wt::WMediaPlayer::MP3;
-}
-
-AudioMediaPlayer::AudioMediaPlayer(Wt::WContainerWidget *parent)
- : Wt::WContainerWidget(parent),
- _mediaResource(nullptr)
-{
- this->setStyleClass("mediaplayer");
-
- Wt::WVBoxLayout* mainLayout = new Wt::WVBoxLayout();
- this->setLayout(mainLayout);
-
- // Determine the encoding to be used
- {
- Wt::Dbo::Transaction transaction(DboSession());
- switch (CurrentUser()->getAudioEncoding())
- {
- case Database::AudioEncoding::MP3: _encoding = Wt::WMediaPlayer::MP3; break;
- case Database::AudioEncoding::WEBMA: _encoding = Wt::WMediaPlayer::WEBMA; break;
- case Database::AudioEncoding::OGA: _encoding = Wt::WMediaPlayer::OGA; break;
- case Database::AudioEncoding::FLA: _encoding = Wt::WMediaPlayer::FLA; break;
- case Database::AudioEncoding::AUTO:
- default:
- _encoding = getBestEncoding();
- }
- }
-
- LMS_LOG(UI, INFO) << "Audio player using encoding " << _encoding;
-
- // Current Media info
- Wt::WHBoxLayout *currentMediaLayout = new Wt::WHBoxLayout();
- mainLayout->addLayout(currentMediaLayout, 1);
-
- currentMediaLayout->addWidget( _mediaCover = new Wt::WImage());
- _mediaCover->setImageLink( LmsApplication::instance()->getCoverResource()->getUnknownTrackUrl(72));
- _mediaCover->setStyleClass("mediaplayer-current-cover");
-
- Wt::WVBoxLayout* mediaInfoLayout = new Wt::WVBoxLayout();
- currentMediaLayout->addLayout(mediaInfoLayout, 1);
-
- mediaInfoLayout->addWidget( _mediaTitle = new Wt::WText("---"));
- mediaInfoLayout->addWidget( _mediaArtistRelease = new Wt::WText("---"));
- _mediaTitle->setStyleClass("mediaplayer-current-track vertical-align");
- _mediaArtistRelease->setStyleClass("mediaplayer-current-artist vertical-align");
-
- // Time control
- Wt::WHBoxLayout *sliderLayout = new Wt::WHBoxLayout();
- mainLayout->addLayout(sliderLayout);
-
- sliderLayout->addWidget(_curTime = new Wt::WText("00:00:00"));
- sliderLayout->addWidget(_timeSlider = new Wt::WSlider( ), 1);
- sliderLayout->addWidget(_duration = new Wt::WText("00:00:00"));
- _timeSlider->setHeight(26); // Default is too big (50)
- _curTime->setStyleClass("vertical-align");
- _duration->setStyleClass("vertical-align");
-
- // Controls
- Wt::WHBoxLayout *controlsLayout = new Wt::WHBoxLayout();
- mainLayout->addLayout(controlsLayout);
-
-
- Wt::WPushButton *prevBtn = new Wt::WPushButton("<<");
- controlsLayout->addWidget(prevBtn);
- prevBtn->setStyleClass("mediaplayer-btn-controls");
-
- Wt::WContainerWidget *btnContainer = new Wt::WContainerWidget();;
-
- _playBtn = new Wt::WPushButton("Play");
- btnContainer->addWidget(_playBtn);
- _playBtn->setWidth(70);
- _playBtn->setStyleClass("mediaplayer-btn-controls");
-
- _pauseBtn = new Wt::WPushButton("Pause");
- btnContainer->addWidget(_pauseBtn);
- _pauseBtn->setWidth(70);
- _pauseBtn->setStyleClass("mediaplayer-btn-controls");
-
- controlsLayout->addWidget(btnContainer);
-
- Wt::WPushButton *nextBtn = new Wt::WPushButton(">>");
- controlsLayout->addWidget(nextBtn);
- nextBtn->setStyleClass("mediaplayer-btn-controls");
-
- _volumeSlider = new Wt::WSlider();
- _volumeSlider->setRange(0,100);
- _volumeSlider->setWidth(60); // Default is too big (150)
- _volumeSlider->setHeight(26); // Default is too big (50)
- _volumeSlider->setMinimumSize(50, Wt::WLength::Auto);
- controlsLayout->addWidget(_volumeSlider, 1);
-
- Wt::WPushButton *loop = new Wt::WPushButton("Loop");
- loop->setCheckable(true);
- loop->setStyleClass("btn-xs");
- loop->checked().connect(std::bind([=] () { _loop.emit( true ); }));
- loop->unChecked().connect(std::bind([=] () { _loop.emit( false ); }));
- controlsLayout->addWidget(loop);
-
- Wt::WPushButton *shuffle = new Wt::WPushButton("Shuffle");
- shuffle->setCheckable(true);
- shuffle->setStyleClass("btn-xs");
- shuffle->checked().connect(std::bind([=] () { _shuffle.emit( true ); }));
- shuffle->unChecked().connect(std::bind([=] () { _shuffle.emit( false );}));
- controlsLayout->addWidget(shuffle);
-
- _mediaPlayer = new Wt::WMediaPlayer( Wt::WMediaPlayer::Audio, btnContainer );
- _mediaPlayer->addSource( _encoding, "" );
- _mediaPlayer->ended().connect(this, &AudioMediaPlayer::handleTrackEnded);
-
- _mediaPlayer->setControlsWidget( 0 );
- _mediaPlayer->setButton(Wt::WMediaPlayer::Play, _playBtn);
- _mediaPlayer->setButton(Wt::WMediaPlayer::Pause, _pauseBtn);
-
- _mediaPlayer->timeUpdated().connect(this, &AudioMediaPlayer::handleTimeUpdated);
-
- _volumeSlider->setValue(_mediaPlayer->volume() * 100);
-
- nextBtn->clicked().connect(std::bind([=] ()
- {
- _mediaPlayer->stop();
- _playNext.emit();
- }));
-
- prevBtn->clicked().connect(std::bind([=] ()
- {
- _mediaPlayer->stop();
- _playPrevious.emit();
- }));
- _timeSlider->valueChanged().connect(this, &AudioMediaPlayer::handlePlayOffset);
- _timeSlider->setDisabled(true);
-
- _volumeSlider->sliderMoved().connect(this, &AudioMediaPlayer::handleVolumeSliderMoved);
-
-}
-
-void
-AudioMediaPlayer::loadPlayer(boost::filesystem::path filePath, Av::TranscodeParameters& parameters)
-{
- _currentFile = filePath;
- _currentParameters = parameters;
-
- _mediaPlayer->clearSources();
-
- if (_mediaResource)
- delete _mediaResource;
-
- _mediaResource = new AvConvTranscodeStreamResource( filePath, parameters, this );
-
- _mediaPlayer->addSource( getEncoding(), Wt::WLink(_mediaResource));
-
- // Auto play
- _mediaPlayer->play();
-}
-
-bool
-AudioMediaPlayer::load(Database::Track::id_type trackId)
-{
- Av::TranscodeParameters parameters;
-
- boost::filesystem::path path;
-
- boost::posix_time::time_duration duration;
-
- {
- Wt::Dbo::Transaction transaction(DboSession());
-
- Database::Track::pointer track = Database::Track::getById(DboSession(), trackId);
- if (!track)
- {
- LMS_LOG(UI, INFO) << "Cannot find track id " << trackId;
- return false;
- }
-
- path = track->getPath();
- parameters.setBitrate(Av::Stream::Type::Audio, CurrentUser()->getAudioBitrate() );
-
- duration = track->getDuration();
-
- _mediaTitle->setText ( Wt::WString::fromUTF8(track->getName()) );
- _mediaArtistRelease->setText ( Wt::WString::fromUTF8(track->getArtist()->getName()) + " - " + Wt::WString::fromUTF8(track->getRelease()->getName()) );
- _mediaCover->setImageLink( Wt::WLink (LmsApplication::instance()->getCoverResource()->getTrackUrl(trackId, 72)));
- }
-
- Av::MediaFile mediaFile(path);
-
- if (!mediaFile.open() || !mediaFile.scan())
- {
- LMS_LOG(UI, INFO) << "Cannot open file '" << path << "'";
- return false;
- }
-
- // It seems to be far better to manually map the streams
- // otherwise, some files may have to be fully transcoded to be played by browser...
- int audioBestStreamId = mediaFile.getBestStreamId(Av::Stream::Type::Audio);
- if (audioBestStreamId != -1)
- parameters.addStream(audioBestStreamId);
-
- Av::Encoding encoding;
- switch (_encoding)
- {
- case Wt::WMediaPlayer::MP3: encoding = Av::Encoding::MP3; break;
- case Wt::WMediaPlayer::FLA: encoding = Av::Encoding::FLA; break;
- case Wt::WMediaPlayer::OGA: encoding = Av::Encoding::OGA; break;
- case Wt::WMediaPlayer::WEBMA: encoding = Av::Encoding::WEBMA; break;
- default:
- encoding = Av::Encoding::MP3;
- }
- parameters.setEncoding(encoding);
-
- _timeSlider->setDisabled(false);
- _timeSlider->setRange(0, duration.total_seconds() );
- _timeSlider->setValue(0);
-
- _duration->setText( boost::posix_time::to_simple_string( duration ));
-
- loadPlayer(path, parameters);
-
- return true;
-}
-
-void
-AudioMediaPlayer::handlePlayOffset(int offsetSecs)
-{
- Av::TranscodeParameters parameters = _currentParameters;
-
- parameters.setOffset( boost::posix_time::seconds(offsetSecs) );
-
- loadPlayer(_currentFile, parameters);
-}
-
-void
-AudioMediaPlayer::handleTrackEnded(void)
-{
- LMS_LOG(UI, INFO) << "Track ended!";
- _playbackEnded.emit();
-}
-
-void
-AudioMediaPlayer::handleTimeUpdated(void)
-{
- boost::posix_time::time_duration currentTime ( boost::posix_time::seconds( _mediaPlayer->currentTime() + _currentParameters.getOffset().total_seconds()));
-
- _timeSlider->setValue( currentTime.total_seconds() );
- _curTime->setText( boost::posix_time::to_simple_string( currentTime) );
-}
-
-void
-AudioMediaPlayer::handleVolumeSliderMoved(int value)
-{
- _mediaPlayer->setVolume( value / 100. );
-}
-
-} // namespace Desktop
-} // namespace UserInterface
diff --git a/src/ui/audio/desktop/AudioMediaPlayer.hpp b/src/ui/audio/desktop/AudioMediaPlayer.hpp
deleted file mode 100644
index 4c3b4416..00000000
--- a/src/ui/audio/desktop/AudioMediaPlayer.hpp
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * Copyright (C) 2013 Emeric Poupon
- *
- * This file is part of LMS.
- *
- * LMS is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * LMS is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with LMS. If not, see .
- */
-
-#ifndef __AUDIO_MEDIA_PLAYER_HPP
-#define __AUDIO_MEDIA_PLAYER_HPP
-
-#include
-
-#include
-#include
-#include
-#include
-#include
-#include
-
-#include "database/Types.hpp"
-
-#include "av/AvTranscoder.hpp"
-#include "resource/AvConvTranscodeStreamResource.hpp"
-#include "resource/CoverResource.hpp"
-
-namespace UserInterface {
-namespace Desktop {
-
-class AudioMediaPlayer : public Wt::WContainerWidget
-{
- public:
- static Wt::WMediaPlayer::Encoding getBestEncoding();
-
- AudioMediaPlayer(Wt::WContainerWidget *parent = 0);
-
- // Load Media to be played
- bool load(Database::Track::id_type trackId);
-
- // Accessors
- Wt::WMediaPlayer::Encoding getEncoding() const { return _encoding; }
-
- // Signal slots
- Wt::Signal& playbackEnded() {return _playbackEnded;}
- Wt::Signal& playNext() {return _playNext;}
- Wt::Signal& playPrevious() {return _playPrevious;}
- Wt::Signal& shuffle() {return _shuffle;}
- Wt::Signal& loop() {return _loop;}
-
-
- private:
-
- void handlePlayOffset(int offsetSecs);
- void handleTrackEnded(void);
-
- void handleValueChanged(double);
- void handleTimeUpdated(void);
- void handleSliderMoved(int value);
-
- void handleVolumeSliderMoved(int value);
-
- void loadPlayer(boost::filesystem::path filePath, Av::TranscodeParameters& parameters);
-
- // Signals
- Wt::Signal _playbackEnded;
- Wt::Signal _playNext;
- Wt::Signal _playPrevious;
- Wt::Signal _shuffle;
- Wt::Signal _loop;
-
- // Core
- Wt::WMediaPlayer* _mediaPlayer;
- AvConvTranscodeStreamResource* _mediaResource;
- Wt::WMediaPlayer::Encoding _encoding;
-
- // Media info
- Wt::WImage* _mediaCover;
- Wt::WText* _mediaTitle;
- Wt::WText* _mediaArtistRelease;
-
- // Controls
- Wt::WPushButton* _playBtn;
- Wt::WPushButton* _pauseBtn;
- Wt::WSlider* _timeSlider;
- Wt::WSlider* _volumeSlider;
- Wt::WText* _curTime;
- Wt::WText* _duration;
-
- // Transcode
- boost::filesystem::path _currentFile;
- Av::TranscodeParameters _currentParameters;
-
-};
-
-} // namespace Desktop
-} // namespace UserInterface
-
-#endif
-
diff --git a/src/ui/audio/desktop/DesktopAudio.cpp b/src/ui/audio/desktop/DesktopAudio.cpp
index 7e898a78..efcd13c3 100644
--- a/src/ui/audio/desktop/DesktopAudio.cpp
+++ b/src/ui/audio/desktop/DesktopAudio.cpp
@@ -120,73 +120,72 @@ _playQueue(nullptr)
_playQueue = new PlayQueue();
// Playlist/PlayQueue
+ Wt::WContainerWidget* playQueueContainer = new Wt::WContainerWidget();
+ playQueueContainer->setStyleClass("playqueue");
+ Wt::WVBoxLayout* playQueueLayout = new Wt::WVBoxLayout();
+ playQueueContainer->setLayout(playQueueLayout);
+
+ playQueueLayout->addWidget( _playQueue, 1);
+
+ Wt::WHBoxLayout* playlistControls = new Wt::WHBoxLayout();
+
+ Wt::WPushButton *playlistBtn = new Wt::WPushButton("Playlist");
+ playlistBtn->setStyleClass("btn-sm btn-primary");
+ playlistControls->addWidget(playlistBtn);
+
+ // Playlist menu
+ {
+ Wt::WPopupMenu *popupMain = new Wt::WPopupMenu();
+
+ _popupMenuSave = new Wt::WPopupMenu();
+ popupMain->addMenu("Save", _popupMenuSave);
+
+ _popupMenuLoad = new Wt::WPopupMenu();
+ popupMain->addMenu("Load", _popupMenuLoad);
+
+ _popupMenuDelete = new Wt::WPopupMenu();
+ popupMain->addMenu("Delete", _popupMenuDelete);
+
+ playlistBtn->setMenu(popupMain);
+ }
+
+ Wt::WPushButton *upBtn = new Wt::WPushButton("UP");
+ upBtn->setStyleClass("btn-sm");
+ playlistControls->addWidget(upBtn);
+ Wt::WPushButton *downBtn = new Wt::WPushButton("DO");
+ downBtn->setStyleClass("btn-sm");
+ playlistControls->addWidget(downBtn);
+ Wt::WPushButton *delBtn = new Wt::WPushButton("DEL");
+ delBtn->setStyleClass("btn-sm btn-warning");
+ playlistControls->addWidget(delBtn);
+ Wt::WPushButton *clearBtn = new Wt::WPushButton("CLR");
+ clearBtn->setStyleClass("btn-sm btn-danger");
+ playlistControls->addWidget(clearBtn);
+
+ delBtn->clicked().connect(_playQueue, &PlayQueue::delSelected);
+ upBtn->clicked().connect(_playQueue, &PlayQueue::moveSelectedUp);
+ downBtn->clicked().connect(_playQueue, &PlayQueue::moveSelectedDown);
+ clearBtn->clicked().connect(_playQueue, &PlayQueue::delAll);
+
+ playQueueLayout->addLayout(playlistControls);
+
+ mainLayout->addWidget(playQueueContainer, 0, 0, 2, 1);
+
+ // Load the last known queue
+ playlistLoadToPlayqueue(CurrentQueuePlaylistName);
+
+ // Select the last known playing track
{
Wt::Dbo::Transaction transaction(DboSession());
-
- Wt::WContainerWidget* playQueueContainer = new Wt::WContainerWidget();
- playQueueContainer->setStyleClass("playqueue");
- Wt::WVBoxLayout* playQueueLayout = new Wt::WVBoxLayout();
- playQueueContainer->setLayout(playQueueLayout);
-
- _mediaPlayer = new AudioMediaPlayer();
- playQueueLayout->addWidget(_mediaPlayer);
-
- playQueueLayout->addWidget( _playQueue, 1);
-
- Wt::WHBoxLayout* playlistControls = new Wt::WHBoxLayout();
-
- Wt::WPushButton *playlistBtn = new Wt::WPushButton("Playlist");
- playlistBtn->setStyleClass("btn-sm btn-primary");
- playlistControls->addWidget(playlistBtn);
-
- // Playlist menu
- {
- Wt::WPopupMenu *popupMain = new Wt::WPopupMenu();
-
- _popupMenuSave = new Wt::WPopupMenu();
- popupMain->addMenu("Save", _popupMenuSave);
-
- _popupMenuLoad = new Wt::WPopupMenu();
- popupMain->addMenu("Load", _popupMenuLoad);
-
- _popupMenuDelete = new Wt::WPopupMenu();
- popupMain->addMenu("Delete", _popupMenuDelete);
-
- playlistBtn->setMenu(popupMain);
- }
-
- Wt::WPushButton *upBtn = new Wt::WPushButton("UP");
- upBtn->setStyleClass("btn-sm");
- playlistControls->addWidget(upBtn);
- Wt::WPushButton *downBtn = new Wt::WPushButton("DO");
- downBtn->setStyleClass("btn-sm");
- playlistControls->addWidget(downBtn);
- Wt::WPushButton *delBtn = new Wt::WPushButton("DEL");
- delBtn->setStyleClass("btn-sm btn-warning");
- playlistControls->addWidget(delBtn);
- Wt::WPushButton *clearBtn = new Wt::WPushButton("CLR");
- clearBtn->setStyleClass("btn-sm btn-danger");
- playlistControls->addWidget(clearBtn);
-
- delBtn->clicked().connect(_playQueue, &PlayQueue::delSelected);
- upBtn->clicked().connect(_playQueue, &PlayQueue::moveSelectedUp);
- downBtn->clicked().connect(_playQueue, &PlayQueue::moveSelectedDown);
- clearBtn->clicked().connect(_playQueue, &PlayQueue::delAll);
-
-
- playQueueLayout->addLayout(playlistControls);
-
- mainLayout->addWidget(playQueueContainer, 0, 0, 2, 1);
-
- // Load the last known queue
- playlistLoadToPlayqueue(CurrentQueuePlaylistName);
- // Select the last known playing track
_playQueue->select(CurrentUser()->getCurPlayingTrackPos());
}
+ _mediaPlayer = new AudioPlayer();
+ mainLayout->addWidget(_mediaPlayer, 2, 0, 1, 4);
+
mainLayout->setRowStretch(1, 1);
mainLayout->setRowResizable(0, true, Wt::WLength(250, Wt::WLength::Pixel));
- mainLayout->setColumnResizable(0, true);
+ mainLayout->setColumnResizable(0, true, Wt::WLength(400, Wt::WLength::Pixel));
// Double click on track
// Set the selected tracks to the play queue
@@ -510,7 +509,7 @@ Audio::playTrack(Track::id_type trackId, int pos)
CurrentUser().modify()->setCurPlayingTrackPos(pos);
}
- if (!_mediaPlayer->load(trackId))
+ if (!_mediaPlayer->loadTrack(trackId))
_playQueue->playNext();
}
diff --git a/src/ui/audio/desktop/DesktopAudio.hpp b/src/ui/audio/desktop/DesktopAudio.hpp
index f5ee34ec..ac7bda65 100644
--- a/src/ui/audio/desktop/DesktopAudio.hpp
+++ b/src/ui/audio/desktop/DesktopAudio.hpp
@@ -17,14 +17,13 @@
* along with LMS. If not, see .
*/
-#ifndef UI_AUDIO_DESKTOP_HPP
-#define UI_AUDIO_DESKTOP_HPP
+#pragma once
#include
#include
-#include "AudioMediaPlayer.hpp"
+#include "audio/AudioPlayer.hpp"
#include "TrackView.hpp"
#include "PlayQueue.hpp"
@@ -66,7 +65,7 @@ class Audio : public UserInterface::Audio
void handlePlaylistSelected(Wt::WString name);
- AudioMediaPlayer* _mediaPlayer;
+ AudioPlayer* _mediaPlayer;
TrackView* _trackView;
PlayQueue* _playQueue;
@@ -80,5 +79,3 @@ class Audio : public UserInterface::Audio
} // namespace Desktop
} // namespace UserInterface
-#endif
-
diff --git a/src/ui/audio/desktop/PlayQueue.cpp b/src/ui/audio/desktop/PlayQueue.cpp
index fb11cedd..294131a1 100644
--- a/src/ui/audio/desktop/PlayQueue.cpp
+++ b/src/ui/audio/desktop/PlayQueue.cpp
@@ -278,10 +278,8 @@ _trackSelector(new TrackSelector())
_model = new Wt::WStandardItemModel(0, 3, this);
// 0 Column is hidden (track id)
- _model->setHeaderData(COLUMN_ID_TRACK_ID, Wt::WString("#"));
- _model->setHeaderData(COLUMN_ID_COVER, Wt::WString("Cover"));
- _model->setHeaderData(COLUMN_ID_NAME, Wt::WString("Track"));
+ this->setHeaderHeight(0);
this->setModel(_model);
this->setSelectionMode(Wt::ExtendedSelection);
this->setSortingEnabled(false);
diff --git a/src/ui/audio/desktop/TrackView.cpp b/src/ui/audio/desktop/TrackView.cpp
index ebf2a432..d01a48ef 100644
--- a/src/ui/audio/desktop/TrackView.cpp
+++ b/src/ui/audio/desktop/TrackView.cpp
@@ -23,6 +23,7 @@
#include
#include "logger/Logger.hpp"
+#include "utils/Utils.hpp"
#include "LmsApplication.hpp"
#include "TrackView.hpp"
@@ -41,18 +42,10 @@ class DurationItemDelegate : public Wt::WItemDelegate
{
boost::posix_time::time_duration duration = boost::any_cast(index.data(Wt::DisplayRole));
- boost::posix_time::time_facet* facet = new boost::posix_time::time_facet();
+ // TODO bug when redrawn
+ std::string format = duration.total_seconds() < 3600 ? "%M:%S" : "%H:%M:%S";
- if (duration.total_seconds() < 3600)
- facet->time_duration_format("%M:%S");
- else
- facet->time_duration_format("%H:%M:%S");
-
- std::ostringstream oss;
- oss.imbue(std::locale(oss.getloc(), facet));
- oss << duration;
-
- return new Wt::WText(oss.str(), Wt::PlainText);
+ return new Wt::WText(durationToString(duration, format), Wt::PlainText);
}
};
diff --git a/src/ui/audio/mobile/ArtistSearch.cpp b/src/ui/audio/mobile/ArtistSearch.cpp
index f2dc41fb..35033792 100644
--- a/src/ui/audio/mobile/ArtistSearch.cpp
+++ b/src/ui/audio/mobile/ArtistSearch.cpp
@@ -20,6 +20,7 @@
#include
#include
#include
+#include
#include "LmsApplication.hpp"
@@ -38,6 +39,11 @@ _resCount(0)
title->setTemplateText(Wt::WString::tr("mobile-search-title"));
title->bindWidget("text", new Wt::WText("Artists", Wt::PlainText));
+
+ Wt::WTemplate* artistWrapper = new Wt::WTemplate(this);
+ artistWrapper->setTemplateText(Wt::WString::tr("wa-artist-wrapper"));
+ _contents = new Wt::WContainerWidget();
+ artistWrapper->bindWidget("contents", _contents );
}
void
@@ -75,7 +81,13 @@ ArtistSearch::addResults(Database::SearchFilter filter, std::size_t nb)
for (Artist::pointer artist : artists)
{
Wt::WTemplate* res = new Wt::WTemplate(this);
- res->setTemplateText(Wt::WString::tr("mobile-artist-res"));
+ res->setTemplateText(Wt::WString::tr("wa-artist-res"));
+
+ Wt::WImage *artistImg = new Wt::WImage();
+ artistImg->setStyleClass("center-block"); // TODO move in css?
+ artistImg->setStyleClass("release_res_shadow release_img-responsive"); // TODO move in css?
+
+ res->bindWidget("gif", artistImg);
Wt::WText *text = new Wt::WText(Wt::WString::fromUTF8(artist->getName()), Wt::PlainText);
res->bindWidget("name", text);
diff --git a/src/ui/audio/mobile/ArtistSearch.hpp b/src/ui/audio/mobile/ArtistSearch.hpp
index dbe361ca..f1218c9c 100644
--- a/src/ui/audio/mobile/ArtistSearch.hpp
+++ b/src/ui/audio/mobile/ArtistSearch.hpp
@@ -20,7 +20,6 @@
#ifndef UI_MOBILE_ARTIST_SEARCH_HPP
#define UI_MOBILE_ARTIST_SEARCH_HPP
-#include
#include
#include "database/Types.hpp"
@@ -48,6 +47,7 @@ class ArtistSearch : public Wt::WContainerWidget
void clear(void);
void addResults(Database::SearchFilter filter, size_t nb);
+ Wt::WContainerWidget* _contents;
std::size_t _resCount;
};
diff --git a/src/ui/audio/mobile/MobileAudio.cpp b/src/ui/audio/mobile/MobileAudio.cpp
index 5d910dd8..b5d0c66a 100644
--- a/src/ui/audio/mobile/MobileAudio.cpp
+++ b/src/ui/audio/mobile/MobileAudio.cpp
@@ -17,32 +17,58 @@
* along with LMS. If not, see .
*/
-#include
-#include
-#include
#include
#include
-#include
#include
-#include
+#include "logger/Logger.hpp"
+#include "utils/Utils.hpp"
+
+#include "audio/AudioPlayer.hpp"
#include "LmsApplication.hpp"
#include "MobileAudio.hpp"
-#include "ArtistSearch.hpp"
-#include "ReleaseSearch.hpp"
-#include "TrackSearch.hpp"
-#include "MobileAudioMediaPlayer.hpp"
-
-#include "logger/Logger.hpp"
+#define SEARCH_NB_ITEMS 4
namespace UserInterface {
namespace Mobile {
using namespace Database;
+static void playTrack(AudioPlayer *audioPlayer, Database::Track::id_type trackId)
+{
+ LMS_LOG(UI, DEBUG) << "Playing track id " << trackId;
+
+ Wt::Dbo::Transaction transaction(DboSession());
+
+ Track::pointer track = Track::getById(DboSession(), trackId);
+
+ if (track)
+ audioPlayer->loadTrack(track.id());
+}
+
+void
+Audio::search(std::string text)
+{
+ // When a new search is done, output some results from:
+ // Artist
+ // Release
+ // Song
+ auto keywords = splitString(text, " ");;
+
+ _releaseSearch->search(SearchFilter::ByNameAnd(SearchFilter::Field::Release, keywords), SEARCH_NB_ITEMS);
+ _artistSearch->search(SearchFilter::ByNameAnd(SearchFilter::Field::Artist, keywords), SEARCH_NB_ITEMS);
+ _trackSearch->search(SearchFilter::ByNameAnd(SearchFilter::Field::Track, keywords), SEARCH_NB_ITEMS);
+
+ _artistSearch->show();
+ _releaseSearch->show();
+ _trackSearch->show();
+ _trackReleaseView->hide();
+}
+
+
Audio::Audio(Wt::WContainerWidget *parent)
: UserInterface::Audio(parent)
{
@@ -50,145 +76,78 @@ Audio::Audio(Wt::WContainerWidget *parent)
this->setStyleClass("container-fluid");
this->setPadding(60, Wt::Bottom);
- Wt::WTemplate* search = new Wt::WTemplate(this);
- search->setTemplateText(Wt::WString::tr("mobile-search"));
+ _artistSearch = new ArtistSearch(this);
+ _releaseSearch = new ReleaseSearch(this);
+ _trackSearch = new TrackSearch(this);
- Wt::WLineEdit *edit = new Wt::WLineEdit();
- edit->setEmptyText("Search...");
-
- search->bindWidget("search", edit);
- search->setMargin(10);
-
- ArtistSearch* artistSearch = new ArtistSearch(this);
- ReleaseSearch* releaseSearch = new ReleaseSearch(this);
- TrackSearch* trackSearch = new TrackSearch(this);
+ _trackReleaseView = new TrackReleaseView(this);
+ _trackReleaseView->hide();
Wt::WTemplate* footer = new Wt::WTemplate(this);
footer->setTemplateText(Wt::WString::tr("mobile-audio-footer"));
- // Determine the encoding to be used
- Wt::WMediaPlayer::Encoding encoding;
+ AudioPlayer* audioPlayer = new AudioPlayer();
+ footer->bindWidget("player", audioPlayer);
+ _artistSearch->moreArtistsSelected().connect(std::bind([=]
{
- Wt::Dbo::Transaction transaction (DboSession());
-
- switch (CurrentUser()->getAudioEncoding())
- {
- case AudioEncoding::MP3: encoding = Wt::WMediaPlayer::MP3; break;
- case AudioEncoding::WEBMA: encoding = Wt::WMediaPlayer::WEBMA; break;
- case AudioEncoding::OGA: encoding = Wt::WMediaPlayer::OGA; break;
- case AudioEncoding::FLA: encoding = Wt::WMediaPlayer::FLA; break;
- case AudioEncoding::AUTO:
- default:
- encoding = AudioMediaPlayer::getBestEncoding();
- }
- }
-
- AudioMediaPlayer* mediaPlayer = new AudioMediaPlayer(encoding);
- footer->bindWidget("player", mediaPlayer);
-
- edit->changed().connect(std::bind([=] () {
- std::string text = edit->text().toUTF8();
-
- // When a new search is done, output some results from:
- // Artist
- // Release
- // Song
- std::vector keywords;
- boost::algorithm::split(keywords, text, boost::is_any_of(" "), boost::token_compress_on);
-
- releaseSearch->search(SearchFilter::NameLikeMatch( {{
- { SearchFilter::Field::Artist, keywords },
- { SearchFilter::Field::Release, keywords }}}),
- 3);
-
- artistSearch->search(SearchFilter::NameLikeMatch({{{SearchFilter::Field::Artist, keywords}}}),
- 3);
-
- trackSearch->search(SearchFilter::NameLikeMatch({{{SearchFilter::Field::Track, keywords}}}),
- 3);
-
- artistSearch->show();
- releaseSearch->show();
- trackSearch->show();
-
+ _releaseSearch->hide();
+ _trackSearch->hide();
+ _artistSearch->show();
+ _trackReleaseView->hide();
}));
- artistSearch->moreArtistsSelected().connect(std::bind([=]
+ _artistSearch->artistSelected().connect(std::bind([=] (Artist::id_type artistId)
{
- releaseSearch->hide();
- trackSearch->hide();
- artistSearch->show();
- }));
+ _artistSearch->hide();
+ _trackSearch->hide();
+ _releaseSearch->show();
+ _trackReleaseView->hide();
- artistSearch->artistSelected().connect(std::bind([=] (Artist::id_type artistId)
- {
- artistSearch->hide();
- trackSearch->hide();
- releaseSearch->show();
-
- releaseSearch->search(SearchFilter::ById(SearchFilter::Field::Artist, artistId), 20);
+ _releaseSearch->search(SearchFilter::ById(SearchFilter::Field::Artist, artistId), 20);
}, std::placeholders::_1));
- releaseSearch->moreReleasesSelected().connect(std::bind([=]
+ _releaseSearch->moreReleasesSelected().connect(std::bind([=]
{
- artistSearch->hide();
- trackSearch->hide();
- releaseSearch->show();
+ _artistSearch->hide();
+ _trackSearch->hide();
+ _releaseSearch->show();
+ _trackReleaseView->hide();
}));
- releaseSearch->releaseSelected().connect(std::bind([=] (Release::id_type releaseId)
+ _releaseSearch->releaseSelected().connect(std::bind([=] (Release::id_type releaseId)
{
- artistSearch->hide();
- releaseSearch->hide();
- trackSearch->show();
+ _artistSearch->hide();
+ _releaseSearch->hide();
+ _trackSearch->hide();
+ _trackReleaseView->show();
- trackSearch->search(SearchFilter::ById(SearchFilter::Field::Release, releaseId), 20);
+ _trackReleaseView->search(SearchFilter::ById(SearchFilter::Field::Release, releaseId), 40);
}, std::placeholders::_1));
- trackSearch->moreTracksSelected().connect(std::bind([=]
+ _trackSearch->moreSelected().connect(std::bind([=]
{
- artistSearch->hide();
- releaseSearch->hide();
+ _artistSearch->hide();
+ _releaseSearch->hide();
+ _trackSearch->show();
+ _trackReleaseView->hide();
}));
- trackSearch->trackPlay().connect(std::bind([=] (Track::id_type id)
+ _trackSearch->trackPlay().connect(std::bind([=] (Track::id_type id)
{
- LMS_LOG(UI, DEBUG) << "Playing track id " << id;
- // TODO reduce transaction scope here
- Wt::Dbo::Transaction transaction(DboSession());
+ playTrack(audioPlayer, id);
+ }, std::placeholders::_1));
- Track::pointer track = Track::getById(DboSession(), id);
-
- if (track)
- {
- Av::TranscodeParameters parameters;
-
- // Determine the output format using the encoding of the player
- Av::Encoding encoding;
- switch(mediaPlayer->getEncoding())
- {
- case Wt::WMediaPlayer::MP3: encoding = Av::Encoding::MP3; break;
- case Wt::WMediaPlayer::FLA: encoding = Av::Encoding::FLA; break;
- case Wt::WMediaPlayer::OGA: encoding = Av::Encoding::OGA; break;
- case Wt::WMediaPlayer::WEBMA: encoding = Av::Encoding::WEBMA; break;
- default:
- encoding = Av::Encoding::MP3;
- }
- parameters.setEncoding(encoding);
-
- // TODO compute parameters using user's profile
- parameters.setBitrate(Av::Stream::Type::Audio, 96000);
-
- mediaPlayer->play(track.id(), parameters);
- }
- } , std::placeholders::_1));
+ _trackReleaseView->trackPlay().connect(std::bind([=] (Track::id_type id)
+ {
+ playTrack(audioPlayer, id);
+ }, std::placeholders::_1));
// Initially, populate the widgets using an empty search
{
- artistSearch->search(SearchFilter(), 3);
- releaseSearch->search(SearchFilter(), 3);
- trackSearch->search(SearchFilter(), 3);
+ _artistSearch->search(SearchFilter(), SEARCH_NB_ITEMS);
+ _releaseSearch->search(SearchFilter(), SEARCH_NB_ITEMS);
+ _trackSearch->search(SearchFilter(), SEARCH_NB_ITEMS);
}
}
diff --git a/src/ui/audio/mobile/MobileAudio.hpp b/src/ui/audio/mobile/MobileAudio.hpp
index e10a3a57..a62e9951 100644
--- a/src/ui/audio/mobile/MobileAudio.hpp
+++ b/src/ui/audio/mobile/MobileAudio.hpp
@@ -24,6 +24,11 @@
#include "audio/Audio.hpp"
+#include "ArtistSearch.hpp"
+#include "ReleaseSearch.hpp"
+#include "TrackSearch.hpp"
+#include "TrackReleaseView.hpp"
+
namespace UserInterface {
namespace Mobile {
@@ -32,10 +37,14 @@ class Audio : public UserInterface::Audio
public:
Audio(Wt::WContainerWidget *parent = 0);
- void search(std::string text) {}
+ void search(std::string text);
private:
+ ArtistSearch* _artistSearch;
+ ReleaseSearch* _releaseSearch;
+ TrackSearch* _trackSearch;
+ TrackReleaseView* _trackReleaseView;
};
} // namespace Mobile
diff --git a/src/ui/audio/mobile/MobileAudioMediaPlayer.cpp b/src/ui/audio/mobile/MobileAudioMediaPlayer.cpp
deleted file mode 100644
index 2127567d..00000000
--- a/src/ui/audio/mobile/MobileAudioMediaPlayer.cpp
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * Copyright (C) 2015 Emeric Poupon
- *
- * This file is part of LMS.
- *
- * LMS is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * LMS is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with LMS. If not, see .
- */
-
-#include
-#include
-#include
-#include
-#include
-
-#include "LmsApplication.hpp"
-
-#include "MobileAudioMediaPlayer.hpp"
-
-#include "resource/AvConvTranscodeStreamResource.hpp"
-
-namespace UserInterface {
-namespace Mobile {
-
-Wt::WMediaPlayer::Encoding
-AudioMediaPlayer::getBestEncoding()
-{
- // MP3 seems to be better supported everywhere
- return Wt::WMediaPlayer::MP3;
-}
-
-AudioMediaPlayer::AudioMediaPlayer(Wt::WMediaPlayer::Encoding encoding, Wt::WContainerWidget *parent)
-:
- Wt::WContainerWidget(parent),
-_player(nullptr),
-_encoding(encoding)
-{
- Wt::WTemplate* container = new Wt::WTemplate(this);
- container->setTemplateText(Wt::WString::tr("mobile-audio-player"));
-
- Wt::WPushButton *play = new Wt::WPushButton("Play");
- play->setStyleClass("btn-sm");
- play->setWidth(52);
- container->bindWidget("play", play);
-
- Wt::WPushButton *pause = new Wt::WPushButton("Pause");
- pause->setStyleClass("btn-sm");
- pause->setWidth(52);
- container->bindWidget("pause", pause);
-
- _track = new Wt::WText("", Wt::PlainText);
- _track->setStyleClass("mobile-track");
- container->bindWidget("track", _track);
-
- _artistRelease = new Wt::WText("", Wt::PlainText);
- _artistRelease->setStyleClass("mobile-artist");
- container->bindWidget("artist", _artistRelease);
-
- _cover = new Wt::WImage();
- container->bindWidget("cover", _cover);
- _cover->setStyleClass("mobile-audio-player-cover");
-
-
- _player = new Wt::WMediaPlayer(Wt::WMediaPlayer::Audio, this);
- _player->addSource( _encoding, "" );
- _player->setControlsWidget( 0 );
- _player->setButton(Wt::WMediaPlayer::Play, play);
- _player->setButton(Wt::WMediaPlayer::Pause, pause);
-
-}
-
-void
-AudioMediaPlayer::play(Database::Track::id_type trackId, Av::TranscodeParameters parameters)
-{
- boost::filesystem::path path;
-
- {
- Wt::Dbo::Transaction transaction(DboSession());
-
- Database::Track::pointer track = Database::Track::getById(DboSession(), trackId);
-
- _track->setText( Wt::WString::fromUTF8(track->getName() ));
- _artistRelease->setText( Wt::WString::fromUTF8(track->getArtist()->getName()) );
- path = track->getPath();
- }
-
- // FIXME memleak here
- AvConvTranscodeStreamResource *resource = new AvConvTranscodeStreamResource( path, parameters, this );
-
- _player->clearSources();
- _player->addSource( _encoding, Wt::WLink(resource));
-
- _player->play();
-
- _cover->setImageLink( Wt::WLink (LmsApplication::instance()->getCoverResource()->getTrackUrl(trackId, 48)));
-
-}
-
-} // namespace UserInterface
-} // namespace Mobile
diff --git a/src/ui/audio/mobile/ReleaseSearch.cpp b/src/ui/audio/mobile/ReleaseSearch.cpp
index d9af0cab..fc9092e1 100644
--- a/src/ui/audio/mobile/ReleaseSearch.cpp
+++ b/src/ui/audio/mobile/ReleaseSearch.cpp
@@ -40,6 +40,11 @@ _resCount(0)
title->setTemplateText(Wt::WString::tr("mobile-search-title"));
title->bindWidget("text", new Wt::WText("Releases", Wt::PlainText));
+
+ Wt::WTemplate* releaseWrapper = new Wt::WTemplate(this);
+ releaseWrapper->setTemplateText(Wt::WString::tr("wa-release-wrapper"));
+ _contents = new Wt::WContainerWidget();
+ releaseWrapper->bindWidget("contents", _contents );
}
void
@@ -58,9 +63,23 @@ ReleaseSearch::search(Database::SearchFilter filter, size_t max)
addResults(filter, max);
}
+static Wt::WString
+getArtistFromRelease(Release::pointer release)
+{
+ auto artists = Artist::getByFilter(DboSession(),
+ SearchFilter::ById(SearchFilter::Field::Release, release.id()), -1, 2);
+
+ if (artists.size() > 1)
+ return Wt::WString::fromUTF8("Various artists", Wt::PlainText);
+ else
+ return Wt::WString::fromUTF8(artists.front()->getName(), Wt::PlainText);
+}
+
void
ReleaseSearch::addResults(Database::SearchFilter filter, size_t nb)
{
+ using namespace Database;
+
Wt::Dbo::Transaction transaction(DboSession());
std::vector releases = Release::getByFilter(DboSession(), filter, _resCount, nb + 1);
@@ -77,14 +96,17 @@ ReleaseSearch::addResults(Database::SearchFilter filter, size_t nb)
for (Release::pointer release : releases)
{
Wt::WTemplate* releaseWidget = new Wt::WTemplate(this);
- releaseWidget->setTemplateText(Wt::WString::tr("mobile-release-res"));
+ releaseWidget->setTemplateText(Wt::WString::tr("wa-release-res"));
Wt::WImage *cover = new Wt::WImage();
cover->setStyleClass("center-block");
- cover->setImageLink( Wt::WLink( LmsApplication::instance()->getCoverResource()->getReleaseUrl(release.id(), 56)));
- releaseWidget->bindWidget("cover", cover);
+ cover->setImageLink( Wt::WLink( LmsApplication::instance()->getCoverResource()->getReleaseUrl(release.id(), 512)));
+ cover->setStyleClass("release_res_shadow release_img-responsive"); // TODO move?
+ releaseWidget->bindWidget("cover", cover);
releaseWidget->bindWidget("name", new Wt::WText(Wt::WString::fromUTF8(release->getName()), Wt::PlainText));
+ releaseWidget->bindString("release_name", Wt::WString::fromUTF8(release->getName()), Wt::PlainText);
+ releaseWidget->bindString("artist", getArtistFromRelease(release));
releaseWidget->clicked().connect(std::bind([=] {
_sigReleaseSelected(release.id());
diff --git a/src/ui/audio/mobile/ReleaseSearch.hpp b/src/ui/audio/mobile/ReleaseSearch.hpp
index 876c1492..8c36c91c 100644
--- a/src/ui/audio/mobile/ReleaseSearch.hpp
+++ b/src/ui/audio/mobile/ReleaseSearch.hpp
@@ -20,8 +20,6 @@
#ifndef UI_MOBILE_RELEASE_SEARCH_HPP
#define UI_MOBILE_RELEASE_SEARCH_HPP
-#include
-
#include
#include
@@ -48,6 +46,7 @@ class ReleaseSearch : public Wt::WContainerWidget
void clear(void);
void addResults(Database::SearchFilter filter, size_t nb);
+ Wt::WContainerWidget* _contents;
std::size_t _resCount;
};
diff --git a/src/ui/audio/mobile/TrackReleaseView.cpp b/src/ui/audio/mobile/TrackReleaseView.cpp
new file mode 100644
index 00000000..73021f86
--- /dev/null
+++ b/src/ui/audio/mobile/TrackReleaseView.cpp
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2015 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#include
+#include
+#include
+#include
+
+#include "logger/Logger.hpp"
+#include "utils/Utils.hpp"
+#include "LmsApplication.hpp"
+
+#include "TrackReleaseView.hpp"
+
+namespace UserInterface {
+namespace Mobile {
+
+using namespace Database;
+
+TrackReleaseView::TrackReleaseView(Wt::WContainerWidget *parent)
+: Wt::WContainerWidget(parent)
+{
+ Wt::WTemplate* wrapper = new Wt::WTemplate(this);
+ wrapper->setTemplateText(Wt::WString::tr("wa-trackview-wrapper"));
+
+ Wt::WTemplate* title = new Wt::WTemplate(this);
+ wrapper->bindWidget("title", title);
+
+ title->setTemplateText(Wt::WString::tr("mobile-search-title"));
+ title->bindString("text", "Releases", Wt::PlainText);
+
+ _releaseContainer = new Wt::WContainerWidget();
+ wrapper->bindWidget("release-container", _releaseContainer);
+
+ _showMore = new Wt::WTemplate();
+ wrapper->bindWidget("show-more", _showMore);
+
+ _showMore->setTemplateText(Wt::WString::tr("mobile-search-more"));
+ _showMore->bindString("text", "Tap to show more results...");
+ _showMore->hide();
+ _showMore->clicked().connect(std::bind([=] {
+ _sigMoreTracksSelected();
+ addResults(20);
+ }));
+}
+
+void
+TrackReleaseView::clear()
+{
+ // Flush the release container
+ _releaseContainer->clear();
+
+ // Flush the current context
+ _currentTrackContainer = nullptr;
+ _nbTracks = 0;
+ _showMore->hide();
+}
+
+void
+TrackReleaseView::search(SearchFilter filter, size_t nb)
+{
+ _filter = filter;
+
+ clear();
+ addResults(nb);
+}
+
+static
+std::vector
+getTracks(SearchFilter filter, size_t offset, size_t nb, bool &moreResults)
+{
+ std::vector tracks = Track::getByFilter(DboSession(), filter, offset, nb + 1);
+
+ if (tracks.size() == nb + 1)
+ {
+ moreResults = true;
+ tracks.pop_back();
+ }
+ else
+ moreResults = false;
+
+ return tracks;
+}
+
+static Wt::WString
+getArtistNameFromRelease(Release::pointer release)
+{
+ auto artists = Artist::getByFilter(DboSession(),
+ SearchFilter::ById(SearchFilter::Field::Release, release.id()), -1, 2);
+
+ if (artists.size() > 1)
+ return Wt::WString::fromUTF8("Various artists", Wt::PlainText);
+ else
+ return Wt::WString::fromUTF8(artists.front()->getName(), Wt::PlainText);
+}
+
+void
+TrackReleaseView::addResults(size_t nb)
+{
+ Wt::Dbo::Transaction transaction(DboSession());
+
+ bool moreResults;
+ std::vector tracks = getTracks(_filter, _nbTracks, nb, moreResults);
+
+ for (Track::pointer track : tracks)
+ {
+ // First check if we need to create a new track container
+
+ // New container if it is the first one or if the release has changed
+ if (!_currentTrackContainer
+ || _currentReleaseId != track->getRelease().id())
+ {
+ Release::pointer release = track->getRelease();
+
+ Wt::WTemplate *releaseContainer = new Wt::WTemplate(_releaseContainer);
+ releaseContainer->setTemplateText(Wt::WString::tr("wa-trackview-release-container"));
+ _currentReleaseId = release.id();
+
+ Wt::WImage *cover = new Wt::WImage();
+ cover->setStyleClass ("center-block img-responsive"); // TODO move to CSS?
+ cover->setImageLink(Wt::WLink (LmsApplication::instance()->getCoverResource()->getReleaseUrl(release.id(), 512)));
+
+ releaseContainer->bindWidget("cover", cover);
+ releaseContainer->bindString("artist-name", getArtistNameFromRelease(release), Wt::PlainText);
+ releaseContainer->bindString("release-name", release->getName(), Wt::PlainText);
+ int year = release->getReleaseYear();
+ if (year > 0)
+ {
+ releaseContainer->setCondition("if-has-year", true);
+ releaseContainer->bindInt("year", year);
+
+ int originalYear = release->getReleaseYear(true);
+ if (originalYear > 0 && originalYear != year)
+ {
+ releaseContainer->setCondition("if-has-orig-year", true);
+ releaseContainer->bindInt("orig-year", originalYear);
+ }
+ }
+
+ _currentTrackContainer = new Wt::WContainerWidget();
+ releaseContainer->bindWidget("track-container", _currentTrackContainer);
+ }
+
+ assert(_currentTrackContainer != nullptr);
+
+ Wt::WTemplate* trackRes = new Wt::WTemplate(_currentTrackContainer);
+ trackRes->setTemplateText(Wt::WString::tr("wa-trackview-track"));
+
+ if (track->getTrackNumber() > 0 && !track->getRelease()->isNone())
+ {
+ trackRes->setCondition("if-has-track-num", true);
+ trackRes->bindInt("track-num", track->getTrackNumber());
+
+ if (track->getDiscNumber() > 0 && track->getTotalDiscNumber() > 1)
+ { trackRes->setCondition("if-has-disc-num", true);
+ trackRes->bindInt("disc-num", track->getDiscNumber());
+ }
+ }
+ trackRes->bindString("track-name", Wt::WString::fromUTF8(track->getName()), Wt::PlainText);
+ // TODO, display artist name for compilation releases?
+
+ // TODO handle large duration (> 1 hour)
+ trackRes->bindString("time", durationToString(track->getDuration(), "%M:%S"), Wt::PlainText);
+
+ Wt::WText *playBtn = new Wt::WText("Play", Wt::PlainText);
+ playBtn->setStyleClass("center-block"); // TODO move to CSS?
+ playBtn->clicked().connect(std::bind([=] {
+ _sigTrackPlay.emit(track.id());
+ }));
+
+ trackRes->bindWidget("btn", playBtn);
+ _nbTracks++;
+ }
+
+ if (moreResults)
+ _showMore->show();
+ else
+ _showMore->hide();
+}
+
+} // namespace Mobile
+} // namespace UserInterface
diff --git a/src/ui/audio/mobile/TrackReleaseView.hpp b/src/ui/audio/mobile/TrackReleaseView.hpp
new file mode 100644
index 00000000..5e49fec4
--- /dev/null
+++ b/src/ui/audio/mobile/TrackReleaseView.hpp
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2015 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#ifndef UI_MOBILE_TRACK_SEARCH_HPP
+#define UI_MOBILE_TRACK_SEARCH_HPP
+
+#include
+#include
+
+#include "resource/CoverResource.hpp"
+
+namespace UserInterface {
+namespace Mobile {
+
+class TrackReleaseView : public Wt::WContainerWidget
+{
+ public:
+
+ TrackReleaseView(Wt::WContainerWidget *parent = 0);
+
+ void search(Database::SearchFilter filter, size_t nb);
+
+ // Slots
+ Wt::Signal& trackPlay() { return _sigTrackPlay;}
+ Wt::Signal& moreTracksSelected() { return _sigMoreTracksSelected;}
+
+ private:
+
+ Wt::Signal _sigTrackPlay;
+ Wt::Signal _sigMoreTracksSelected;
+
+ void clear(void);
+ void addResults(size_t nb);
+
+ Wt::WTemplate* _showMore;
+
+ Database::SearchFilter _filter;
+ std::size_t _nbTracks;
+
+ // Main container that holds the releases
+ Wt::WContainerWidget* _releaseContainer;
+
+ // Used to add more results on the fly
+ Database::Release::id_type _currentReleaseId;
+ Wt::WContainerWidget* _currentTrackContainer;
+};
+
+} // namespace Mobile
+} // namespace UserInterface
+
+#endif
diff --git a/src/ui/audio/mobile/TrackSearch.cpp b/src/ui/audio/mobile/TrackSearch.cpp
index 0c1b9b94..23de7974 100644
--- a/src/ui/audio/mobile/TrackSearch.cpp
+++ b/src/ui/audio/mobile/TrackSearch.cpp
@@ -23,6 +23,7 @@
#include
#include "logger/Logger.hpp"
+#include "utils/Utils.hpp"
#include "LmsApplication.hpp"
#include "TrackSearch.hpp"
@@ -33,101 +34,103 @@ namespace Mobile {
using namespace Database;
TrackSearch::TrackSearch(Wt::WContainerWidget *parent)
-: Wt::WContainerWidget(parent),
-_resCount(0)
+: Wt::WContainerWidget(parent)
{
+ Wt::WTemplate* wrapper = new Wt::WTemplate(this);
+ wrapper->setTemplateText(Wt::WString::tr("wa-track-wrapper"));
+
Wt::WTemplate* title = new Wt::WTemplate(this);
+ wrapper->bindWidget("title", title);
+
title->setTemplateText(Wt::WString::tr("mobile-search-title"));
+ title->bindString("text", "Tracks", Wt::PlainText);
- title->bindWidget("text", new Wt::WText("Tracks", Wt::PlainText));
+ _container = new Wt::WContainerWidget();
+ wrapper->bindWidget("track-container", _container);
+ _showMore = new Wt::WTemplate();
+ wrapper->bindWidget("show-more", _showMore);
+
+ _showMore->setTemplateText(Wt::WString::tr("mobile-search-more"));
+ _showMore->bindString("text", "Tap to show more results...");
+ _showMore->hide();
+ _showMore->clicked().connect(std::bind([=] {
+ _sigMoreSelected();
+ addResults(20);
+ }));
}
void
TrackSearch::clear()
{
- while (count() > 1)
- removeWidget(this->widget(1));
+ // Flush the release container
+ _container->clear();
- _resCount = 0;
+ _nbTracks = 0;
+ _showMore->hide();
}
void
-TrackSearch::search(Database::SearchFilter filter, size_t max)
+TrackSearch::search(SearchFilter filter, size_t nb)
{
+ _filter = filter;
+
clear();
- addResults(filter, max);
+ addResults(nb);
}
-void
-TrackSearch::addResults(Database::SearchFilter filter, size_t nb)
+static
+std::vector
+getTracks(SearchFilter filter, size_t offset, size_t nb, bool &moreResults)
{
- Wt::Dbo::Transaction transaction(DboSession());
+ std::vector tracks = Track::getByFilter(DboSession(), filter, offset, nb + 1);
- std::vector tracks = Track::getByFilter(DboSession(), filter, _resCount, nb + 1);
-
- bool expectMoreResults;
if (tracks.size() == nb + 1)
{
- expectMoreResults = true;
+ moreResults = true;
tracks.pop_back();
}
else
- expectMoreResults = false;
+ moreResults = false;
+
+ return tracks;
+}
+
+void
+TrackSearch::addResults(size_t nb)
+{
+ Wt::Dbo::Transaction transaction(DboSession());
+
+ bool moreResults;
+ std::vector tracks = getTracks(_filter, _nbTracks, nb, moreResults);
for (Track::pointer track : tracks)
{
- Wt::WTemplate* trackWidget = new Wt::WTemplate(this);
- trackWidget->setTemplateText(Wt::WString::tr("mobile-track-res"));
+ Wt::WTemplate* trackRes = new Wt::WTemplate(_container);
+ trackRes->setTemplateText(Wt::WString::tr("wa-track"));
Wt::WImage *cover = new Wt::WImage();
- cover->setStyleClass("center-block");
- cover->setImageLink( Wt::WLink (LmsApplication::instance()->getCoverResource()->getTrackUrl(track.id(), 56)) );
- trackWidget->bindWidget("cover", cover);
+ cover->setStyleClass ("center-block img-responsive");
+ cover->setImageLink(Wt::WLink(LmsApplication::instance()->getCoverResource()->getTrackUrl(track.id(), 128)));
+ trackRes->bindWidget("cover", cover);
- // Track Name (bold)
- // Artist - Album (italic)
- Wt::WContainerWidget *container = new Wt::WContainerWidget();
- Wt::WText *title = new Wt::WText(Wt::WString::fromUTF8(track->getName()), Wt::PlainText);
- title->setStyleClass("mobile-track");
- container->addWidget(title);
+ trackRes->bindString("track-name", Wt::WString::fromUTF8(track->getName()), Wt::PlainText);
+ trackRes->bindString("artist-name", Wt::WString::fromUTF8(track->getArtist()->getName()), Wt::PlainText);
- if (!track->getArtist()->getName().empty()
- || !track->getRelease()->getName().empty())
- {
- title->setInline(false);
- Wt::WText *artistRelease = new Wt::WText(Wt::WString::fromUTF8(track->getArtist()->getName() + " - " + track->getRelease()->getName()), Wt::PlainText);
- artistRelease->setStyleClass("mobile-artist");
- container->addWidget(artistRelease);
- }
- trackWidget->bindWidget("name", container);
-
- Wt::WPushButton *playBtn = new Wt::WPushButton("Play");
- playBtn->setStyleClass("btn-primary center-block");
+ Wt::WText *playBtn = new Wt::WText("Play", Wt::PlainText);
+ playBtn->setStyleClass("center-block"); // TODO move to CSS?
playBtn->clicked().connect(std::bind([=] {
_sigTrackPlay.emit(track.id());
}));
- trackWidget->bindWidget("btn", playBtn);
-
+ trackRes->bindWidget("btn", playBtn);
+ _nbTracks++;
}
- _resCount += tracks.size();
-
- if (expectMoreResults)
- {
- Wt::WTemplate* moreRes = new Wt::WTemplate(this);
- moreRes->setTemplateText(Wt::WString::tr("mobile-search-more"));
-
- moreRes->bindWidget("text", new Wt::WText("Tap to show more results..."));
-
- moreRes->clicked().connect(std::bind([=] {
- _sigMoreTracksSelected();
- removeWidget(moreRes);
-
- addResults(filter, 20);
- }));
- }
+ if (moreResults)
+ _showMore->show();
+ else
+ _showMore->hide();
}
} // namespace Mobile
diff --git a/src/ui/audio/mobile/TrackSearch.hpp b/src/ui/audio/mobile/TrackSearch.hpp
index 6cf3b317..ca8c03d9 100644
--- a/src/ui/audio/mobile/TrackSearch.hpp
+++ b/src/ui/audio/mobile/TrackSearch.hpp
@@ -17,16 +17,11 @@
* along with LMS. If not, see .
*/
-#ifndef UI_MOBILE_TRACK_SEARCH_HPP
-#define UI_MOBILE_TRACK_SEARCH_HPP
-
-#include
+#pragma once
#include
#include
-#include "resource/CoverResource.hpp"
-
namespace UserInterface {
namespace Mobile {
@@ -40,20 +35,23 @@ class TrackSearch : public Wt::WContainerWidget
// Slots
Wt::Signal& trackPlay() { return _sigTrackPlay;}
- Wt::Signal& moreTracksSelected() { return _sigMoreTracksSelected;}
+ Wt::Signal& moreSelected() { return _sigMoreSelected;}
private:
Wt::Signal _sigTrackPlay;
- Wt::Signal _sigMoreTracksSelected;
+ Wt::Signal _sigMoreSelected;
void clear(void);
- void addResults(Database::SearchFilter filter, size_t nb);
+ void addResults(size_t nb);
- std::size_t _resCount;
+ Wt::WTemplate* _showMore;
+
+ Database::SearchFilter _filter;
+ std::size_t _nbTracks;
+ Wt::WContainerWidget* _container;
};
} // namespace Mobile
} // namespace UserInterface
-#endif
diff --git a/src/ui/common/InputRange.cpp b/src/ui/common/InputRange.cpp
new file mode 100644
index 00000000..3ff0da7b
--- /dev/null
+++ b/src/ui/common/InputRange.cpp
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2015 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#include
+
+#if WT_VERSION < 0X03030500
+#include
+#endif
+
+#include "InputRange.hpp"
+
+InputRange::InputRange(Wt::WContainerWidget *parent)
+: Wt::WWebWidget(parent)
+{
+ //TODO woraround for older Wt versions
+#if WT_VERSION >= 0X03030500
+ setHtmlTagName("input");
+ setAttributeValue("type", "range");
+#else
+ Wt::WTemplate *t = new Wt::WTemplate(" ", parent);
+#endif
+}
+
+
+std::string
+InputRange::jsRef(void) const
+{
+#if WT_VERSION >= 0X03030500
+ return Wt::WWebWidget::jsRef();
+#else
+ return this->jsRef() + ".getElementsByTagName(\"input\")[0]";
+#endif
+}
diff --git a/src/ui/common/InputRange.hpp b/src/ui/common/InputRange.hpp
new file mode 100644
index 00000000..7f03abb4
--- /dev/null
+++ b/src/ui/common/InputRange.hpp
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2015 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#pragma once
+
+#include
+#include
+
+class InputRange : public Wt::WWebWidget
+{
+ public:
+ InputRange(Wt::WContainerWidget *parent = 0);
+
+ Wt::DomElementType domElementType() const
+ {
+ return Wt::DomElement_INPUT;
+ }
+
+ std::string jsRef() const;
+};
+
diff --git a/src/ui/resource/AvConvTranscodeStreamResource.hpp b/src/ui/resource/AvConvTranscodeStreamResource.hpp
index 7ee088c5..b6fbbb94 100644
--- a/src/ui/resource/AvConvTranscodeStreamResource.hpp
+++ b/src/ui/resource/AvConvTranscodeStreamResource.hpp
@@ -42,7 +42,7 @@ class AvConvTranscodeStreamResource : public Wt::WResource
boost::filesystem::path _filePath;
Av::TranscodeParameters _parameters;
- static const std::size_t _bufferSize = 8192*16;
+ static const std::size_t _bufferSize = 8192;
};
} // namespace UserInterface
diff --git a/src/ui/resource/CoverResource.cpp b/src/ui/resource/CoverResource.cpp
index 885ad94b..54e03972 100644
--- a/src/ui/resource/CoverResource.cpp
+++ b/src/ui/resource/CoverResource.cpp
@@ -21,6 +21,7 @@
#include
#include "logger/Logger.hpp"
+#include "LmsApplication.hpp"
#include "cover/CoverArtGrabber.hpp"
@@ -36,7 +37,7 @@ _db(db)
{
}
-CoverResource:: ~CoverResource()
+CoverResource::~CoverResource()
{
beingDeleted();
}
@@ -121,7 +122,7 @@ CoverResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Respons
{
// transactions are not thread safe
- std::unique_lock lock(_mutex);
+ Wt::WApplication::UpdateLock lock(LmsApplication::instance());
Wt::Dbo::Transaction transaction(_db.getSession());
@@ -147,8 +148,9 @@ CoverResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Respons
else if (releaseIdStr)
{
Database::Release::id_type releaseId = std::stol(*releaseIdStr);
+
// transactions are not thread safe
- std::unique_lock lock(_mutex);
+ Wt::WApplication::UpdateLock lock(LmsApplication::instance());
Wt::Dbo::Transaction transaction(_db.getSession());
covers = CoverArt::Grabber::instance().getFromRelease(_db.getSession(), releaseId);
diff --git a/src/ui/resource/CoverResource.hpp b/src/ui/resource/CoverResource.hpp
index 82fad5d4..9a8ee139 100644
--- a/src/ui/resource/CoverResource.hpp
+++ b/src/ui/resource/CoverResource.hpp
@@ -41,6 +41,7 @@ class CoverResource : public Wt::WResource
std::string getReleaseUrl(Database::Release::id_type releaseId, size_t size) const;
std::string getTrackUrl(Database::Track::id_type trackId, size_t size) const;
+ std::string getArtistUrl(Database::Artist::id_type artistId, size_t size) const;
std::string getUnknownTrackUrl(size_t size) const;
void handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response);
@@ -50,6 +51,7 @@ class CoverResource : public Wt::WResource
Image::Image getDefaultCover(std::size_t size);
void putCover(Wt::Http::Response& response, Image::Image image);
+ // Used to protect transactions since they are not thread safe
std::mutex _mutex;
Database::Handler& _db;
diff --git a/src/ui/resource/TranscodeResource.cpp b/src/ui/resource/TranscodeResource.cpp
new file mode 100644
index 00000000..8acef15c
--- /dev/null
+++ b/src/ui/resource/TranscodeResource.cpp
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2015 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#include
+
+#include "logger/Logger.hpp"
+#include "LmsApplication.hpp"
+
+#include "TranscodeResource.hpp"
+
+namespace UserInterface {
+
+TranscodeResource::TranscodeResource(Database::Handler& db, Wt::WObject *parent)
+: Wt::WResource(parent),
+_db(db)
+{
+ LMS_LOG(UI, DEBUG) << "CONSTRUCTING RESOURCE";
+}
+
+TranscodeResource:: ~TranscodeResource()
+{
+ LMS_LOG(UI, DEBUG) << "DESTRUCTING RESOURCE";
+ beingDeleted();
+}
+
+std::string
+TranscodeResource::getUrl(Database::Track::id_type trackId, Av::Encoding encoding, std::size_t offset, std::vector streamIds) const
+{
+ std::string res = url()+ "&trackid=" + std::to_string(trackId) + "&encoding=" + std::to_string(Av::encoding_to_int(encoding));
+ if (!streamIds.empty())
+ {
+ for (std::size_t streamId : streamIds)
+ res += "&stream=" + std::to_string(streamId);
+ }
+ res += "&offset=" + std::to_string(offset);
+ return res;
+}
+
+void
+TranscodeResource::handleRequest(const Wt::Http::Request& request,
+ Wt::Http::Response& response)
+{
+ // Retrieve parameters
+ const std::string *trackIdStr = request.getParameter("trackid");
+ const std::string *offsetStr = request.getParameter("offset");
+ const std::string *encodingStr = request.getParameter("encoding");
+ std::vector streams = request.getParameterValues ("stream");
+
+ LMS_LOG(UI, DEBUG) << "Handling new request...";
+
+ try
+ {
+ std::shared_ptr transcoder;
+
+ // First, see if this request is for a continuation
+ Wt::Http::ResponseContinuation *continuation = request.continuation();
+ if (continuation)
+ {
+ LMS_LOG(UI, DEBUG) << "Continuation! " << continuation ;
+ transcoder = boost::any_cast >(continuation->data());
+ if (!transcoder)
+ {
+ LMS_LOG(UI, ERROR) << "No transcoder set -> abort!";
+ return;
+ }
+ }
+ else
+ {
+ LMS_LOG(UI, DEBUG) << "No continuation yet";
+
+ if (!trackIdStr
+ || !offsetStr
+ || !encodingStr)
+ {
+ LMS_LOG(UI, ERROR) << "Missing transcode parameter";
+ return;
+ }
+
+ Database::Track::id_type trackId = std::stol(*trackIdStr);
+
+ // transactions are not thread safe
+ {
+ Wt::WApplication::UpdateLock lock(LmsApplication::instance());
+
+ Wt::Dbo::Transaction transaction(_db.getSession());
+
+ Database::User::pointer user = _db.getCurrentUser();
+ Database::Track::pointer track = Database::Track::getById(_db.getSession(), trackId);
+
+ if (!track)
+ {
+ LMS_LOG(UI, ERROR) << "Missing track";
+ return;
+ }
+
+ if (!user)
+ {
+ LMS_LOG(UI, ERROR) << "Missing user";
+ return;
+ }
+
+ Av::TranscodeParameters parameters;
+
+ parameters.setOffset(boost::posix_time::seconds(std::stol(*offsetStr)));
+ parameters.setEncoding(Av::encoding_from_int(std::stol(*encodingStr)));
+ parameters.setBitrate(Av::Stream::Type::Audio, user->getAudioBitrate() );
+ for (std::string strStream: streams)
+ {
+ LMS_LOG(UI, DEBUG) << "Added stream " << std::stol(strStream);
+ parameters.addStream(std::stol(strStream));
+ }
+
+ LMS_LOG(UI, DEBUG) << "Offset set to " << parameters.getOffset();
+ transcoder = std::make_shared(track->getPath(), parameters);
+ }
+
+ std::string mimeType = Av::encoding_to_mimetype(transcoder->getParameters().getEncoding());
+
+ LMS_LOG(UI, DEBUG) << "Mime type set to '" << mimeType << "'";
+ response.setMimeType(mimeType);
+
+ if (!transcoder->start())
+ {
+ LMS_LOG(UI, ERROR) << "Cannot start transcoder";
+ return;
+ }
+
+ LMS_LOG(UI, DEBUG) << "Transcoder started";
+ }
+
+ if (!transcoder->isComplete())
+ {
+ std::vector data;
+ data.reserve(_chunkSize);
+
+ transcoder->process(data, _chunkSize);
+
+ response.out().write(reinterpret_cast(&data[0]), data.size());
+ LMS_LOG(UI, DEBUG) << "Written " << data.size() << " bytes! complete = " << std::boolalpha << transcoder->isComplete();
+
+ if (!response.out())
+ {
+ LMS_LOG(UI, ERROR) << "Write failed!";
+ }
+ }
+
+ if (!transcoder->isComplete() && response.out()) {
+ continuation = response.createContinuation();
+ continuation->setData(transcoder);
+ }
+ else
+ LMS_LOG(UI, DEBUG) << "No more data!";
+ }
+ catch (std::invalid_argument& e)
+ {
+ LMS_LOG(UI, ERROR) << "Invalid argument: " << e.what();
+ }
+}
+
+} // namespace UserInterface
+
+
diff --git a/src/ui/audio/mobile/MobileAudioMediaPlayer.hpp b/src/ui/resource/TranscodeResource.hpp
similarity index 55%
rename from src/ui/audio/mobile/MobileAudioMediaPlayer.hpp
rename to src/ui/resource/TranscodeResource.hpp
index cf970073..4ed7b4c5 100644
--- a/src/ui/audio/mobile/MobileAudioMediaPlayer.hpp
+++ b/src/ui/resource/TranscodeResource.hpp
@@ -17,43 +17,38 @@
* along with LMS. If not, see .
*/
-#ifndef UI_AUDIO_MOBILE_MEDIA_PLAYER_HPP
-#define UI_AUDIO_MOBILE_MEDIA_PLAYER_HPP
+#pragma once
-#include
-#include
-#include
+#include
+
+#include
-#include "database/DatabaseHandler.hpp"
#include "av/AvTranscoder.hpp"
-namespace UserInterface {
-namespace Mobile {
+#include "database/DatabaseHandler.hpp"
-class AudioMediaPlayer : public Wt::WContainerWidget
+namespace UserInterface {
+
+class TranscodeResource : public Wt::WResource
{
public:
+ TranscodeResource(Database::Handler& db, Wt::WObject *parent);
+ ~TranscodeResource();
- static Wt::WMediaPlayer::Encoding getBestEncoding();
+ std::string getUrl(Database::Track::id_type trackId, Av::Encoding encoding = Av::Encoding::OGA, size_t offset_secs = 0, std::vector streamIds = {}) const;
- AudioMediaPlayer(Wt::WMediaPlayer::Encoding encoding, Wt::WContainerWidget *parent = 0);
-
- void play(Database::Track::id_type trackId, Av::TranscodeParameters parameters);
-
- Wt::WMediaPlayer::Encoding getEncoding() const { return _encoding; }
+ void handleRequest(const Wt::Http::Request& request,
+ Wt::Http::Response& response);
private:
- Wt::WMediaPlayer* _player;
- Wt::WMediaPlayer::Encoding _encoding;
-
- Wt::WImage* _cover;
- Wt::WText* _track;
- Wt::WText* _artistRelease;
+ Database::Handler& _db;
+ static const std::size_t _chunkSize = 65536*4;
};
} // namespace UserInterface
-} // namespace Mobile
-#endif
+
+
+
diff --git a/src/ui/settings/SettingsAudioFormView.cpp b/src/ui/settings/SettingsAudioFormView.cpp
index d92312f0..93475c48 100644
--- a/src/ui/settings/SettingsAudioFormView.cpp
+++ b/src/ui/settings/SettingsAudioFormView.cpp
@@ -41,7 +41,6 @@ std::string encodingToString(Database::AudioEncoding encoding)
case Database::AudioEncoding::MP3: return "MP3";
case Database::AudioEncoding::OGA: return "OGG";
case Database::AudioEncoding::WEBMA: return "WebM";
- case Database::AudioEncoding::FLA: return "Flash";
}
return "?";
@@ -65,7 +64,7 @@ class AudioFormModel : public Wt::WFormModel
initializeModels();
addField(BitrateField);
- addField(EncodingField, "Session must be reloaded to apply this setting");
+ addField(EncodingField);
setValidator(BitrateField, new Wt::WValidator(true)); // mandatory
setValidator(EncodingField, new Wt::WValidator(true)); // mandatory
diff --git a/src/metadata/Utils.cpp b/src/utils/Utils.cpp
similarity index 65%
rename from src/metadata/Utils.cpp
rename to src/utils/Utils.cpp
index ff926363..517a9e4e 100644
--- a/src/metadata/Utils.cpp
+++ b/src/utils/Utils.cpp
@@ -21,12 +21,11 @@
#include
#include
-#include
+#include
+#include
#include "Utils.hpp"
-namespace MetaData
-{
bool readAsPosixTime(const std::string& str, boost::posix_time::ptime& time)
{
@@ -56,11 +55,11 @@ bool readList(const std::string& str, const std::string& separators, std::listtime_duration_format(format.c_str());
+ std::ostringstream oss;
+ oss.imbue(std::locale(oss.getloc(), facet));
+ oss << duration;
+ return oss.str();
}
+std::vector
+splitString(std::string string, std::string separators)
+{
+ std::vector res;
+
+ boost::algorithm::split(res, string, boost::is_any_of(separators), boost::token_compress_on);
+
+ return res;
+}
+
+std::string
+stringTrim(const std::string& str, const std::string& whitespace)
+{
+ const auto strBegin = str.find_first_not_of(whitespace);
+ if (strBegin == std::string::npos)
+ return ""; // no content
+
+ const auto strEnd = str.find_last_not_of(whitespace);
+ const auto strRange = strEnd - strBegin + 1;
+
+ return str.substr(strBegin, strRange);
+}
+
+std::string
+stringToUTF8(const std::string& str)
+{
+ return boost::locale::conv::to_utf(str, "UTF-8");
+}
+
+
diff --git a/src/metadata/Utils.hpp b/src/utils/Utils.hpp
similarity index 53%
rename from src/metadata/Utils.hpp
rename to src/utils/Utils.hpp
index 786e1a2b..5c9013a7 100644
--- a/src/metadata/Utils.hpp
+++ b/src/utils/Utils.hpp
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2013 Emeric Poupon
+ * Copyright (C) 2015 Emeric Poupon
*
* This file is part of LMS.
*
@@ -17,20 +17,32 @@
* along with LMS. If not, see .
*/
-#ifndef METADATA_UTILS_HPP
-#define METADATA_UTILS_HPP
+#pragma once
#include
+#include
#include
#include
+#include
-namespace MetaData
-{
+bool
+readAsPosixTime(const std::string& str, boost::posix_time::ptime& time);
-bool readAsPosixTime(const std::string& str, boost::posix_time::ptime& time);
+bool
+readList(const std::string& str, const std::string& separators, std::list& results);
-bool readList(const std::string& str, const std::string& separators, std::list& results);
+std::string
+durationToString(boost::posix_time::time_duration duration, std::string format);
+
+std::vector
+splitString(std::string string, std::string separators);
+
+std::string
+stringTrim(const std::string& str, const std::string& whitespace = " \t");
+
+std::string
+stringToUTF8(const std::string& str);
template
static inline bool readAs(const std::string& str, T& data)
@@ -40,30 +52,3 @@ static inline bool readAs(const std::string& str, T& data)
return !iss.fail();
}
-
-
-std::string
-static inline string_trim(const std::string& str,
- const std::string& whitespace = " \t")
-{
- const auto strBegin = str.find_first_not_of(whitespace);
- if (strBegin == std::string::npos)
- return ""; // no content
-
- const auto strEnd = str.find_last_not_of(whitespace);
- const auto strRange = strEnd - strBegin + 1;
-
- return str.substr(strBegin, strRange);
-}
-
-
-std::string
-static inline string_to_utf8(const std::string& str)
-{
- return boost::locale::conv::to_utf(str, "UTF-8");
-}
-
-} // namespace MetaData
-
-#endif
-
diff --git a/test/CheckDbBasics.cpp b/test/CheckDbBasics.cpp
index c993105b..716873a8 100644
--- a/test/CheckDbBasics.cpp
+++ b/test/CheckDbBasics.cpp
@@ -185,8 +185,6 @@ int main(void)
assert(res.front().id() == 1);
assert(res.front()->getName() == "release01");
- // Make sure release has an artist
- assert(res.front()->getArtists().size() == 1);
}
// Select genre by name
diff --git a/test/Makefile.am b/test/Makefile.am
index 15cea22f..8025dcab 100644
--- a/test/Makefile.am
+++ b/test/Makefile.am
@@ -1,7 +1,7 @@
TESTS = database-basics database-integrity sql-query database-user
-check_PROGRAMS = database-basics database-integrity sql-query database-user test-wt
+check_PROGRAMS = database-basics database-integrity sql-query database-user test-wt test-avmetadata test-avtranscoder test-wt-audio
database_basics_SOURCES = \
$(srcdir)/CheckDbBasics.cpp \
@@ -60,6 +60,48 @@ sql_query_CXXFLAGS=-std=c++11 -Wall -Wextra -I$(top_srcdir)/src
test_wt_SOURCES = TestWt.cpp
-
test_wt_CXXFLAGS=-std=c++11 -Wall -Wextra
+test_wt_audio_SOURCES = TestWtAudio.cpp\
+ $(top_srcdir)/src/logger/Logger.cpp \
+ $(top_srcdir)/src/utils/Utils.cpp \
+ $(top_srcdir)/src/metadata/AvFormat.cpp \
+ $(top_srcdir)/src/av/AvInfo.cpp \
+ $(top_srcdir)/src/av/AvTranscoder.cpp \
+ $(top_srcdir)/src/cover/CoverArtGrabber.cpp \
+ $(top_srcdir)/src/database/Artist.cpp \
+ $(top_srcdir)/src/database/Playlist.cpp \
+ $(top_srcdir)/src/database/Track.cpp \
+ $(top_srcdir)/src/database/DatabaseHandler.cpp \
+ $(top_srcdir)/src/database/MediaDirectory.cpp \
+ $(top_srcdir)/src/database/Release.cpp \
+ $(top_srcdir)/src/database/SearchFilter.cpp \
+ $(top_srcdir)/src/database/SqlQuery.cpp \
+ $(top_srcdir)/src/database/User.cpp \
+ $(top_srcdir)/src/database/Video.cpp \
+ $(top_srcdir)/src/image/Image.cpp \
+ $(top_srcdir)/src/ui/resource/CoverResource.cpp \
+ $(top_srcdir)/src/ui/resource/TranscodeResource.cpp
+
+test_wt_audio_CXXFLAGS=-std=c++11 -Wall -Wextra -I$(top_srcdir)/src $(MAGICKXX_CFLAGS)
+test_wt_audio_LDADD=$(MAGICKXX_LIBS)
+
+
+test_avmetadata_SOURCES = TestAvMetadata.cpp \
+ $(top_srcdir)/src/logger/Logger.cpp \
+ $(top_srcdir)/src/utils/Utils.cpp \
+ $(top_srcdir)/src/metadata/AvFormat.cpp \
+ $(top_srcdir)/src/av/AvInfo.cpp
+
+test_avmetadata_CXXFLAGS=-std=c++11 -Wall -Wextra -I$(top_srcdir)/src
+
+
+test_avtranscoder_SOURCES = TestAvTranscoder.cpp \
+ $(top_srcdir)/src/logger/Logger.cpp \
+ $(top_srcdir)/src/utils/Utils.cpp \
+ $(top_srcdir)/src/av/AvInfo.cpp \
+ $(top_srcdir)/src/av/AvTranscoder.cpp
+
+test_avtranscoder_CXXFLAGS=-std=c++11 -Wall -Wextra -I$(top_srcdir)/src
+
+
diff --git a/test/TestAvMetadata.cpp b/test/TestAvMetadata.cpp
new file mode 100644
index 00000000..ede3931d
--- /dev/null
+++ b/test/TestAvMetadata.cpp
@@ -0,0 +1,63 @@
+#include
+
+#include
+#include
+
+#include "av/AvInfo.hpp"
+#include "metadata/AvFormat.hpp"
+
+int main(int argc, char *argv[])
+{
+ if (argc != 2)
+ {
+ std::cerr << "Usage: " << std::endl;
+ return EXIT_FAILURE;
+ }
+
+ try
+ {
+ Av::AvInit();
+
+ MetaData::AvFormat parser;
+ MetaData::Items items;
+
+ if (!parser.parse(argv[1], items))
+ {
+ std::cout << "Parsing failed" << std::endl;
+ return EXIT_FAILURE;
+ }
+
+ for (auto item : items)
+ {
+ switch (item.first)
+ {
+ case MetaData::Type::TrackNumber:
+ std::cout << "Track: " << boost::any_cast(item.second) << std::endl;
+ break;
+
+ case MetaData::Type::TotalTrack:
+ std::cout << "TotalTrack: " << boost::any_cast(item.second) << std::endl;
+ break;
+
+ case MetaData::Type::DiscNumber:
+ std::cout << "Disc: " << boost::any_cast(item.second) << std::endl;
+ break;
+
+ case MetaData::Type::TotalDisc:
+ std::cout << "TotalDisc: " << boost::any_cast(item.second) << std::endl;
+
+ default:
+ break;
+ }
+ }
+
+ return EXIT_SUCCESS;
+ }
+ catch (std::exception& e)
+ {
+ std::cerr << "Caught exception: " << e.what();
+ return EXIT_FAILURE;
+ }
+
+}
+
diff --git a/test/TestAvTranscoder.cpp b/test/TestAvTranscoder.cpp
new file mode 100644
index 00000000..f5a7e932
--- /dev/null
+++ b/test/TestAvTranscoder.cpp
@@ -0,0 +1,55 @@
+#include
+
+#include
+#include
+
+#include "av/AvInfo.hpp"
+#include "av/AvTranscoder.hpp"
+
+int main(int argc, char *argv[])
+{
+ if (argc != 2)
+ {
+ std::cerr << "Usage: " << std::endl;
+ return EXIT_FAILURE;
+ }
+
+ try
+ {
+ Av::AvInit();
+ Av::Transcoder::init();
+
+ // Make pstream work with ffmpeg
+ close(STDIN_FILENO);
+
+ Av::TranscodeParameters parameters;
+ parameters.setEncoding(Av::Encoding::MP3);
+ parameters.setOffset( boost::posix_time::seconds(0) );
+ parameters.setBitrate( Av::Stream::Type::Audio, 160000 );
+// parameters.addStream(0);
+
+ Av::Transcoder transcoder(argv[1], parameters);
+
+ if (!transcoder.start())
+ throw std::runtime_error("transcoder.start failed!");
+
+ while (!transcoder.isComplete())
+ {
+ std::vector data;
+ std::cout << "Processing ..." << std::endl;
+ transcoder.process(data, 65536);
+ std::cout << "Processing done" << std::endl;
+ }
+
+ std::cout << "Complete!" << std::endl;
+
+ return EXIT_SUCCESS;
+ }
+ catch (std::exception& e)
+ {
+ std::cerr << "Caught exception: " << e.what();
+ return EXIT_FAILURE;
+ }
+
+}
+
diff --git a/test/TestWtAudio.cpp b/test/TestWtAudio.cpp
new file mode 100644
index 00000000..d53541c5
--- /dev/null
+++ b/test/TestWtAudio.cpp
@@ -0,0 +1,322 @@
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "database/DatabaseHandler.hpp"
+
+#include "ui/resource/TranscodeResource.hpp"
+#include "ui/resource/CoverResource.hpp"
+
+
+class InputRange : public Wt::WWebWidget
+{
+ public:
+ InputRange(Wt::WContainerWidget *parent = 0)
+ : Wt::WWebWidget(parent)
+ {
+ setHtmlTagName("input");
+ setAttributeValue("type", "range");
+ }
+
+ Wt::DomElementType domElementType() const
+ {
+ return Wt::DomElement_INPUT;
+ }
+};
+
+
+Wt::WString MyPlayerTemplate = "${shuffle} ${repeat} ${playlist} ${prev} ${play-pause} ${next} ${cover} ${artist} ${track} ${release} ${curtime} ${seekbar} ${duration} ${volume}";
+
+class MyPlayer : public Wt::WContainerWidget
+{
+ public:
+
+ void playbackComplete(void)
+ {
+ // Switch to the next track
+ }
+
+ void loadTrack(Database::Track::id_type trackId)
+ {
+ Wt::Dbo::Transaction transaction(_db.getSession());
+
+ Database::Track::pointer track = Database::Track::getById(_db.getSession(), trackId);
+ if (!track)
+ {
+ std::cerr << "no track for this id!" << std::endl;
+ return;
+ }
+
+ _trackName->setText(Wt::WString::fromUTF8(track->getName()));
+ _artistName->setText( Wt::WString::fromUTF8(track->getArtist()->getName()));
+ _releaseName->setText( Wt::WString::fromUTF8(track->getRelease()->getName()));
+ _cover->setImageLink(_coverResource->getTrackUrl(trackId, 64));
+ _trackDuration->setText( boost::posix_time::to_simple_string( track->getDuration() ));
+
+ // Analyse track, select the best media stream
+ Av::MediaFile mediaFile(track->getPath());
+
+ if (!mediaFile.open() || !mediaFile.scan())
+ {
+ std::cerr << "cannot open file '" << track->getPath() << std::endl;
+ return;
+ }
+
+
+ int audioBestStreamId = mediaFile.getBestStreamId(Av::Stream::Type::Audio);
+ std::vector streams;
+ if (audioBestStreamId != -1)
+ streams.push_back(audioBestStreamId);
+
+ this->doJavaScript("\
+ document.lms.audio.state = \"loaded\";\
+ document.lms.audio.seekbar.min = " + std::to_string(0) + ";\
+ document.lms.audio.seekbar.max = " + std::to_string(track->getDuration().total_seconds()) + ";\
+ document.lms.audio.seekbar.value = 0;\
+ document.lms.audio.seekbar.disabled = false;\
+ document.lms.audio.offset = 0;\
+ document.lms.audio.curTime = 0;\
+ ");
+
+ _audio->pause();
+ _audio->clearSources();
+ _audio->addSource(_transcodeResource->getUrl(trackId, Av::Encoding::MP3, 0, streams));
+ _audio->play();
+ }
+
+
+ MyPlayer(Database::Handler &db, Wt::WContainerWidget *parent = 0)
+ : Wt::WContainerWidget(parent),
+ _db(db)
+ {
+
+ _transcodeResource = new UserInterface::TranscodeResource(db, this);
+ _coverResource = new UserInterface::CoverResource(db, this);
+
+ Wt::WTemplate *t = new Wt::WTemplate(MyPlayerTemplate, this);
+
+ _audio = new Wt::WAudio(this);
+
+ _cover = new Wt::WImage();
+ t->bindWidget("cover", _cover);
+ _cover->setImageLink(_coverResource->getUnknownTrackUrl(64));
+
+ InputRange *seekbar = new InputRange();
+ t->bindWidget("seekbar", seekbar);
+
+ _trackName = new Wt::WText();
+ t->bindWidget("track", _trackName);
+
+ _artistName = new Wt::WText();
+ t->bindWidget("artist", _artistName);
+
+ _releaseName = new Wt::WText();
+ t->bindWidget("release", _releaseName);
+
+ InputRange *volumeSlider = new InputRange();
+ t->bindWidget("volume", volumeSlider);
+
+ Wt::WPushButton *playlistBtn = new Wt::WPushButton(" ", Wt::XHTMLText);
+ t->bindWidget("playlist", playlistBtn);
+
+ Wt::WPushButton *repeatBtn = new Wt::WPushButton(" ", Wt::XHTMLText);
+ t->bindWidget("repeat", repeatBtn);
+
+ Wt::WPushButton *shuffleBtn = new Wt::WPushButton(" ", Wt::XHTMLText);
+ t->bindWidget("shuffle", shuffleBtn);
+
+ Wt::WPushButton *prevBtn = new Wt::WPushButton(" ", Wt::XHTMLText);
+ t->bindWidget("prev", prevBtn);
+
+ Wt::WPushButton *nextBtn = new Wt::WPushButton(" ", Wt::XHTMLText);
+ t->bindWidget("next", nextBtn);
+
+ Wt::WPushButton *playPauseBtn = new Wt::WPushButton(" ", Wt::XHTMLText);
+ t->bindWidget("play-pause", playPauseBtn);
+
+ Wt::WText *trackCurrentTime = new Wt::WText("00:00");
+ t->bindWidget("curtime", trackCurrentTime);
+
+ _trackDuration = new Wt::WText("00:00");
+ t->bindWidget("duration", _trackDuration);
+
+ //Wt::WTemplate *_volumeSlider = new Wt::WTemplate(VolumeSliderTemplate, this);
+
+ this->doJavaScript(
+"\
+ document.lms = {};\
+ document.lms.audio = {};\
+ document.lms.audio.audio = " + _audio->jsRef() + ";\
+ document.lms.audio.seekbar = " + seekbar->jsRef() +";\
+ document.lms.audio.volumeSlider = " + volumeSlider->jsRef() + ";\
+ document.lms.audio.curTimeText = " + trackCurrentTime->jsRef() + ";\
+ document.lms.audio.playPause = " + playPauseBtn->jsRef() + ";\
+\
+ document.lms.audio.offset = 0;\
+ document.lms.audio.curTime = 0;\
+ document.lms.audio.state = \"init\";\
+ document.lms.audio.volume = 1;\
+\
+ document.lms.audio.seekbar.value = 0;\
+ document.lms.audio.seekbar.disabled = true;\
+\
+ document.lms.audio.volumeSlider.min = 0;\
+ document.lms.audio.volumeSlider.max = 100;\
+ document.lms.audio.volumeSlider.value = 100;\
+\
+ function updateUI() {\
+ document.lms.audio.curTimeText.innerHTML = document.lms.audio.curTime;\
+ document.lms.audio.seekbar.value = document.lms.audio.curTime;\
+ }\
+\
+ var mouseDown = 0;\
+ function seekMouseDown(e) {\
+ ++mouseDown;\
+ }\
+ function seekMouseUp(e) {\
+ --mouseDown;\
+ }\
+\
+ function seeking(e) {\
+ if (document.lms.audio.state == \"init\")\
+ return;\
+\
+ document.lms.audio.curTimeText.innerHTML = document.lms.audio.seekbar.value;\
+ }\
+\
+ function seek(e) {\
+ if (document.lms.audio.state == \"init\")\
+ return;\
+\
+ document.lms.audio.audio.pause(); \
+ document.lms.audio.offset = parseInt(document.lms.audio.seekbar.value);\
+ document.lms.audio.curTime = document.lms.audio.seekbar.value;\
+ var audioSource = document.lms.audio.audio.getElementsByTagName(\"source\")[0];\
+ var src = audioSource.src;\
+ src = src.slice(0, src.lastIndexOf(\"=\") + 1);\
+ audioSource.src = src + document.lms.audio.seekbar.value;\
+ document.lms.audio.audio.load(); \
+ document.lms.audio.audio.play(); \
+ document.lms.audio.curTimeText.innerHTML = ~~document.lms.audio.curTime + \" \";\
+ }\
+\
+ function volumeChanged() {\
+ document.lms.audio.audio.volume = document.lms.audio.volumeSlider.value / 100;\
+ }\
+\
+ function updateCurTime() {\
+ document.lms.audio.curTime = document.lms.audio.offset + ~~document.lms.audio.audio.currentTime; \
+ if (mouseDown == 0)\
+ updateUI();\
+ } \
+\
+ function playPause() {\
+ if (document.lms.audio.state == \"init\") \
+ return;\
+\
+ if (document.lms.audio.audio.paused)\
+ document.lms.audio.audio.play();\
+ else\
+ document.lms.audio.audio.pause();\
+\
+ }\
+\
+ document.lms.audio.audio.addEventListener('timeupdate', updateCurTime); \
+ document.lms.audio.seekbar.addEventListener('change', seek);\
+ document.lms.audio.seekbar.addEventListener('input', seeking);\
+ document.lms.audio.seekbar.addEventListener('mousedown', seekMouseDown);\
+ document.lms.audio.seekbar.addEventListener('mouseup', seekMouseUp);\
+ document.lms.audio.volumeSlider.addEventListener('input', volumeChanged);\
+ document.lms.audio.playPause.addEventListener('click', playPause);\
+"
+);
+ }
+
+ UserInterface::TranscodeResource* _transcodeResource;
+ UserInterface::CoverResource* _coverResource;
+ Database::Handler& _db;
+ Wt::WAudio* _audio;
+ Wt::WText* _trackDuration;
+ Wt::WText* _trackName;
+ Wt::WText* _artistName;
+ Wt::WText* _releaseName;
+ Wt::WImage* _cover;
+};
+
+class TestApplication : public Wt::WApplication
+{
+ public:
+ TestApplication(const Wt::WEnvironment& env, Wt::Dbo::SqlConnectionPool& connectionPool)
+ : Wt::WApplication(env)
+ , _db(connectionPool)
+ {
+ Wt::WBootstrapTheme *bootstrapTheme = new Wt::WBootstrapTheme(this);
+ bootstrapTheme->setVersion(Wt::WBootstrapTheme::Version3);
+ bootstrapTheme->setResponsive(true);
+ setTheme(bootstrapTheme);
+
+ useStyleSheet("resources/font-awesome/css/font-awesome.min.css");
+
+ Wt::WLineEdit* trackSelector = new Wt::WLineEdit();
+ MyPlayer* player = new MyPlayer(_db);
+
+ trackSelector->changed().connect(std::bind([=] {
+ player->loadTrack(Wt::asNumber(trackSelector->valueText()));
+ }));
+
+ root()->addWidget(trackSelector);
+ root()->addWidget(player);
+ }
+
+ private:
+ Database::Handler _db;
+
+};
+
+static Wt::WApplication *createTestApplication(const Wt::WEnvironment& env, Wt::Dbo::SqlConnectionPool& connectionPool )
+{
+ return new TestApplication(env, connectionPool);
+}
+
+int main(int argc, char *argv[])
+{
+ try
+ {
+ Av::AvInit();
+ Av::Transcoder::init();
+
+ Wt::WServer server(argv[0]);
+ server.setServerConfiguration (argc, argv);
+
+ // Make pstream work with ffmpeg
+ close(STDIN_FILENO);
+
+ Database::Handler::configureAuth();
+ std::unique_ptr connectionPool( Database::Handler::createConnectionPool("/var/lms/lms.db"));
+
+ server.addEntryPoint(Wt::Application, boost::bind(createTestApplication, _1, boost::ref(*connectionPool)));
+
+ server.start();
+
+ Wt::WServer::waitForShutdown(argv[0]);
+
+ server.stop();
+
+ return EXIT_SUCCESS;
+ }
+ catch (std::exception& e)
+ {
+ std::cerr << "Caught exception: " << e.what();
+ return EXIT_FAILURE;
+ }
+
+}
+