diff --git a/src/Makefile.am b/src/Makefile.am index 5507f6e3..3a3aa96d 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -40,14 +40,18 @@ lms_SOURCES = \ $(srcdir)/transcode/InputMediaFile.cpp \ $(srcdir)/ui/LmsApplication.cpp \ $(srcdir)/ui/auth/LmsAuth.cpp \ - $(srcdir)/ui/audio/desktop/DesktopAudio.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 \ $(srcdir)/ui/audio/desktop/TableFilter.cpp \ $(srcdir)/ui/audio/desktop/TrackView.cpp \ - $(srcdir)/ui/audio/mobile/MobileAudio.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/TrackSearch.cpp \ $(srcdir)/ui/common/DirectoryValidator.cpp \ $(srcdir)/ui/common/LineEdit.cpp \ $(srcdir)/ui/common/SessionData.cpp \ diff --git a/src/cover/CoverArt.cpp b/src/cover/CoverArt.cpp index 473fe3b5..35b2c991 100644 --- a/src/cover/CoverArt.cpp +++ b/src/cover/CoverArt.cpp @@ -49,6 +49,10 @@ CoverArt::scale(std::size_t size) boost::gil::read_image(iss, source, boost::gil::jpeg_tag()); } + if (source.width() == static_cast(size) + && source.height() == static_cast(size)) + return true; + // Resize boost::gil::resize_view(boost::gil::const_view(source), boost::gil::view(dest), diff --git a/src/cover/CoverArt.hpp b/src/cover/CoverArt.hpp index ccca8741..4594841c 100644 --- a/src/cover/CoverArt.hpp +++ b/src/cover/CoverArt.hpp @@ -32,7 +32,7 @@ class CoverArt public: typedef std::vector data_type; - CoverArt(); + CoverArt() {} CoverArt(const std::string& mime, const data_type& data) : _mimeType(mime), _data(data) {} const std::string& getMimeType() const { return _mimeType; } diff --git a/src/cover/CoverArtGrabber.cpp b/src/cover/CoverArtGrabber.cpp index 0a3fce62..ba2ced72 100644 --- a/src/cover/CoverArtGrabber.cpp +++ b/src/cover/CoverArtGrabber.cpp @@ -72,8 +72,6 @@ Grabber::getFromTrack(const boost::filesystem::path& p) return res; } - - std::vector Grabber::getFromTrack(Database::Track::pointer track) { @@ -96,5 +94,25 @@ Grabber::getFromTrack(Database::Track::pointer track) return res; } +std::vector +Grabber::getFromRelease(Wt::Dbo::Session& session, std::string releaseName) +{ + using namespace Database; + + // For now, just return the embedded cover of the first track + SearchFilter filter; + filter.exactMatch[SearchFilter::Field::Release].push_back(releaseName); + + Wt::Dbo::collection tracks + = Track::getAll(session, filter, -1, 1 /* limit result size */); + + Wt::Dbo::collection::iterator it = tracks.begin(); + + if (it != tracks.end()) + return getFromTrack(*it); + else + return std::vector(); +} + } // namespace CoverArt diff --git a/src/cover/CoverArtGrabber.hpp b/src/cover/CoverArtGrabber.hpp index e6c809a6..43555b7b 100644 --- a/src/cover/CoverArtGrabber.hpp +++ b/src/cover/CoverArtGrabber.hpp @@ -37,6 +37,7 @@ class Grabber static std::vector getFromInputFormatContext(const Av::InputFormatContext& input); static std::vector getFromTrack(Database::Track::pointer track); static std::vector getFromTrack(const boost::filesystem::path& path); + static std::vector getFromRelease(Wt::Dbo::Session& session, std::string releaseName); }; } // namespace CoverArt diff --git a/src/database/Track.cpp b/src/database/Track.cpp index 3701cb89..28e8f459 100644 --- a/src/database/Track.cpp +++ b/src/database/Track.cpp @@ -186,7 +186,7 @@ Track::getAllQuery(Wt::Dbo::Session& session, SearchFilter filter) SqlQuery sqlQuery = generatePartialQuery(filter); Wt::Dbo::Query query - = session.query( "SELECT t FROM track t " + sqlQuery.innerJoin().get() + " " + sqlQuery.where().get()).groupBy("t.id"); + = session.query( "SELECT t FROM track t " + sqlQuery.innerJoin().get() + " " + sqlQuery.where().get()).groupBy("t.id").orderBy("t.artist_name,t.date,t.release_name,t.disc_number,t.track_number"); BOOST_FOREACH(const std::string& bindArg, sqlQuery.where().getBindArgs()) query.bind(bindArg); @@ -200,6 +200,14 @@ Track::getAll(Wt::Dbo::Session& session, SearchFilter filter, int offset, int si return getAllQuery(session, filter).limit(size).offset(offset); } +std::vector +Track::getTracks(Wt::Dbo::Session& session, SearchFilter filter, int offset, int size) +{ + Wt::Dbo::collection< Track::pointer > tracks = getAll(session, filter, offset, size); + + return std::vector(tracks.begin(), tracks.end()); +} + Wt::Dbo::Query Track::getReleasesQuery(Wt::Dbo::Session& session, SearchFilter filter) { @@ -264,7 +272,7 @@ Track::updateArtistQueryModel(Wt::Dbo::Session& session, Wt::Dbo::QueryModel& model, SearchFilter filter, const std::vector& columnNames) { - Wt::Dbo::Query< pointer > query = getAllQuery(session, filter).orderBy("t.artist_name,t.date,t.release_name,t.disc_number,t.track_number"); + Wt::Dbo::Query< pointer > query = getAllQuery(session, filter); model.setQuery(query, columnNames.empty() ? true : false); // TODO do something better diff --git a/src/database/Track.hpp b/src/database/Track.hpp index 12274838..524ab4ec 100644 --- a/src/database/Track.hpp +++ b/src/database/Track.hpp @@ -111,6 +111,7 @@ class Track static Wt::Dbo::collection< pointer > getAll(Wt::Dbo::Session& session); // Used for remote static Wt::Dbo::collection< pointer > getAll(Wt::Dbo::Session& session, SearchFilter filter, int offset = -1, int size = -1); + static std::vector getTracks(Wt::Dbo::Session& session, SearchFilter filter, int offset = -1, int size = -1); static std::vector getReleases(Wt::Dbo::Session& session, SearchFilter filter, int offset = -1, int size = -1); static std::vector getArtists(Wt::Dbo::Session& session, SearchFilter filter, int offset = -1, int size = -1); diff --git a/src/transcode/AvConvTranscoder.cpp b/src/transcode/AvConvTranscoder.cpp index 8700ed20..1b4dd815 100644 --- a/src/transcode/AvConvTranscoder.cpp +++ b/src/transcode/AvConvTranscoder.cpp @@ -70,7 +70,8 @@ AvConvTranscoder::AvConvTranscoder(const Parameters& parameters) _source(_outputPipe.source, boost::iostreams::close_handle), _is(_source), _in(&_is), - _isComplete(false) + _isComplete(false), + _outputBytes(0) { if (!boost::filesystem::exists(_parameters.getInputMediaFile().getPath())) { @@ -179,6 +180,7 @@ AvConvTranscoder::process(std::vector& output, std::size_t maxSiz while(readDataSize < maxSize && _in && _in.get(ch)) { output.push_back(ch); readDataSize++; + _outputBytes++; // stats } if (!_in || _in.fail() || _in.eof()) { diff --git a/src/transcode/AvConvTranscoder.hpp b/src/transcode/AvConvTranscoder.hpp index e3663809..2209b819 100644 --- a/src/transcode/AvConvTranscoder.hpp +++ b/src/transcode/AvConvTranscoder.hpp @@ -50,6 +50,7 @@ class AvConvTranscoder void process(std::vector& output, std::size_t maxSize); bool isComplete(void) const { return _isComplete;}; + std::size_t getOutputBytes(void) const { return _outputBytes; } private: @@ -72,6 +73,7 @@ class AvConvTranscoder static boost::filesystem::path _avConvPath; bool _isComplete; + std::size_t _outputBytes; // Bytes produced so far }; } // naspace Transcode diff --git a/src/transcode/Parameters.cpp b/src/transcode/Parameters.cpp index e129dbac..3d35591f 100644 --- a/src/transcode/Parameters.cpp +++ b/src/transcode/Parameters.cpp @@ -72,7 +72,7 @@ Parameters::getOutputBitrate(Stream::Type type) const { std::map::const_iterator it = _outputBitrate.find(type); - if (it != _inputStreams.end()) + if (it != _outputBitrate.end()) { return it->second; } diff --git a/src/ui/LmsApplication.cpp b/src/ui/LmsApplication.cpp index 052a1908..7aaaebc1 100644 --- a/src/ui/LmsApplication.cpp +++ b/src/ui/LmsApplication.cpp @@ -50,8 +50,11 @@ namespace { bool agentIsMobile() { const Wt::WEnvironment& env = Wt::WApplication::instance()->environment(); - - return (env.agentIsIEMobile() || env.agentIsMobileWebKit()); + return (env.agentIsIEMobile() + || env.agentIsMobileWebKit() + || env.userAgent().find("Mobile") != std::string::npos // Workaround for firefox + || env.userAgent().find("Tablet") != std::string::npos // Workaround for firefox + ); } } @@ -139,7 +142,7 @@ LmsApplication::handleAuthEvent(void) { Wt::Auth::User user(_sessionData.getDatabaseHandler().getLogin().user()); - LMS_LOG(MOD_UI, SEV_NOTICE) << "User '" << user.identity(Wt::Auth::Identity::LoginName) << "' logged in"; + LMS_LOG(MOD_UI, SEV_NOTICE) << "User '" << user.identity(Wt::Auth::Identity::LoginName) << "' logged in from '" << Wt::WApplication::instance()->environment().clientAddress() << "', user agent = " << Wt::WApplication::instance()->environment().agent() << ", session = " << Wt::WApplication::instance()->sessionId(); this->root()->setOverflow(Wt::WContainerWidget::OverflowHidden); @@ -164,9 +167,9 @@ LmsApplication::handleAuthEvent(void) Audio *audio; if (agentIsMobile()) - audio = new Desktop::Audio(_sessionData); + audio = new Mobile::Audio(_sessionData.getDatabaseHandler()); else - audio = new Mobile::Audio(); + audio = new Desktop::Audio(_sessionData); VideoWidget *videoWidget = new VideoWidget(_sessionData); @@ -210,7 +213,7 @@ LmsApplication::handleAuthEvent(void) } else { - LMS_LOG(MOD_UI, SEV_NOTICE) << "User logged out"; + LMS_LOG(MOD_UI, SEV_NOTICE) << "User logged out, session = " << Wt::WApplication::instance()->sessionId(); quit(""); redirect("/"); diff --git a/src/ui/approot/templates.xml b/src/ui/approot/templates.xml index 4a635a85..c4f1bf71 100644 --- a/src/ui/approot/templates.xml +++ b/src/ui/approot/templates.xml @@ -318,6 +318,68 @@ + +
+
${search}
+
+
+ + +
+
+
${text}
+
+
+
+ +
+
+
${text}
+
+
+
+ + +
+
${name}
+
+
+ + +
+
+
+
+
+
${cover}
+
${name}
+
+
+
+
+
+
+ + +
+
+
+
+
+
${cover}
+
${name}
+
${btn}
+
+
+
+
+
+
+ + +
+
${player}
+
+
+ - - diff --git a/src/ui/audio/desktop/PlayQueue.cpp b/src/ui/audio/desktop/PlayQueue.cpp index 62552bee..721813f9 100644 --- a/src/ui/audio/desktop/PlayQueue.cpp +++ b/src/ui/audio/desktop/PlayQueue.cpp @@ -377,7 +377,7 @@ PlayQueue::addTracks(const std::vector& trackIds) std::string coverUrl; if (track->hasCover()) - coverUrl = _coverResource->url() + "&coverid=" + Wt::asString(track.id()).toUTF8(); + coverUrl = _coverResource->getTrackUrl(track.id()); else coverUrl = "images/unknown-cover.jpg"; _model->setData(dataRow, COLUMN_ID_COVER, coverUrl, Wt::DecorationRole); diff --git a/src/ui/audio/mobile/ArtistSearch.cpp b/src/ui/audio/mobile/ArtistSearch.cpp new file mode 100644 index 00000000..bbdb4d40 --- /dev/null +++ b/src/ui/audio/mobile/ArtistSearch.cpp @@ -0,0 +1,111 @@ +/* + * 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 "ArtistSearch.hpp" + +namespace UserInterface { +namespace Mobile { + +ArtistSearch::ArtistSearch(Database::Handler& db, Wt::WContainerWidget *parent) +: Wt::WContainerWidget(parent), +_db(db), +_resCount(0) +{ + Wt::WTemplate *title = new Wt::WTemplate(this); + title->setTemplateText(Wt::WString::tr("mobile-search-title")); + + title->bindWidget("text", new Wt::WText("Artists", Wt::PlainText)); +} + +void +ArtistSearch::clear() +{ + while (count() > 1) + removeWidget(this->widget(1)); + + _resCount = 0; +} + +void +ArtistSearch::search(Database::SearchFilter filter, size_t nb) +{ + clear(); + addResults(filter, nb); +} + +void +ArtistSearch::addResults(Database::SearchFilter filter, std::size_t nb) +{ + + std::vector artists; + + { + Wt::Dbo::Transaction transaction(_db.getSession()); + + // Request one more to see if more results are to be expected + artists = Database::Track::getArtists(_db.getSession(), filter, _resCount, nb + 1); + } + + bool expectMoreResults; + if (artists.size() == nb + 1) + { + expectMoreResults = true; + artists.pop_back(); + } + else + expectMoreResults = false; + + BOOST_FOREACH(std::string artist, artists) + { + Wt::WTemplate* res = new Wt::WTemplate(this); + res->setTemplateText(Wt::WString::tr("mobile-artist-res")); + + Wt::WText *text = new Wt::WText(Wt::WString::fromUTF8(artist), Wt::PlainText); + res->bindWidget("name", text); + + res->clicked().connect(std::bind([=] { + _sigArtistSelected(artist); + })); + } + + _resCount += artists.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([=] { + _sigMoreArtistsSelected(); + removeWidget(moreRes); + + addResults(filter, 20); + })); + } + +} + +} // namespace Mobile +} // namespace UserInterface diff --git a/src/ui/audio/mobile/ArtistSearch.hpp b/src/ui/audio/mobile/ArtistSearch.hpp new file mode 100644 index 00000000..937b89bb --- /dev/null +++ b/src/ui/audio/mobile/ArtistSearch.hpp @@ -0,0 +1,58 @@ +/* + * 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_ARTIST_SEARCH_HPP +#define UI_MOBILE_ARTIST_SEARCH_HPP + +#include +#include + +#include "database/DatabaseHandler.hpp" + +namespace UserInterface { +namespace Mobile { + +class ArtistSearch : public Wt::WContainerWidget +{ + public: + + ArtistSearch(Database::Handler& db, Wt::WContainerWidget *parent = 0); + + void search(Database::SearchFilter filter, std::size_t nb); + + // Slots + Wt::Signal& artistSelected() { return _sigArtistSelected;} + Wt::Signal& moreArtistsSelected() { return _sigMoreArtistsSelected;} + + private: + + Wt::Signal _sigArtistSelected; + Wt::Signal _sigMoreArtistsSelected; + + void clear(void); + void addResults(Database::SearchFilter filter, size_t nb); + + Database::Handler& _db; + std::size_t _resCount; +}; + +} // namespace Mobile +} // namespace UserInterface + +#endif diff --git a/src/ui/audio/mobile/MobileAudio.cpp b/src/ui/audio/mobile/MobileAudio.cpp index 2431c8d5..c5d5c87b 100644 --- a/src/ui/audio/mobile/MobileAudio.cpp +++ b/src/ui/audio/mobile/MobileAudio.cpp @@ -17,18 +17,191 @@ * along with LMS. If not, see . */ +#include +#include +#include + #include +#include +#include +#include #include +#include #include "MobileAudio.hpp" +#include "ArtistSearch.hpp" +#include "ReleaseSearch.hpp" +#include "TrackSearch.hpp" +#include "MobileAudioMediaPlayer.hpp" + +#include "logger/Logger.hpp" + namespace UserInterface { namespace Mobile { -Audio::Audio(Wt::WContainerWidget *parent) -: UserInterface::Audio(parent) +Audio::Audio(Database::Handler& db, Wt::WContainerWidget *parent) +: UserInterface::Audio(parent), +_db(db) { - new Wt::WText("Mobile version not implemented", this); + // Root div has to be a "container" + this->setStyleClass("container"); + + Wt::WTemplate* search = new Wt::WTemplate(this); + search->setTemplateText(Wt::WString::tr("mobile-search")); + + Wt::WLineEdit *edit = new Wt::WLineEdit(); + edit->setEmptyText("Search..."); + + search->bindWidget("search", edit); + search->setMargin(10); + + ArtistSearch* artistSearch = new ArtistSearch(_db, this); + ReleaseSearch* releaseSearch = new ReleaseSearch(_db, this); + TrackSearch* trackSearch = new TrackSearch(_db, this); + + Wt::WTemplate* playerTemplate = new Wt::WTemplate(this); + playerTemplate->setTemplateText(Wt::WString::tr("mobile-audio-player")); + + AudioMediaPlayer* mediaPlayer = new AudioMediaPlayer(); + playerTemplate->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); + + { + Database::SearchFilter filter; + BOOST_FOREACH(std::string keyword, keywords) + { + Database::SearchFilter::FieldValues likeMatch; + + likeMatch[Database::SearchFilter::Field::Artist].push_back(keyword); + likeMatch[Database::SearchFilter::Field::Release].push_back(keyword); + filter.likeMatches.push_back(likeMatch); + } + + releaseSearch->search(filter, 3); + } + + { + Database::SearchFilter filter; + BOOST_FOREACH(std::string keyword, keywords) + { + Database::SearchFilter::FieldValues likeMatch; + + likeMatch[Database::SearchFilter::Field::Artist].push_back(keyword); + + filter.likeMatches.push_back(likeMatch); + } + + artistSearch->search(filter, 3); + } + + { + Database::SearchFilter filter; + BOOST_FOREACH(std::string keyword, keywords) + { + Database::SearchFilter::FieldValues likeMatch; + + likeMatch[Database::SearchFilter::Field::Track].push_back(keyword); + + filter.likeMatches.push_back(likeMatch); + } + + trackSearch->search(filter, 3); + } + + artistSearch->show(); + releaseSearch->show(); + trackSearch->show(); + + })); + + artistSearch->moreArtistsSelected().connect(std::bind([=] { + releaseSearch->hide(); + trackSearch->hide(); + artistSearch->show(); + })); + + artistSearch->artistSelected().connect(std::bind([=] (std::string artist) { + artistSearch->hide(); + trackSearch->hide(); + releaseSearch->show(); + + Database::SearchFilter filter; + filter.exactMatch[Database::SearchFilter::Field::Artist].push_back(artist); + + releaseSearch->search(filter, 20); + }, std::placeholders::_1)); + + releaseSearch->moreReleasesSelected().connect(std::bind([=] { + artistSearch->hide(); + trackSearch->hide(); + releaseSearch->show(); + })); + + releaseSearch->releaseSelected().connect(std::bind([=] (std::string release) + { + artistSearch->hide(); + releaseSearch->hide(); + trackSearch->show(); + + // TODO load track search with selected release + Database::SearchFilter filter; + filter.exactMatch[Database::SearchFilter::Field::Release].push_back(release); + + trackSearch->search(filter, 20); + }, std::placeholders::_1)); + + trackSearch->moreTracksSelected().connect(std::bind([=] + { + artistSearch->hide(); + releaseSearch->hide(); + })); + + trackSearch->trackPlay().connect(std::bind([=] (Database::Track::id_type id) + { + LMS_LOG(MOD_UI, SEV_DEBUG) << "Playing track id " << id; + Wt::Dbo::Transaction transaction(_db.getSession()); + + Database::Track::pointer track = Database::Track::getById(_db.getSession(), id); + + if (track) + { + // Determine the output format using the encoding of the player + Transcode::Format::Encoding encoding; + switch(AudioMediaPlayer::getEncoding()) + { + case Wt::WMediaPlayer::MP3: encoding = Transcode::Format::MP3; break; + case Wt::WMediaPlayer::M4A: encoding = Transcode::Format::M4A; break; + case Wt::WMediaPlayer::OGA: encoding = Transcode::Format::OGA; break; + default: + encoding = Transcode::Format::MP3; + } + // TODO compute parameters using user s profile + Transcode::InputMediaFile inputFile(track->getPath()); + Transcode::Parameters parameters(inputFile, Transcode::Format::get(encoding)); + parameters.setBitrate(Transcode::Stream::Audio, 96000); + + mediaPlayer->play(parameters); + } + } , std::placeholders::_1)); + + // Initially, populate the widgets using an empty search + { + Database::SearchFilter filter; // empty filter + artistSearch->search(filter, 3); + releaseSearch->search(filter, 3); + trackSearch->search(filter, 3); + } + } } // namespace Mobile diff --git a/src/ui/audio/mobile/MobileAudio.hpp b/src/ui/audio/mobile/MobileAudio.hpp index a36753fc..143159ad 100644 --- a/src/ui/audio/mobile/MobileAudio.hpp +++ b/src/ui/audio/mobile/MobileAudio.hpp @@ -22,6 +22,8 @@ #include +#include "database/DatabaseHandler.hpp" + #include "audio/Audio.hpp" namespace UserInterface { @@ -30,10 +32,13 @@ namespace Mobile { class Audio : public UserInterface::Audio { public: - Audio(Wt::WContainerWidget *parent = 0); + Audio(Database::Handler& db, Wt::WContainerWidget *parent = 0); - void search(std::string text) {}; + void search(std::string text) {} + private: + + Database::Handler& _db; }; } // namespace Mobile diff --git a/src/ui/audio/mobile/MobileAudioMediaPlayer.cpp b/src/ui/audio/mobile/MobileAudioMediaPlayer.cpp new file mode 100644 index 00000000..d459a8bc --- /dev/null +++ b/src/ui/audio/mobile/MobileAudioMediaPlayer.cpp @@ -0,0 +1,60 @@ +/* + * 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 "MobileAudioMediaPlayer.hpp" + +#include "resource/AvConvTranscodeStreamResource.hpp" + +namespace UserInterface { +namespace Mobile { + +Wt::WMediaPlayer::Encoding +AudioMediaPlayer::getEncoding() +{ + const Wt::WEnvironment& env = Wt::WApplication::instance()->environment(); + + if (env.agentIsIE()) + return Wt::WMediaPlayer::MP3; + else + return Wt::WMediaPlayer::OGA; +} + +AudioMediaPlayer::AudioMediaPlayer(Wt::WContainerWidget *parent) +: Wt::WContainerWidget(parent) +{ + _player = new Wt::WMediaPlayer(Wt::WMediaPlayer::Audio, this); + _player->addSource( getEncoding(), "" ); +} + +void +AudioMediaPlayer::play(const Transcode::Parameters& parameters) +{ + AvConvTranscodeStreamResource *resource = new AvConvTranscodeStreamResource( parameters, this ); + + _player->clearSources(); + _player->addSource( getEncoding(), Wt::WLink(resource)); + + _player->play(); +} + +} // namespace UserInterface +} // namespace Mobile diff --git a/src/ui/audio/mobile/MobileAudioMediaPlayer.hpp b/src/ui/audio/mobile/MobileAudioMediaPlayer.hpp new file mode 100644 index 00000000..bcc77753 --- /dev/null +++ b/src/ui/audio/mobile/MobileAudioMediaPlayer.hpp @@ -0,0 +1,50 @@ +/* + * 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_AUDIO_MOBILE_MEDIA_PLAYER_HPP +#define UI_AUDIO_MOBILE_MEDIA_PLAYER_HPP + +#include +#include + +#include "transcode/Parameters.hpp" + +namespace UserInterface { +namespace Mobile { + +class AudioMediaPlayer : public Wt::WContainerWidget +{ + public: + + static Wt::WMediaPlayer::Encoding getEncoding(); + + AudioMediaPlayer(Wt::WContainerWidget *parent = 0); + + void play(const Transcode::Parameters& parameters); + + private: + + Wt::WMediaPlayer *_player; + +}; + +} // namespace UserInterface +} // namespace Mobile + +#endif diff --git a/src/ui/audio/mobile/ReleaseSearch.cpp b/src/ui/audio/mobile/ReleaseSearch.cpp new file mode 100644 index 00000000..0e2d39ac --- /dev/null +++ b/src/ui/audio/mobile/ReleaseSearch.cpp @@ -0,0 +1,120 @@ +/* + * 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 "ReleaseSearch.hpp" + +namespace UserInterface { +namespace Mobile { + +ReleaseSearch::ReleaseSearch(Database::Handler& db, Wt::WContainerWidget *parent) +: Wt::WContainerWidget(parent), +_db(db), +_resCount(0) +{ + Wt::WTemplate* title = new Wt::WTemplate(this); + title->setTemplateText(Wt::WString::tr("mobile-search-title")); + + title->bindWidget("text", new Wt::WText("Releases", Wt::PlainText)); + + _coverResource = new CoverResource(db, 56); +} + +void +ReleaseSearch::clear() +{ + while (count() > 1) + removeWidget(this->widget(1)); + + _resCount = 0; +} + +void +ReleaseSearch::search(Database::SearchFilter filter, size_t max) +{ + clear(); + addResults(filter, max); +} + +void +ReleaseSearch::addResults(Database::SearchFilter filter, size_t nb) +{ + + std::vector releases; + + { + Wt::Dbo::Transaction transaction(_db.getSession()); + + // Request one more to see if more results are to be expected + releases = Database::Track::getReleases(_db.getSession(), filter, _resCount, nb + 1); + } + + bool expectMoreResults; + if (releases.size() == nb + 1) + { + expectMoreResults = true; + releases.pop_back(); + } + else + expectMoreResults = false; + + BOOST_FOREACH(std::string release, releases) + { + Wt::WTemplate* releaseWidget = new Wt::WTemplate(this); + releaseWidget->setTemplateText(Wt::WString::tr("mobile-release-res")); + + Wt::WImage *cover = new Wt::WImage(); + cover->setStyleClass("center-block"); + cover->setImageLink( Wt::WLink( _coverResource->getReleaseUrl(release))); + releaseWidget->bindWidget("cover", cover); + + releaseWidget->bindWidget("name", new Wt::WText(Wt::WString::fromUTF8(release), Wt::PlainText)); + + releaseWidget->clicked().connect(std::bind([=] { + _sigReleaseSelected(release); + })); + + } + + _resCount += releases.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([=] { + _sigMoreReleasesSelected(); + removeWidget(moreRes); + + addResults(filter, 20); + })); + } +} + +} // namespace Mobile +} // namespace UserInterface diff --git a/src/ui/audio/mobile/ReleaseSearch.hpp b/src/ui/audio/mobile/ReleaseSearch.hpp new file mode 100644 index 00000000..97f6e4bb --- /dev/null +++ b/src/ui/audio/mobile/ReleaseSearch.hpp @@ -0,0 +1,65 @@ +/* + * 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_RELEASE_SEARCH_HPP +#define UI_MOBILE_RELEASE_SEARCH_HPP + +#include + +#include +#include + +#include "resource/CoverResource.hpp" + +#include "database/DatabaseHandler.hpp" + +namespace UserInterface { +namespace Mobile { + +class ReleaseSearch : public Wt::WContainerWidget +{ + public: + + ReleaseSearch(Database::Handler& db, Wt::WContainerWidget *parent = 0); + + void search(Database::SearchFilter filter, size_t nb); + + // Slots + Wt::Signal& releaseSelected() { return _sigReleaseSelected;} + Wt::Signal& moreReleasesSelected() { return _sigMoreReleasesSelected;} + + private: + + Wt::Signal _sigReleaseSelected; + Wt::Signal _sigMoreReleasesSelected; + + void clear(void); + void addResults(Database::SearchFilter filter, size_t nb); + + Database::Handler& _db; + + CoverResource* _coverResource; + + std::size_t _resCount; +}; + +} // namespace Mobile +} // namespace UserInterface + +#endif diff --git a/src/ui/audio/mobile/TrackSearch.cpp b/src/ui/audio/mobile/TrackSearch.cpp new file mode 100644 index 00000000..c6c5c46d --- /dev/null +++ b/src/ui/audio/mobile/TrackSearch.cpp @@ -0,0 +1,133 @@ +/* + * 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 "TrackSearch.hpp" + +namespace UserInterface { +namespace Mobile { + +TrackSearch::TrackSearch(Database::Handler& db, Wt::WContainerWidget *parent) +: Wt::WContainerWidget(parent), +_db(db), +_resCount(0) +{ + Wt::WTemplate* title = new Wt::WTemplate(this); + title->setTemplateText(Wt::WString::tr("mobile-search-title")); + + title->bindWidget("text", new Wt::WText("Tracks", Wt::PlainText)); + + _coverResource = new CoverResource(db, 56); +} + +void +TrackSearch::clear() +{ + while (count() > 1) + removeWidget(this->widget(1)); + + _resCount = 0; +} + +void +TrackSearch::search(Database::SearchFilter filter, size_t max) +{ + clear(); + addResults(filter, max); +} + +void +TrackSearch::addResults(Database::SearchFilter filter, size_t nb) +{ + Wt::Dbo::Transaction transaction(_db.getSession()); + + std::vector< Database::Track::pointer > tracks = Database::Track::getTracks(_db.getSession(), filter, _resCount, nb + 1); + + bool expectMoreResults; + if (tracks.size() == nb + 1) + { + expectMoreResults = true; + tracks.pop_back(); + } + else + expectMoreResults = false; + + BOOST_FOREACH(Database::Track::pointer track, tracks) + { + Wt::WTemplate* trackWidget = new Wt::WTemplate(this); + trackWidget->setTemplateText(Wt::WString::tr("mobile-track-res")); + + Wt::WImage *cover = new Wt::WImage(); + cover->setStyleClass("center-block"); + cover->setImageLink( Wt::WLink (_coverResource->getTrackUrl(track.id())) ); + trackWidget->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); + + if (!track->getArtistName().empty() + || !track->getReleaseName().empty()) + { + title->setInline(false); + Wt::WText *artistRelease = new Wt::WText(Wt::WString::fromUTF8(track->getArtistName() + " - " + track->getReleaseName()), 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"); + playBtn->clicked().connect(std::bind([=] { + _sigTrackPlay.emit(track.id()); + })); + + trackWidget->bindWidget("btn", playBtn); + + } + + _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); + })); + } +} + +} // namespace Mobile +} // namespace UserInterface diff --git a/src/ui/audio/mobile/TrackSearch.hpp b/src/ui/audio/mobile/TrackSearch.hpp new file mode 100644 index 00000000..16076df2 --- /dev/null +++ b/src/ui/audio/mobile/TrackSearch.hpp @@ -0,0 +1,65 @@ +/* + * 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 + +#include "resource/CoverResource.hpp" + +#include "database/DatabaseHandler.hpp" + +namespace UserInterface { +namespace Mobile { + +class TrackSearch : public Wt::WContainerWidget +{ + public: + + TrackSearch(Database::Handler& db, 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(Database::SearchFilter filter, size_t nb); + + Database::Handler& _db; + + CoverResource* _coverResource; + + std::size_t _resCount; +}; + +} // namespace Mobile +} // namespace UserInterface + +#endif diff --git a/src/ui/docroot/css/lms.css b/src/ui/docroot/css/lms.css index 23c78024..68c40fa1 100644 --- a/src/ui/docroot/css/lms.css +++ b/src/ui/docroot/css/lms.css @@ -55,3 +55,44 @@ div.contents { font-weight: bold; } +.mobile-search-title { + font-weight: bold; + height: 32px; + background-color: grey; + color: white; + text-align: center; +} + +.mobile-search-entry { + min-height: 64px; + border-bottom: 1px solid lightgray; + overflow: hidden; + text-overflow: ellipsis; +} + +.mobile-search-entry:active { + background-color: lightgrey; +} + +.mobile-search-more { + height: 64px; + border-bottom: 1px solid lightgray; +} + +.mobile-search-more:active { + background-color: lightgrey; +} + +.mobile-track { + font-weight: bold; +} + +.mobile-artist { + font-style: italic; +} + +.vertical-align { + display: flex; + align-items: center; +} + diff --git a/src/ui/resource/AvConvTranscodeStreamResource.cpp b/src/ui/resource/AvConvTranscodeStreamResource.cpp index a8e22ea1..025455a2 100644 --- a/src/ui/resource/AvConvTranscodeStreamResource.cpp +++ b/src/ui/resource/AvConvTranscodeStreamResource.cpp @@ -44,11 +44,11 @@ void AvConvTranscodeStreamResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response) { - static const std::size_t chunkSize = 8192; // TODO parametrize? - // see if this request is for a continuation: Wt::Http::ResponseContinuation *continuation = request.continuation(); + LMS_LOG(MOD_UI, SEV_DEBUG) << "Handling new request. Continuation = " << std::boolalpha << continuation; + std::shared_ptr transcoder; if (continuation) transcoder = boost::any_cast >(continuation->data()); @@ -65,13 +65,15 @@ AvConvTranscodeStreamResource::handleRequest(const Wt::Http::Request& request, if (!transcoder->isComplete()) { std::vector data; - data.reserve(chunkSize); + data.reserve(_bufferSize); - transcoder->process(data, chunkSize); + transcoder->process(data, _bufferSize); // Give the client all the output data response.out().write(reinterpret_cast(&data[0]), data.size()); + LMS_LOG(MOD_UI, SEV_DEBUG) << "Written " << data.size() << " bytes! complete = " << std::boolalpha << transcoder->isComplete() << ", produced bytes = " << transcoder->getOutputBytes(); + if (!response.out()) LMS_LOG(MOD_UI, SEV_ERROR) << "Write failed!"; } diff --git a/src/ui/resource/CoverResource.cpp b/src/ui/resource/CoverResource.cpp index 5af7ed2e..6866512e 100644 --- a/src/ui/resource/CoverResource.cpp +++ b/src/ui/resource/CoverResource.cpp @@ -35,6 +35,21 @@ CoverResource::CoverResource(Database::Handler& db, std::size_t size, Wt::WObjec _db(db), _size(size) { + // Load default cover art + + std::vector data; + + { + std::ifstream ist(Wt::WApplication::instance()->docRoot() + "/images/unknown-cover.jpg"); + char c; + while(ist.get(c)) + data.push_back(c); + } + + _defaultCover.setData(data); + _defaultCover.setMimeType("image/jpeg"); + + _defaultCover.scale(size); } CoverResource:: ~CoverResource() @@ -42,55 +57,66 @@ CoverResource:: ~CoverResource() beingDeleted(); } +std::string +CoverResource::getReleaseUrl(std::string releaseName) +{ + return url() + "&release=" + releaseName; +} + +std::string +CoverResource::getTrackUrl(Database::Track::id_type trackId) +{ + return url()+ "&trackid=" + std::to_string(trackId); +} void CoverResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response) { // Get the id of the track - const std::string *trackIdStr = request.getParameter("coverid"); + const std::string *trackIdStr = request.getParameter("trackid"); + const std::string *releaseStr = request.getParameter("release"); + + std::vector covers; if (trackIdStr) { Database::Track::id_type trackId; { - std::istringstream iss(*trackIdStr); iss >> trackId; + std::istringstream iss(*trackIdStr); + iss >> trackId; } Wt::Dbo::Transaction transaction(_db.getSession()); Database::Track::pointer track = Database::Track::getById(_db.getSession(), trackId); - std::vector covers = CoverArt::Grabber::getFromTrack(track); + covers = CoverArt::Grabber::getFromTrack(track); transaction.commit(); + } + else if (releaseStr) + { + Wt::Dbo::Transaction transaction(_db.getSession()); + covers = CoverArt::Grabber::getFromRelease(_db.getSession(), *releaseStr); + } - BOOST_FOREACH(CoverArt::CoverArt& cover, covers) + BOOST_FOREACH(CoverArt::CoverArt& cover, covers) + { + if (cover.scale(_size)) { - if (cover.scale(_size)) - { - response.setMimeType( cover.getMimeType() ); + response.setMimeType( cover.getMimeType() ); - BOOST_FOREACH(unsigned char c, cover.getData()) - response.out().put( c ); - - return; - } - else - LMS_LOG(MOD_UI, SEV_DEBUG) << "Resize error for track id = " << trackId; - } - LMS_LOG(MOD_UI, SEV_DEBUG) << "no valid cover found for track id = " << trackId; - { - std::ifstream ist(Wt::WApplication::instance()->docRoot() + "/images/unknown-cover.jpg"); - - char c; - while(ist.get(c)) + BOOST_FOREACH(unsigned char c, cover.getData()) response.out().put( c ); - response.setMimeType("image/jpeg"); + return; } } - else - LMS_LOG(MOD_UI, SEV_DEBUG) << "no cover id parameter"; + + // If no cover found, just send default one + response.setMimeType( _defaultCover.getMimeType() ); + BOOST_FOREACH(unsigned char c, _defaultCover.getData()) + response.out().put( c ); + } - } // namespace UserInterface diff --git a/src/ui/resource/CoverResource.hpp b/src/ui/resource/CoverResource.hpp index 59efde51..9436899f 100644 --- a/src/ui/resource/CoverResource.hpp +++ b/src/ui/resource/CoverResource.hpp @@ -26,6 +26,7 @@ #include #include "database/DatabaseHandler.hpp" +#include "cover/CoverArt.hpp" namespace UserInterface { @@ -37,12 +38,16 @@ class CoverResource : public Wt::WResource Wt::WObject *parent = 0); ~CoverResource(); + std::string getReleaseUrl(std::string releaseName); + std::string getTrackUrl(Database::Track::id_type trackId); + void handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response); private: Database::Handler& _db; std::size_t _size; + CoverArt::CoverArt _defaultCover; }; } // namespace UserInterface