From 36ccd62a846a2c9bb073911e9d9a0785fbbb2541 Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 24 Jul 2020 14:32:26 +0200 Subject: [PATCH 01/14] Replaced show-more button by infinite scrolling. fixes #83 --- approot/artists.xml | 4 +-- approot/messages.xml | 2 +- approot/messages_fr.xml | 2 +- approot/playqueue.xml | 4 +-- approot/releases.xml | 4 +-- approot/templates.xml | 5 ++++ approot/tracks.xml | 4 +-- docroot/css/lms.css | 10 +++---- src/lms/CMakeLists.txt | 1 + src/lms/ui/LmsApplication.cpp | 1 + src/lms/ui/PlayQueue.cpp | 13 ++++++---- src/lms/ui/PlayQueue.hpp | 3 +-- src/lms/ui/common/LoadingIndicator.cpp | 36 ++++++++++++++++++++++++++ src/lms/ui/common/LoadingIndicator.hpp | 28 ++++++++++++++++++++ src/lms/ui/explore/ArtistsView.cpp | 10 ++++--- src/lms/ui/explore/ArtistsView.hpp | 8 +++--- src/lms/ui/explore/ReleasesView.cpp | 10 ++++--- src/lms/ui/explore/ReleasesView.hpp | 15 ++++++----- src/lms/ui/explore/TracksView.cpp | 10 ++++--- src/lms/ui/explore/TracksView.hpp | 5 +--- 20 files changed, 124 insertions(+), 51 deletions(-) create mode 100644 src/lms/ui/common/LoadingIndicator.cpp create mode 100644 src/lms/ui/common/LoadingIndicator.hpp diff --git a/approot/artists.xml b/approot/artists.xml index b5ed55b3..83d44747 100644 --- a/approot/artists.xml +++ b/approot/artists.xml @@ -13,9 +13,7 @@ ${artists} -
- ${show-more class="btn-primary Lms-show-more"} -
+ ${loading-indicator class="Lms-horizontal-center Lms-loading-indicator"} diff --git a/approot/messages.xml b/approot/messages.xml index 45e0280f..ac96275f 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -9,6 +9,7 @@ Discard Cancel Create +Loading... Login Logout Not a directory @@ -117,7 +118,6 @@ Recently added Recently played Albums -Show more Tracks Type Value diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml index e9dc699a..d28c0b35 100644 --- a/approot/messages_fr.xml +++ b/approot/messages_fr.xml @@ -9,6 +9,7 @@ Annuler Annuler Créer +Chargement... Login Quitter N'est pas un répertoire @@ -117,7 +118,6 @@ Ajoutés récemment Joués récemment Albums -Voir plus Pistes Type Valeur diff --git a/approot/playqueue.xml b/approot/playqueue.xml index e4e8ebcd..3fb28e46 100644 --- a/approot/playqueue.xml +++ b/approot/playqueue.xml @@ -16,9 +16,7 @@

${nb-tracks}

${entries} -
- ${show-more class="btn-primary Lms-show-more"} -
+ ${loading-indicator class="Lms-horizontal-center Lms-loading-indicator"}
diff --git a/approot/releases.xml b/approot/releases.xml index 3ccc42f5..288ac0a0 100644 --- a/approot/releases.xml +++ b/approot/releases.xml @@ -11,9 +11,7 @@
${releases}
-
- ${show-more class="btn-primary Lms-show-more"} -
+ ${loading-indicator class="Lms-horizontal-center Lms-loading-indicator"} diff --git a/approot/templates.xml b/approot/templates.xml index 78d826d1..2145b7f7 100644 --- a/approot/templates.xml +++ b/approot/templates.xml @@ -56,4 +56,9 @@ ${player class="Lms-player"} + + + ${tr:Lms.loading} + + diff --git a/approot/tracks.xml b/approot/tracks.xml index fe402479..ff664056 100644 --- a/approot/tracks.xml +++ b/approot/tracks.xml @@ -12,9 +12,7 @@ ${play-btn}${add-btn} ${tracks} -
- ${show-more class="btn-primary Lms-show-more"} -
+ ${loading-indicator class="Lms-horizontal-center Lms-loading-indicator"} diff --git a/docroot/css/lms.css b/docroot/css/lms.css index 3fecc982..b47302bb 100644 --- a/docroot/css/lms.css +++ b/docroot/css/lms.css @@ -259,6 +259,11 @@ a.Lms-artistname:hover, a.Lms-artistname:focus { justify-content: center; } +.Lms-loading-indicator { + margin-top: 8px; + margin-bottom: 8px; +} + .Lms-navbar-search { max-width: 175px; padding-top: 7.5px; @@ -478,9 +483,4 @@ a.Lms-releasename:hover, a.Lms-releasename:focus { padding-bottom: 100%; } -.Lms-show-more { - margin-top: 8px; - margin-bottom: 8px; -} - diff --git a/src/lms/CMakeLists.txt b/src/lms/CMakeLists.txt index 40fb59bf..beb8ac4a 100644 --- a/src/lms/CMakeLists.txt +++ b/src/lms/CMakeLists.txt @@ -15,6 +15,7 @@ add_executable(lms ui/admin/UserView.cpp ui/admin/UsersView.cpp ui/common/AuthModeModel.cpp + ui/common/LoadingIndicator.cpp ui/common/Validators.cpp ui/explore/ArtistListHelpers.cpp ui/explore/ArtistView.cpp diff --git a/src/lms/ui/LmsApplication.cpp b/src/lms/ui/LmsApplication.cpp index 86fc7a15..250ceaab 100644 --- a/src/lms/ui/LmsApplication.cpp +++ b/src/lms/ui/LmsApplication.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include diff --git a/src/lms/ui/PlayQueue.cpp b/src/lms/ui/PlayQueue.cpp index 04a895b7..f7c14424 100644 --- a/src/lms/ui/PlayQueue.cpp +++ b/src/lms/ui/PlayQueue.cpp @@ -32,6 +32,7 @@ #include "utils/Service.hpp" #include "utils/String.hpp" +#include "common/LoadingIndicator.hpp" #include "resource/ImageResource.hpp" #include "LmsApplication.hpp" #include "MediaPlayer.hpp" @@ -66,10 +67,12 @@ PlayQueue::PlayQueue() _entriesContainer = bindNew("entries"); - _showMore = bindNew("show-more", Wt::WString::tr("Lms.Explore.show-more")); - _showMore->setHidden(true); - _showMore->clicked().connect([=] + _loadingIndicator = bindWidget("loading-indicator", createLoadingIndicator()); + _loadingIndicator->scrollVisibilityChanged().connect([this](bool visible) { + if (!visible) + return; + addSome(); updateCurrentTrack(true); }); @@ -202,7 +205,7 @@ PlayQueue::clearTracks() getTrackList().modify()->clear(); } - _showMore->setHidden(true); + _loadingIndicator->setHidden(true); _entriesContainer->clear(); updateInfo(); } @@ -447,7 +450,7 @@ PlayQueue::addSome() } - _showMore->setHidden(static_cast(_entriesContainer->count()) >= tracklist->getCount()); + _loadingIndicator->setHidden(static_cast(_entriesContainer->count()) >= tracklist->getCount()); } void diff --git a/src/lms/ui/PlayQueue.hpp b/src/lms/ui/PlayQueue.hpp index a2088ecd..ed417656 100644 --- a/src/lms/ui/PlayQueue.hpp +++ b/src/lms/ui/PlayQueue.hpp @@ -22,7 +22,6 @@ #include #include -#include #include #include @@ -84,7 +83,7 @@ class PlayQueue : public Wt::WTemplate bool _mediaPlayerSettingsLoaded {}; Database::IdType _tracklistId {}; Wt::WContainerWidget* _entriesContainer {}; - Wt::WPushButton* _showMore {}; + Wt::WTemplate* _loadingIndicator {}; Wt::WText* _nbTracks {}; Wt::WText* _repeatBtn {}; Wt::WText* _radioBtn {}; diff --git a/src/lms/ui/common/LoadingIndicator.cpp b/src/lms/ui/common/LoadingIndicator.cpp new file mode 100644 index 00000000..d3063f1a --- /dev/null +++ b/src/lms/ui/common/LoadingIndicator.cpp @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2020 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 "LoadingIndicator.hpp" + +namespace UserInterface +{ + std::unique_ptr + createLoadingIndicator() + { + auto res {std::make_unique(Wt::WString::tr("Lms.LoadingIndicator.template"))}; + + res->addFunction("tr", &Wt::WTemplate::Functions::tr); + res->setScrollVisibilityMargin(200); + res->setScrollVisibilityEnabled(true); + + return res; + } +} + diff --git a/src/lms/ui/common/LoadingIndicator.hpp b/src/lms/ui/common/LoadingIndicator.hpp new file mode 100644 index 00000000..e118f567 --- /dev/null +++ b/src/lms/ui/common/LoadingIndicator.hpp @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2020 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 + +namespace UserInterface +{ + std::unique_ptr createLoadingIndicator(); +} + diff --git a/src/lms/ui/explore/ArtistsView.cpp b/src/lms/ui/explore/ArtistsView.cpp index a2fae1f5..5214c142 100644 --- a/src/lms/ui/explore/ArtistsView.cpp +++ b/src/lms/ui/explore/ArtistsView.cpp @@ -29,6 +29,7 @@ #include "database/TrackList.hpp" #include "utils/Logger.hpp" +#include "common/LoadingIndicator.hpp" #include "ArtistListHelpers.hpp" #include "LmsApplication.hpp" #include "Filters.hpp" @@ -76,9 +77,12 @@ Artists::Artists(Filters* filters) _container = bindNew("artists"); - _showMore = bindNew("show-more", Wt::WString::tr("Lms.Explore.show-more")); - _showMore->clicked().connect([=] + _loadingIndicator = bindWidget("loading-indicator", createLoadingIndicator()); + _loadingIndicator->scrollVisibilityChanged().connect([this](bool visible) { + if (!visible) + return; + addSome(); }); @@ -203,7 +207,7 @@ Artists::addSome() _container->addWidget(ArtistListHelpers::createEntry(artist)); } - _showMore->setHidden(!moreResults); + _loadingIndicator->setHidden(!moreResults); } } // namespace UserInterface diff --git a/src/lms/ui/explore/ArtistsView.hpp b/src/lms/ui/explore/ArtistsView.hpp index 546290eb..4169f74d 100644 --- a/src/lms/ui/explore/ArtistsView.hpp +++ b/src/lms/ui/explore/ArtistsView.hpp @@ -24,9 +24,7 @@ #include #include -#include #include -#include #include "database/Types.hpp" @@ -67,15 +65,15 @@ class Artists : public Wt::WTemplate { {Mode::Random, batchSize * 4}, {Mode::RecentlyPlayed, batchSize * 4}, - {Mode::RecentlyAdded, batchSize * 2}, - {Mode::MostPlayed, batchSize * 2}, + {Mode::RecentlyAdded, batchSize * 4}, + {Mode::MostPlayed, batchSize * 4}, {Mode::All, batchSize * 50}, }; Mode _mode {defaultMode}; std::vector _randomArtists; Filters* _filters {}; - Wt::WPushButton* _showMore {}; + Wt::WTemplate* _loadingIndicator {}; Wt::WContainerWidget* _container {}; Wt::WComboBox* _linkType {}; }; diff --git a/src/lms/ui/explore/ReleasesView.cpp b/src/lms/ui/explore/ReleasesView.cpp index a6a4e254..8489a1f8 100644 --- a/src/lms/ui/explore/ReleasesView.cpp +++ b/src/lms/ui/explore/ReleasesView.cpp @@ -32,6 +32,7 @@ #include "utils/Logger.hpp" #include "utils/String.hpp" +#include "common/LoadingIndicator.hpp" #include "resource/ImageResource.hpp" #include "ReleaseListHelpers.hpp" #include "Filters.hpp" @@ -79,9 +80,12 @@ _filters {filters} _container = bindNew("releases"); - _showMore = bindNew("show-more", Wt::WString::tr("Lms.Explore.show-more")); - _showMore->clicked().connect([this] + _loadingIndicator = bindWidget("loading-indicator", createLoadingIndicator()); + _loadingIndicator->scrollVisibilityChanged().connect([this](bool visible) { + if (!visible) + return; + addSome(); }); @@ -118,7 +122,7 @@ Releases::addSome() _container->addWidget(ReleaseListHelpers::createEntry(release)); } - _showMore->setHidden(!moreResults); + _loadingIndicator->setHidden(!moreResults); } std::vector diff --git a/src/lms/ui/explore/ReleasesView.hpp b/src/lms/ui/explore/ReleasesView.hpp index e873d2f4..21f9a5dc 100644 --- a/src/lms/ui/explore/ReleasesView.hpp +++ b/src/lms/ui/explore/ReleasesView.hpp @@ -22,7 +22,6 @@ #include #include -#include #include #include "database/Types.hpp" @@ -57,19 +56,21 @@ class Releases : public Wt::WTemplate void refreshView(); void refreshView(Mode mode); + void addSome(); std::vector> getReleases(std::optional range, bool& moreResults); std::vector> getRandomReleases(std::optional range, bool& moreResults); std::vector getAllReleases(); static constexpr Mode defaultMode {Mode::Random}; - static constexpr std::size_t batchSize {18}; + static constexpr std::size_t maxItemsPerLine {6}; + static constexpr std::size_t batchSize {maxItemsPerLine * 3}; static inline std::unordered_map> maxItemsPerMode { - {Mode::Random, batchSize * 6}, - {Mode::RecentlyPlayed, batchSize * 3}, - {Mode::RecentlyAdded, batchSize * 2}, - {Mode::MostPlayed, batchSize * 2}, + {Mode::Random, batchSize * 10}, + {Mode::RecentlyPlayed, batchSize * 10}, + {Mode::RecentlyAdded, batchSize * 10}, + {Mode::MostPlayed, batchSize * 10}, {Mode::All, batchSize * 30}, }; @@ -77,7 +78,7 @@ class Releases : public Wt::WTemplate Filters* _filters {}; std::vector _randomReleases; Wt::WContainerWidget* _container {}; - Wt::WPushButton* _showMore {}; + Wt::WTemplate* _loadingIndicator {}; }; } // namespace UserInterface diff --git a/src/lms/ui/explore/TracksView.cpp b/src/lms/ui/explore/TracksView.cpp index db852f48..701c7ff0 100644 --- a/src/lms/ui/explore/TracksView.cpp +++ b/src/lms/ui/explore/TracksView.cpp @@ -34,6 +34,7 @@ #include "resource/ImageResource.hpp" +#include "common/LoadingIndicator.hpp" #include "Filters.hpp" #include "LmsApplication.hpp" #include "MediaPlayer.hpp" @@ -83,9 +84,12 @@ _filters {filters} _tracksContainer = bindNew("tracks"); - _showMore = bindNew("show-more", Wt::WString::tr("Lms.Explore.show-more")); - _showMore->clicked().connect([this] + _loadingIndicator = bindWidget("loading-indicator", createLoadingIndicator()); + _loadingIndicator->scrollVisibilityChanged().connect([this](bool visible) { + if (!visible) + return; + addSome(); }); @@ -206,7 +210,7 @@ Tracks::addSome() _tracksContainer->addWidget(TrackListHelpers::createEntry(track)); } - _showMore->setHidden(!moreResults); + _loadingIndicator->setHidden(!moreResults); } } // namespace UserInterface diff --git a/src/lms/ui/explore/TracksView.hpp b/src/lms/ui/explore/TracksView.hpp index 430f8a41..eba94b06 100644 --- a/src/lms/ui/explore/TracksView.hpp +++ b/src/lms/ui/explore/TracksView.hpp @@ -22,8 +22,6 @@ #include #include -#include -#include #include #include "database/Types.hpp" @@ -78,8 +76,7 @@ class Tracks : public Wt::WTemplate Filters* _filters {}; std::vector _randomTracks; Wt::WContainerWidget* _tracksContainer {}; - Wt::WPushButton* _showMore {}; - Wt::WLineEdit* _search {}; + Wt::WTemplate* _loadingIndicator {}; }; } // namespace UserInterface From d0826daa419a3935af32389040138c3b730f0f63 Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 25 Jul 2020 14:58:02 +0200 Subject: [PATCH 02/14] Can now interrupt/relaunch scan + scan force from the web interface. ref #32 --- approot/admin-database.xml | 39 +--------- approot/admin-scannercontroller.xml | 44 +++++++++++ approot/messages.xml | 35 +++++---- approot/messages_fr.xml | 37 +++++---- src/libs/scanner/impl/MediaScanner.cpp | 77 ++++++++++--------- src/libs/scanner/impl/MediaScanner.hpp | 20 ++--- .../scanner/include/scanner/IMediaScanner.hpp | 13 ++-- src/libs/subsonic/impl/Scan.cpp | 2 +- src/lms/CMakeLists.txt | 2 +- src/lms/ui/LmsApplication.cpp | 1 + src/lms/ui/admin/DatabaseSettingsView.cpp | 8 +- ...tabaseStatus.cpp => ScannerController.cpp} | 56 +++++++++----- ...tabaseStatus.hpp => ScannerController.hpp} | 17 ++-- 13 files changed, 197 insertions(+), 154 deletions(-) create mode 100644 approot/admin-scannercontroller.xml rename src/lms/ui/admin/{DatabaseStatus.cpp => ScannerController.cpp} (68%) rename src/lms/ui/admin/{DatabaseStatus.hpp => ScannerController.hpp} (83%) diff --git a/approot/admin-database.xml b/approot/admin-database.xml index 4e01c81b..91e9347b 100644 --- a/approot/admin-database.xml +++ b/approot/admin-database.xml @@ -60,7 +60,7 @@
- ${apply-btn class="btn-primary"} ${discard-btn} ${immediate-scan-btn class="btn-info"} + ${apply-btn class="btn-primary"} ${discard-btn}
@@ -69,42 +69,7 @@
-
-
${tr:Lms.Admin.Database.Status.status}
-
- ${status} -
-
-
-
- - - -
-
- -
-
- ${status} -
-
-
-
- -
-
- ${last-scan} -
-
-
-
-
- ${btn-report class="btn"} -
+ ${scanner-controller}
diff --git a/approot/admin-scannercontroller.xml b/approot/admin-scannercontroller.xml new file mode 100644 index 00000000..234e298c --- /dev/null +++ b/approot/admin-scannercontroller.xml @@ -0,0 +1,44 @@ + + + + +
+
${tr:Lms.Admin.ScannerController.scanner}
+
+
+
+ +
+
+ ${status} +
+
+
+
+ +
+
+ ${last-scan} +
+
+
+
+
+ ${btn-report class="btn btn-xs"} +
+
+
+
+ ${btn-action} +
+
+
+
+
+
+ +
diff --git a/approot/messages.xml b/approot/messages.xml index ac96275f..eb64a2f4 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -54,22 +54,25 @@ Update start time Weekly -Cannot get track duration -Cannot parse file -Cannot read file -{1} duplicate files: -{1} errors: -Get report -Last scan -Not available -Scanned {1} files in {2} on {3} ({4} errors, {5} duplicates) -No audio track -Duplicated file hash -Duplicated MBID -Status -Not scheduled -Scheduled on {1} -Scanning {1} of {2} files ({3} %) +Cannot get track duration +Cannot parse file +Cannot read file +{1} duplicate files: +{1} errors: +Force scan now +Get report +Last scan +Not available +Scanned {1} files in {2} on {3} ({4} errors, {5} duplicates) +No audio track +Duplicated file hash +Duplicated MBID +Scan now +Scanner +Status +Not scheduled +Scheduled on {1} +Scanning {1} of {2} files ({3} %) New user diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml index d28c0b35..fa3ffb37 100644 --- a/approot/messages_fr.xml +++ b/approot/messages_fr.xml @@ -54,22 +54,25 @@ Heure de départ de la mise à jour Toutes les semaines -Impossible de récupérer la durée de la piste -Impossible d'analyser le fichier -Impossible de lire le fichier -{1} fichiers dupliqués : -{1} erreurs : -Rapport -Dernier scan -Non disponible -{1} fichiers scannés en {2} le {3} ({4} erreurs, {5} duplicatas) -Pas de piste audio -Hash dupliqué -MBID dupliqué -Statut -Non planifié -Planifié le {1} -Scan de {1} fichiers sur {2} ({3} %) +Impossible de récupérer la durée de la piste +Impossible d'analyser le fichier +Impossible de lire le fichier +{1} fichiers dupliqués : +{1} erreurs : +Lancer un scan forcé +Rapport +Dernier scan +Non disponible +{1} fichiers scannés en {2} le {3} ({4} erreurs, {5} duplicatas) +Pas de piste audio +Hash dupliqué +MBID dupliqué +Lancer un scan +Scanner +Statut +Non planifié +Planifié le {1} +Scan de {1} fichiers sur {2} ({3} %) Ajouter @@ -132,7 +135,7 @@ Artistes d'album -Albums similaires +Albums similaires Copyright Disque {1} diff --git a/src/libs/scanner/impl/MediaScanner.cpp b/src/libs/scanner/impl/MediaScanner.cpp index 0529ff4a..ccc83e34 100644 --- a/src/libs/scanner/impl/MediaScanner.cpp +++ b/src/libs/scanner/impl/MediaScanner.cpp @@ -257,49 +257,51 @@ MediaScanner::MediaScanner(Database::Db& db) } MediaScanner::~MediaScanner() -{ - if (_running) - stop(); -} - -void -MediaScanner::restart(void) { stop(); - start(); } void -MediaScanner::start(void) +MediaScanner::start() { - _running = true; - scheduleNextScan(); _ioService.start(); } void -MediaScanner::stop(void) +MediaScanner::stop() { - _running = false; + _abortScan = true; _scheduleTimer.cancel(); - _ioService.stop(); } void -MediaScanner::requestImmediateScan() +MediaScanner::abortScan() { + _abortScan = true; + + _scheduleTimer.cancel(); + _ioService.stop(); + + _abortScan = false; + _ioService.start(); +} + +void +MediaScanner::requestImmediateScan(bool force) +{ + abortScan(); _ioService.post([=]() { - scheduleScan(); + scheduleScan(force); }); } void -MediaScanner::requestReschedule() +MediaScanner::requestReload() { _ioService.post([=]() { @@ -308,7 +310,7 @@ MediaScanner::requestReschedule() } MediaScanner::Status -MediaScanner::getStatus() +MediaScanner::getStatus() const { Status res; @@ -365,7 +367,7 @@ MediaScanner::scheduleNextScan() if (nextScanDate.isValid()) { nextScanDateTime = Wt::WDateTime {nextScanDate, _startTime}; - scheduleScan(nextScanDateTime); + scheduleScan(false, nextScanDateTime); } { @@ -384,7 +386,7 @@ MediaScanner::countAllFiles(ScanStats& stats) exploreFilesRecursive(_mediaDirectory, [&](std::error_code ec, const std::filesystem::path& path) { - if (!_running) + if (_abortScan) return false; if (!ec && isFileSupported(path, _fileExtensions)) @@ -398,13 +400,21 @@ MediaScanner::countAllFiles(ScanStats& stats) } void -MediaScanner::scheduleScan(const Wt::WDateTime& dateTime) +MediaScanner::scheduleScan(bool force, const Wt::WDateTime& dateTime) { + auto cb {[=](boost::system::error_code ec) + { + if (ec) + return; + + scan(force); + }}; + if (dateTime.isNull()) { LMS_LOG(DBUPDATER, INFO) << "Scheduling next scan right now"; - _scheduleTimer.expires_from_now(std::chrono::seconds(0)); - _scheduleTimer.async_wait(std::bind(&MediaScanner::scan, this, std::placeholders::_1)); + _scheduleTimer.expires_from_now(std::chrono::seconds {0}); + _scheduleTimer.async_wait(cb); } else { @@ -413,16 +423,13 @@ MediaScanner::scheduleScan(const Wt::WDateTime& dateTime) LMS_LOG(DBUPDATER, INFO) << "Scheduling next scan at " << std::string(std::ctime(&t)); _scheduleTimer.expires_at(timePoint); - _scheduleTimer.async_wait(std::bind(&MediaScanner::scan, this, std::placeholders::_1)); + _scheduleTimer.async_wait(cb); } } void -MediaScanner::scan(boost::system::error_code err) +MediaScanner::scan(bool forceScan) { - if (err) - return; - scanStarted().emit(); { @@ -438,8 +445,6 @@ MediaScanner::scan(boost::system::error_code err) refreshScanSettings(); - bool forceScan {false}; - LMS_LOG(DBUPDATER, DEBUG) << "Counting files in media directory '" << _mediaDirectory.string() << "'..."; countAllFiles(stats); LMS_LOG(DBUPDATER, DEBUG) << "-> Nb files = " << stats.filesToScan; @@ -454,17 +459,17 @@ MediaScanner::scan(boost::system::error_code err) removeOrphanEntries(); - if (_running) + if (!_abortScan) checkDuplicatedAudioFiles(stats); // Now update all the track features if needed fetchTrackFeatures(stats); - LMS_LOG(DBUPDATER, INFO) << "Scan " << (_running ? "complete" : "aborted") << ". Changes = " << stats.nbChanges() << " (added = " << stats.additions << ", removed = " << stats.deletions << ", updated = " << stats.updates << "), Not changed = " << stats.skips << ", Scanned = " << stats.scans << " (errors = " << stats.errors.size() << "), features fetched = " << stats.featuresFetched << "/" << stats.featuresToFetch <<", duplicates = " << stats.duplicates.size(); + LMS_LOG(DBUPDATER, INFO) << "Scan " << (_abortScan ? "aborted" : "complete") << ". Changes = " << stats.nbChanges() << " (added = " << stats.additions << ", removed = " << stats.deletions << ", updated = " << stats.updates << "), Not changed = " << stats.skips << ", Scanned = " << stats.scans << " (errors = " << stats.errors.size() << "), features fetched = " << stats.featuresFetched << "/" << stats.featuresToFetch <<", duplicates = " << stats.duplicates.size(); _dbSession.optimize(); - if (_running) + if (!_abortScan) { stats.stopTime = Wt::WLocalDateTime::currentDateTime().toUTC(); { @@ -546,7 +551,7 @@ MediaScanner::fetchTrackFeatures(ScanStats& stats) for (const TrackInfo& trackToFetch : tracksToFetch) { - if (!_running) + if (_abortScan) return; if (fetchTrackFeatures(trackToFetch.id, trackToFetch.mbid)) @@ -774,7 +779,7 @@ MediaScanner::scanMediaDirectory(const std::filesystem::path& mediaDirectory, bo { exploreFilesRecursive(mediaDirectory, [&](std::error_code ec, const std::filesystem::path& path) { - if (!_running) + if (_abortScan) return false; if (ec) @@ -861,7 +866,7 @@ MediaScanner::removeMissingTracks(ScanStats& stats) for (const auto& [trackId, trackPath] : trackPaths) { - if (!_running) + if (_abortScan) return; if (!checkFile(trackPath, _mediaDirectory, _fileExtensions)) diff --git a/src/libs/scanner/impl/MediaScanner.hpp b/src/libs/scanner/impl/MediaScanner.hpp index 8c19defb..3a2c737e 100644 --- a/src/libs/scanner/impl/MediaScanner.hpp +++ b/src/libs/scanner/impl/MediaScanner.hpp @@ -52,12 +52,10 @@ class MediaScanner : public IMediaScanner void start() override; void stop() override; - void restart() override; + void requestReload() override; + void requestImmediateScan(bool force) override; - void requestImmediateScan() override; - void requestReschedule() override ; - - Status getStatus() override; + Status getStatus() const override; Wt::Signal<>& scanStarted() override { return _sigScanStarted; } Wt::Signal<>& scanComplete() override { return _sigScanComplete; } @@ -68,10 +66,12 @@ class MediaScanner : public IMediaScanner // Job handling void scheduleNextScan(); - void scheduleScan(const Wt::WDateTime& dateTime = {}); + void scheduleScan(bool force, const Wt::WDateTime& dateTime = {}); + + void abortScan(); // Update database (scheduled callback) - void scan(boost::system::error_code ec); + void scan(bool force); void scanMediaDirectory( const std::filesystem::path& mediaDirectory, bool forceScan, ScanStats& stats); bool fetchTrackFeatures(Database::IdType trackId, const UUID& MBID); @@ -89,7 +89,9 @@ class MediaScanner : public IMediaScanner void notifyInProgressIfNeeded(const ScanStats& stats); void notifyInProgress(const ScanStats& stats); - bool _running {}; + std::mutex _scanInProgress {}; + std::atomic _abortScan {}; + Wt::WIOService _ioService; boost::asio::system_timer _scheduleTimer {_ioService}; Wt::Signal<> _sigScanStarted; @@ -100,7 +102,7 @@ class MediaScanner : public IMediaScanner Database::Session _dbSession; std::unique_ptr _metadataParser; - std::shared_mutex _statusMutex; + mutable std::shared_mutex _statusMutex; State _curState {State::NotScheduled}; std::optional _lastCompleteScanStats; std::optional _inProgressScanStats; diff --git a/src/libs/scanner/include/scanner/IMediaScanner.hpp b/src/libs/scanner/include/scanner/IMediaScanner.hpp index 1c0d3fdc..c70358c4 100644 --- a/src/libs/scanner/include/scanner/IMediaScanner.hpp +++ b/src/libs/scanner/include/scanner/IMediaScanner.hpp @@ -40,11 +40,10 @@ class IMediaScanner virtual void start() = 0; virtual void stop() = 0; - virtual void restart() = 0; // Async requests - virtual void requestImmediateScan() = 0; - virtual void requestReschedule() = 0; + virtual void requestReload() = 0; + virtual void requestImmediateScan(bool force) = 0; enum class State @@ -56,13 +55,13 @@ class IMediaScanner struct Status { - State currentState {State::NotScheduled}; - Wt::WDateTime nextScheduledScan; - std::optional lastCompleteScanStats; + State currentState {State::NotScheduled}; + Wt::WDateTime nextScheduledScan; + std::optional lastCompleteScanStats; std::optional inProgressScanStats; }; - virtual Status getStatus() = 0; + virtual Status getStatus() const = 0; // Called just after scan start virtual Wt::Signal<>& scanStarted() = 0; diff --git a/src/libs/subsonic/impl/Scan.cpp b/src/libs/subsonic/impl/Scan.cpp index 70b2deb0..46231758 100644 --- a/src/libs/subsonic/impl/Scan.cpp +++ b/src/libs/subsonic/impl/Scan.cpp @@ -54,7 +54,7 @@ namespace API::Subsonic::Scan Response handleStartScan(RequestContext& context) { - ServiceProvider::get()->requestImmediateScan(); + ServiceProvider::get()->requestImmediateScan(false); Response response {Response::createOkResponse(context)}; response.addNode("scanStatus", createStatusResponseNode()); diff --git a/src/lms/CMakeLists.txt b/src/lms/CMakeLists.txt index beb8ac4a..269986c3 100644 --- a/src/lms/CMakeLists.txt +++ b/src/lms/CMakeLists.txt @@ -10,7 +10,7 @@ add_executable(lms ui/SettingsView.cpp ui/TrackStringUtils.cpp ui/admin/DatabaseSettingsView.cpp - ui/admin/DatabaseStatus.cpp + ui/admin/ScannerController.cpp ui/admin/InitWizardView.cpp ui/admin/UserView.cpp ui/admin/UsersView.cpp diff --git a/src/lms/ui/LmsApplication.cpp b/src/lms/ui/LmsApplication.cpp index 250ceaab..c9efc903 100644 --- a/src/lms/ui/LmsApplication.cpp +++ b/src/lms/ui/LmsApplication.cpp @@ -123,6 +123,7 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env, // Add a resource bundle messageResourceBundle().use(appRoot() + "admin-database"); messageResourceBundle().use(appRoot() + "admin-initwizard"); + messageResourceBundle().use(appRoot() + "admin-scannercontroller"); messageResourceBundle().use(appRoot() + "admin-user"); messageResourceBundle().use(appRoot() + "admin-users"); messageResourceBundle().use(appRoot() + "artist"); diff --git a/src/lms/ui/admin/DatabaseSettingsView.cpp b/src/lms/ui/admin/DatabaseSettingsView.cpp index 5b612ff6..3b734728 100644 --- a/src/lms/ui/admin/DatabaseSettingsView.cpp +++ b/src/lms/ui/admin/DatabaseSettingsView.cpp @@ -34,7 +34,7 @@ #include "common/Validators.hpp" #include "common/ValueStringModel.hpp" -#include "DatabaseStatus.hpp" +#include "ScannerController.hpp" #include "LmsApplication.hpp" namespace UserInterface { @@ -220,7 +220,7 @@ DatabaseSettingsView::refreshView() Wt::WPushButton *discardBtn = t->bindWidget("discard-btn", std::make_unique(Wt::WString::tr("Lms.discard"))); Wt::WPushButton *immScanBtn = t->bindWidget("immediate-scan-btn", std::make_unique(Wt::WString::tr("Lms.Admin.Database.immediate-scan"))); - t->bindNew("status"); + t->bindNew("scanner-controller"); saveBtn->clicked().connect([=] () { @@ -230,7 +230,7 @@ DatabaseSettingsView::refreshView() { model->saveData(); - ServiceProvider::get()->requestReschedule(); + ServiceProvider::get()->requestReload(); LmsApp->notifyMsg(MsgType::Success, Wt::WString::tr("Lms.Admin.Database.settings-saved")); } @@ -247,7 +247,7 @@ DatabaseSettingsView::refreshView() immScanBtn->clicked().connect([=] () { - ServiceProvider::get()->requestImmediateScan(); + ServiceProvider::get()->requestImmediateScan(false); }); t->updateView(model.get()); diff --git a/src/lms/ui/admin/DatabaseStatus.cpp b/src/lms/ui/admin/ScannerController.cpp similarity index 68% rename from src/lms/ui/admin/DatabaseStatus.cpp rename to src/lms/ui/admin/ScannerController.cpp index 1030c452..5bc6cb35 100644 --- a/src/lms/ui/admin/DatabaseStatus.cpp +++ b/src/lms/ui/admin/ScannerController.cpp @@ -17,14 +17,16 @@ * along with LMS. If not, see . */ -#include "DatabaseStatus.hpp" +#include "ScannerController.hpp" #include #include #include +#include #include #include +#include #include "utils/Service.hpp" #include "LmsApplication.hpp" @@ -66,7 +68,7 @@ class ReportResource : public Wt::WResource void handleRequest(const Wt::Http::Request&, Wt::Http::Response& response) { - response.out() << Wt::WString::tr("Lms.Admin.Database.Status.errors-header").arg(_stats.errors.size()).toUTF8() << std::endl; + response.out() << Wt::WString::tr("Lms.Admin.ScannerController.errors-header").arg(_stats.errors.size()).toUTF8() << std::endl; for (const auto& error : _stats.errors) { @@ -78,7 +80,7 @@ class ReportResource : public Wt::WResource response.out() << std::endl; - response.out() << Wt::WString::tr("Lms.Admin.Database.Status.duplicates-header").arg(_stats.duplicates.size()).toUTF8() << std::endl; + response.out() << Wt::WString::tr("Lms.Admin.ScannerController.duplicates-header").arg(_stats.duplicates.size()).toUTF8() << std::endl; for (const auto& duplicate : _stats.duplicates) response.out() << duplicate.file.string() << " - " << duplicateReasonToWString(duplicate.reason).toUTF8() << std::endl; @@ -90,10 +92,10 @@ class ReportResource : public Wt::WResource { switch (error) { - case Scanner::ScanErrorType::CannotReadFile: return Wt::WString::tr("Lms.Admin.Database.Status.cannot-read-file"); - case Scanner::ScanErrorType::CannotParseFile: return Wt::WString::tr("Lms.Admin.Database.Status.cannot-parse-file"); - case Scanner::ScanErrorType::NoAudioTrack: return Wt::WString::tr("Lms.Admin.Database.Status.no-audio-track"); - case Scanner::ScanErrorType::BadDuration: return Wt::WString::tr("Lms.Admin.Database.Status.bad-duration"); + case Scanner::ScanErrorType::CannotReadFile: return Wt::WString::tr("Lms.Admin.ScannerController.cannot-read-file"); + case Scanner::ScanErrorType::CannotParseFile: return Wt::WString::tr("Lms.Admin.ScannerController.cannot-parse-file"); + case Scanner::ScanErrorType::NoAudioTrack: return Wt::WString::tr("Lms.Admin.ScannerController.no-audio-track"); + case Scanner::ScanErrorType::BadDuration: return Wt::WString::tr("Lms.Admin.ScannerController.bad-duration"); } return "?"; } @@ -102,8 +104,8 @@ class ReportResource : public Wt::WResource { switch (reason) { - case Scanner::DuplicateReason::SameHash: return Wt::WString::tr("Lms.Admin.Database.Status.same-hash"); - case Scanner::DuplicateReason::SameMBID: return Wt::WString::tr("Lms.Admin.Database.Status.same-mbid"); + case Scanner::DuplicateReason::SameHash: return Wt::WString::tr("Lms.Admin.ScannerController.same-hash"); + case Scanner::DuplicateReason::SameMBID: return Wt::WString::tr("Lms.Admin.ScannerController.same-mbid"); } return "?"; } @@ -112,8 +114,8 @@ class ReportResource : public Wt::WResource }; -DatabaseStatus::DatabaseStatus() -: WTemplate {Wt::WString::tr("Lms.Admin.Database.Status.template")} +ScannerController::ScannerController() +: WTemplate {Wt::WString::tr("Lms.Admin.ScannerController.template")} { addFunction("tr", &Wt::WTemplate::Functions::tr); @@ -133,16 +135,34 @@ DatabaseStatus::DatabaseStatus() } void -DatabaseStatus::refreshContents() +ScannerController::refreshContents() { using namespace Scanner; - Wt::WPushButton* reportBtn {bindNew("btn-report", Wt::WString::tr("Lms.Admin.Database.Status.get-report"))}; + Wt::WPushButton* reportBtn {bindNew("btn-report", Wt::WString::tr("Lms.Admin.ScannerController.get-report"))}; + + Wt::WSplitButton* actionBtn {bindNew("btn-action")}; + actionBtn->actionButton()->addStyleClass("btn-primary"); + actionBtn->actionButton()->setText(Wt::WString::tr("Lms.Admin.ScannerController.scan-now")); + actionBtn->actionButton()->clicked().connect([] + { + ServiceProvider::get()->requestImmediateScan(false); + }); + + auto popup = std::make_unique(); + popup->addItem(Wt::WString::tr("Lms.Admin.ScannerController.force-scan-now")); + popup->itemSelected().connect([] + { + ServiceProvider::get()->requestImmediateScan(true); + }); + actionBtn->dropDownButton()->setMenu(std::move(popup)); + actionBtn->dropDownButton()->addStyleClass("btn-primary"); + const IMediaScanner::Status status {ServiceProvider::get()->getStatus()}; if (status.lastCompleteScanStats) { - bindString("last-scan", Wt::WString::tr("Lms.Admin.Database.Status.last-scan-status") + bindString("last-scan", Wt::WString::tr("Lms.Admin.ScannerController.last-scan-status") .arg(status.lastCompleteScanStats->nbFiles()) .arg(durationToString(status.lastCompleteScanStats->startTime, status.lastCompleteScanStats->stopTime)) .arg(status.lastCompleteScanStats->stopTime.toString()) @@ -156,7 +176,7 @@ DatabaseStatus::refreshContents() } else { - bindString("last-scan", Wt::WString::tr("Lms.Admin.Database.Status.last-scan-not-available")); + bindString("last-scan", Wt::WString::tr("Lms.Admin.ScannerController.last-scan-not-available")); reportBtn->setEnabled(false); } @@ -164,16 +184,16 @@ DatabaseStatus::refreshContents() switch (status.currentState) { case IMediaScanner::State::NotScheduled: - bindString("status", Wt::WString::tr("Lms.Admin.Database.Status.status-not-scheduled")); + bindString("status", Wt::WString::tr("Lms.Admin.ScannerController.status-not-scheduled")); break; case IMediaScanner::State::Scheduled: - bindString("status", Wt::WString::tr("Lms.Admin.Database.Status.status-scheduled") + bindString("status", Wt::WString::tr("Lms.Admin.ScannerController.status-scheduled") .arg(status.nextScheduledScan.toString())); break; case IMediaScanner::State::InProgress: { std::ostringstream oss; - bindString("status", Wt::WString::tr("Lms.Admin.Database.Status.status-in-progress") + bindString("status", Wt::WString::tr("Lms.Admin.ScannerController.status-in-progress") .arg(status.inProgressScanStats->processedFiles) .arg(status.inProgressScanStats->filesToScan) .arg(status.inProgressScanStats->progress())); diff --git a/src/lms/ui/admin/DatabaseStatus.hpp b/src/lms/ui/admin/ScannerController.hpp similarity index 83% rename from src/lms/ui/admin/DatabaseStatus.hpp rename to src/lms/ui/admin/ScannerController.hpp index d970ca36..498d28fe 100644 --- a/src/lms/ui/admin/DatabaseStatus.hpp +++ b/src/lms/ui/admin/ScannerController.hpp @@ -21,16 +21,17 @@ #include -namespace UserInterface { - -class DatabaseStatus : public Wt::WTemplate +namespace UserInterface { - public: - DatabaseStatus(); - private: - void refreshContents(); -}; + class ScannerController : public Wt::WTemplate + { + public: + ScannerController(); + + private: + void refreshContents(); + }; } // namespace DatabaseStatus From 8f236c16cae07716a45271798a3a6e6dc2b5fa8a Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 25 Jul 2020 16:42:02 +0200 Subject: [PATCH 03/14] Added more details during scan. fixed #32 --- approot/admin-scannercontroller.xml | 1 + approot/messages.xml | 6 +- approot/messages_fr.xml | 6 +- src/libs/scanner/impl/MediaScanner.cpp | 78 +++++++++++++------ src/libs/scanner/impl/MediaScanner.hpp | 13 ++-- src/libs/scanner/impl/MediaScannerStats.cpp | 10 +-- .../scanner/include/scanner/IMediaScanner.hpp | 4 +- .../include/scanner/MediaScannerStats.hpp | 30 ++++--- src/libs/subsonic/impl/Scan.cpp | 11 ++- src/lms/ui/LmsApplication.cpp | 4 +- src/lms/ui/LmsApplication.hpp | 2 +- src/lms/ui/admin/ScannerController.cpp | 35 +++++++-- 12 files changed, 136 insertions(+), 64 deletions(-) diff --git a/approot/admin-scannercontroller.xml b/approot/admin-scannercontroller.xml index 234e298c..484fc64a 100644 --- a/approot/admin-scannercontroller.xml +++ b/approot/admin-scannercontroller.xml @@ -13,6 +13,7 @@
${status} +
${step-status}
diff --git a/approot/messages.xml b/approot/messages.xml index eb64a2f4..e155a61e 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -72,7 +72,11 @@ Status Not scheduled Scheduled on {1} -Scanning {1} of {2} files ({3} %) +Scanning: step {1}/{2} +Checking removed files... {1}% +Discovering files: {1} files +Fetching track features from AcousticBrainz: {1}/{2} tracks ({3}%)... +Scanning files: {1}/{2} files ({3}%)... New user diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml index fa3ffb37..4182355f 100644 --- a/approot/messages_fr.xml +++ b/approot/messages_fr.xml @@ -72,7 +72,11 @@ Statut Non planifié Planifié le {1} -Scan de {1} fichiers sur {2} ({3} %) +En cours de scan : étape {1}/{2} +Vérification des fichiers... {1}% +Découverte des fichiers: {1} fichiers +Récupération des métadonnées AcousticBrainz: {1}/{2} fichiers ({3}%)... +Scan des fichiers: {1}/{2} fichiers ({3}%)... Ajouter diff --git a/src/libs/scanner/impl/MediaScanner.cpp b/src/libs/scanner/impl/MediaScanner.cpp index ccc83e34..70e478dc 100644 --- a/src/libs/scanner/impl/MediaScanner.cpp +++ b/src/libs/scanner/impl/MediaScanner.cpp @@ -264,6 +264,8 @@ MediaScanner::~MediaScanner() void MediaScanner::start() { + std::scoped_lock lock {_controlMutex}; + scheduleNextScan(); _ioService.start(); @@ -272,6 +274,8 @@ MediaScanner::start() void MediaScanner::stop() { + std::scoped_lock lock {_controlMutex}; + _abortScan = true; _scheduleTimer.cancel(); @@ -281,10 +285,15 @@ MediaScanner::stop() void MediaScanner::abortScan() { - _abortScan = true; + LMS_LOG(DBUPDATER, DEBUG) << "Aborting scan..."; + std::scoped_lock lock {_controlMutex}; + LMS_LOG(DBUPDATER, DEBUG) << "Waiting for the scan to abort..."; + + _abortScan = true; _scheduleTimer.cancel(); _ioService.stop(); + LMS_LOG(DBUPDATER, DEBUG) << "Scan abort done!"; _abortScan = false; _ioService.start(); @@ -303,6 +312,7 @@ MediaScanner::requestImmediateScan(bool force) void MediaScanner::requestReload() { + abortScan(); _ioService.post([=]() { scheduleNextScan(); @@ -319,7 +329,7 @@ MediaScanner::getStatus() const res.currentState = _curState; res.nextScheduledScan = _nextScheduledScan; res.lastCompleteScanStats = _lastCompleteScanStats; - res.inProgressScanStats = _inProgressScanStats; + res.currentScanStepStats = _currentScanStepStats; return res; } @@ -382,7 +392,10 @@ MediaScanner::scheduleNextScan() void MediaScanner::countAllFiles(ScanStats& stats) { - stats.filesToScan = 0; + ScanStepStats stepStats{stats.startTime, ScanProgressStep::DiscoveringFiles}; + + stats.filesScanned = 0; + notifyInProgress(stepStats); exploreFilesRecursive(_mediaDirectory, [&](std::error_code ec, const std::filesystem::path& path) { @@ -391,8 +404,9 @@ MediaScanner::countAllFiles(ScanStats& stats) if (!ec && isFileSupported(path, _fileExtensions)) { - stats.filesToScan++; - notifyInProgressIfNeeded(stats); + stats.filesScanned++; + stepStats.processedFiles++; + notifyInProgressIfNeeded(stepStats); } return true; @@ -445,11 +459,11 @@ MediaScanner::scan(bool forceScan) refreshScanSettings(); + removeMissingTracks(stats); + LMS_LOG(DBUPDATER, DEBUG) << "Counting files in media directory '" << _mediaDirectory.string() << "'..."; countAllFiles(stats); - LMS_LOG(DBUPDATER, DEBUG) << "-> Nb files = " << stats.filesToScan; - - removeMissingTracks(stats); + LMS_LOG(DBUPDATER, DEBUG) << "-> Nb files = " << stats.filesScanned; LMS_LOG(UI, INFO) << "Checks complete, force scan = " << forceScan; @@ -465,7 +479,7 @@ MediaScanner::scan(bool forceScan) // Now update all the track features if needed fetchTrackFeatures(stats); - LMS_LOG(DBUPDATER, INFO) << "Scan " << (_abortScan ? "aborted" : "complete") << ". Changes = " << stats.nbChanges() << " (added = " << stats.additions << ", removed = " << stats.deletions << ", updated = " << stats.updates << "), Not changed = " << stats.skips << ", Scanned = " << stats.scans << " (errors = " << stats.errors.size() << "), features fetched = " << stats.featuresFetched << "/" << stats.featuresToFetch <<", duplicates = " << stats.duplicates.size(); + LMS_LOG(DBUPDATER, INFO) << "Scan " << (_abortScan ? "aborted" : "complete") << ". Changes = " << stats.nbChanges() << " (added = " << stats.additions << ", removed = " << stats.deletions << ", updated = " << stats.updates << "), Not changed = " << stats.skips << ", Scanned = " << stats.scans << " (errors = " << stats.errors.size() << "), features fetched = " << stats.featuresFetched << ", duplicates = " << stats.duplicates.size(); _dbSession.optimize(); @@ -476,19 +490,22 @@ MediaScanner::scan(bool forceScan) std::unique_lock lock {_statusMutex}; _lastCompleteScanStats = std::move(stats); - _inProgressScanStats.reset(); + _currentScanStepStats.reset(); } + LMS_LOG(DBUPDATER, DEBUG) << "Scan not aborted, scheduling next scan!"; scheduleNextScan(); scanComplete().emit(); } else { + LMS_LOG(DBUPDATER, DEBUG) << "Scan aborted, not scheduling next scan!"; + std::unique_lock lock {_statusMutex}; _curState = State::NotScheduled; - _inProgressScanStats.reset(); + _currentScanStepStats.reset(); } } @@ -524,6 +541,8 @@ MediaScanner::fetchTrackFeatures(ScanStats& stats) if (_recommendationEngineType != ScanSettings::RecommendationEngineType::Features) return; + ScanStepStats stepStats{stats.startTime, ScanProgressStep::FetchingTrackFeatures}; + LMS_LOG(DBUPDATER, INFO) << "Fetching missing track features..."; struct TrackInfo @@ -545,7 +564,8 @@ MediaScanner::fetchTrackFeatures(ScanStats& stats) return res; }()}; - stats.featuresToFetch = tracksToFetch.size(); + stepStats.filesToProcess = tracksToFetch.size(); + notifyInProgress(stepStats); LMS_LOG(DBUPDATER, INFO) << "Found " << tracksToFetch.size() << " track(s) to fetch!"; @@ -557,6 +577,8 @@ MediaScanner::fetchTrackFeatures(ScanStats& stats) if (fetchTrackFeatures(trackToFetch.id, trackToFetch.mbid)) stats.featuresFetched++; + stepStats.processedFiles++; + notifyInProgressIfNeeded(stepStats); } LMS_LOG(DBUPDATER, INFO) << "Track features fetched!"; @@ -596,27 +618,25 @@ MediaScanner::refreshScanSettings() } void -MediaScanner::notifyInProgress(const ScanStats& stats) +MediaScanner::notifyInProgress(const ScanStepStats& stepStats) { - const ScanProgressStats progressStats {stats.toProgressStats()}; - { std::unique_lock lock {_statusMutex}; - _inProgressScanStats = progressStats; + _currentScanStepStats = stepStats; } const std::chrono::system_clock::time_point now {std::chrono::system_clock::now()}; - _sigScanInProgress(progressStats); + _sigScanInProgress(stepStats); _lastScanInProgressEmit = now; } void -MediaScanner::notifyInProgressIfNeeded(const ScanStats& stats) +MediaScanner::notifyInProgressIfNeeded(const ScanStepStats& stepStats) { std::chrono::system_clock::time_point now {std::chrono::system_clock::now()}; - if (std::chrono::duration_cast(now - _lastScanInProgressEmit).count() > 2) - notifyInProgress(stats); + if (std::chrono::duration_cast(now - _lastScanInProgressEmit).count() > 1) + notifyInProgress(stepStats); } void @@ -777,6 +797,10 @@ MediaScanner::scanAudioFile(const std::filesystem::path& file, bool forceScan, S void MediaScanner::scanMediaDirectory(const std::filesystem::path& mediaDirectory, bool forceScan, ScanStats& stats) { + ScanStepStats stepStats{stats.startTime, ScanProgressStep::ScanningFiles}; + stepStats.filesToProcess = stats.filesScanned; + notifyInProgress(stepStats); + exploreFilesRecursive(mediaDirectory, [&](std::error_code ec, const std::filesystem::path& path) { if (_abortScan) @@ -791,13 +815,12 @@ MediaScanner::scanMediaDirectory(const std::filesystem::path& mediaDirectory, bo { scanAudioFile(path, forceScan, stats ); - notifyInProgressIfNeeded(stats); + stepStats.processedFiles++; + notifyInProgressIfNeeded(stepStats); } return true; }); - - notifyInProgress(stats); } // Check if a file exists and is still in a media directory @@ -842,6 +865,8 @@ MediaScanner::removeMissingTracks(ScanStats& stats) { static constexpr std::size_t batchSize {50}; + ScanStepStats stepStats{stats.startTime, ScanProgressStep::CheckingRemovedFiles}; + LMS_LOG(DBUPDATER, DEBUG) << "Checking tracks to be removed..."; std::size_t trackCount {}; @@ -851,6 +876,9 @@ MediaScanner::removeMissingTracks(ScanStats& stats) } LMS_LOG(DBUPDATER, DEBUG) << trackCount << " tracks to be checked..."; + stepStats.filesToProcess = trackCount; + notifyInProgress(stepStats); + std::vector> trackPaths; std::vector tracksToRemove; @@ -859,6 +887,8 @@ MediaScanner::removeMissingTracks(ScanStats& stats) trackPaths.clear(); tracksToRemove.clear(); + stepStats.processedFiles++; + { auto transaction {_dbSession.createSharedTransaction()}; trackPaths = Track::getAllPaths(_dbSession, i, batchSize); @@ -888,7 +918,7 @@ MediaScanner::removeMissingTracks(ScanStats& stats) } } - notifyInProgressIfNeeded(stats); + notifyInProgressIfNeeded(stepStats); if (i == 0) break; diff --git a/src/libs/scanner/impl/MediaScanner.hpp b/src/libs/scanner/impl/MediaScanner.hpp index 3a2c737e..0ff66730 100644 --- a/src/libs/scanner/impl/MediaScanner.hpp +++ b/src/libs/scanner/impl/MediaScanner.hpp @@ -59,7 +59,7 @@ class MediaScanner : public IMediaScanner Wt::Signal<>& scanStarted() override { return _sigScanStarted; } Wt::Signal<>& scanComplete() override { return _sigScanComplete; } - Wt::Signal& scanInProgress() override { return _sigScanInProgress; } + Wt::Signal& scanInProgress() override { return _sigScanInProgress; } Wt::Signal& scheduled() override { return _sigScheduled; } private: @@ -86,17 +86,16 @@ class MediaScanner : public IMediaScanner void checkDuplicatedAudioFiles(ScanStats& stats); void scanAudioFile(const std::filesystem::path& file, bool forceScan, ScanStats& stats); Database::IdType doScanAudioFile(const std::filesystem::path& file, ScanStats& stats); - void notifyInProgressIfNeeded(const ScanStats& stats); - void notifyInProgress(const ScanStats& stats); + void notifyInProgressIfNeeded(const ScanStepStats& stats); + void notifyInProgress(const ScanStepStats& stats); - std::mutex _scanInProgress {}; + std::mutex _controlMutex; std::atomic _abortScan {}; - Wt::WIOService _ioService; boost::asio::system_timer _scheduleTimer {_ioService}; Wt::Signal<> _sigScanStarted; Wt::Signal<> _sigScanComplete; - Wt::Signal _sigScanInProgress; + Wt::Signal _sigScanInProgress; std::chrono::system_clock::time_point _lastScanInProgressEmit {}; Wt::Signal _sigScheduled; Database::Session _dbSession; @@ -105,7 +104,7 @@ class MediaScanner : public IMediaScanner mutable std::shared_mutex _statusMutex; State _curState {State::NotScheduled}; std::optional _lastCompleteScanStats; - std::optional _inProgressScanStats; + std::optional _currentScanStepStats; Wt::WDateTime _nextScheduledScan; // Current scan settings diff --git a/src/libs/scanner/impl/MediaScannerStats.cpp b/src/libs/scanner/impl/MediaScannerStats.cpp index a2a984da..29af4235 100644 --- a/src/libs/scanner/impl/MediaScannerStats.cpp +++ b/src/libs/scanner/impl/MediaScannerStats.cpp @@ -40,16 +40,10 @@ ScanStats::nbChanges() const return additions + deletions + updates; } -ScanProgressStats -ScanStats::toProgressStats() const -{ - return ScanProgressStats {startTime, filesToScan, nbFiles()}; -} - unsigned -ScanProgressStats::progress() const +ScanStepStats::progress() const { - return (processedFiles / static_cast(filesToScan ? filesToScan : 1)) * 100; + return (processedFiles / static_cast(filesToProcess ? filesToProcess : 1)) * 100; } } // namespace Scanner diff --git a/src/libs/scanner/include/scanner/IMediaScanner.hpp b/src/libs/scanner/include/scanner/IMediaScanner.hpp index c70358c4..83e675fb 100644 --- a/src/libs/scanner/include/scanner/IMediaScanner.hpp +++ b/src/libs/scanner/include/scanner/IMediaScanner.hpp @@ -58,7 +58,7 @@ class IMediaScanner State currentState {State::NotScheduled}; Wt::WDateTime nextScheduledScan; std::optional lastCompleteScanStats; - std::optional inProgressScanStats; + std::optional currentScanStepStats; }; virtual Status getStatus() const = 0; @@ -70,7 +70,7 @@ class IMediaScanner virtual Wt::Signal<>& scanComplete() = 0; // Called during scan in progress - virtual Wt::Signal& scanInProgress() = 0; + virtual Wt::Signal& scanInProgress() = 0; // Called after a schedule virtual Wt::Signal& scheduled() = 0; diff --git a/src/libs/scanner/include/scanner/MediaScannerStats.hpp b/src/libs/scanner/include/scanner/MediaScannerStats.hpp index 99c898b6..6b05bd95 100644 --- a/src/libs/scanner/include/scanner/MediaScannerStats.hpp +++ b/src/libs/scanner/include/scanner/MediaScannerStats.hpp @@ -56,15 +56,26 @@ namespace Scanner { }; + enum class ScanProgressStep : unsigned + { + CheckingRemovedFiles = 0, + DiscoveringFiles, + ScanningFiles, + FetchingTrackFeatures, + }; + static inline constexpr unsigned ScanProgressStepCount {4}; + // reduced scan stats - struct ScanProgressStats + struct ScanStepStats { Wt::WDateTime startTime; - std::size_t filesToScan {}; - std::size_t processedFiles {}; + ScanProgressStep currentStep; - unsigned progress() const; + std::size_t filesToProcess {}; + std::size_t processedFiles {}; + + unsigned progress() const; }; struct ScanStats @@ -72,25 +83,22 @@ namespace Scanner { Wt::WDateTime startTime; Wt::WDateTime stopTime; - std::size_t filesToScan {}; // Total number of files to be scanned (estimated) + std::size_t filesScanned {}; // Total number of files scanned (estimated) - std::size_t skips {}; // no change since last scan - std::size_t scans {}; // actually scanned filed + std::size_t skips {}; // no change since last scan + std::size_t scans {}; // actually scanned filed std::size_t additions {}; // added in DB std::size_t deletions {}; // removed from DB - std::size_t updates {}; // updated file in DB + std::size_t updates {}; // updated file in DB std::size_t featuresFetched {}; // features fetched in DB - std::size_t featuresToFetch {}; // features to be fetched in DB std::vector errors; std::vector duplicates; std::size_t nbFiles() const; std::size_t nbChanges() const; - - ScanProgressStats toProgressStats() const; }; } diff --git a/src/libs/subsonic/impl/Scan.cpp b/src/libs/subsonic/impl/Scan.cpp index 46231758..b663aed2 100644 --- a/src/libs/subsonic/impl/Scan.cpp +++ b/src/libs/subsonic/impl/Scan.cpp @@ -35,8 +35,15 @@ namespace API::Subsonic::Scan const IMediaScanner::Status scanStatus {ServiceProvider::get()->getStatus()}; statusResponse.setAttribute("scanning", scanStatus.currentState == IMediaScanner::State::InProgress); - if (scanStatus.currentState == IMediaScanner::State::InProgress && scanStatus.inProgressScanStats) - statusResponse.setAttribute("count", scanStatus.inProgressScanStats->processedFiles); + if (scanStatus.currentState == IMediaScanner::State::InProgress) + { + std::size_t count{}; + + if (scanStatus.currentScanStepStats && scanStatus.currentScanStepStats->currentStep == ScanProgressStep::ScanningFiles) + count = scanStatus.currentScanStepStats->processedFiles; + + statusResponse.setAttribute("count", count); + } return statusResponse; } diff --git a/src/lms/ui/LmsApplication.cpp b/src/lms/ui/LmsApplication.cpp index c9efc903..3b6edf29 100644 --- a/src/lms/ui/LmsApplication.cpp +++ b/src/lms/ui/LmsApplication.cpp @@ -544,11 +544,11 @@ LmsApplication::createHome() }); }); - ServiceProvider::get()->scanInProgress().connect(this, [=] (Scanner::ScanProgressStats stats) + ServiceProvider::get()->scanInProgress().connect(this, [=] (Scanner::ScanStepStats stepStats) { Wt::WServer::instance()->post(sessionId, [=] { - _events.dbScanInProgress.emit(stats); + _events.dbScanInProgress.emit(stepStats); triggerUpdate(); }); }); diff --git a/src/lms/ui/LmsApplication.hpp b/src/lms/ui/LmsApplication.hpp index 2b3b4534..ad0945ef 100644 --- a/src/lms/ui/LmsApplication.hpp +++ b/src/lms/ui/LmsApplication.hpp @@ -57,7 +57,7 @@ struct Events // Database events Wt::Signal<> dbScanStarted; Wt::Signal<> dbScanned; - Wt::Signal dbScanInProgress; + Wt::Signal dbScanInProgress; Wt::Signal dbScanScheduled; }; diff --git a/src/lms/ui/admin/ScannerController.cpp b/src/lms/ui/admin/ScannerController.cpp index 5bc6cb35..77b9ffb3 100644 --- a/src/lms/ui/admin/ScannerController.cpp +++ b/src/lms/ui/admin/ScannerController.cpp @@ -185,18 +185,43 @@ ScannerController::refreshContents() { case IMediaScanner::State::NotScheduled: bindString("status", Wt::WString::tr("Lms.Admin.ScannerController.status-not-scheduled")); + bindEmpty("step-status"); break; case IMediaScanner::State::Scheduled: bindString("status", Wt::WString::tr("Lms.Admin.ScannerController.status-scheduled") .arg(status.nextScheduledScan.toString())); + bindEmpty("step-status"); break; case IMediaScanner::State::InProgress: + bindString("status", Wt::WString::tr("Lms.Admin.ScannerController.status-in-progress") + .arg(static_cast(status.currentScanStepStats->currentStep) + 1) + .arg(Scanner::ScanProgressStepCount)); + + switch (status.currentScanStepStats->currentStep) { - std::ostringstream oss; - bindString("status", Wt::WString::tr("Lms.Admin.ScannerController.status-in-progress") - .arg(status.inProgressScanStats->processedFiles) - .arg(status.inProgressScanStats->filesToScan) - .arg(status.inProgressScanStats->progress())); + case Scanner::ScanProgressStep::CheckingRemovedFiles: + bindString("step-status", Wt::WString::tr("Lms.Admin.ScannerController.step-checking-removed-files") + .arg(status.currentScanStepStats->progress())); + break; + + case Scanner::ScanProgressStep::DiscoveringFiles: + bindString("step-status", Wt::WString::tr("Lms.Admin.ScannerController.step-discovering-files") + .arg(status.currentScanStepStats->processedFiles)); + break; + + case Scanner::ScanProgressStep::ScanningFiles: + bindString("step-status", Wt::WString::tr("Lms.Admin.ScannerController.step-scanning-files") + .arg(status.currentScanStepStats->processedFiles) + .arg(status.currentScanStepStats->filesToProcess) + .arg(status.currentScanStepStats->progress())); + break; + + case Scanner::ScanProgressStep::FetchingTrackFeatures: + bindString("step-status", Wt::WString::tr("Lms.Admin.ScannerController.step-fetching-track-features") + .arg(status.currentScanStepStats->processedFiles) + .arg(status.currentScanStepStats->filesToProcess) + .arg(status.currentScanStepStats->progress())); + break; } break; } From 0ca0b9e66f9bca0e50fcbf56909dab1aa253c9d5 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 26 Jul 2020 20:42:59 +0200 Subject: [PATCH 04/14] Fixed stats when checking for missing files --- approot/messages.xml | 2 +- approot/messages_fr.xml | 2 +- src/libs/scanner/impl/MediaScanner.cpp | 6 +++--- src/libs/scanner/include/scanner/MediaScannerStats.hpp | 2 +- src/lms/ui/admin/ScannerController.cpp | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/approot/messages.xml b/approot/messages.xml index e155a61e..88354bf4 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -73,7 +73,7 @@ Not scheduled Scheduled on {1} Scanning: step {1}/{2} -Checking removed files... {1}% +Checking for missing files... {1}% Discovering files: {1} files Fetching track features from AcousticBrainz: {1}/{2} tracks ({3}%)... Scanning files: {1}/{2} files ({3}%)... diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml index 4182355f..88b5c4d6 100644 --- a/approot/messages_fr.xml +++ b/approot/messages_fr.xml @@ -73,7 +73,7 @@ Non planifié Planifié le {1} En cours de scan : étape {1}/{2} -Vérification des fichiers... {1}% +Vérification des fichiers supprimés... {1}% Découverte des fichiers: {1} fichiers Récupération des métadonnées AcousticBrainz: {1}/{2} fichiers ({3}%)... Scan des fichiers: {1}/{2} fichiers ({3}%)... diff --git a/src/libs/scanner/impl/MediaScanner.cpp b/src/libs/scanner/impl/MediaScanner.cpp index 70e478dc..e463f518 100644 --- a/src/libs/scanner/impl/MediaScanner.cpp +++ b/src/libs/scanner/impl/MediaScanner.cpp @@ -865,7 +865,7 @@ MediaScanner::removeMissingTracks(ScanStats& stats) { static constexpr std::size_t batchSize {50}; - ScanStepStats stepStats{stats.startTime, ScanProgressStep::CheckingRemovedFiles}; + ScanStepStats stepStats{stats.startTime, ScanProgressStep::ChekingForMissingFiles}; LMS_LOG(DBUPDATER, DEBUG) << "Checking tracks to be removed..."; std::size_t trackCount {}; @@ -887,8 +887,6 @@ MediaScanner::removeMissingTracks(ScanStats& stats) trackPaths.clear(); tracksToRemove.clear(); - stepStats.processedFiles++; - { auto transaction {_dbSession.createSharedTransaction()}; trackPaths = Track::getAllPaths(_dbSession, i, batchSize); @@ -901,6 +899,8 @@ MediaScanner::removeMissingTracks(ScanStats& stats) if (!checkFile(trackPath, _mediaDirectory, _fileExtensions)) tracksToRemove.push_back(trackId); + + stepStats.processedFiles++; } if (!tracksToRemove.empty()) diff --git a/src/libs/scanner/include/scanner/MediaScannerStats.hpp b/src/libs/scanner/include/scanner/MediaScannerStats.hpp index 6b05bd95..f5eb67f7 100644 --- a/src/libs/scanner/include/scanner/MediaScannerStats.hpp +++ b/src/libs/scanner/include/scanner/MediaScannerStats.hpp @@ -58,7 +58,7 @@ namespace Scanner { enum class ScanProgressStep : unsigned { - CheckingRemovedFiles = 0, + ChekingForMissingFiles = 0, DiscoveringFiles, ScanningFiles, FetchingTrackFeatures, diff --git a/src/lms/ui/admin/ScannerController.cpp b/src/lms/ui/admin/ScannerController.cpp index 77b9ffb3..2e971064 100644 --- a/src/lms/ui/admin/ScannerController.cpp +++ b/src/lms/ui/admin/ScannerController.cpp @@ -199,8 +199,8 @@ ScannerController::refreshContents() switch (status.currentScanStepStats->currentStep) { - case Scanner::ScanProgressStep::CheckingRemovedFiles: - bindString("step-status", Wt::WString::tr("Lms.Admin.ScannerController.step-checking-removed-files") + case Scanner::ScanProgressStep::ChekingForMissingFiles: + bindString("step-status", Wt::WString::tr("Lms.Admin.ScannerController.step-checking-for-missing-files") .arg(status.currentScanStepStats->progress())); break; From 6b4e17931ef10abfb53d97f463c85b5023c1f416 Mon Sep 17 00:00:00 2001 From: emeric Date: Mon, 10 Aug 2020 10:24:07 +0200 Subject: [PATCH 05/14] Early quit when no space left on device --- src/lms/main.cpp | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/lms/main.cpp b/src/lms/main.cpp index 3cdfa1a1..51244cc1 100644 --- a/src/lms/main.cpp +++ b/src/lms/main.cpp @@ -36,7 +36,9 @@ #include "utils/Service.hpp" #include "utils/WtLogger.hpp" -std::vector generateWtConfig(std::string execPath) +static +std::vector +generateWtConfig(std::string execPath) { std::vector args; @@ -92,8 +94,16 @@ std::vector generateWtConfig(std::string execPath) } - std::ofstream oss(wtConfigPath.string().c_str(), std::ios::out); - boost::property_tree::xml_parser::write_xml(oss, pt); + { + std::ofstream oss {wtConfigPath.string().c_str(), std::ios::out}; + if (!oss) + throw LmsException {"Can't open '" + wtConfigPath.string() + "' for writing!"}; + + boost::property_tree::xml_parser::write_xml(oss, pt); + + if (!oss) + throw LmsException {"Can't write in file '" + wtConfigPath.string() + "', no space left?"}; + } return args; } From f342ad3155bec02d7e49694dc788f3264faa0a36 Mon Sep 17 00:00:00 2001 From: emeric Date: Mon, 10 Aug 2020 13:48:23 +0200 Subject: [PATCH 06/14] Made the space key to toggle play/pause. ref #85 --- docroot/js/mediaplayer.js | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/docroot/js/mediaplayer.js b/docroot/js/mediaplayer.js index a5c553f1..a1788c26 100644 --- a/docroot/js/mediaplayer.js +++ b/docroot/js/mediaplayer.js @@ -65,6 +65,15 @@ LMS.mediaplayer = function () { } } + var _playPause = function() { + _audioCtx.resume(); + if (_elems.audio.paused && _elems.audio.children.length > 0) { + _playTrack(); + } + else + _elems.audio.pause(); + } + var _requestPreviousTrack = function() { Wt.emit(_root, "playPrevious"); } @@ -146,12 +155,7 @@ LMS.mediaplayer = function () { _gainNode.connect(_audioCtx.destination); _elems.playpause.addEventListener("click", function() { - _audioCtx.resume(); - if (_elems.audio.paused && _elems.audio.children.length > 0) { - _playTrack(); - } - else - _elems.audio.pause(); + _playPause(); }); _elems.previous.addEventListener("click", function() { @@ -225,6 +229,15 @@ LMS.mediaplayer = function () { } }); + document.addEventListener("keypress", function(event) { + if (event.keyCode == 32) { + if (event.target == document.body) { + event.preventDefault(); + } + _playPause(); + } + }); + if ('mediaSession' in navigator) { navigator.mediaSession.setActionHandler("previoustrack", function() { _requestPreviousTrack(); From 10abee4d16c27cb0287f3473b181df28c1539c74 Mon Sep 17 00:00:00 2001 From: emeric Date: Mon, 10 Aug 2020 14:17:54 +0200 Subject: [PATCH 07/14] Added shortcuts to play prev/next songs. fixes #85 --- README.md | 5 +++++ docroot/js/mediaplayer.js | 35 +++++++++++++++++++++++++++-------- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 0d889fc2..34712ce8 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,11 @@ The Subsonic API is enabled by default. __Note__: since _LMS_ stores hashed and salted passwords, it cannot handle the __token authentication__ method defined from version 1.13.0. +## Keyboard shortcuts +* Play/pause: Space +* Previous track: Ctrl + Left +* Next track: Ctrl + Right + ## Installation ### Docker diff --git a/docroot/js/mediaplayer.js b/docroot/js/mediaplayer.js index a1788c26..5f100cc5 100644 --- a/docroot/js/mediaplayer.js +++ b/docroot/js/mediaplayer.js @@ -74,6 +74,16 @@ LMS.mediaplayer = function () { _elems.audio.pause(); } + var _playPrevious = function() { + _audioCtx.resume(); + _requestPreviousTrack(); + } + + var _playNext = function() { + _audioCtx.resume(); + _requestNextTrack(); + } + var _requestPreviousTrack = function() { Wt.emit(_root, "playPrevious"); } @@ -159,12 +169,10 @@ LMS.mediaplayer = function () { }); _elems.previous.addEventListener("click", function() { - _audioCtx.resume(); - _requestPreviousTrack(); + _playPrevious(); }); _elems.next.addEventListener("click", function() { - _audioCtx.resume(); - _requestNextTrack(); + _playNext(); }); _elems.seek.addEventListener("change", function() { _audioCtx.resume(); @@ -229,13 +237,24 @@ LMS.mediaplayer = function () { } }); - document.addEventListener("keypress", function(event) { + document.addEventListener("keydown", function(event) { + let handled = false; + if (event.keyCode == 32) { - if (event.target == document.body) { - event.preventDefault(); - } _playPause(); + handled = true; } + else if (event.ctrlKey && event.keyCode == 37) { + _playPrevious(); + handled = true; + } + else if (event.ctrlKey && event.keyCode == 39) { + _playNext(); + handled = true; + } + + if (handled) + event.preventDefault(); }); if ('mediaSession' in navigator) { From 2ba1bb3c794b68c0ba87f64381a869b8566bf64a Mon Sep 17 00:00:00 2001 From: emeric Date: Tue, 11 Aug 2020 13:20:39 +0200 Subject: [PATCH 08/14] Fixed crashes during infinite scrolling, ref #83 --- src/lms/ui/PlayQueue.cpp | 39 ++++++++++++++++++++--------- src/lms/ui/PlayQueue.hpp | 2 ++ src/lms/ui/explore/ArtistsView.cpp | 35 ++++++++++++++++++-------- src/lms/ui/explore/ArtistsView.hpp | 2 ++ src/lms/ui/explore/ReleasesView.cpp | 35 ++++++++++++++++++-------- src/lms/ui/explore/ReleasesView.hpp | 2 ++ src/lms/ui/explore/TracksView.cpp | 35 ++++++++++++++++++-------- src/lms/ui/explore/TracksView.hpp | 2 ++ 8 files changed, 110 insertions(+), 42 deletions(-) diff --git a/src/lms/ui/PlayQueue.cpp b/src/lms/ui/PlayQueue.cpp index f7c14424..ccfd5486 100644 --- a/src/lms/ui/PlayQueue.cpp +++ b/src/lms/ui/PlayQueue.cpp @@ -66,16 +66,7 @@ PlayQueue::PlayQueue() }); _entriesContainer = bindNew("entries"); - - _loadingIndicator = bindWidget("loading-indicator", createLoadingIndicator()); - _loadingIndicator->scrollVisibilityChanged().connect([this](bool visible) - { - if (!visible) - return; - - addSome(); - updateCurrentTrack(true); - }); + hideLoadingIndicator(); Wt::WText* shuffleBtn = bindNew("shuffle-btn", Wt::WString::tr("Lms.PlayQueue.template.shuffle-btn"), Wt::TextFormat::XHTML); setToolTip(*shuffleBtn, Wt::WString::tr("Lms.PlayQueue.shuffle")); @@ -191,6 +182,27 @@ PlayQueue::updateRadioBtn() _radioBtn->toggleStyleClass("text-muted", !_radioMode); } +void +PlayQueue::displayLoadingIndicator() +{ + _loadingIndicator = bindWidget("loading-indicator", createLoadingIndicator()); + _loadingIndicator->scrollVisibilityChanged().connect([this](bool visible) + { + if (!visible) + return; + + addSome(); + updateCurrentTrack(true); + }); +} + +void +PlayQueue::hideLoadingIndicator() +{ + _loadingIndicator = nullptr; + bindEmpty("loading-indicator"); +} + Database::TrackList::pointer PlayQueue::getTrackList() const { @@ -205,7 +217,7 @@ PlayQueue::clearTracks() getTrackList().modify()->clear(); } - _loadingIndicator->setHidden(true); + hideLoadingIndicator(); _entriesContainer->clear(); updateInfo(); } @@ -450,7 +462,10 @@ PlayQueue::addSome() } - _loadingIndicator->setHidden(static_cast(_entriesContainer->count()) >= tracklist->getCount()); + if (static_cast(_entriesContainer->count()) < tracklist->getCount()) + displayLoadingIndicator(); + else + hideLoadingIndicator(); } void diff --git a/src/lms/ui/PlayQueue.hpp b/src/lms/ui/PlayQueue.hpp index ed417656..211ef960 100644 --- a/src/lms/ui/PlayQueue.hpp +++ b/src/lms/ui/PlayQueue.hpp @@ -70,6 +70,8 @@ class PlayQueue : public Wt::WTemplate void updateCurrentTrack(bool selected); void updateRepeatBtn(); void updateRadioBtn(); + void displayLoadingIndicator(); + void hideLoadingIndicator(); void loadTrack(std::size_t pos, bool play); void stop(); diff --git a/src/lms/ui/explore/ArtistsView.cpp b/src/lms/ui/explore/ArtistsView.cpp index 5214c142..0084d009 100644 --- a/src/lms/ui/explore/ArtistsView.cpp +++ b/src/lms/ui/explore/ArtistsView.cpp @@ -76,15 +76,7 @@ Artists::Artists(Filters* filters) _linkType->changed().connect([this] { refreshView(); }); _container = bindNew("artists"); - - _loadingIndicator = bindWidget("loading-indicator", createLoadingIndicator()); - _loadingIndicator->scrollVisibilityChanged().connect([this](bool visible) - { - if (!visible) - return; - - addSome(); - }); + hideLoadingIndicator(); refreshView(); @@ -106,6 +98,26 @@ Artists::refreshView(Mode mode) refreshView(); } +void +Artists::displayLoadingIndicator() +{ + _loadingIndicator = bindWidget("loading-indicator", createLoadingIndicator()); + _loadingIndicator->scrollVisibilityChanged().connect([this](bool visible) + { + if (!visible) + return; + + addSome(); + }); +} + +void +Artists::hideLoadingIndicator() +{ + _loadingIndicator = nullptr; + bindEmpty("loading-indicator"); +} + std::vector Artists::getRandomArtists(std::optional range, bool& moreResults) { @@ -207,7 +219,10 @@ Artists::addSome() _container->addWidget(ArtistListHelpers::createEntry(artist)); } - _loadingIndicator->setHidden(!moreResults); + if (moreResults) + displayLoadingIndicator(); + else + hideLoadingIndicator(); } } // namespace UserInterface diff --git a/src/lms/ui/explore/ArtistsView.hpp b/src/lms/ui/explore/ArtistsView.hpp index 4169f74d..1daf80ed 100644 --- a/src/lms/ui/explore/ArtistsView.hpp +++ b/src/lms/ui/explore/ArtistsView.hpp @@ -54,6 +54,8 @@ class Artists : public Wt::WTemplate void refreshView(); void refreshView(Mode mode); + void displayLoadingIndicator(); + void hideLoadingIndicator(); void addSome(); std::vector> getArtists(std::optional range, bool& moreResults); diff --git a/src/lms/ui/explore/ReleasesView.cpp b/src/lms/ui/explore/ReleasesView.cpp index 8489a1f8..e21bb0f5 100644 --- a/src/lms/ui/explore/ReleasesView.cpp +++ b/src/lms/ui/explore/ReleasesView.cpp @@ -79,15 +79,7 @@ _filters {filters} }); _container = bindNew("releases"); - - _loadingIndicator = bindWidget("loading-indicator", createLoadingIndicator()); - _loadingIndicator->scrollVisibilityChanged().connect([this](bool visible) - { - if (!visible) - return; - - addSome(); - }); + hideLoadingIndicator(); refreshView(defaultMode); @@ -109,6 +101,26 @@ Releases::refreshView(Mode mode) refreshView(); } +void +Releases::displayLoadingIndicator() +{ + _loadingIndicator = bindWidget("loading-indicator", createLoadingIndicator()); + _loadingIndicator->scrollVisibilityChanged().connect([this](bool visible) + { + if (!visible) + return; + + addSome(); + }); +} + +void +Releases::hideLoadingIndicator() +{ + _loadingIndicator = nullptr; + bindEmpty("loading-indicator"); +} + void Releases::addSome() { @@ -122,7 +134,10 @@ Releases::addSome() _container->addWidget(ReleaseListHelpers::createEntry(release)); } - _loadingIndicator->setHidden(!moreResults); + if (moreResults) + displayLoadingIndicator(); + else + hideLoadingIndicator(); } std::vector diff --git a/src/lms/ui/explore/ReleasesView.hpp b/src/lms/ui/explore/ReleasesView.hpp index 21f9a5dc..6cc8a67b 100644 --- a/src/lms/ui/explore/ReleasesView.hpp +++ b/src/lms/ui/explore/ReleasesView.hpp @@ -56,6 +56,8 @@ class Releases : public Wt::WTemplate void refreshView(); void refreshView(Mode mode); + void displayLoadingIndicator(); + void hideLoadingIndicator(); void addSome(); std::vector> getReleases(std::optional range, bool& moreResults); diff --git a/src/lms/ui/explore/TracksView.cpp b/src/lms/ui/explore/TracksView.cpp index 701c7ff0..640ab4df 100644 --- a/src/lms/ui/explore/TracksView.cpp +++ b/src/lms/ui/explore/TracksView.cpp @@ -83,15 +83,7 @@ _filters {filters} }); _tracksContainer = bindNew("tracks"); - - _loadingIndicator = bindWidget("loading-indicator", createLoadingIndicator()); - _loadingIndicator->scrollVisibilityChanged().connect([this](bool visible) - { - if (!visible) - return; - - addSome(); - }); + hideLoadingIndicator(); filters->updated().connect([this] { @@ -116,6 +108,26 @@ Tracks::refreshView(Mode mode) refreshView(); } +void +Tracks::displayLoadingIndicator() +{ + _loadingIndicator = bindWidget("loading-indicator", createLoadingIndicator()); + _loadingIndicator->scrollVisibilityChanged().connect([this](bool visible) + { + if (!visible) + return; + + addSome(); + }); +} + +void +Tracks::hideLoadingIndicator() +{ + _loadingIndicator = nullptr; + bindEmpty("loading-indicator"); +} + std::vector Tracks::getRandomTracks(std::optional range, bool& moreResults) { @@ -210,7 +222,10 @@ Tracks::addSome() _tracksContainer->addWidget(TrackListHelpers::createEntry(track)); } - _loadingIndicator->setHidden(!moreResults); + if (moreResults) + displayLoadingIndicator(); + else + hideLoadingIndicator(); } } // namespace UserInterface diff --git a/src/lms/ui/explore/TracksView.hpp b/src/lms/ui/explore/TracksView.hpp index eba94b06..771292d7 100644 --- a/src/lms/ui/explore/TracksView.hpp +++ b/src/lms/ui/explore/TracksView.hpp @@ -55,6 +55,8 @@ class Tracks : public Wt::WTemplate void refreshView(); void refreshView(Mode mode); + void displayLoadingIndicator(); + void hideLoadingIndicator(); void addSome(); std::vector> getRandomTracks(std::optional range, bool& moreResults); From cb0fe96e4a06b186fb1c9cf123c6d5a687d701e0 Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 14 Aug 2020 11:51:03 +0200 Subject: [PATCH 09/14] Fixed bad error handling when transcoding. fixes #75 --- src/libs/av/impl/AvTranscoder.cpp | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/libs/av/impl/AvTranscoder.cpp b/src/libs/av/impl/AvTranscoder.cpp index 47454837..cf36860e 100644 --- a/src/libs/av/impl/AvTranscoder.cpp +++ b/src/libs/av/impl/AvTranscoder.cpp @@ -199,16 +199,6 @@ Transcoder::process(std::vector& output, std::size_t maxSize) if (!_child || _isComplete) return; - if (_child->out().fail()) - { - LOG(DEBUG) << "Stdout FAILED 2"; - } - - if (_child->out().eof()) - { - LOG(DEBUG) << "Stdout ENDED 2"; - } - output.resize(maxSize); //Read on the output stream @@ -218,14 +208,19 @@ Transcoder::process(std::vector& output, std::size_t maxSize) if (_child->out().fail()) { LOG(DEBUG) << "Stdout FAILED"; + _isComplete = true; } if (_child->out().eof()) { LOG(DEBUG) << "Stdout EOF!"; - _child->clear(); - _isComplete = true; + } + + if (_isComplete) + { + LOG(DEBUG) << "Transcode complete!"; + _child->clear(); _child.reset(); } From 02c663debe4a3256c580459be02d9f90f16d0e5a Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 15 Aug 2020 17:16:30 +0200 Subject: [PATCH 10/14] Prevent space bar to toggle play/pause --- docroot/js/mediaplayer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docroot/js/mediaplayer.js b/docroot/js/mediaplayer.js index 5f100cc5..81fa0f9c 100644 --- a/docroot/js/mediaplayer.js +++ b/docroot/js/mediaplayer.js @@ -240,7 +240,7 @@ LMS.mediaplayer = function () { document.addEventListener("keydown", function(event) { let handled = false; - if (event.keyCode == 32) { + if (event.keyCode == 32 && !(event.target instanceof HTMLInputElement) { _playPause(); handled = true; } From 06b429da78f6b9beedea6436aac75470b5ca6b85 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 16 Aug 2020 17:14:22 +0200 Subject: [PATCH 11/14] Really prevent space bar from toggling play/pause when in input --- docroot/js/mediaplayer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docroot/js/mediaplayer.js b/docroot/js/mediaplayer.js index 81fa0f9c..776b5b0a 100644 --- a/docroot/js/mediaplayer.js +++ b/docroot/js/mediaplayer.js @@ -240,7 +240,7 @@ LMS.mediaplayer = function () { document.addEventListener("keydown", function(event) { let handled = false; - if (event.keyCode == 32 && !(event.target instanceof HTMLInputElement) { + if (event.keyCode == 32 && !(event.target instanceof HTMLInputElement)) { _playPause(); handled = true; } From 09f5ea5b0f43e761b27243c5bea7d648464e2577 Mon Sep 17 00:00:00 2001 From: emeric Date: Tue, 18 Aug 2020 12:49:37 +0200 Subject: [PATCH 12/14] Really fixed bad error handling when transcoding. fixes #75 --- src/libs/av/impl/AvTranscoder.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/libs/av/impl/AvTranscoder.cpp b/src/libs/av/impl/AvTranscoder.cpp index cf36860e..f1b9ed94 100644 --- a/src/libs/av/impl/AvTranscoder.cpp +++ b/src/libs/av/impl/AvTranscoder.cpp @@ -1,5 +1,5 @@ /* - + * Copyright (C) 2020 Emeric Poupon * * This file is part of LMS. * @@ -59,17 +59,20 @@ Transcoder::start() if (!std::filesystem::exists(_filePath)) { LOG(ERROR) << "File '" << _filePath << "' does not exist!"; + _isComplete = true; return false; } else if (!std::filesystem::is_regular_file( _filePath) ) { LOG(ERROR) << "File '" << _filePath << "' is not regular!"; + _isComplete = true; return false; } } catch (const std::filesystem::filesystem_error& e) { LOG(ERROR) << "File error on '" << _filePath.string() << "': " << e.what(); + _isComplete = true; return false; } @@ -154,6 +157,7 @@ Transcoder::start() break; default: + _isComplete = true; return false; } @@ -178,12 +182,14 @@ Transcoder::start() if (!_child->is_open()) { LOG(DEBUG) << "Exec failed!"; + _isComplete = true; return false; } if (_child->out().eof()) { LOG(DEBUG) << "Early end of file!"; + _isComplete = true; return false; } From bc87e16d087f44b70222e82022c677503a2e9ff9 Mon Sep 17 00:00:00 2001 From: emeric Date: Thu, 20 Aug 2020 13:10:54 +0200 Subject: [PATCH 13/14] Changed the add button to a contextual menu --- README.md | 6 +- approot/artist.xml | 4 +- approot/explore.xml | 2 +- approot/messages.xml | 7 +-- approot/messages_fr.xml | 7 +-- approot/release.xml | 6 +- approot/releases.xml | 2 +- approot/tracks.xml | 4 +- .../av/impl/AvTranscodeResourceHandler.hpp | 1 - src/lms/CMakeLists.txt | 1 + src/lms/ui/LmsApplication.cpp | 14 +++-- src/lms/ui/LmsApplication.hpp | 23 ++++---- src/lms/ui/PlayQueue.cpp | 59 ++++++++++++++----- src/lms/ui/PlayQueue.hpp | 6 +- src/lms/ui/PlayQueueAction.hpp | 11 +++- src/lms/ui/explore/ArtistView.cpp | 23 +++++++- src/lms/ui/explore/ArtistView.hpp | 2 +- src/lms/ui/explore/Explore.cpp | 14 +++++ src/lms/ui/explore/Explore.hpp | 6 +- src/lms/ui/explore/ReleasePopup.cpp | 49 +++++++++++++++ src/lms/ui/explore/ReleasePopup.hpp | 34 +++++++++++ src/lms/ui/explore/ReleaseView.cpp | 26 +++++--- src/lms/ui/explore/ReleaseView.hpp | 8 +-- src/lms/ui/explore/ReleasesView.cpp | 19 +++++- src/lms/ui/explore/ReleasesView.hpp | 2 +- src/lms/ui/explore/SearchView.cpp | 6 +- src/lms/ui/explore/SearchView.hpp | 6 +- src/lms/ui/explore/TrackListHelpers.cpp | 21 ++++--- src/lms/ui/explore/TrackListHelpers.hpp | 3 +- src/lms/ui/explore/TracksView.cpp | 24 ++++++-- src/lms/ui/explore/TracksView.hpp | 2 +- 31 files changed, 304 insertions(+), 94 deletions(-) create mode 100644 src/lms/ui/explore/ReleasePopup.cpp create mode 100644 src/lms/ui/explore/ReleasePopup.hpp diff --git a/README.md b/README.md index 34712ce8..7318abd0 100644 --- a/README.md +++ b/README.md @@ -61,9 +61,9 @@ The Subsonic API is enabled by default. __Note__: since _LMS_ stores hashed and salted passwords, it cannot handle the __token authentication__ method defined from version 1.13.0. ## Keyboard shortcuts -* Play/pause: Space -* Previous track: Ctrl + Left -* Next track: Ctrl + Right +* Play/pause: Space +* Previous track: Ctrl + Left +* Next track: Ctrl + Right ## Installation diff --git a/approot/artist.xml b/approot/artist.xml index 8534eb07..3e7bca0a 100644 --- a/approot/artist.xml +++ b/approot/artist.xml @@ -6,18 +6,20 @@

${name}

${clusters} - ${play-btn}${add-btn} + ${play-btn}${more-btn}
${releases class="Lms-explore-artist-entries-container"}
+ ${}

${tr:Lms.Explore.Artist.similar-artists}

${similar-artists}
+ ${
} ${}
diff --git a/approot/explore.xml b/approot/explore.xml index 2cd86c7c..80881197 100644 --- a/approot/explore.xml +++ b/approot/explore.xml @@ -5,7 +5,7 @@ ${contents class="Lms-explore-contents"} - + diff --git a/approot/messages.xml b/approot/messages.xml index 88354bf4..146afb5e 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -121,6 +121,8 @@ Most played MusicBrainz Artist MusicBrainz Release +Play last +Play shuffled Random Recently added Recently played @@ -157,11 +159,8 @@ Added {1} track Added {1} tracks - - Playing {1} track - Playing {1} tracks - Play Queue +Play Queue full! Radio mode Repeat Shuffle diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml index 88b5c4d6..11dc324e 100644 --- a/approot/messages_fr.xml +++ b/approot/messages_fr.xml @@ -121,6 +121,8 @@ Plus joués MusicBrainz Artist MusicBrainz Release +Jouer en dernier +Jouer aléatoirement Aléatoire Ajoutés récemment Joués récemment @@ -157,11 +159,8 @@ {1} piste ajoutée {1} pistes ajoutées - - {1} piste ajoutée - {1} pistes ajoutées - Liste de lecture +Liste de lecture pleine! Mode radio Repéter Mélanger diff --git a/approot/release.xml b/approot/release.xml index 86e6d9c2..cb49ffd7 100644 --- a/approot/release.xml +++ b/approot/release.xml @@ -17,12 +17,13 @@ ${}

${artist class="Lms-artistname text-muted"}

${
}
${}${year}${}${} (${orig-year})${}
${clusters} - ${play-btn}${add-btn} + ${play-btn}${more-btn}
${container}
+ ${}

${tr:Lms.Explore.Release.similar-releases}

@@ -31,6 +32,7 @@
+ ${} ${}
@@ -66,7 +68,7 @@
- ${play-btn}${add-btn} + ${play-btn}${more-btn}
${duration} diff --git a/approot/releases.xml b/approot/releases.xml index 288ac0a0..430e48dd 100644 --- a/approot/releases.xml +++ b/approot/releases.xml @@ -6,7 +6,7 @@

${tr:Lms.Explore.releases}

${mode class="nav nav-pills Lms-explore-mode-container"} - ${play-btn}${add-btn} + ${play-btn}${more-btn}
${releases} diff --git a/approot/tracks.xml b/approot/tracks.xml index ff664056..4c34bab7 100644 --- a/approot/tracks.xml +++ b/approot/tracks.xml @@ -9,7 +9,7 @@

${tr:Lms.Explore.tracks}

${mode class="nav nav-pills Lms-explore-mode-container"} - ${play-btn}${add-btn} + ${play-btn}${more-btn}
${tracks} ${loading-indicator class="Lms-horizontal-center Lms-loading-indicator"} @@ -33,7 +33,7 @@
- ${play-btn}${add-btn} + ${play-btn}${more-btn}
${duration} diff --git a/src/libs/av/impl/AvTranscodeResourceHandler.hpp b/src/libs/av/impl/AvTranscodeResourceHandler.hpp index 351e6e45..543ca74b 100644 --- a/src/libs/av/impl/AvTranscodeResourceHandler.hpp +++ b/src/libs/av/impl/AvTranscodeResourceHandler.hpp @@ -39,7 +39,6 @@ namespace Av static constexpr std::size_t _chunkSize {262144}; const std::filesystem::path _trackPath; Transcoder _transcoder; - }; } diff --git a/src/lms/CMakeLists.txt b/src/lms/CMakeLists.txt index 269986c3..88905053 100644 --- a/src/lms/CMakeLists.txt +++ b/src/lms/CMakeLists.txt @@ -23,6 +23,7 @@ add_executable(lms ui/explore/Explore.cpp ui/explore/Filters.cpp ui/explore/ReleaseListHelpers.cpp + ui/explore/ReleasePopup.cpp ui/explore/ReleasesView.cpp ui/explore/ReleaseView.cpp ui/explore/SearchView.cpp diff --git a/src/lms/ui/LmsApplication.cpp b/src/lms/ui/LmsApplication.cpp index 3b6edf29..de5ad4c3 100644 --- a/src/lms/ui/LmsApplication.cpp +++ b/src/lms/ui/LmsApplication.cpp @@ -35,7 +35,6 @@ #include "database/User.hpp" #include "explore/Explore.hpp" #include "explore/Filters.hpp" -#include "explore/SearchView.hpp" #include "utils/Logger.hpp" #include "utils/Service.hpp" #include "utils/String.hpp" @@ -308,6 +307,13 @@ LmsApplication::createCluster(Database::Cluster::pointer cluster, bool canDelete return res; } +Wt::WPopupMenu* +LmsApplication::createPopupMenu() +{ + _popupMenu = std::make_unique(); + return _popupMenu.get(); +} + void LmsApplication::handleException(LmsApplicationException& e) { @@ -334,7 +340,6 @@ enum IdxRoot { IdxExplore = 0, IdxPlayQueue, - IdxSearch, IdxSettings, IdxAdminDatabase, IdxAdminUsers, @@ -355,9 +360,9 @@ handlePathChange(Wt::WStackedWidget* stack, bool isAdmin) { "/artist", IdxExplore, false }, { "/releases", IdxExplore, false }, { "/release", IdxExplore, false }, + { "/search", IdxExplore, false }, { "/tracks", IdxExplore, false }, { "/playqueue", IdxPlayQueue, false }, - { "/search", IdxSearch, false }, { "/settings", IdxSettings, false }, { "/admin/database", IdxAdminDatabase, true }, { "/admin/users", IdxAdminUsers, true }, @@ -471,7 +476,6 @@ LmsApplication::createHome() Explore* explore = mainStack->addNew(filters); _playQueue = mainStack->addNew(); - auto* search {mainStack->addNew(filters)}; mainStack->addNew(); searchEdit->enterPressed().connect([=] @@ -482,7 +486,7 @@ LmsApplication::createHome() searchEdit->textInput().connect([=] { setInternalPath("/search", true); - search->refreshView(searchEdit->text().toUTF8()); + explore->search(searchEdit->text()); }); // Admin stuff diff --git a/src/lms/ui/LmsApplication.hpp b/src/lms/ui/LmsApplication.hpp index ad0945ef..9cf31760 100644 --- a/src/lms/ui/LmsApplication.hpp +++ b/src/lms/ui/LmsApplication.hpp @@ -22,6 +22,7 @@ #include #include +#include #include "database/Db.hpp" #include "database/Session.hpp" @@ -101,6 +102,7 @@ class LmsApplication : public Wt::WApplication static Wt::WLink createReleaseLink(Wt::Dbo::ptr release); static std::unique_ptr createReleaseAnchor(Wt::Dbo::ptr release, bool addText = true); static std::unique_ptr createCluster(Wt::Dbo::ptr cluster, bool canDelete = false); + Wt::WPopupMenu* createPopupMenu(); MediaPlayer& getMediaPlayer() const { return *_mediaPlayer; } PlayQueue& getPlayQueue() const { return *_playQueue; } @@ -124,17 +126,18 @@ class LmsApplication : public Wt::WApplication void createHome(); - Wt::Signal<> _preQuit; - Database::Session _dbSession; - LmsApplicationGroupContainer& _appGroups; - Events _events; - std::optional _userId; - std::optional _userAuthStrong; + Wt::Signal<> _preQuit; + Database::Session _dbSession; + LmsApplicationGroupContainer& _appGroups; + Events _events; + std::optional _userId; + std::optional _userAuthStrong; std::shared_ptr _audioTranscodeResource; - std::shared_ptr _audioFileResource; - std::shared_ptr _imageResource; - MediaPlayer* _mediaPlayer {}; - PlayQueue* _playQueue {}; + std::shared_ptr _audioFileResource; + std::shared_ptr _imageResource; + MediaPlayer* _mediaPlayer {}; + PlayQueue* _playQueue {}; + std::unique_ptr _popupMenu {}; }; diff --git a/src/lms/ui/PlayQueue.cpp b/src/lms/ui/PlayQueue.cpp index ccfd5486..ab3d06da 100644 --- a/src/lms/ui/PlayQueue.cpp +++ b/src/lms/ui/PlayQueue.cpp @@ -209,6 +209,13 @@ PlayQueue::getTrackList() const return Database::TrackList::getById(LmsApp->getDbSession(), _tracklistId); } +bool +PlayQueue::isFull() const +{ + auto transaction {LmsApp->getDbSession().createSharedTransaction()}; + return getTrackList()->getCount() == _nbMaxEntries; +} + void PlayQueue::clearTracks() { @@ -321,55 +328,77 @@ PlayQueue::updateCurrentTrack(bool selected) entry->bindString("is-selected", selected ? "Lms-playqueue-selected" : ""); } -void +std::size_t PlayQueue::enqueueTracks(const std::vector& trackIds) { + std::size_t nbTracksQueued {}; + { auto transaction {LmsApp->getDbSession().createUniqueTransaction()}; - auto tracklist = getTrackList(); + auto tracklist {getTrackList()}; + + std::size_t nbTracksToEnqueue {tracklist->getCount() + trackIds.size() > _nbMaxEntries ? _nbMaxEntries - tracklist->getCount() : trackIds.size()}; for (Database::IdType trackId : trackIds) { Database::Track::pointer track {Database::Track::getById(LmsApp->getDbSession(), trackId)}; if (!track) continue; + if (nbTracksQueued == nbTracksToEnqueue) + break; + Database::TrackListEntry::create(LmsApp->getDbSession(), track, tracklist); + nbTracksQueued++; } } updateInfo(); addSome(); -} -void -PlayQueue::enqueueTrack(Database::IdType trackId) -{ - enqueueTracks({trackId}); + return nbTracksQueued; } void PlayQueue::processTracks(PlayQueueAction action, const std::vector& trackIds) { + std::size_t nbAddedTracks {}; + switch (action) { - case PlayQueueAction::AddLast: - enqueueTracks(trackIds); - LmsApp->notifyMsg(MsgType::Info, Wt::WString::trn("Lms.PlayQueue.nb-tracks-added", trackIds.size()).arg(trackIds.size()), std::chrono::milliseconds(2000)); - break; - - case PlayQueueAction::AddNext: + case PlayQueueAction::PlayLast: + nbAddedTracks = enqueueTracks(trackIds); + if (!_trackPos) + loadTrack(0, true); break; case PlayQueueAction::Play: clearTracks(); - enqueueTracks(trackIds); + nbAddedTracks = enqueueTracks(trackIds); loadTrack(0, true); - LmsApp->notifyMsg(MsgType::Info, Wt::WString::trn("Lms.PlayQueue.nb-tracks-playing", trackIds.size()).arg(trackIds.size()), std::chrono::milliseconds(2000)); break; + case PlayQueueAction::PlayShuffled: + { + clearTracks(); + { + std::vector shuffledTrackIds {trackIds}; + Random::shuffleContainer(shuffledTrackIds); + nbAddedTracks = enqueueTracks(shuffledTrackIds); + } + loadTrack(0, true); + break; + } } + + if (nbAddedTracks > 0) + LmsApp->notifyMsg(MsgType::Info, Wt::WString::trn("Lms.PlayQueue.nb-tracks-added", nbAddedTracks).arg(nbAddedTracks), std::chrono::milliseconds(2000)); + + if (isFull()) + LmsApp->notifyMsg(MsgType::Warning, Wt::WString::tr("Lms.PlayQueue.playqueue-full"), std::chrono::milliseconds(2000)); + + } void diff --git a/src/lms/ui/PlayQueue.hpp b/src/lms/ui/PlayQueue.hpp index 211ef960..8cfae3e6 100644 --- a/src/lms/ui/PlayQueue.hpp +++ b/src/lms/ui/PlayQueue.hpp @@ -60,10 +60,10 @@ class PlayQueue : public Wt::WTemplate private: Wt::Dbo::ptr getTrackList() const; + bool isFull() const; void clearTracks(); - void enqueueTracks(const std::vector& trackIds); - void enqueueTrack(Database::IdType trackId); + std::size_t enqueueTracks(const std::vector& trackIds); void addSome(); void enqueueRadioTrack(); void updateInfo(); @@ -80,6 +80,8 @@ class PlayQueue : public Wt::WTemplate void addRadioTrackFromClusters(); std::optional getReplayGain(std::size_t pos, const Wt::Dbo::ptr& track) const; + static inline constexpr std::size_t _nbMaxEntries {1000}; + bool _repeatAll {}; bool _radioMode {}; bool _mediaPlayerSettingsLoaded {}; diff --git a/src/lms/ui/PlayQueueAction.hpp b/src/lms/ui/PlayQueueAction.hpp index 2892537b..29a91351 100644 --- a/src/lms/ui/PlayQueueAction.hpp +++ b/src/lms/ui/PlayQueueAction.hpp @@ -19,15 +19,22 @@ #pragma once +#include +#include + +#include "database/Types.hpp" + namespace UserInterface { enum class PlayQueueAction { Play, - AddNext, - AddLast, + PlayLast, + PlayShuffled, }; + using PlayQueueActionSignal = Wt::Signal&>; + } diff --git a/src/lms/ui/explore/ArtistView.cpp b/src/lms/ui/explore/ArtistView.cpp index bdbe5602..082f0634 100644 --- a/src/lms/ui/explore/ArtistView.cpp +++ b/src/lms/ui/explore/ArtistView.cpp @@ -115,11 +115,24 @@ Artist::refreshView() } { - Wt::WText* addBtn = bindNew("add-btn", Wt::WString::tr("Lms.Explore.template.add-btn"), Wt::TextFormat::XHTML); + Wt::WText* moreBtn = bindNew("more-btn", Wt::WString::tr("Lms.Explore.template.more-btn"), Wt::TextFormat::XHTML); - addBtn->clicked().connect([=] + moreBtn->clicked().connect([=] { - artistsAction.emit(PlayQueueAction::AddLast, {*artistId}); + Wt::WPopupMenu* popup {LmsApp->createPopupMenu()}; + + popup->addItem(Wt::WString::tr("Lms.Explore.play-shuffled")) + ->triggered().connect(this, [=] + { + artistsAction.emit(PlayQueueAction::PlayShuffled, {*artistId}); + }); + popup->addItem(Wt::WString::tr("Lms.Explore.play-last")) + ->triggered().connect(this, [=] + { + artistsAction.emit(PlayQueueAction::PlayLast, {*artistId}); + }); + + popup->exec(moreBtn); }); } @@ -135,6 +148,10 @@ Artist::refreshView() void Artist::refreshSimilarArtists(const std::vector& similarArtistsId) { + if (similarArtistsId.empty()) + return; + + setCondition("if-has-similar-artists", true); Wt::WContainerWidget* similarArtistsContainer {bindNew("similar-artists")}; for (Database::IdType artistId : similarArtistsId) diff --git a/src/lms/ui/explore/ArtistView.hpp b/src/lms/ui/explore/ArtistView.hpp index 944c9504..54639412 100644 --- a/src/lms/ui/explore/ArtistView.hpp +++ b/src/lms/ui/explore/ArtistView.hpp @@ -42,7 +42,7 @@ class Artist : public Wt::WTemplate public: Artist(Filters* filters); - Wt::Signal&> artistsAction; + PlayQueueActionSignal artistsAction; private: void refreshView(); diff --git a/src/lms/ui/explore/Explore.cpp b/src/lms/ui/explore/Explore.cpp index 463ea8fd..6e89977d 100644 --- a/src/lms/ui/explore/Explore.cpp +++ b/src/lms/ui/explore/Explore.cpp @@ -35,6 +35,7 @@ #include "Filters.hpp" #include "ReleasesView.hpp" #include "ReleaseView.hpp" +#include "SearchView.hpp" #include "TracksView.hpp" namespace UserInterface { @@ -50,6 +51,7 @@ handleContentsPathChange(Wt::WStackedWidget* stack) IdxArtist, IdxReleases, IdxRelease, + IdxSearch, IdxTracks, }; @@ -59,6 +61,7 @@ handleContentsPathChange(Wt::WStackedWidget* stack) { "/artist", IdxArtist }, { "/releases", IdxReleases }, { "/release", IdxRelease }, + { "/search", IdxSearch }, { "/tracks", IdxTracks }, }; @@ -100,6 +103,11 @@ Explore::Explore(Filters* filters) release->tracksAction.connect(this, &Explore::handleTracksAction); contentsStack->addWidget(std::move(release)); + auto search = std::make_unique(_filters); + search->tracksAction.connect(this, &Explore::handleTracksAction); + _search = search.get(); + contentsStack->addWidget(std::move(search)); + auto tracks = std::make_unique(_filters); tracks->tracksAction.connect(this, &Explore::handleTracksAction); contentsStack->addWidget(std::move(tracks)); @@ -112,6 +120,12 @@ Explore::Explore(Filters* filters) handleContentsPathChange(contentsStack); } +void +Explore::search(const Wt::WString& searchText) +{ + _search->refreshView(searchText); +} + static std::vector getArtistsTracks(Database::Session& session, const std::vector& artistsId, const std::set&) diff --git a/src/lms/ui/explore/Explore.hpp b/src/lms/ui/explore/Explore.hpp index 7f476f64..2db65540 100644 --- a/src/lms/ui/explore/Explore.hpp +++ b/src/lms/ui/explore/Explore.hpp @@ -27,13 +27,16 @@ namespace UserInterface { class Filters; +class SearchView; class Explore : public Wt::WTemplate { public: Explore(Filters* filters); - Wt::Signal&> tracksAction; + void search(const Wt::WString& searchText); + + PlayQueueActionSignal tracksAction; private: @@ -42,6 +45,7 @@ class Explore : public Wt::WTemplate void handleTracksAction(PlayQueueAction action, const std::vector& tracksId); Filters* _filters {}; + SearchView* _search {}; }; } // namespace UserInterface diff --git a/src/lms/ui/explore/ReleasePopup.cpp b/src/lms/ui/explore/ReleasePopup.cpp new file mode 100644 index 00000000..7ce4d099 --- /dev/null +++ b/src/lms/ui/explore/ReleasePopup.cpp @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2020 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 "ReleasePopup.hpp" + +#include "LmsApplication.hpp" + +namespace UserInterface +{ + + void + displayReleasePopupMenu(Wt::WInteractWidget& target, + Database::IdType releaseId, + PlayQueueActionSignal& releasesAction) + { + Wt::WPopupMenu* popup {LmsApp->createPopupMenu()}; + + popup->addItem(Wt::WString::tr("Lms.Explore.play-shuffled")) + ->triggered().connect(&target, [&releasesAction, releaseId] + { + releasesAction.emit(PlayQueueAction::PlayShuffled, {releaseId}); + }); + popup->addItem(Wt::WString::tr("Lms.Explore.play-last")) + ->triggered().connect(&target, [&releasesAction, releaseId] + { + releasesAction.emit(PlayQueueAction::PlayLast, {releaseId}); + }); + + popup->popup(&target); + } + +} // namespace UserInterface + diff --git a/src/lms/ui/explore/ReleasePopup.hpp b/src/lms/ui/explore/ReleasePopup.hpp new file mode 100644 index 00000000..23e576b2 --- /dev/null +++ b/src/lms/ui/explore/ReleasePopup.hpp @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2020 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 "database/Types.hpp" +#include "PlayQueueAction.hpp" + +namespace UserInterface +{ + void displayReleasePopupMenu(Wt::WInteractWidget& target, + Database::IdType releaseId, + PlayQueueActionSignal& releasesAction); +} // namespace UserInterface + diff --git a/src/lms/ui/explore/ReleaseView.cpp b/src/lms/ui/explore/ReleaseView.cpp index e19f83f9..aa2d4350 100644 --- a/src/lms/ui/explore/ReleaseView.cpp +++ b/src/lms/ui/explore/ReleaseView.cpp @@ -22,7 +22,6 @@ #include #include #include -#include #include #include @@ -39,6 +38,7 @@ #include "LmsApplicationException.hpp" #include "MediaPlayer.hpp" #include "ReleaseListHelpers.hpp" +#include "ReleasePopup.hpp" #include "TrackStringUtils.hpp" using namespace Database; @@ -157,10 +157,10 @@ Release::refreshView() } { - Wt::WText* addBtn {bindNew("add-btn", Wt::WString::tr("Lms.Explore.template.add-btn"), Wt::TextFormat::XHTML)}; - addBtn->clicked().connect([=] + Wt::WText* moreBtn {bindNew("more-btn", Wt::WString::tr("Lms.Explore.template.more-btn"), Wt::TextFormat::XHTML)}; + moreBtn->clicked().connect([=] { - releasesAction.emit(PlayQueueAction::AddLast, {*releaseId}); + displayReleasePopupMenu(*moreBtn, *releaseId, releasesAction); }); } @@ -237,10 +237,18 @@ Release::refreshView() tracksAction.emit(PlayQueueAction::Play, {trackId}); }); - Wt::WText* addBtn {entry->bindNew("add-btn", Wt::WString::tr("Lms.Explore.template.add-btn"), Wt::TextFormat::XHTML)}; - addBtn->clicked().connect([=]() + Wt::WText* moreBtn {entry->bindNew("more-btn", Wt::WString::tr("Lms.Explore.template.more-btn"), Wt::TextFormat::XHTML)}; + moreBtn->clicked().connect([=]() { - tracksAction.emit(PlayQueueAction::AddLast, {trackId}); + Wt::WPopupMenu* popup {LmsApp->createPopupMenu()}; + + popup->addItem(Wt::WString::tr("Lms.Explore.play-last")) + ->triggered().connect(moreBtn, [=] + { + tracksAction.emit(PlayQueueAction::PlayLast, {trackId}); + }); + + popup->popup(moreBtn); }); entry->bindString("duration", trackDurationToString(track->getDuration()), Wt::TextFormat::Plain); @@ -306,6 +314,10 @@ Release::refreshLinks(const Database::Release::pointer& release) void Release::refreshSimilarReleases(const std::vector& similarReleasesId) { + if (similarReleasesId.empty()) + return; + + setCondition("if-has-similar-releases", true); auto* similarReleasesContainer {bindNew("similar-releases")}; for (Database::IdType id : similarReleasesId) diff --git a/src/lms/ui/explore/ReleaseView.hpp b/src/lms/ui/explore/ReleaseView.hpp index 5f1db58e..7908c405 100644 --- a/src/lms/ui/explore/ReleaseView.hpp +++ b/src/lms/ui/explore/ReleaseView.hpp @@ -19,7 +19,6 @@ #pragma once -#include #include #include "database/Types.hpp" @@ -39,11 +38,8 @@ namespace UserInterface public: Release(Filters* filters); - Wt::Signal&> releasesAction; - Wt::Signal&> tracksAction; - - Wt::Signal createReleaseActionMenu; - Wt::Signal createTrackActionMenu; + PlayQueueActionSignal releasesAction; + PlayQueueActionSignal tracksAction; private: void refreshView(); diff --git a/src/lms/ui/explore/ReleasesView.cpp b/src/lms/ui/explore/ReleasesView.cpp index e21bb0f5..dd713fef 100644 --- a/src/lms/ui/explore/ReleasesView.cpp +++ b/src/lms/ui/explore/ReleasesView.cpp @@ -72,10 +72,23 @@ _filters {filters} { releasesAction.emit(PlayQueueAction::Play, getAllReleases()); }); - Wt::WText* addBtn {bindNew("add-btn", Wt::WString::tr("Lms.Explore.template.add-btn"), Wt::TextFormat::XHTML)}; - addBtn->clicked().connect([this] + Wt::WText* moreBtn {bindNew("more-btn", Wt::WString::tr("Lms.Explore.template.more-btn"), Wt::TextFormat::XHTML)}; + moreBtn->clicked().connect([=] { - releasesAction.emit(PlayQueueAction::AddLast, getAllReleases()); + Wt::WPopupMenu* popup {LmsApp->createPopupMenu()}; + + popup->addItem(Wt::WString::tr("Lms.Explore.play-shuffled")) + ->triggered().connect([this] + { + releasesAction.emit(PlayQueueAction::PlayShuffled, getAllReleases()); + }); + popup->addItem(Wt::WString::tr("Lms.Explore.play-last")) + ->triggered().connect([this] + { + releasesAction.emit(PlayQueueAction::PlayLast, getAllReleases()); + }); + + popup->popup(moreBtn); }); _container = bindNew("releases"); diff --git a/src/lms/ui/explore/ReleasesView.hpp b/src/lms/ui/explore/ReleasesView.hpp index 6cc8a67b..479002c3 100644 --- a/src/lms/ui/explore/ReleasesView.hpp +++ b/src/lms/ui/explore/ReleasesView.hpp @@ -41,7 +41,7 @@ class Releases : public Wt::WTemplate public: Releases(Filters* filters); - Wt::Signal&> releasesAction; + PlayQueueActionSignal releasesAction; private: diff --git a/src/lms/ui/explore/SearchView.cpp b/src/lms/ui/explore/SearchView.cpp index 6f37b4c6..bd8a8e09 100644 --- a/src/lms/ui/explore/SearchView.cpp +++ b/src/lms/ui/explore/SearchView.cpp @@ -51,9 +51,9 @@ namespace UserInterface } void - SearchView::refreshView(const std::string& searchText) + SearchView::refreshView(const Wt::WString& searchText) { - _keywords = StringUtils::splitString(searchText, " "); + _keywords = StringUtils::splitString(searchText.toUTF8(), " "); refreshView(); } @@ -127,7 +127,7 @@ namespace UserInterface auto* container {bindNew("tracks")}; for (const Database::Track::pointer& track : tracks) - container->addWidget(TrackListHelpers::createEntry(track)); + container->addWidget(TrackListHelpers::createEntry(track, tracksAction)); } } diff --git a/src/lms/ui/explore/SearchView.hpp b/src/lms/ui/explore/SearchView.hpp index a7b2460a..8711547d 100644 --- a/src/lms/ui/explore/SearchView.hpp +++ b/src/lms/ui/explore/SearchView.hpp @@ -24,6 +24,8 @@ #include +#include "PlayQueueAction.hpp" + namespace UserInterface { class Filters; @@ -33,7 +35,9 @@ class SearchView : public Wt::WTemplate public: SearchView(Filters* filters); - void refreshView(const std::string& searchText); + PlayQueueActionSignal tracksAction; + + void refreshView(const Wt::WString& searchText); private: void refreshView(); diff --git a/src/lms/ui/explore/TrackListHelpers.cpp b/src/lms/ui/explore/TrackListHelpers.cpp index 3d4ec648..649f92d5 100644 --- a/src/lms/ui/explore/TrackListHelpers.cpp +++ b/src/lms/ui/explore/TrackListHelpers.cpp @@ -30,7 +30,6 @@ #include "resource/ImageResource.hpp" #include "LmsApplication.hpp" #include "MediaPlayer.hpp" -#include "PlayQueue.hpp" #include "TrackStringUtils.hpp" using namespace Database; @@ -38,7 +37,7 @@ using namespace Database; namespace UserInterface::TrackListHelpers { std::unique_ptr - createEntry(const Wt::Dbo::ptr& track) + createEntry(const Wt::Dbo::ptr& track, PlayQueueActionSignal& tracksAction) { auto entry {std::make_unique(Wt::WString::tr("Lms.Explore.Tracks.template.entry"))}; auto* entryPtr {entry.get()}; @@ -89,15 +88,23 @@ namespace UserInterface::TrackListHelpers entry->bindString("duration", trackDurationToString(track->getDuration()), Wt::TextFormat::Plain); Wt::WText* playBtn = entry->bindNew("play-btn", Wt::WString::tr("Lms.Explore.template.play-btn"), Wt::TextFormat::XHTML); - playBtn->clicked().connect([=] + playBtn->clicked().connect([trackId, &tracksAction] { - LmsApp->getPlayQueue().processTracks(PlayQueueAction::Play, {trackId}); + tracksAction.emit(PlayQueueAction::Play, {trackId}); }); - Wt::WText* addBtn = entry->bindNew("add-btn", Wt::WString::tr("Lms.Explore.template.add-btn"), Wt::TextFormat::XHTML); - addBtn->clicked().connect([=] + Wt::WText* moreBtn = entry->bindNew("more-btn", Wt::WString::tr("Lms.Explore.template.more-btn"), Wt::TextFormat::XHTML); + moreBtn->clicked().connect([=, &tracksAction] { - LmsApp->getPlayQueue().processTracks(PlayQueueAction::AddLast, {trackId}); + Wt::WPopupMenu* popup {LmsApp->createPopupMenu()}; + + popup->addItem(Wt::WString::tr("Lms.Explore.play-last")) + ->triggered().connect(moreBtn, [=, &tracksAction] + { + tracksAction.emit(PlayQueueAction::PlayLast, {trackId}); + }); + + popup->popup(moreBtn); }); LmsApp->getMediaPlayer().trackLoaded.connect(entryPtr, [=] (Database::IdType loadedTrackId) diff --git a/src/lms/ui/explore/TrackListHelpers.hpp b/src/lms/ui/explore/TrackListHelpers.hpp index 2766e3e8..32ac5b12 100644 --- a/src/lms/ui/explore/TrackListHelpers.hpp +++ b/src/lms/ui/explore/TrackListHelpers.hpp @@ -22,6 +22,7 @@ #include #include +#include "PlayQueueAction.hpp" namespace Database { @@ -30,6 +31,6 @@ namespace Database namespace UserInterface::TrackListHelpers { - std::unique_ptr createEntry(const Wt::Dbo::ptr& track); + std::unique_ptr createEntry(const Wt::Dbo::ptr& track, PlayQueueActionSignal& tracksAction); } // namespace UserInterface diff --git a/src/lms/ui/explore/TracksView.cpp b/src/lms/ui/explore/TracksView.cpp index 640ab4df..c9451d85 100644 --- a/src/lms/ui/explore/TracksView.cpp +++ b/src/lms/ui/explore/TracksView.cpp @@ -32,9 +32,8 @@ #include "utils/Logger.hpp" #include "utils/String.hpp" -#include "resource/ImageResource.hpp" - #include "common/LoadingIndicator.hpp" +#include "resource/ImageResource.hpp" #include "Filters.hpp" #include "LmsApplication.hpp" #include "MediaPlayer.hpp" @@ -76,10 +75,23 @@ _filters {filters} tracksAction.emit(PlayQueueAction::Play, getAllTracks()); }); - Wt::WText* addBtn = bindNew("add-btn", Wt::WString::tr("Lms.Explore.template.add-btn"), Wt::TextFormat::XHTML); - addBtn->clicked().connect([=] + Wt::WText* moreBtn = bindNew("more-btn", Wt::WString::tr("Lms.Explore.template.more-btn"), Wt::TextFormat::XHTML); + moreBtn->clicked().connect([=] { - tracksAction.emit(PlayQueueAction::AddLast, getAllTracks()); + Wt::WPopupMenu* popup {LmsApp->createPopupMenu()}; + + popup->addItem(Wt::WString::tr("Lms.Explore.play-shuffled")) + ->triggered().connect(this, [this] + { + tracksAction.emit(PlayQueueAction::PlayShuffled, getAllTracks()); + }); + popup->addItem(Wt::WString::tr("Lms.Explore.play-last")) + ->triggered().connect(this, [this] + { + tracksAction.emit(PlayQueueAction::PlayLast, getAllTracks()); + }); + + popup->popup(moreBtn); }); _tracksContainer = bindNew("tracks"); @@ -219,7 +231,7 @@ Tracks::addSome() bool moreResults; for (const Track::pointer& track : getTracks(Range {static_cast(_tracksContainer->count()), batchSize}, moreResults)) { - _tracksContainer->addWidget(TrackListHelpers::createEntry(track)); + _tracksContainer->addWidget(TrackListHelpers::createEntry(track, tracksAction)); } if (moreResults) diff --git a/src/lms/ui/explore/TracksView.hpp b/src/lms/ui/explore/TracksView.hpp index 771292d7..d66f3b14 100644 --- a/src/lms/ui/explore/TracksView.hpp +++ b/src/lms/ui/explore/TracksView.hpp @@ -40,7 +40,7 @@ class Tracks : public Wt::WTemplate public: Tracks(Filters* filters); - Wt::Signal&> tracksAction; + PlayQueueActionSignal tracksAction; private: From 9fbbca97a4f086cb4be0016a137fa280ed2c99b9 Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 21 Aug 2020 10:13:10 +0200 Subject: [PATCH 14/14] Made av libs private to liblmsav --- src/libs/av/CMakeLists.txt | 10 ++++++---- src/libs/av/impl/AvInfo.cpp | 8 ++++++++ src/libs/av/include/av/AvInfo.hpp | 10 ++-------- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/libs/av/CMakeLists.txt b/src/libs/av/CMakeLists.txt index 30e72176..ce3fb126 100644 --- a/src/libs/av/CMakeLists.txt +++ b/src/libs/av/CMakeLists.txt @@ -6,16 +6,15 @@ add_library(lmsav SHARED impl/AvTypes.cpp ) -# TODO make av libs private target_include_directories(lmsav INTERFACE include - ${AVCODEC_INCLUDE_DIR} - ${AVFORMAT_INCLUDE_DIR} - ${AVUTIL_INCLUDE_DIR} ) target_include_directories(lmsav PRIVATE include/ + ${AVCODEC_INCLUDE_DIR} + ${AVFORMAT_INCLUDE_DIR} + ${AVUTIL_INCLUDE_DIR} ) target_link_libraries(lmsav PUBLIC @@ -24,6 +23,9 @@ target_link_libraries(lmsav PUBLIC avutil std::filesystem wt + ) + +target_link_libraries(lmsav PRIVATE ${AVFORMAT_LIBRARY} ${AVUTIL_LIBRARY} ) diff --git a/src/libs/av/impl/AvInfo.cpp b/src/libs/av/impl/AvInfo.cpp index ff4d010d..aca3c045 100644 --- a/src/libs/av/impl/AvInfo.cpp +++ b/src/libs/av/impl/AvInfo.cpp @@ -19,6 +19,14 @@ #include "av/AvInfo.hpp" +extern "C" +{ +#define __STDC_CONSTANT_MACROS +#include +#include +#include +} + #include #include "utils/Logger.hpp" diff --git a/src/libs/av/include/av/AvInfo.hpp b/src/libs/av/include/av/AvInfo.hpp index 96dc33f4..d719ae02 100644 --- a/src/libs/av/include/av/AvInfo.hpp +++ b/src/libs/av/include/av/AvInfo.hpp @@ -21,14 +21,6 @@ #pragma once -extern "C" -{ -#define __STDC_CONSTANT_MACROS -#include -#include -#include -} - #include #include #include @@ -38,6 +30,8 @@ extern "C" #include "AvTypes.hpp" +struct AVFormatContext; + namespace Av {