/* * Copyright (C) 2018 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 "PlayQueueView.hpp" #include #include #include #include #include #include "utils/Logger.hpp" #include "database/Playlist.hpp" #include "database/TrackStats.hpp" #include "LmsApplication.hpp" namespace UserInterface { PlayQueue::PlayQueue() : Wt::WTemplate(Wt::WString::tr("Lms.PlayQueue.template")) { addFunction("tr", &Wt::WTemplate::Functions::tr); Wt::WText* clearBtn = bindNew("clear-btn", Wt::WString::tr("Lms.PlayQueue.clear"), Wt::TextFormat::XHTML); _entriesContainer = bindNew("entries"); _showMore = bindNew("show-more", Wt::WString::tr("Lms.Explore.show-more")); _showMore->addFunction("tr", &Wt::WTemplate::Functions::tr); _showMore->setHidden(true); Wt::WText* shuffleBtn = bindNew("shuffle-btn", Wt::WString::tr("Lms.PlayQueue.shuffle"), Wt::TextFormat::XHTML); _radioMode = bindNew("radio-mode", Wt::WString::tr("Lms.PlayQueue.radio-mode")); _nbTracks = bindNew("nb-tracks"); clearBtn->clicked().connect([=] { clearTracks(); }); shuffleBtn->clicked().connect([=] { { Wt::Dbo::Transaction transaction(LmsApp->getDboSession()); getPlaylist().modify()->shuffle(); } _entriesContainer->clear(); addSome(); }); _showMore->clicked().connect([=] { addSome(); }); LmsApp->preQuit().connect([=] { if (_playlistId) { LMS_LOG(UI, DEBUG) << "Removing playlist id " << *_playlistId; Wt::Dbo::Transaction transaction(LmsApp->getDboSession()); auto playlist = Database::Playlist::getById(LmsApp->getDboSession(), *_playlistId); if (playlist) playlist.remove(); } }); updateInfo(); addSome(); } Database::Playlist::pointer PlayQueue::getPlaylist() { static const std::string currentPlayQueueName = "__current__playqueue__"; Database::Playlist::pointer res; if (LmsApp->getUser()->isDemo()) { if (!_playlistId) { res = Database::Playlist::create(LmsApp->getDboSession(), currentPlayQueueName, false, LmsApp->getUser()); LmsApp->getDboSession().flush(); _playlistId = res.id(); return res; } return Database::Playlist::getById(LmsApp->getDboSession(), *_playlistId); } res = Database::Playlist::get(LmsApp->getDboSession(), currentPlayQueueName, LmsApp->getUser()); if (!res) res = Database::Playlist::create(LmsApp->getDboSession(), currentPlayQueueName, false, LmsApp->getUser()); return res; } void PlayQueue::clearTracks() { Wt::Dbo::Transaction transaction(LmsApp->getDboSession()); getPlaylist().modify()->clear(); _showMore->setHidden(true); _entriesContainer->clear(); updateInfo(); } void PlayQueue::stop() { updateCurrentTrack(false); _trackPos.reset(); playbackStop.emit(); } void PlayQueue::play(std::size_t pos) { updateCurrentTrack(false); Database::IdType trackId; { Wt::Dbo::Transaction transaction(LmsApp->getDboSession()); auto playlist = getPlaylist(); // If out of range, stop playing if (pos >= playlist->getCount()) { stop(); return; } // If last and radio mode, fill the next song if (_radioMode->checkState() == Wt::CheckState::Checked && pos == playlist->getCount() - 1) addRadioTrack(); _trackPos = pos; auto track = playlist->getEntry(*_trackPos)->getTrack(); trackId = track.id(); auto stats = Database::TrackStats::get(LmsApp->getDboSession(), track, LmsApp->getUser()); stats.modify()->incPlayCount(); stats.modify()->setLastPlayed(Wt::WLocalDateTime::currentServerDateTime().toUTC()); updateCurrentTrack(true); } playTrack.emit(trackId); } void PlayQueue::playPrevious() { if (!_trackPos) return; if (*_trackPos == 0) stop(); else play(*_trackPos - 1); } void PlayQueue::playNext() { if (!_trackPos) { play(0); return; } play(*_trackPos + 1); } void PlayQueue::updateInfo() { Wt::Dbo::Transaction transaction(LmsApp->getDboSession()); _nbTracks->setText(Wt::WString::tr("Lms.PlayQueue.nb-tracks").arg(static_cast(getPlaylist()->getCount()))); } void PlayQueue::updateCurrentTrack(bool selected) { if (!_trackPos || *_trackPos >= static_cast(_entriesContainer->count())) return; auto track = _entriesContainer->widget(*_trackPos); if (track) { if (selected) track->addStyleClass("Lms-playqueue-selected"); else track->removeStyleClass("Lms-playqueue-selected"); } } void PlayQueue::enqueueTracks(const std::vector& tracks) { // Use a "session" playqueue in order to store the current playqueue // so that the user can disconnect and get its playqueue back auto playlist = getPlaylist(); for (auto track : tracks) Database::PlaylistEntry::create(LmsApp->getDboSession(), track, playlist); updateInfo(); addSome(); } void PlayQueue::enqueueTrack(Database::Track::pointer track) { enqueueTracks(std::vector(1, track)); } void PlayQueue::addTracks(const std::vector& tracks) { enqueueTracks(tracks); LmsApp->notifyMsg(Wt::WString::tr("Lms.PlayQueue.nb-tracks-added").arg(tracks.size())); } void PlayQueue::playTracks(const std::vector& tracks) { Wt::Dbo::Transaction transaction(LmsApp->getDboSession()); clearTracks(); enqueueTracks(tracks); play(0); LmsApp->notifyMsg(Wt::WString::tr("Lms.PlayQueue.nb-tracks-playing").arg(tracks.size())); } void PlayQueue::addSome() { Wt::Dbo::Transaction transaction (LmsApp->getDboSession()); auto playlist = getPlaylist(); bool moreResults; auto playlistEntries = playlist->getEntries(_entriesContainer->count(), 50, moreResults); for (auto playlistEntry : playlistEntries) { auto playlistEntryId = playlistEntry.id(); auto track = playlistEntry->getTrack(); Wt::WTemplate* entry = _entriesContainer->addNew(Wt::WString::tr("Lms.PlayQueue.template.entry")); entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain); auto artist = track->getArtist(); if (artist) { entry->setCondition("if-has-artist", true); entry->bindWidget("artist-name", LmsApplication::createArtistAnchor(track->getArtist())); } auto release = track->getRelease(); if (release) { entry->setCondition("if-has-release", true); entry->bindWidget("release-name", LmsApplication::createReleaseAnchor(track->getRelease())); } Wt::WText* playBtn = entry->bindNew("play-btn", Wt::WString::tr("Lms.PlayQueue.play"), Wt::TextFormat::XHTML); playBtn->clicked().connect(std::bind([=] { auto pos = _entriesContainer->indexOf(entry); if (pos >= 0) play(pos); })); Wt::WText* delBtn = entry->bindNew("del-btn", Wt::WString::tr("Lms.PlayQueue.delete"), Wt::TextFormat::XHTML); delBtn->clicked().connect(std::bind([=] { // Remove the entry n both the widget tree and the playqueue { Wt::Dbo::Transaction transaction (LmsApp->getDboSession()); auto entryToRemove = Database::PlaylistEntry::getById(LmsApp->getDboSession(), playlistEntryId); entryToRemove.remove(); } if (_trackPos) { auto pos = _entriesContainer->indexOf(entry); if (pos > 0 && *_trackPos >= static_cast(pos)) (*_trackPos)--; } _entriesContainer->removeWidget(entry); updateInfo(); })); } _showMore->setHidden(!moreResults); } void PlayQueue::addRadioTrack() { auto now = std::chrono::system_clock::now(); std::mt19937 randGenerator(std::chrono::duration_cast(now.time_since_epoch()).count()); auto playlist = getPlaylist(); std::set playlistTrackIds; { auto ids = playlist->getTrackIds(); playlistTrackIds = std::set(ids.begin(), ids.end()); } // Get all the tracks of the playlist, get the cluster that is mostly used // and reuse it to get the next track auto clusters = playlist->getClusters(); if (clusters.empty()) return; for (auto cluster : clusters) { std::set clusterTrackIds = cluster->getTrackIds(); std::set candidateTrackIds; std::set_difference(clusterTrackIds.begin(), clusterTrackIds.end(), playlistTrackIds.begin(), playlistTrackIds.end(), std::inserter(candidateTrackIds, candidateTrackIds.end())); if (candidateTrackIds.empty()) continue; std::uniform_int_distribution dist(0, candidateTrackIds.size() - 1); auto trackToAdd = Database::Track::getById(LmsApp->getDboSession(), *std::next(candidateTrackIds.begin(), dist(randGenerator))); enqueueTrack(trackToAdd); return; } LMS_LOG(UI, INFO) << "No more track to be added!"; } } // namespace UserInterface