diff --git a/TODO b/TODO index 1dabca79..76224fbd 100644 --- a/TODO +++ b/TODO @@ -12,9 +12,10 @@ [Cover] - Scaling: find something more "reliable" than GIL and its custom extensions (adobe work, io_new)? +- Get the cover from image files in the same folder ("cover.jpg", etc.) [Database] -- Optim, use SQL query to get genre orphans +- Optim, use SQL query to get the "genre" orphans - Use Inotify like system to watch modified/added files? - count scan import errors @@ -28,12 +29,13 @@ - handle internationalization [Settings] - logout users that are being changed (loss of admin 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? + - "signal not exposed" problem if a user logout and login again. Bad resource destruction? [user/transcoding] - - Prefered codecs for audio/video? - - use a slider instead of a combo box for the bitrates + - Prefered codecs for video + - use a slider instead of a combo box for the bitrates? [admin/Users] - do not activate the update period/start time when 'admin' is checked + - do not activate the bitrate limit when creating a new admin user [admin/Database update] - do not make the update start time field active when update perdiod is "Never" - refresh the audio/video view when the database directories have been updated @@ -41,10 +43,11 @@ [Audio] - Style eveything nicely... - MediaPlayer: move the slider using js (http://redmine.webtoolkit.eu/boards/2/topics/7924?r=8478) - - TrackView : handle duration > 1 hour + - TrackView : handle durations > 1 hour - 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 [Video] @@ -52,12 +55,15 @@ - remove codec choice from parameters [Layout] - - Make a dedicated audio layout in mobile environment (keep the same layout for videos) - Style eveything nicely... + - Make a dedicated audio layout in mobile environment (keep the same layout for videos?) - Handle internal path [Remote API] - Implement partial text search options in the GetXXX messages +[REST API] +- Make another dedicated REST API. Maybe use the SubSonic API? + [Logs] -- capture transcoder log output to get more information on errors +- Capture transcoder log output to get more information on errors diff --git a/src/ui/audio/desktop/AudioMediaPlayer.cpp b/src/ui/audio/desktop/AudioMediaPlayer.cpp index ebdc6fb3..8f88ea75 100644 --- a/src/ui/audio/desktop/AudioMediaPlayer.cpp +++ b/src/ui/audio/desktop/AudioMediaPlayer.cpp @@ -24,6 +24,10 @@ #include #include +#include "logger/Logger.hpp" + +#include "LmsApplication.hpp" + #include "AudioMediaPlayer.hpp" namespace UserInterface { @@ -36,75 +40,91 @@ AudioMediaPlayer::getBestEncoding() return Wt::WMediaPlayer::MP3; } -AudioMediaPlayer::AudioMediaPlayer( Wt::WMediaPlayer::Encoding encoding, Wt::WContainerWidget *parent) +AudioMediaPlayer::AudioMediaPlayer(Wt::WContainerWidget *parent) : Wt::WContainerWidget(parent), - _mediaResource(nullptr), - _encoding(encoding) + _mediaResource(nullptr) { this->setStyleClass("mediaplayer"); - this->setHeight(90); Wt::WVBoxLayout* mainLayout = new Wt::WVBoxLayout(); this->setLayout(mainLayout); - /* TODO add media info here + // 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(MOD_UI, SEV_INFO) << "Audio player using encoding " << _encoding; + // Current Media info Wt::WHBoxLayout *currentMediaLayout = new Wt::WHBoxLayout(); - mainLayout->addLayout(currentMediaLayout); + mainLayout->addLayout(currentMediaLayout, 1); currentMediaLayout->addWidget( _mediaCover = new Wt::WImage()); - _mediaCover->setImageLink( Wt::WLink("images/unknown-cover.jpg") ); + _mediaCover->setImageLink( LmsApplication::instance()->getCoverResource()->getUnkownTrackUrl(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()); - */ + 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")); - _curTime->setLineHeight(30); sliderLayout->addWidget(_timeSlider = new Wt::WSlider( ), 1); sliderLayout->addWidget(_duration = new Wt::WText("00:00:00")); - _duration->setLineHeight(30); + _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::WContainerWidget *btnContainer = new Wt::WContainerWidget(); - // Do not allow button to wrap - btnContainer->setMinimumSize(155, Wt::WLength::Auto); - Wt::WTemplate *t = new Wt::WTemplate(Wt::WString::tr("mediaplayer-controls"), btnContainer); Wt::WPushButton *prevBtn = new Wt::WPushButton("<<"); - t->bindWidget("prev", prevBtn); - prevBtn->setStyleClass("mediaplayer-controls"); + controlsLayout->addWidget(prevBtn); + prevBtn->setStyleClass("mediaplayer-btn-controls"); + + Wt::WContainerWidget *btnContainer = new Wt::WContainerWidget();; _playBtn = new Wt::WPushButton("Play"); - t->bindWidget("play", _playBtn); + btnContainer->addWidget(_playBtn); _playBtn->setWidth(70); - _playBtn->setStyleClass("mediaplayer-controls"); + _playBtn->setStyleClass("mediaplayer-btn-controls"); _pauseBtn = new Wt::WPushButton("Pause"); - t->bindWidget("pause", _pauseBtn); + btnContainer->addWidget(_pauseBtn); _pauseBtn->setWidth(70); - _pauseBtn->setStyleClass("mediaplayer-controls"); - - Wt::WPushButton *nextBtn = new Wt::WPushButton(">>"); - t->bindWidget("next", nextBtn); - nextBtn->setStyleClass("mediaplayer-controls"); + _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); - _volumeSlider->setMinimumSize(60, Wt::WLength::Auto); + _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"); @@ -149,10 +169,8 @@ AudioMediaPlayer::AudioMediaPlayer( Wt::WMediaPlayer::Encoding encoding, Wt::WCo _volumeSlider->sliderMoved().connect(this, &AudioMediaPlayer::handleVolumeSliderMoved); - } - void AudioMediaPlayer::loadPlayer(void) { @@ -168,19 +186,57 @@ AudioMediaPlayer::loadPlayer(void) } void -AudioMediaPlayer::load(const Transcode::Parameters& parameters) +AudioMediaPlayer::load(Database::Track::id_type trackId) { + std::size_t bitrate = 0; + boost::filesystem::path trackPath; + + { + Wt::Dbo::Transaction transaction(DboSession()); + + Database::Track::pointer track = Database::Track::getById(DboSession(), trackId); + + bitrate = CurrentUser()->getAudioBitrate(); + trackPath = track->getPath(); + _mediaTitle->setText ( Wt::WString::fromUTF8(track->getName()) ); + _mediaArtistRelease->setText ( Wt::WString::fromUTF8(track->getArtistName()) + " - " + Wt::WString::fromUTF8(track->getReleaseName()) ); + _mediaCover->setImageLink( Wt::WLink (LmsApplication::instance()->getCoverResource()->getTrackUrl(trackId, 72))); + } + + Transcode::Format::Encoding encoding; + switch (_encoding) + { + case Wt::WMediaPlayer::MP3: encoding = Transcode::Format::MP3; break; + case Wt::WMediaPlayer::FLA: encoding = Transcode::Format::FLA; break; + case Wt::WMediaPlayer::OGA: encoding = Transcode::Format::OGA; break; + case Wt::WMediaPlayer::WEBMA: encoding = Transcode::Format::WEBMA; break; + default: + encoding = Transcode::Format::MP3; + } + _timeSlider->setDisabled(false); - _currentParameters = std::make_shared( parameters ); + try + { + Transcode::Parameters parameters(trackPath, Transcode::Format::get(encoding)); + parameters.setBitrate(Transcode::Stream::Audio, bitrate); + + _currentParameters = std::make_shared( parameters ); + } + catch(std::exception &e) + { + LMS_LOG(MOD_UI, SEV_ERROR) << "Cannot load input file '" << trackPath << "'"; + return; + } loadPlayer(); - _timeSlider->setRange(0, parameters.getInputMediaFile().getDuration().total_seconds() ); + _timeSlider->setRange(0, _currentParameters->getInputMediaFile().getDuration().total_seconds() ); _timeSlider->setValue(0); - _duration->setText( boost::posix_time::to_simple_string( parameters.getInputMediaFile().getDuration() )); + _duration->setText( boost::posix_time::to_simple_string( _currentParameters->getInputMediaFile().getDuration() )); + // Auto play _mediaPlayer->play(); } @@ -203,18 +259,6 @@ AudioMediaPlayer::handleTrackEnded(void) _playbackEnded.emit(); } -void -AudioMediaPlayer::handleValueChanged(double value) -{ - // TODO -} - -void -AudioMediaPlayer::handleSliderMoved(int value) -{ - ; -} - void AudioMediaPlayer::handleTimeUpdated(void) { diff --git a/src/ui/audio/desktop/AudioMediaPlayer.hpp b/src/ui/audio/desktop/AudioMediaPlayer.hpp index 1c319e43..0483bad0 100644 --- a/src/ui/audio/desktop/AudioMediaPlayer.hpp +++ b/src/ui/audio/desktop/AudioMediaPlayer.hpp @@ -29,6 +29,8 @@ #include #include +#include "database/Types.hpp" + #include "transcode/Parameters.hpp" #include "resource/AvConvTranscodeStreamResource.hpp" #include "resource/CoverResource.hpp" @@ -41,9 +43,10 @@ class AudioMediaPlayer : public Wt::WContainerWidget public: static Wt::WMediaPlayer::Encoding getBestEncoding(); - AudioMediaPlayer( Wt::WMediaPlayer::Encoding encoding, Wt::WContainerWidget *parent = 0); + AudioMediaPlayer(Wt::WContainerWidget *parent = 0); - void load(const Transcode::Parameters& parameters); + // Load Media to be played + void load(Database::Track::id_type trackId); // Accessors Wt::WMediaPlayer::Encoding getEncoding() const { return _encoding; } diff --git a/src/ui/audio/desktop/DesktopAudio.cpp b/src/ui/audio/desktop/DesktopAudio.cpp index 9e538875..297fd342 100644 --- a/src/ui/audio/desktop/DesktopAudio.cpp +++ b/src/ui/audio/desktop/DesktopAudio.cpp @@ -120,21 +120,7 @@ _playQueue(nullptr) Wt::WVBoxLayout* playQueueLayout = new Wt::WVBoxLayout(); playQueueContainer->setLayout(playQueueLayout); - // Determine the encoding to be used - Wt::WMediaPlayer::Encoding encoding; - 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 = AudioMediaPlayer::getBestEncoding(); - } - - LMS_LOG(MOD_UI, SEV_INFO) << "Audio player using encoding " << encoding; - _mediaPlayer = new AudioMediaPlayer(encoding); + _mediaPlayer = new AudioMediaPlayer(); playQueueLayout->addWidget(_mediaPlayer); playQueueLayout->addWidget( _playQueue, 1); @@ -192,7 +178,7 @@ _playQueue(nullptr) mainLayout->setRowStretch(1, 1); mainLayout->setRowResizable(0, true, Wt::WLength(250, Wt::WLength::Pixel)); - mainLayout->setColumnResizable(0, true, Wt::WLength(400, Wt::WLength::Pixel)); + mainLayout->setColumnResizable(0, true); // Double click on track // Set the selected tracks to the play queue @@ -499,45 +485,16 @@ Audio::playSelectedTracks(PlayQueueAddType addType) } void -Audio::playTrack(boost::filesystem::path p, int pos) +Audio::playTrack(Database::Track::id_type trackId, int pos) { - LMS_LOG(MOD_UI, SEV_DEBUG) << "play track '" << p << "'"; - try { - - std::size_t bitrate = 0; - - // Get user preferences - { - Wt::Dbo::Transaction transaction(DboSession()); - - bitrate = CurrentUser()->getAudioBitrate(); - CurrentUser().modify()->setCurPlayingTrackPos(pos); - } - - Transcode::InputMediaFile inputFile(p); - - // Determine the output format using the encoding of the player - Transcode::Format::Encoding encoding; - switch (_mediaPlayer->getEncoding()) - { - case Wt::WMediaPlayer::MP3: encoding = Transcode::Format::MP3; break; - case Wt::WMediaPlayer::FLA: encoding = Transcode::Format::FLA; break; - case Wt::WMediaPlayer::OGA: encoding = Transcode::Format::OGA; break; - case Wt::WMediaPlayer::WEBMA: encoding = Transcode::Format::WEBMA; break; - default: - encoding = Transcode::Format::MP3; - } - - Transcode::Parameters parameters(inputFile, Transcode::Format::get(encoding)); - - parameters.setBitrate(Transcode::Stream::Audio, bitrate); - - _mediaPlayer->load( parameters ); - } - catch( std::exception &e) { - LMS_LOG(MOD_UI, SEV_ERROR) << "Caught exception while loading '" << p << "': " << e.what(); + Wt::Dbo::Transaction transaction(DboSession()); + + // Update user current track position + CurrentUser().modify()->setCurPlayingTrackPos(pos); } + + _mediaPlayer->load(trackId); } } // namespace Desktop diff --git a/src/ui/audio/desktop/DesktopAudio.hpp b/src/ui/audio/desktop/DesktopAudio.hpp index 7b5a2d75..f5ee34ec 100644 --- a/src/ui/audio/desktop/DesktopAudio.hpp +++ b/src/ui/audio/desktop/DesktopAudio.hpp @@ -53,7 +53,7 @@ class Audio : public UserInterface::Audio void playlistShowDeleteDialog(std::string name); void playlistRefreshMenus(); - void playTrack(boost::filesystem::path p, int pos); + void playTrack(Database::Track::id_type id, int pos); enum PlayQueueAddType { diff --git a/src/ui/audio/desktop/PlayQueue.cpp b/src/ui/audio/desktop/PlayQueue.cpp index 3d592d83..7ba53fcf 100644 --- a/src/ui/audio/desktop/PlayQueue.cpp +++ b/src/ui/audio/desktop/PlayQueue.cpp @@ -340,8 +340,7 @@ PlayQueue::play() { _trackSelector->setPos(0); - if (!readTrack(_trackSelector->getCurrent())) - playNext(); + readTrack(_trackSelector->getCurrent()); } void @@ -350,8 +349,7 @@ PlayQueue::play(int rowId) // Update the track selector to use the requested track as current position _trackSelector->setPosByRowId(rowId); - if (!readTrack(_trackSelector->getCurrent())) - playNext(); + readTrack(_trackSelector->getCurrent()); } void @@ -389,6 +387,7 @@ PlayQueue::addTracks(const std::vector& trackIds) else coverUrl = LmsApplication::instance()->getCoverResource()->getUnkownTrackUrl(64); _model->setData(dataRow, COLUMN_ID_COVER, coverUrl, Wt::DecorationRole); + _model->setData(dataRow, COLUMN_ID_COVER, "playqueue-cover", Wt::StyleClassRole); TrackInfo trackInfo; trackInfo.track = Wt::WString::fromUTF8(track->getName()); @@ -432,10 +431,9 @@ PlayQueue::playNext(void) if (pos == trackPosInvalid) break; - if (readTrack(pos)) - break; + readTrack(pos); - --nbTries; + break; } } @@ -450,34 +448,25 @@ PlayQueue::playPrevious(void) if (pos == trackPosInvalid) break; - if (readTrack(pos)) - break; + readTrack(pos); + break; - --nbTries; } } bool PlayQueue::readTrack(int rowPos) { - Wt::Dbo::Transaction transaction(DboSession()); - LMS_LOG(MOD_UI, SEV_DEBUG) << "Reading track at pos " << rowPos; Database::Track::id_type trackId = boost::any_cast(_model->data(rowPos, COLUMN_ID_TRACK_ID, Wt::UserRole)); - Database::Track::pointer track = Database::Track::getById(DboSession(), trackId); - if (track) - { - setPlayingTrackPos(rowPos); + setPlayingTrackPos(rowPos); - _sigTrackPlay.emit(track->getPath(), rowPos); + _sigTrackPlay.emit(trackId, rowPos); - this->scrollTo( _model->index(_trackSelector->getCurrent(), 0)); + this->scrollTo( _model->index(_trackSelector->getCurrent(), 0)); - return true; - } - else - return false; + return true; } void diff --git a/src/ui/audio/desktop/PlayQueue.hpp b/src/ui/audio/desktop/PlayQueue.hpp index ed055c7f..7b80aa8a 100644 --- a/src/ui/audio/desktop/PlayQueue.hpp +++ b/src/ui/audio/desktop/PlayQueue.hpp @@ -61,7 +61,7 @@ class PlayQueue : public Wt::WTableView // Signals // Emitted when a song has to be played - Wt::Signal< boost::filesystem::path, int >& playTrack() { return _sigTrackPlay; } + Wt::Signal< Database::Track::id_type, int >& playTrack() { return _sigTrackPlay; } // Emitted when the list has changed Wt::Signal< void >& tracksUpdated() { return _sigTracksUpdated; } @@ -76,7 +76,7 @@ class PlayQueue : public Wt::WTableView void setPlayingTrackPos(int newRowPos); void renumber(int firstId, int lastId); - Wt::Signal< boost::filesystem::path, int > _sigTrackPlay; + Wt::Signal< Database::Track::id_type, int > _sigTrackPlay; Wt::Signal< void > _sigTracksUpdated; Wt::WStandardItemModel* _model; diff --git a/src/ui/docroot/css/lms.css b/src/ui/docroot/css/lms.css index d8888a55..025641d4 100644 --- a/src/ui/docroot/css/lms.css +++ b/src/ui/docroot/css/lms.css @@ -14,15 +14,6 @@ div.contents { padding: 0px 12px 6px; } -.playqueue { - background-color: #EEE; - border-radius: 10px; -} - -.playqueue-playing { - background-color: #4A42CA; -} - .Wt-tableview .playqueue-playing { color: #FFF; } @@ -35,6 +26,15 @@ div.contents { background-color: #4A42CA; } +.playqueue { + background-color: #EEE; + border-radius: 10px; +} + +.playqueue-playing { + background-color: #4A42CA; +} + .playqueue-track { margin-top: 12px; line-height: normal; @@ -46,15 +46,36 @@ div.contents { font-style: italic; } +.playqueue-cover { + width: 64px; + height: 64px; +} + .mediaplayer { background-color: #CCC; border-radius: 10px; + min-width: 360px; } -.mediaplayer-controls { +.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 { + font-weight: bold; +} + +.mediaplayer-current-artist { + font-style: italic; +} + .mobile-search-title { font-weight: bold; height: 32px; diff --git a/src/ui/resource/CoverResource.cpp b/src/ui/resource/CoverResource.cpp index 06e32f62..3219f639 100644 --- a/src/ui/resource/CoverResource.cpp +++ b/src/ui/resource/CoverResource.cpp @@ -81,13 +81,13 @@ CoverResource::getReleaseUrl(std::string releaseName, std::size_t size) const std::string CoverResource::getTrackUrl(Database::Track::id_type trackId, std::size_t size) const { - return url()+ "&trackid=" + std::to_string(trackId) + "&size=" + std::to_string(size); + return url() + "&trackid=" + std::to_string(trackId) + "&size=" + std::to_string(size); } std::string CoverResource::getUnkownTrackUrl(size_t size) const { - return Wt::WApplication::instance()->docRoot() + unknownCoverPath + "&size=" + std::to_string(size); + return url() + "&size=" + std::to_string(size); } void