diff --git a/README.md b/README.md index 76a47db4..ff89fca0 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,15 @@ $ /usr/bin/lms --docroot='/usr/share/lms/docroot/;/resources,/css,/images' --app It is highly recommended to run LMS as a non root user. The exectuable needs write accesses to the /var/lms/ directory. +## Mobile + +Add the following code in your wt_config.xml file: +``` + + true + +``` + ## Setting up SSL materials (optional) Here is just a self signed certificate example, you could do use a CA if you want. @@ -70,4 +79,5 @@ Depending on your SSL parameters, you may be asked for the PEM passphrase to unl - Wt (http://www.webtoolkit.eu/) - bootstrap3 (http://getbootstrap.com/) -- libav project (https://www.libav.org/). +- libav project (https://www.libav.org/) +- Magick++ (http://www.imagemagick.org/Magick++/) diff --git a/TODO b/TODO index ab2db6e2..513e9f20 100644 --- a/TODO +++ b/TODO @@ -1,10 +1,10 @@ [Book] -- Make the feature +- Make the feature? [Cover] -- Handle preferred cover file names -- Implement a cache and a grabber from some web service (mandatory for artists) +- Handle preferred cover file names ("front.xxx", "cover.xxx", ...) +- Implement a cache and a grabber from some web service (mandatory for artists?) [Database] - Optim, use SQL query to get the "genre" orphans @@ -13,7 +13,6 @@ - add a global play counter for tracks. This will help people to spot most popular files - rework the exception process in av/metadata/updater in case of bad files - Use the WServer::post method to notify the end of the database scan? (with results?) -- Put more config information in the database, access from the UI [Metadata] - OGG metadata -> properly handle metadata nested in the audio stream @@ -30,9 +29,11 @@ - consider adding support for external web servers (new FCGI service?) [UI] - - handle internationalization + - Handle internationalization + - Handle internal path + - Cannot log again when refreshing the application in firefox (have to close/open a new browser tab) [Settings] - - logout users that are being changed (loss of admin admin rights), or make sure they are still admin when they make changes + - logout users that are being changed (loss of admin rights), or make sure they are still admin when they make changes - "signal not exposed" problem if a user logout and login again. Bad resource destruction? - add a scroll area in order for settings to work on mobile devices [user/transcoding] @@ -46,23 +47,28 @@ - refresh the audio/video view when the database directories have been updated [Audio] - - Style eveything nicely... - - MediaPlayer: move the slider using js (http://redmine.webtoolkit.eu/boards/2/topics/7924?r=8478, http://redmine.emweb.be/boards/2/topics/10994) - - TrackView: Reselect the current selected item when displaying the updated search results - - Add covers in the release filter? - - Add a download button to get the current playlist in a streamed zip file - - Add a upload button to upload media files in a dedicated directory - - Add keyboard shortcuts + - drop the winamp like interface? + - Add a download button to get the current playlist in a streamed zip file + - Add a upload button to upload media files in a dedicated directory + - Perform a search for each key pressed (using a timer, in JS) + - Save user prefs in local storage (volume, loop, shuffle, current playqueue, etc.) + [desktop] + - Better style... + - TrackView: Reselect the current selected item when displaying the updated search results + - Add keyboard shortcuts + [mobile] + - Reorganize the views to ease internal paths + - Covers not always the same height + - 'duration' and 'play' tags are broken if title is too long + - Cover loading breaks the vertical alignment + - Implement a play queue + - Implement a decent player + - Make the release/artist clickable when displayed to the user [Video] - implement a decent mediaplayer - remove codec choice from parameters - [Layout] - - Style eveything nicely... - - Make a dedicated audio layout in mobile environment (keep the same layout for videos?) - - Handle internal path - [REST API] - Make a dedicated REST API. Maybe use the SubSonic API or Ampache API? diff --git a/approot/templates.xml b/approot/templates.xml index 1e9d1148..411523a4 100644 --- a/approot/templates.xml +++ b/approot/templates.xml @@ -2,6 +2,18 @@ + +
+ ${collapse-button} + + ${contents} +
+
+ ${title}
@@ -419,29 +431,121 @@ - -
-
-
-
- -
+ +
+
+
+ ${contents}
+ +
+
${cover} +

${name}

${artist}

+
+
+ + +
+
+
+ ${contents} +
+
+
+
+ + +
+
${gif} +

${name}

+
+
+ + + ${title} + + ${show-more} + + + + + + + + + + ${title} + ${release-container} + ${show-more} + + + +
+
+
+
+ ${cover} +
+

${release-name}
${artist-name}${} - ${year}${}${} (${orig-year})${}

+
+ +
+
+ + + + + + + ${audio} + ${prev}${play-pause}${next} +
+ ${volume} +
+ +
+
+ ${track} ${artist} +
+
+ ${curtime}${seekbar}${duration} +
+
+ ${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; + } + +} +