From 4423890b876e75bc43d09b910a7c35bac60573ea Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 6 Aug 2022 14:18:57 +0200 Subject: [PATCH 01/20] fixed style --- src/libs/utils/impl/ChildProcess.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/utils/impl/ChildProcess.cpp b/src/libs/utils/impl/ChildProcess.cpp index ae0ae91b..90b44881 100644 --- a/src/libs/utils/impl/ChildProcess.cpp +++ b/src/libs/utils/impl/ChildProcess.cpp @@ -168,7 +168,7 @@ ChildProcess::wait(bool block) void -ChildProcess::asyncRead(std::byte* data, std::size_t bufferSize, ReadCallback callback) +ChildProcess::asyncRead(std::byte* data, std::size_t bufferSize, ReadCallback callback) { assert(!finished()); From 671d7242413a7f710d9e1c9a0e583508e382f139 Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 6 Aug 2022 14:58:26 +0200 Subject: [PATCH 02/20] Share message strings across sessions --- src/lms/ui/LmsApplication.cpp | 79 +++++++++++++++++++++++------------ src/lms/ui/LmsApplication.hpp | 1 + 2 files changed, 54 insertions(+), 26 deletions(-) diff --git a/src/lms/ui/LmsApplication.cpp b/src/lms/ui/LmsApplication.cpp index 9fd4527a..b724204f 100644 --- a/src/lms/ui/LmsApplication.cpp +++ b/src/lms/ui/LmsApplication.cpp @@ -64,6 +64,48 @@ namespace UserInterface { +static +std::shared_ptr +createMessageResourceBundle() +{ + const std::string appRoot {Wt::WApplication::appRoot()}; + + auto res {std::make_shared()}; + res->use(appRoot + "admin-database"); + res->use(appRoot + "admin-initwizard"); + res->use(appRoot + "admin-scannercontroller"); + res->use(appRoot + "admin-user"); + res->use(appRoot + "admin-users"); + res->use(appRoot + "artist"); + res->use(appRoot + "artists"); + res->use(appRoot + "error"); + res->use(appRoot + "explore"); + res->use(appRoot + "login"); + res->use(appRoot + "main"); + res->use(appRoot + "mediaplayer"); + res->use(appRoot + "messages"); + res->use(appRoot + "misc"); + res->use(appRoot + "notifications"); + res->use(appRoot + "playqueue"); + res->use(appRoot + "release"); + res->use(appRoot + "releases"); + res->use(appRoot + "search"); + res->use(appRoot + "settings"); + res->use(appRoot + "tracklist"); + res->use(appRoot + "tracklists"); + res->use(appRoot + "tracks"); + + return res; +} + +static +std::shared_ptr +getOrCreateMessageBundle() +{ + static std::shared_ptr res {createMessageResourceBundle()}; + return res; +} + static constexpr const char* defaultPath {"/releases"}; std::unique_ptr @@ -169,32 +211,8 @@ LmsApplication::init() useStyleSheet("resources/font-awesome/css/font-awesome.min.css"); require("js/mediaplayer.js"); - setTitle("LMS"); - - // 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"); - messageResourceBundle().use(appRoot() + "artists"); - messageResourceBundle().use(appRoot() + "error"); - messageResourceBundle().use(appRoot() + "explore"); - messageResourceBundle().use(appRoot() + "login"); - messageResourceBundle().use(appRoot() + "main"); - messageResourceBundle().use(appRoot() + "mediaplayer"); - messageResourceBundle().use(appRoot() + "messages"); - messageResourceBundle().use(appRoot() + "misc"); - messageResourceBundle().use(appRoot() + "notifications"); - messageResourceBundle().use(appRoot() + "playqueue"); - messageResourceBundle().use(appRoot() + "release"); - messageResourceBundle().use(appRoot() + "releases"); - messageResourceBundle().use(appRoot() + "search"); - messageResourceBundle().use(appRoot() + "settings"); - messageResourceBundle().use(appRoot() + "tracklist"); - messageResourceBundle().use(appRoot() + "tracklists"); - messageResourceBundle().use(appRoot() + "tracks"); + setTitle(); + setLocalizedStrings(getOrCreateMessageBundle()); // Handle Media Scanner events and other session events enableUpdates(true); @@ -599,6 +617,15 @@ LmsApplication::post(std::function func) Wt::WServer::instance()->post(LmsApp->sessionId(), std::move(func)); } +void +LmsApplication::setTitle(const Wt::WString& title) +{ + if (title.empty()) + WApplication::setTitle("LMS"); + else + WApplication::setTitle(title + " | LMS"); +} + void LmsApplication::notifyMsg(Notification::Type type, const Wt::WString& category, const Wt::WString& message, std::chrono::milliseconds duration) { diff --git a/src/lms/ui/LmsApplication.hpp b/src/lms/ui/LmsApplication.hpp index 17619d40..35a58747 100644 --- a/src/lms/ui/LmsApplication.hpp +++ b/src/lms/ui/LmsApplication.hpp @@ -75,6 +75,7 @@ class LmsApplication : public Wt::WApplication // Utils void post(std::function func); + void setTitle(const Wt::WString& title = ""); // Used to classify the message sent to the user void notifyMsg(Notification::Type type, const Wt::WString& category, const Wt::WString& message, std::chrono::milliseconds duration = std::chrono::milliseconds {5000}); From 167865d0775d25c315f822a3ec0becfe21589500 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 7 Aug 2022 13:48:29 +0200 Subject: [PATCH 03/20] Setting window title to help browsing in history. ref #253 --- approot/messages.xml | 4 ++-- approot/messages_fr.xml | 1 + approot/messages_it.xml | 2 +- approot/messages_zh.xml | 1 + src/lms/ui/LmsApplication.cpp | 32 ++++++++++++++++------------ src/lms/ui/admin/UserView.cpp | 10 +++++++-- src/lms/ui/explore/ArtistView.cpp | 1 + src/lms/ui/explore/ReleaseView.cpp | 2 ++ src/lms/ui/explore/TrackListView.cpp | 1 + 9 files changed, 35 insertions(+), 19 deletions(-) diff --git a/approot/messages.xml b/approot/messages.xml index bb07bcc6..446b7c4b 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -104,7 +104,7 @@ User already exists! New user New user created! -Edit user '{1}' +User '{1}' User updated! @@ -133,6 +133,7 @@ Recently modified Recently played Albums +Search Star Starred Playlists @@ -187,7 +188,6 @@ Play History - Audio These audio settings are local to your browser! diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml index 8d6a1b38..ef4a9fb1 100644 --- a/approot/messages_fr.xml +++ b/approot/messages_fr.xml @@ -133,6 +133,7 @@ Modifiés récemment Joués récemment Albums +Rechercher Ajouter aux favoris Favoris Playlists diff --git a/approot/messages_it.xml b/approot/messages_it.xml index 4a254842..2eb16b86 100644 --- a/approot/messages_it.xml +++ b/approot/messages_it.xml @@ -133,9 +133,9 @@ Riprodotti di recente Album +Ricerca Aggiungi ai preferiti Preferiti - Tracce Tipo Rimuovi dai preferiti diff --git a/approot/messages_zh.xml b/approot/messages_zh.xml index 7738c0f6..a73ab6b6 100644 --- a/approot/messages_zh.xml +++ b/approot/messages_zh.xml @@ -130,6 +130,7 @@ 最近更改 最近播放 专辑 +搜索 收藏 已收藏 播放列表 diff --git a/src/lms/ui/LmsApplication.cpp b/src/lms/ui/LmsApplication.cpp index b724204f..e458a367 100644 --- a/src/lms/ui/LmsApplication.cpp +++ b/src/lms/ui/LmsApplication.cpp @@ -106,6 +106,7 @@ getOrCreateMessageBundle() return res; } +static constexpr const char* titleSeparator {" | "}; static constexpr const char* defaultPath {"/releases"}; std::unique_ptr @@ -378,21 +379,22 @@ handlePathChange(Wt::WStackedWidget& stack, bool isAdmin) std::string path; int index; bool admin; + std::optional title; } views[] = { - { "/artists", IdxExplore, false }, - { "/artist", IdxExplore, false }, - { "/releases", IdxExplore, false }, - { "/release", IdxExplore, false }, - { "/search", IdxExplore, false }, - { "/tracks", IdxExplore, false }, - { "/tracklists", IdxExplore, false }, - { "/tracklist", IdxExplore, false }, - { "/playqueue", IdxPlayQueue, false }, - { "/settings", IdxSettings, false }, - { "/admin/database", IdxAdminDatabase, true }, - { "/admin/users", IdxAdminUsers, true }, - { "/admin/user", IdxAdminUser, true }, + { "/artists", IdxExplore, false, Wt::WString::tr("Lms.Explore.artists") }, + { "/artist", IdxExplore, false, std::nullopt }, + { "/releases", IdxExplore, false, Wt::WString::tr("Lms.Explore.releases") }, + { "/release", IdxExplore, false, std::nullopt }, + { "/search", IdxExplore, false, Wt::WString::tr("Lms.Explore.search") }, + { "/tracks", IdxExplore, false, Wt::WString::tr("Lms.Explore.tracks") }, + { "/tracklists", IdxExplore, false, Wt::WString::tr("Lms.Explore.tracklists") }, + { "/tracklist", IdxExplore, false, std::nullopt }, + { "/playqueue", IdxPlayQueue, false, Wt::WString::tr("Lms.PlayQueue.playqueue") }, + { "/settings", IdxSettings, false, Wt::WString::tr("Lms.Settings.settings") }, + { "/admin/database", IdxAdminDatabase, true, Wt::WString::tr("Lms.Admin.Database.database") }, + { "/admin/users", IdxAdminUsers, true, Wt::WString::tr("Lms.Admin.Users.users") }, + { "/admin/user", IdxAdminUser, true, std::nullopt }, }; LMS_LOG(UI, DEBUG) << "Internal path changed to '" << wApp->internalPath() << "'"; @@ -406,6 +408,8 @@ handlePathChange(Wt::WStackedWidget& stack, bool isAdmin) break; stack.setCurrentIndex(view.index); + if (view.title) + LmsApp->setTitle(*view.title); return; } } @@ -623,7 +627,7 @@ LmsApplication::setTitle(const Wt::WString& title) if (title.empty()) WApplication::setTitle("LMS"); else - WApplication::setTitle(title + " | LMS"); + WApplication::setTitle(Wt::WString {"LMS"} + titleSeparator + title); } void diff --git a/src/lms/ui/admin/UserView.cpp b/src/lms/ui/admin/UserView.cpp index 99f41e6c..64fecb36 100644 --- a/src/lms/ui/admin/UserView.cpp +++ b/src/lms/ui/admin/UserView.cpp @@ -215,16 +215,22 @@ UserView::refreshView() if (!user) throw UserNotFoundException {}; - t->bindString("title", Wt::WString::tr("Lms.Admin.User.user-edit").arg(user->getLoginName()), Wt::TextFormat::Plain); + const Wt::WString title {Wt::WString::tr("Lms.Admin.User.user-edit").arg(user->getLoginName())}; + LmsApp->setTitle(title); + + t->bindString("title", title, Wt::TextFormat::Plain); t->setCondition("if-has-last-login", true); t->bindString("last-login", user->getLastLogin().toString(), Wt::TextFormat::Plain); } else { + const Wt::WString title {Wt::WString::tr("Lms.Admin.User.user-create")}; + LmsApp->setTitle(title); + // Login t->setCondition("if-has-login", true); t->setFormWidget(UserModel::LoginField, std::make_unique()); - t->bindString("title", Wt::WString::tr("Lms.Admin.User.user-create")); + t->bindString("title", title); } if (authPasswordService) diff --git a/src/lms/ui/explore/ArtistView.cpp b/src/lms/ui/explore/ArtistView.cpp index df9d5927..778ca615 100644 --- a/src/lms/ui/explore/ArtistView.cpp +++ b/src/lms/ui/explore/ArtistView.cpp @@ -109,6 +109,7 @@ Artist::refreshView() if (!artist) throw ArtistNotFoundException {}; + LmsApp->setTitle(artist->getName()); _artistId = *artistId; std::size_t sectionCount{}; diff --git a/src/lms/ui/explore/ReleaseView.cpp b/src/lms/ui/explore/ReleaseView.cpp index 78c924ff..b4f38bf2 100644 --- a/src/lms/ui/explore/ReleaseView.cpp +++ b/src/lms/ui/explore/ReleaseView.cpp @@ -106,6 +106,8 @@ Release::refreshView() if (!release) throw ReleaseNotFoundException {}; + LmsApp->setTitle(release->getName()); + refreshCopyright(release); refreshLinks(release); refreshSimilarReleases(similarReleasesIds); diff --git a/src/lms/ui/explore/TrackListView.cpp b/src/lms/ui/explore/TrackListView.cpp index e3c76726..30a0daa8 100644 --- a/src/lms/ui/explore/TrackListView.cpp +++ b/src/lms/ui/explore/TrackListView.cpp @@ -88,6 +88,7 @@ namespace UserInterface if (!trackList) throw TrackListNotFoundException {}; + LmsApp->setTitle(std::string {trackList->getName()}); _trackListId = *trackListId; clear(); From 4e246ac0b6ef49c41264be9bf843119a6508eeee Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 7 Aug 2022 14:12:53 +0200 Subject: [PATCH 04/20] Fixed arch build --- Dockerfile-build-arch | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile-build-arch b/Dockerfile-build-arch index eaecaaef..1d8ecd92 100644 --- a/Dockerfile-build-arch +++ b/Dockerfile-build-arch @@ -13,7 +13,7 @@ ARG BUILD_PACKAGES="\ taglib \ wt" -RUN pacman -Syy +RUN pacman -Syu --noconfirm RUN pacman -S --noconfirm ${BUILD_PACKAGES} # LMS From b58b2de58d7d0d9a783732be2a2072cd24ca3894 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 7 Aug 2022 14:13:34 +0200 Subject: [PATCH 05/20] Fixed some compilation warnings --- src/libs/services/database/impl/Artist.cpp | 2 +- src/lms/ui/explore/Filters.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/services/database/impl/Artist.cpp b/src/libs/services/database/impl/Artist.cpp index c267b3c9..84038255 100644 --- a/src/libs/services/database/impl/Artist.cpp +++ b/src/libs/services/database/impl/Artist.cpp @@ -277,7 +277,7 @@ Artist::getClusterGroups(std::vector clusterTypes, std::si Wt::Dbo::collection> queryRes = query; std::map> clustersByType; - for (const Cluster::pointer& cluster : queryRes) + for (Cluster::pointer cluster : queryRes) { if (clustersByType[cluster->getType()->getId()].size() < size) clustersByType[cluster->getType()->getId()].push_back(cluster); diff --git a/src/lms/ui/explore/Filters.cpp b/src/lms/ui/explore/Filters.cpp index 7310405f..73195990 100644 --- a/src/lms/ui/explore/Filters.cpp +++ b/src/lms/ui/explore/Filters.cpp @@ -94,7 +94,7 @@ Filters::showDialog() { const auto clusterType {ClusterType::find(LmsApp->getDbSession(), clusterTypesIds.results.front())}; - for (const Cluster::pointer cluster : clusterType->getClusters()) + for (const Cluster::pointer& cluster : clusterType->getClusters()) { if (std::find(std::cbegin(_clusterIds), std::cend(_clusterIds), cluster->getId()) == _clusterIds.end()) valueCombo->addItem(Wt::WString::fromUTF8(cluster->getName())); From 556d8ccb09f1b3591888d5bae6904eba8229d642 Mon Sep 17 00:00:00 2001 From: emeric Date: Thu, 18 Aug 2022 15:23:39 +0200 Subject: [PATCH 06/20] Add custom field 'type' in subsonic response to ease client hacks or custom behaviors. fixes #243 --- src/libs/subsonic/impl/SubsonicResponse.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libs/subsonic/impl/SubsonicResponse.cpp b/src/libs/subsonic/impl/SubsonicResponse.cpp index 38f6b631..da67c15a 100644 --- a/src/libs/subsonic/impl/SubsonicResponse.cpp +++ b/src/libs/subsonic/impl/SubsonicResponse.cpp @@ -117,6 +117,7 @@ Response::createOkResponse(ProtocolVersion protocolVersion) responseNode.setAttribute("status", "ok"); responseNode.setVersionAttribute(protocolVersion); + responseNode.setAttribute("type", "lms"); // non standard field to ease client hacks return response; } @@ -129,6 +130,7 @@ Response::createFailedResponse(ProtocolVersion protocolVersion, const Error& err responseNode.setAttribute("status", "failed"); responseNode.setVersionAttribute(protocolVersion); + responseNode.setAttribute("type", "lms"); // non standard field to ease client hacks Node& errorNode {responseNode.createChild("error")}; errorNode.setAttribute("code", std::to_string(static_cast(error.getCode()))); From 2aafeddb1bfba65a3470a4373d216eb8245ab7b7 Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 20 Aug 2022 14:26:04 +0200 Subject: [PATCH 07/20] Added missing translations --- approot/messages.xml | 1 + approot/messages_fr.xml | 1 + approot/messages_it.xml | 1 + approot/messages_zh.xml | 1 + 4 files changed, 4 insertions(+) diff --git a/approot/messages.xml b/approot/messages.xml index 446b7c4b..19b1a95e 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -208,6 +208,7 @@ Internal ListenBrainz ListenBrainz token +Settings Artist list mode All artists Album artists diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml index ef4a9fb1..571e40c4 100644 --- a/approot/messages_fr.xml +++ b/approot/messages_fr.xml @@ -208,6 +208,7 @@ Interne ListenBrainz Jeton ListenBrainz +Paramètres Mode de listage des artistes Tous les artistes Tous les artistes d'album diff --git a/approot/messages_it.xml b/approot/messages_it.xml index 2eb16b86..9dc877b8 100644 --- a/approot/messages_it.xml +++ b/approot/messages_it.xml @@ -207,6 +207,7 @@ + Modalità di elencazione artisti Tutti gli artisti Artisti album diff --git a/approot/messages_zh.xml b/approot/messages_zh.xml index a73ab6b6..cdfb1a8d 100644 --- a/approot/messages_zh.xml +++ b/approot/messages_zh.xml @@ -202,6 +202,7 @@ + 歌手列表模式 所有歌手 专辑歌手 From 49f0d40515600560d9534bb91dea2f85da438cc4 Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 20 Aug 2022 14:27:54 +0200 Subject: [PATCH 08/20] Handle track duration with milliseconds precision. ref #244 --- src/libs/metadata/impl/TagLibParser.cpp | 2 +- src/libs/services/database/impl/Migration.cpp | 10 ++++++++++ src/libs/services/database/impl/Migration.hpp | 2 +- src/tools/metadata/LmsMetadata.cpp | 4 ++-- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/libs/metadata/impl/TagLibParser.cpp b/src/libs/metadata/impl/TagLibParser.cpp index 0e8d827c..cad5f258 100644 --- a/src/libs/metadata/impl/TagLibParser.cpp +++ b/src/libs/metadata/impl/TagLibParser.cpp @@ -286,7 +286,7 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug) { const TagLib::AudioProperties *properties {f.audioProperties() }; - track.duration = std::chrono::milliseconds {properties->length() * 1000}; + track.duration = std::chrono::milliseconds {properties->lengthInMilliseconds()}; MetaData::AudioStream audioStream {static_cast(properties->bitrate() * 1000)}; track.audioStreams = {std::move(audioStream)}; diff --git a/src/libs/services/database/impl/Migration.cpp b/src/libs/services/database/impl/Migration.cpp index ef897499..b78a5138 100644 --- a/src/libs/services/database/impl/Migration.cpp +++ b/src/libs/services/database/impl/Migration.cpp @@ -591,6 +591,15 @@ CREATE TABLE IF NOT EXISTS "track_artist_link_backup" ( session.getDboSession().execute("ALTER TABLE tracklist ADD last_modified_date_time TEXT"); } + static + void + migrateFromV36(Session& session) + { + // Increased precision for track durations (now in milliseconds instead of secodns) + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + ScanSettings::get(session).modify()->incScanVersion(); + } + void doDbMigration(Session& session) { @@ -633,6 +642,7 @@ CREATE TABLE IF NOT EXISTS "track_artist_link_backup" ( {33, migrateFromV33}, {34, migrateFromV34}, {35, migrateFromV35}, + {36, migrateFromV36}, }; while (1) diff --git a/src/libs/services/database/impl/Migration.hpp b/src/libs/services/database/impl/Migration.hpp index 2e7f2f04..9a84c0b4 100644 --- a/src/libs/services/database/impl/Migration.hpp +++ b/src/libs/services/database/impl/Migration.hpp @@ -26,7 +26,7 @@ namespace Database class Session; using Version = std::size_t; - static constexpr Version LMS_DATABASE_VERSION {36}; + static constexpr Version LMS_DATABASE_VERSION {37}; class VersionInfo { public: diff --git a/src/tools/metadata/LmsMetadata.cpp b/src/tools/metadata/LmsMetadata.cpp index a64bd501..3d674da9 100644 --- a/src/tools/metadata/LmsMetadata.cpp +++ b/src/tools/metadata/LmsMetadata.cpp @@ -69,7 +69,7 @@ void parse(MetaData::IParser& parser, const std::filesystem::path& file) } const auto end {std::chrono::steady_clock::now()}; - std::cout << "Parsing time: " << std::chrono::duration_cast(end - start).count() / 1000. << "ms" << std::endl; + std::cout << "Parsing time: " << std::fixed << std::setprecision(2) << std::chrono::duration_cast(end - start).count() / 1000. << "ms" << std::endl; std::cout << "Track metadata:" << std::endl; @@ -117,7 +117,7 @@ void parse(MetaData::IParser& parser, const std::filesystem::path& file) } } - std::cout << "Duration: " << track->duration.count() / 1000 << "s" << std::endl; + std::cout << "Duration: " << std::fixed << std::setprecision(2) << track->duration.count() / 1000. << "s" << std::endl; if (track->trackNumber) std::cout << "Track: " << *track->trackNumber << std::endl; From 1fb838c8d039a7133474a4507284a395eda1a4b1 Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 20 Aug 2022 14:56:16 +0200 Subject: [PATCH 09/20] Subonsic API: added estimateContentLength parameter support for stream endpoint. fixes #244 --- src/libs/av/impl/TranscodeResourceHandler.cpp | 51 +++++++++-- src/libs/av/impl/TranscodeResourceHandler.hpp | 9 +- src/libs/av/impl/Transcoder.cpp | 40 ++++----- src/libs/av/impl/Transcoder.hpp | 10 +-- .../av/include/av/TranscodeParameters.hpp | 7 ++ .../av/TranscodeResourceHandlerCreator.hpp | 4 +- src/libs/subsonic/impl/Stream.cpp | 13 ++- src/libs/subsonic/impl/SubsonicId.cpp | 13 --- src/libs/subsonic/impl/SubsonicId.hpp | 4 - src/libs/utils/impl/String.cpp | 12 +++ src/libs/utils/include/utils/String.hpp | 5 ++ src/libs/utils/test/String.cpp | 9 ++ .../ui/resource/AudioTranscodeResource.cpp | 89 ++++++++++--------- 13 files changed, 162 insertions(+), 104 deletions(-) diff --git a/src/libs/av/impl/TranscodeResourceHandler.cpp b/src/libs/av/impl/TranscodeResourceHandler.cpp index 6554999c..54064a8a 100644 --- a/src/libs/av/impl/TranscodeResourceHandler.cpp +++ b/src/libs/av/impl/TranscodeResourceHandler.cpp @@ -18,32 +18,48 @@ */ #include "TranscodeResourceHandler.hpp" +#include "utils/Logger.hpp" namespace Av { + namespace + { + std::size_t + doEstimateContentLength(const InputFileParameters& inputFileParameters, const TranscodeParameters& transcodeParameters) + { + const std::size_t estimatedContentLength {transcodeParameters.bitrate / 8 * std::chrono::duration_cast(inputFileParameters.duration).count() / 1000}; + return estimatedContentLength; + } + } std::unique_ptr - createTranscodeResourceHandler(const std::filesystem::path& trackPath, const TranscodeParameters& parameters) + createTranscodeResourceHandler(const InputFileParameters& inputFileParameters, const TranscodeParameters& transcodeParameters, bool estimateContentLength) { - return std::make_unique(trackPath, parameters); + return std::make_unique(inputFileParameters, transcodeParameters, estimateContentLength); } // TODO set some nice HTTP return code - TranscodeResourceHandler::TranscodeResourceHandler(const std::filesystem::path& trackPath, const TranscodeParameters& parameters) - : _transcoder {trackPath, parameters} + TranscodeResourceHandler::TranscodeResourceHandler(const InputFileParameters& inputFileParameters, const TranscodeParameters& transcodeParameters, bool estimateContentLength) + : _estimatedContentLength {estimateContentLength ? std::make_optional(doEstimateContentLength(inputFileParameters, transcodeParameters)) : std::nullopt} + , _transcoder {inputFileParameters, transcodeParameters} { + if (_estimatedContentLength) + LMS_LOG(TRANSCODE, DEBUG) << "Estimated content length = " << *_estimatedContentLength; } Wt::Http::ResponseContinuation* TranscodeResourceHandler::processRequest(const Wt::Http::Request& /*request*/, Wt::Http::Response& response) { + if (_estimatedContentLength) + response.setContentLength(*_estimatedContentLength); response.setMimeType(_transcoder.getOutputMimeType()); - if (_nbBytesReady > 0) + if (_bytesReadyCount > 0) { - response.out().write(reinterpret_cast(&_buffer[0]), _nbBytesReady); - _nbBytesReady = 0; + response.out().write(reinterpret_cast(&_buffer[0]), _bytesReadyCount); + _bytesReadyCount = 0; + _totalServedByteCount += _bytesReadyCount; } if (!_transcoder.finished()) @@ -52,13 +68,30 @@ namespace Av continuation->waitForMoreData(); _transcoder.asyncRead(_buffer.data(), _buffer.size(), [=](std::size_t nbBytesRead) { - assert(_nbBytesReady == 0); - _nbBytesReady = nbBytesRead; + assert(_bytesReadyCount == 0); + _bytesReadyCount = nbBytesRead; continuation->haveMoreData(); }); return continuation; } + else + { + // pad with 0 if necessary as duration may not be accurate + if (_estimatedContentLength && *_estimatedContentLength > _totalServedByteCount) + { + const std::size_t padSize {*_estimatedContentLength - _totalServedByteCount}; + + LMS_LOG(TRANSCODE, DEBUG) << "Adding " << padSize << " padding bytes"; + + for (std::size_t i {}; i < padSize; ++i) + response.out().put(0); + + _totalServedByteCount += padSize; + } + + LMS_LOG(TRANSCODE, DEBUG) << "Transcoding finished. Total served byte count = " << _totalServedByteCount; + } return {}; } diff --git a/src/libs/av/impl/TranscodeResourceHandler.hpp b/src/libs/av/impl/TranscodeResourceHandler.hpp index 2395a6cc..47d76f36 100644 --- a/src/libs/av/impl/TranscodeResourceHandler.hpp +++ b/src/libs/av/impl/TranscodeResourceHandler.hpp @@ -21,6 +21,7 @@ #include #include +#include #include "av/TranscodeParameters.hpp" #include "utils/IResourceHandler.hpp" @@ -28,19 +29,19 @@ namespace Av { - class TranscodeResourceHandler final : public IResourceHandler { public: - TranscodeResourceHandler(const std::filesystem::path& trackPath, const TranscodeParameters& parameters); + TranscodeResourceHandler(const InputFileParameters& inputFileParameters, const TranscodeParameters& parameters, bool estimateContentLength); private: Wt::Http::ResponseContinuation* processRequest(const Wt::Http::Request& request, Wt::Http::Response& reponse) override; static constexpr std::size_t _chunkSize {32768}; + std::optional _estimatedContentLength; std::array _buffer; - std::size_t _nbBytesReady {}; - const std::filesystem::path _trackPath; + std::size_t _bytesReadyCount {}; + std::size_t _totalServedByteCount {}; Transcoder _transcoder; }; } diff --git a/src/libs/av/impl/Transcoder.cpp b/src/libs/av/impl/Transcoder.cpp index 55b00d1b..4b828d5a 100644 --- a/src/libs/av/impl/Transcoder.cpp +++ b/src/libs/av/impl/Transcoder.cpp @@ -30,7 +30,7 @@ namespace Av { -#define LOG(sev) LMS_LOG(TRANSCODE, sev) << "[" << _id << "] - " +#define LOG(sev) LMS_LOG(TRANSCODE, sev) << "[" << _debugId << "] - " static std::atomic globalId {}; static std::filesystem::path ffmpegPath; @@ -43,10 +43,10 @@ Transcoder::init() throw Exception {"File '" + ffmpegPath.string() + "' does not exist!"}; } -Transcoder::Transcoder(const std::filesystem::path& filePath, const TranscodeParameters& parameters) -: _id {globalId++} -, _filePath {filePath} -, _parameters {parameters} +Transcoder::Transcoder(const InputFileParameters& inputFileParameters, const TranscodeParameters& transcodeParameters) +: _debugId {globalId++} +, _inputFileParameters {inputFileParameters} +, _transcodeParameters {transcodeParameters} { start(); } @@ -61,17 +61,17 @@ Transcoder::start() try { - if (!std::filesystem::exists(_filePath)) - throw Exception {"File '" + _filePath.string() + "' does not exist!"}; - else if (!std::filesystem::is_regular_file( _filePath) ) - throw Exception {"File '" + _filePath.string() + "' is not regular!"}; + if (!std::filesystem::exists(_inputFileParameters.trackPath)) + throw Exception {"File '" + _inputFileParameters.trackPath.string() + "' does not exist!"}; + else if (!std::filesystem::is_regular_file( _inputFileParameters.trackPath) ) + throw Exception {"File '" + _inputFileParameters.trackPath.string() + "' is not regular!"}; } catch (const std::filesystem::filesystem_error& e) { - throw Exception {"File error '" + _filePath.string() + "': " + e.what()}; + throw Exception {"File error '" + _inputFileParameters.trackPath.string() + "': " + e.what()}; } - LOG(INFO) << "Transcoding file '" << _filePath.string() << "'"; + LOG(INFO) << "Transcoding file '" << _inputFileParameters.trackPath.string() << "'"; std::vector args; @@ -90,22 +90,22 @@ Transcoder::start() args.emplace_back("-ss"); std::ostringstream oss; - oss << std::fixed << std::showpoint << std::setprecision(3) << (_parameters.offset.count() / float {1000}); + oss << std::fixed << std::showpoint << std::setprecision(3) << (_transcodeParameters.offset.count() / float {1000}); args.emplace_back(oss.str()); } // Input file args.emplace_back("-i"); - args.emplace_back(_filePath.string()); + args.emplace_back(_inputFileParameters.trackPath.string()); // Stream mapping, if set - if (_parameters.stream) + if (_transcodeParameters.stream) { args.emplace_back("-map"); - args.emplace_back("0:" + std::to_string(*_parameters.stream)); + args.emplace_back("0:" + std::to_string(*_transcodeParameters.stream)); } - if (_parameters.stripMetadata) + if (_transcodeParameters.stripMetadata) { // Strip metadata args.emplace_back("-map_metadata"); @@ -117,10 +117,10 @@ Transcoder::start() // Output bitrates args.emplace_back("-b:a"); - args.emplace_back(std::to_string(_parameters.bitrate)); + args.emplace_back(std::to_string(_transcodeParameters.bitrate)); // Codecs and formats - switch (_parameters.format) + switch (_transcodeParameters.format) { case Format::MP3: args.emplace_back("-f"); @@ -156,10 +156,10 @@ Transcoder::start() break; default: - throw Exception {"Unhandled format (" + std::to_string(static_cast(_parameters.format)) + ")"}; + throw Exception {"Unhandled format (" + std::to_string(static_cast(_transcodeParameters.format)) + ")"}; } - _outputMimeType = formatToMimetype(_parameters.format); + _outputMimeType = formatToMimetype(_transcodeParameters.format); args.emplace_back("pipe:1"); diff --git a/src/libs/av/impl/Transcoder.hpp b/src/libs/av/impl/Transcoder.hpp index 54ebe5e5..a4de2976 100644 --- a/src/libs/av/impl/Transcoder.hpp +++ b/src/libs/av/impl/Transcoder.hpp @@ -32,7 +32,7 @@ namespace Av class Transcoder { public: - Transcoder(const std::filesystem::path& file, const TranscodeParameters& parameters); + Transcoder(const InputFileParameters& inputFileParameters, const TranscodeParameters& transcodeParameters); ~Transcoder(); Transcoder(const Transcoder&) = delete; @@ -49,7 +49,7 @@ namespace Av std::size_t readSome(std::byte* buffer, std::size_t bufferSize); const std::string& getOutputMimeType() const { return _outputMimeType; } - const TranscodeParameters& getParameters() const { return _parameters; } + const TranscodeParameters& getParameters() const { return _transcodeParameters; } bool finished() const; @@ -58,9 +58,9 @@ namespace Av void start(); - const std::size_t _id {}; - const std::filesystem::path _filePath; - const TranscodeParameters _parameters; + const std::size_t _debugId {}; + const InputFileParameters _inputFileParameters; + const TranscodeParameters _transcodeParameters; std::unique_ptr _childProcess; diff --git a/src/libs/av/include/av/TranscodeParameters.hpp b/src/libs/av/include/av/TranscodeParameters.hpp index c155f6e7..f709fef8 100644 --- a/src/libs/av/include/av/TranscodeParameters.hpp +++ b/src/libs/av/include/av/TranscodeParameters.hpp @@ -20,12 +20,19 @@ #pragma once #include +#include #include #include "Types.hpp" namespace Av { + struct InputFileParameters + { + std::filesystem::path trackPath; + std::chrono::milliseconds duration; + }; + struct TranscodeParameters { Format format; diff --git a/src/libs/av/include/av/TranscodeResourceHandlerCreator.hpp b/src/libs/av/include/av/TranscodeResourceHandlerCreator.hpp index 0255cfc4..8b232d8a 100644 --- a/src/libs/av/include/av/TranscodeResourceHandlerCreator.hpp +++ b/src/libs/av/include/av/TranscodeResourceHandlerCreator.hpp @@ -19,15 +19,15 @@ #pragma once -#include #include #include "utils/IResourceHandler.hpp" namespace Av { + struct InputFileParameters; struct TranscodeParameters; - std::unique_ptr createTranscodeResourceHandler(const std::filesystem::path& trackPath, const TranscodeParameters& parameters); + std::unique_ptr createTranscodeResourceHandler(const InputFileParameters& inputFileParameters, const TranscodeParameters& parameters, bool estimateContentLength); } diff --git a/src/libs/subsonic/impl/Stream.cpp b/src/libs/subsonic/impl/Stream.cpp index 8089779c..91c13eaf 100644 --- a/src/libs/subsonic/impl/Stream.cpp +++ b/src/libs/subsonic/impl/Stream.cpp @@ -54,8 +54,9 @@ userTranscodeFormatToAvFormat(AudioFormat format) struct StreamParameters { - std::filesystem::path trackPath; + Av::InputFileParameters inputFileParameters; std::optional transcodeParameters; + bool estimateContentLength {}; }; static @@ -68,9 +69,12 @@ getStreamParameters(RequestContext& context) // Optional params std::optional maxBitRate {getParameterAs(context.parameters, "maxBitRate")}; std::optional format {getParameterAs(context.parameters, "format")}; + bool estimateContentLength {getParameterAs(context.parameters, "estimateContentLength").value_or(false)}; StreamParameters parameters; + parameters.estimateContentLength = estimateContentLength; + auto transaction {context.dbSession.createSharedTransaction()}; { @@ -78,7 +82,8 @@ getStreamParameters(RequestContext& context) if (!track) throw RequestedDataNotFoundError {}; - parameters.trackPath = track->getPath(); + parameters.inputFileParameters.trackPath = track->getPath(); + parameters.inputFileParameters.duration = track->getDuration(); } { @@ -155,9 +160,9 @@ handleStream(RequestContext& context, const Wt::Http::Request& request, Wt::Http { StreamParameters streamParameters {getStreamParameters(context)}; if (streamParameters.transcodeParameters) - resourceHandler = Av::createTranscodeResourceHandler(streamParameters.trackPath, *streamParameters.transcodeParameters); + resourceHandler = Av::createTranscodeResourceHandler(streamParameters.inputFileParameters, *streamParameters.transcodeParameters, streamParameters.estimateContentLength); else - resourceHandler = createFileResourceHandler(streamParameters.trackPath); + resourceHandler = createFileResourceHandler(streamParameters.inputFileParameters.trackPath); } else { diff --git a/src/libs/subsonic/impl/SubsonicId.cpp b/src/libs/subsonic/impl/SubsonicId.cpp index edd052f1..1252a7b8 100644 --- a/src/libs/subsonic/impl/SubsonicId.cpp +++ b/src/libs/subsonic/impl/SubsonicId.cpp @@ -136,17 +136,4 @@ namespace StringUtils return std::nullopt; } - - template<> - std::optional - readAs(std::string_view str) - { - if (str == "true") - return true; - else if (str == "false") - return false; - - return {}; - } } - diff --git a/src/libs/subsonic/impl/SubsonicId.hpp b/src/libs/subsonic/impl/SubsonicId.hpp index 32b45715..96db3e79 100644 --- a/src/libs/subsonic/impl/SubsonicId.hpp +++ b/src/libs/subsonic/impl/SubsonicId.hpp @@ -58,9 +58,5 @@ namespace StringUtils template<> std::optional readAs(std::string_view str); - - template<> - std::optional - readAs(std::string_view str); } diff --git a/src/libs/utils/impl/String.cpp b/src/libs/utils/impl/String.cpp index 09c55dd3..47adbd85 100644 --- a/src/libs/utils/impl/String.cpp +++ b/src/libs/utils/impl/String.cpp @@ -63,6 +63,18 @@ readAs(std::string_view str) return std::string {str}; } +template<> +std::optional +readAs(std::string_view str) +{ + if (str == "1" || str == "true") + return true; + else if (str == "0" || str == "false") + return false; + + return std::nullopt; +} + std::vector splitStringCopy(std::string_view string, std::string_view separators) { diff --git a/src/libs/utils/include/utils/String.hpp b/src/libs/utils/include/utils/String.hpp index 8ff743c5..9dfe8742 100644 --- a/src/libs/utils/include/utils/String.hpp +++ b/src/libs/utils/include/utils/String.hpp @@ -85,6 +85,11 @@ template<> std::optional readAs(std::string_view str); +template<> +[[nodiscard]] +std::optional +readAs(std::string_view str); + [[nodiscard]] std::string replaceInString(std::string_view str, const std::string& from, const std::string& to); diff --git a/src/libs/utils/test/String.cpp b/src/libs/utils/test/String.cpp index 3935f6c7..87d67ee3 100644 --- a/src/libs/utils/test/String.cpp +++ b/src/libs/utils/test/String.cpp @@ -109,3 +109,12 @@ TEST(StringUtils, escapeString) EXPECT_EQ(StringUtils::escapeString("**||", "*|", '_'), "_*_*_|_|"); } +TEST(StringUtils, readAs) +{ + EXPECT_EQ(StringUtils::readAs("true"), true); + EXPECT_EQ(StringUtils::readAs("1"), true); + EXPECT_EQ(StringUtils::readAs("false"), false); + EXPECT_EQ(StringUtils::readAs("0"), false); + EXPECT_EQ(StringUtils::readAs("foo"), std::nullopt); + EXPECT_EQ(StringUtils::readAs(""), std::nullopt); +} diff --git a/src/lms/ui/resource/AudioTranscodeResource.cpp b/src/lms/ui/resource/AudioTranscodeResource.cpp index c8982fbd..b7797724 100644 --- a/src/lms/ui/resource/AudioTranscodeResource.cpp +++ b/src/lms/ui/resource/AudioTranscodeResource.cpp @@ -118,59 +118,62 @@ readParameterAs(const Wt::Http::Request& request, const std::string& parameterNa return res; } -struct TranscodeParameters +namespace { - std::filesystem::path file; - Av::TranscodeParameters transcodeParameters; -}; - -static -std::optional -readTranscodeParameters(const Wt::Http::Request& request) -{ - TranscodeParameters parameters; - - // mandatory parameters - const std::optional trackId {readParameterAs(request, "trackid")}; - const auto format {readParameterAs(request, "format")}; - const auto bitrate {readParameterAs(request, "bitrate")}; - - if (!trackId || !format || !bitrate) - return std::nullopt; - - const std::optional avFormat {AudioFormatToAvFormat(*format)}; - if (!avFormat) - return std::nullopt; - - // optional parameter - std::size_t offset {readParameterAs(request, "offset").value_or(0)}; - - std::filesystem::path trackPath; + struct TranscodeParameters { - auto transaction {LmsApp->getDbSession().createSharedTransaction()}; + Av::InputFileParameters inputFileParameters; + Av::TranscodeParameters transcodeParameters; + }; - const Database::Track::pointer track {Database::Track::find(LmsApp->getDbSession(), *trackId)}; - if (!track) - { - LOG(ERROR) << "Missing track"; + std::optional + readTranscodeParameters(const Wt::Http::Request& request) + { + TranscodeParameters parameters; + + // mandatory parameters + const std::optional trackId {readParameterAs(request, "trackid")}; + const auto format {readParameterAs(request, "format")}; + const auto bitrate {readParameterAs(request, "bitrate")}; + + if (!trackId || !format || !bitrate) return std::nullopt; - } - - parameters.file = track->getPath(); if (!Database::isAudioBitrateAllowed(*bitrate)) { LOG(ERROR) << "Bitrate '" << *bitrate << "' is not allowed"; return std::nullopt; } + + const std::optional avFormat {AudioFormatToAvFormat(*format)}; + if (!avFormat) + return std::nullopt; + + // optional parameter + std::size_t offset {readParameterAs(request, "offset").value_or(0)}; + + std::filesystem::path trackPath; + { + auto transaction {LmsApp->getDbSession().createSharedTransaction()}; + + const Database::Track::pointer track {Database::Track::find(LmsApp->getDbSession(), *trackId)}; + if (!track) + { + LOG(ERROR) << "Missing track"; + return std::nullopt; + } + + parameters.inputFileParameters.trackPath = track->getPath(); + parameters.inputFileParameters.duration = track->getDuration(); + } + + parameters.transcodeParameters.stripMetadata = true; + parameters.transcodeParameters.format = *avFormat; + parameters.transcodeParameters.bitrate = *bitrate; + parameters.transcodeParameters.offset = std::chrono::seconds {offset}; + + return parameters; } - - parameters.transcodeParameters.stripMetadata = true; - parameters.transcodeParameters.format = *avFormat; - parameters.transcodeParameters.bitrate = *bitrate; - parameters.transcodeParameters.offset = std::chrono::seconds {offset}; - - return parameters; } void @@ -186,7 +189,7 @@ AudioTranscodeResource::handleRequest(const Wt::Http::Request& request, { const std::optional& parameters {readTranscodeParameters(request)}; if (parameters) - resourceHandler = Av::createTranscodeResourceHandler(parameters->file, parameters->transcodeParameters); + resourceHandler = Av::createTranscodeResourceHandler(parameters->inputFileParameters, parameters->transcodeParameters, false /* estimate content length */); } else { From 5324d1fd15eae1d34fa82bb10e4241f2615f15ab Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 21 Aug 2022 14:17:20 +0200 Subject: [PATCH 10/20] Added a config parameter to set the read style used by the metadata parser. Default value switched from fast to accurate --- conf/lms.conf | 3 ++ src/libs/metadata/CMakeLists.txt | 1 + src/libs/metadata/impl/AvFormatParser.cpp | 2 +- .../metadata => impl}/AvFormatParser.hpp | 0 src/libs/metadata/impl/Factory.cpp | 48 +++++++++++++++++++ src/libs/metadata/impl/TagLibParser.cpp | 27 ++++++++++- .../metadata => impl}/TagLibParser.hpp | 7 ++- src/libs/metadata/impl/Utils.cpp | 15 ++++++ src/libs/metadata/impl/Utils.hpp | 3 ++ .../metadata/include/metadata/IParser.hpp | 13 +++++ .../services/scanner/impl/ScannerService.cpp | 21 ++++++-- src/tools/metadata/LmsMetadata.cpp | 11 ++--- 12 files changed, 137 insertions(+), 14 deletions(-) rename src/libs/metadata/{include/metadata => impl}/AvFormatParser.hpp (100%) create mode 100644 src/libs/metadata/impl/Factory.cpp rename src/libs/metadata/{include/metadata => impl}/TagLibParser.hpp (88%) diff --git a/conf/lms.conf b/conf/lms.conf index ca7dae59..5729eaa9 100644 --- a/conf/lms.conf +++ b/conf/lms.conf @@ -80,3 +80,6 @@ cover-preferred-file-names = ("cover", "front" ); # Set to true if you want to hide duplicate tracks scanner-skip-duplicate-recording-mbid = false; + +# Scanner read style for metadata, maybe be 'fast', 'average' or 'accurate' +scanner-parser-read-style = "accurate"; diff --git a/src/libs/metadata/CMakeLists.txt b/src/libs/metadata/CMakeLists.txt index 90b6fd92..b2502aa3 100644 --- a/src/libs/metadata/CMakeLists.txt +++ b/src/libs/metadata/CMakeLists.txt @@ -5,6 +5,7 @@ endif() add_library(lmsmetadata SHARED impl/AvFormatParser.cpp + impl/Factory.cpp impl/TagLibParser.cpp impl/Utils.cpp ) diff --git a/src/libs/metadata/impl/AvFormatParser.cpp b/src/libs/metadata/impl/AvFormatParser.cpp index e5a472dd..1284ffb7 100644 --- a/src/libs/metadata/impl/AvFormatParser.cpp +++ b/src/libs/metadata/impl/AvFormatParser.cpp @@ -17,7 +17,7 @@ * along with LMS. If not, see . */ -#include "metadata/AvFormatParser.hpp" +#include "AvFormatParser.hpp" #include #include diff --git a/src/libs/metadata/include/metadata/AvFormatParser.hpp b/src/libs/metadata/impl/AvFormatParser.hpp similarity index 100% rename from src/libs/metadata/include/metadata/AvFormatParser.hpp rename to src/libs/metadata/impl/AvFormatParser.hpp diff --git a/src/libs/metadata/impl/Factory.cpp b/src/libs/metadata/impl/Factory.cpp new file mode 100644 index 00000000..3325693e --- /dev/null +++ b/src/libs/metadata/impl/Factory.cpp @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2022 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 "metadata/IParser.hpp" + +#include "utils/Exception.hpp" +#include "utils/Logger.hpp" + +#include "AvFormatParser.hpp" +#include "TagLibParser.hpp" +#include "Utils.hpp" + +namespace MetaData +{ + std::unique_ptr + createParser(ParserType parserType, ParserReadStyle parserReadStyle) + { + + switch (parserType) + { + case ParserType::TagLib: + LMS_LOG(METADATA, INFO) << "Creating TagLib parser with read style = " << Utils::readStyleToString(parserReadStyle); + return std::make_unique(parserReadStyle); + case ParserType::AvFormat: + LMS_LOG(METADATA, INFO) << "Creating AvFormat parser"; + return std::make_unique(); + } + + throw LmsException {"Unhandled parser type"}; + } +} + diff --git a/src/libs/metadata/impl/TagLibParser.cpp b/src/libs/metadata/impl/TagLibParser.cpp index cad5f258..d5c9ba6c 100644 --- a/src/libs/metadata/impl/TagLibParser.cpp +++ b/src/libs/metadata/impl/TagLibParser.cpp @@ -17,7 +17,7 @@ * along with LMS. If not, see . */ -#include "metadata/TagLibParser.hpp" +#include "TagLibParser.hpp" #include #include @@ -33,7 +33,10 @@ #include #include +#include "utils/IConfig.hpp" +#include "utils/Exception.hpp" #include "utils/Logger.hpp" +#include "utils/Service.hpp" #include "utils/String.hpp" #include "Utils.hpp" @@ -145,6 +148,26 @@ getAlbum(const TagLib::PropertyMap& properties) return Album {std::move(albumName.front()), albumMBID.front()}; } +static +TagLib::AudioProperties::ReadStyle +readStyleToTagLibReadStyle(ParserReadStyle readStyle) +{ + switch (readStyle) + { + case ParserReadStyle::Fast: return TagLib::AudioProperties::ReadStyle::Fast; + case ParserReadStyle::Average: return TagLib::AudioProperties::ReadStyle::Average; + case ParserReadStyle::Accurate: return TagLib::AudioProperties::ReadStyle::Accurate; + } + + throw LmsException {"Cannot convert read style"}; +} + + +TagLibParser::TagLibParser(ParserReadStyle readStyle) + : _readStyle {readStyleToTagLibReadStyle(readStyle)} +{ +} + void TagLibParser::processTag(Track& track, const std::string& tag, const TagLib::StringList& values, bool debug) { @@ -267,7 +290,7 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug) { TagLib::FileRef f {p.string().c_str(), true, // read audio properties - TagLib::AudioProperties::Fast}; // TODO parametrize this + _readStyle}; if (f.isNull()) { diff --git a/src/libs/metadata/include/metadata/TagLibParser.hpp b/src/libs/metadata/impl/TagLibParser.hpp similarity index 88% rename from src/libs/metadata/include/metadata/TagLibParser.hpp rename to src/libs/metadata/impl/TagLibParser.hpp index ee298f8d..8990fd1c 100644 --- a/src/libs/metadata/include/metadata/TagLibParser.hpp +++ b/src/libs/metadata/impl/TagLibParser.hpp @@ -19,6 +19,7 @@ #pragma once +#include #include "metadata/IParser.hpp" namespace TagLib @@ -32,10 +33,14 @@ namespace MetaData // Parse that makes use of AvFormat class TagLibParser : public IParser { + public: + TagLibParser(ParserReadStyle readStyle); + private: std::optional parse(const std::filesystem::path& p, bool debug = false) override; - void processTag(Track& track, const std::string& tag, const TagLib::StringList& values, bool debug); + + const TagLib::AudioProperties::ReadStyle _readStyle; }; } // namespace MetaData diff --git a/src/libs/metadata/impl/Utils.cpp b/src/libs/metadata/impl/Utils.cpp index 090db19c..7919ca8a 100644 --- a/src/libs/metadata/impl/Utils.cpp +++ b/src/libs/metadata/impl/Utils.cpp @@ -23,6 +23,8 @@ #include #include +#include "utils/Exception.hpp" + namespace MetaData::Utils { Wt::WDate @@ -56,5 +58,18 @@ namespace MetaData::Utils return {}; } + + std::string_view + readStyleToString(ParserReadStyle readStyle) + { + switch (readStyle) + { + case ParserReadStyle::Fast: return "fast"; + case ParserReadStyle::Average: return "average"; + case ParserReadStyle::Accurate: return "accurate"; + } + + throw LmsException {"Unknown read style"}; + } } diff --git a/src/libs/metadata/impl/Utils.hpp b/src/libs/metadata/impl/Utils.hpp index 30025627..0bbd4485 100644 --- a/src/libs/metadata/impl/Utils.hpp +++ b/src/libs/metadata/impl/Utils.hpp @@ -20,8 +20,11 @@ #include +#include "metadata/IParser.hpp" + namespace MetaData::Utils { Wt::WDate parseDate(const std::string& dateStr); + std::string_view readStyleToString(ParserReadStyle readStyle); } diff --git a/src/libs/metadata/include/metadata/IParser.hpp b/src/libs/metadata/include/metadata/IParser.hpp index 18db7f1d..b4555d31 100644 --- a/src/libs/metadata/include/metadata/IParser.hpp +++ b/src/libs/metadata/include/metadata/IParser.hpp @@ -100,5 +100,18 @@ namespace MetaData std::set _clusterTypeNames; }; + enum class ParserType + { + TagLib, + AvFormat, + }; + + enum class ParserReadStyle + { + Fast, + Average, + Accurate, + }; + std::unique_ptr createParser(ParserType parserType, ParserReadStyle parserReadStyle); } // namespace MetaData diff --git a/src/libs/services/scanner/impl/ScannerService.cpp b/src/libs/services/scanner/impl/ScannerService.cpp index c970b9d2..dea3c796 100644 --- a/src/libs/services/scanner/impl/ScannerService.cpp +++ b/src/libs/services/scanner/impl/ScannerService.cpp @@ -31,7 +31,7 @@ #include "services/database/Track.hpp" #include "services/database/TrackArtistLink.hpp" #include "services/database/TrackFeatures.hpp" -#include "metadata/TagLibParser.hpp" +#include "metadata/IParser.hpp" #include "services/recommendation/IRecommendationService.hpp" #include "utils/Exception.hpp" #include "utils/IConfig.hpp" @@ -255,16 +255,29 @@ createScannerService(Db& db, Recommendation::IRecommendationService& recommendat return std::make_unique(db, recommendationService); } +MetaData::ParserReadStyle +getParserReadStyle() +{ + std::string_view readStyle {Service::get()->getString("scanner-parser-read-style", "accurate")}; + + if (readStyle == "fast") + return MetaData::ParserReadStyle::Fast; + else if (readStyle == "average") + return MetaData::ParserReadStyle::Average; + else if (readStyle == "accurate") + return MetaData::ParserReadStyle::Accurate; + + throw LmsException {"Invalid value for 'scanner-parser-read-style'"}; +} + ScannerService::ScannerService(Db& db, Recommendation::IRecommendationService& recommendationService) : _recommendationService {recommendationService} , _skipDuplicateRecordingMBID {Service::get()->getBool("scanner-skip-duplicate-recording-mbid", false)} , _dbSession {db} +, _metadataParser {MetaData::createParser(MetaData::ParserType::TagLib, getParserReadStyle())} // For now, always use TagLib { LMS_LOG(DBUPDATER, INFO) << "skipDuplicateRecordingMBID = " << _skipDuplicateRecordingMBID; - // For now, always use TagLib - _metadataParser = std::make_unique(); - _ioService.setThreadCount(1); refreshScanSettings(); diff --git a/src/tools/metadata/LmsMetadata.cpp b/src/tools/metadata/LmsMetadata.cpp index 3d674da9..bfeea1e3 100644 --- a/src/tools/metadata/LmsMetadata.cpp +++ b/src/tools/metadata/LmsMetadata.cpp @@ -25,8 +25,7 @@ #include -#include "metadata/AvFormatParser.hpp" -#include "metadata/TagLibParser.hpp" +#include "metadata/IParser.hpp" #include "utils/StreamLogger.hpp" std::ostream& operator<<(std::ostream& os, const MetaData::Artist& artist) @@ -184,14 +183,14 @@ int main(int argc, char *argv[]) { std::cout << "Using av:" << std::endl; - MetaData::AvFormatParser parser; - parse(parser, file); + auto parser {MetaData::createParser(MetaData::ParserType::AvFormat, MetaData::ParserReadStyle::Accurate)}; + parse(*parser, file); } { std::cout << "Using TagLib:" << std::endl; - MetaData::TagLibParser parser; - parse(parser, file); + auto parser {MetaData::createParser(MetaData::ParserType::TagLib, MetaData::ParserReadStyle::Accurate)}; + parse(*parser, file); } } } From 31fe884c6dc8f42206e6c606515f6c7c996bffac Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 21 Aug 2022 14:18:16 +0200 Subject: [PATCH 11/20] Fixed 32bits build --- src/libs/av/impl/TranscodeResourceHandler.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/av/impl/TranscodeResourceHandler.cpp b/src/libs/av/impl/TranscodeResourceHandler.cpp index 54064a8a..0f2687d3 100644 --- a/src/libs/av/impl/TranscodeResourceHandler.cpp +++ b/src/libs/av/impl/TranscodeResourceHandler.cpp @@ -27,7 +27,7 @@ namespace Av std::size_t doEstimateContentLength(const InputFileParameters& inputFileParameters, const TranscodeParameters& transcodeParameters) { - const std::size_t estimatedContentLength {transcodeParameters.bitrate / 8 * std::chrono::duration_cast(inputFileParameters.duration).count() / 1000}; + const std::size_t estimatedContentLength {transcodeParameters.bitrate / 8 * static_cast(std::chrono::duration_cast(inputFileParameters.duration).count()) / 1000}; return estimatedContentLength; } } From e3f5d9c027312d9eb503cef41d747e6a91944917 Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 3 Sep 2022 14:42:53 +0200 Subject: [PATCH 12/20] defer audio ctx initialization to first user interaction. fixes #194 --- docroot/js/mediaplayer.js | 95 ++++++++++++++++++++++++++------------- 1 file changed, 65 insertions(+), 30 deletions(-) diff --git a/docroot/js/mediaplayer.js b/docroot/js/mediaplayer.js index f9e81d54..21acc695 100644 --- a/docroot/js/mediaplayer.js +++ b/docroot/js/mediaplayer.js @@ -25,10 +25,41 @@ LMS.mediaplayer = function () { var _audioNativeSrc; var _audioTranscodeSrc; var _settings = {}; - var _audioCtx = new (window.AudioContext || window.webkitAudioContext)(); - var _gainNode = _audioCtx.createGain(); var _playedDuration = 0; var _lastStartPlaying = null; + var _audioIsInit = false; + var _pendingTrackParameters = null; + + var _unlock = function() { + document.removeEventListener("touchstart", _unlock); + document.removeEventListener("touchend", _unlock); + document.removeEventListener("click", _unlock); + _initAudioCtx(); + }; + + document.addEventListener("touchstart", _unlock); + document.addEventListener("touchend", _unlock); + document.addEventListener("click", _unlock); + + var _initAudioCtx = function() { + if (_audioIsInit) + return; + + _audioIsInit = true; + + _audioCtx = new (window.AudioContext || window.webkitAudioContext)(); + _gainNode = _audioCtx.createGain(); + source = _audioCtx.createMediaElementSource(_elems.audio); + source.connect(_gainNode); + _gainNode.connect(_audioCtx.destination); + _audioCtx.resume(); // not sure of this + + if (_pendingTrackParameters != null) { + _applyAudioTrackParameters(_pendingTrackParameters, false); + _pendingTrackParameters = null; + } + + } var _updateControls = function() { const pauseClass = "fa-pause"; @@ -82,14 +113,15 @@ LMS.mediaplayer = function () { playPromise.then(_ => { // Automatic playback started }) - .catch(error => { - // Auto-play was prevented - }); + .catch(error => { + // Auto-play was prevented + }); } } var _playPause = function() { - _audioCtx.resume(); + _initAudioCtx(); + if (_elems.audio.paused && _elems.audio.children.length > 0) { _playTrack(); } @@ -98,20 +130,12 @@ LMS.mediaplayer = function () { } var _playPrevious = function() { - _audioCtx.resume(); - _requestPreviousTrack(); - } - - var _playNext = function() { - _audioCtx.resume(); - _requestNextTrack(); - } - - var _requestPreviousTrack = function() { + _initAudioCtx(); Wt.emit(_root, "playPrevious"); } - var _requestNextTrack = function() { + var _playNext = function() { + _initAudioCtx(); Wt.emit(_root, "playNext"); } @@ -181,10 +205,6 @@ LMS.mediaplayer = function () { _elems.volumeslider = document.getElementById("lms-mp-volume-slider"); _elems.transcodingActive = document.getElementById("lms-transcoding-active"); - var source = _audioCtx.createMediaElementSource(_elems.audio); - source.connect(_gainNode); - _gainNode.connect(_audioCtx.destination); - _elems.playpause.addEventListener("click", function() { _playPause(); }); @@ -196,7 +216,7 @@ LMS.mediaplayer = function () { _playNext(); }); _elems.seek.addEventListener("change", function() { - _audioCtx.resume(); + _initAudioCtx(); let mode = _getAudioMode(); if (!mode) return; @@ -286,14 +306,19 @@ LMS.mediaplayer = function () { }); if ('mediaSession' in navigator) { + navigator.mediaSession.setActionHandler("play", function() { + _playPause(); + }); + navigator.mediaSession.setActionHandler("pause", function() { + _playPause(); + }); navigator.mediaSession.setActionHandler("previoustrack", function() { - _requestPreviousTrack(); + _playPrevious(); }); navigator.mediaSession.setActionHandler("nexttrack", function() { - _requestNextTrack(); + _playNext(); }); } - } var _removeAudioSources = function() { @@ -343,14 +368,9 @@ LMS.mediaplayer = function () { } _elems.audio.load(); - _setReplayGain(params.replayGain); - _elems.curtime.innerHTML = _durationToString(_offset); _elems.duration.innerHTML = _durationToString(_duration); - if (autoplay && _audioCtx.state == "running") - _playTrack(); - if ('mediaSession' in navigator) { navigator.mediaSession.metadata = new MediaMetadata({ title: params.title, @@ -359,6 +379,21 @@ LMS.mediaplayer = function () { artwork: params.artwork, }); } + + if (!_audioIsInit) { + _pendingTrackParameters = params; + return; + } + + _applyAudioTrackParameters(params, autoplay); + + if (autoplay && _audioCtx.state == "running") + _playTrack(); + } + + var _applyAudioTrackParameters = function(params) + { + _setReplayGain(params.replayGain); } var stop = function() { From 91bc963c4278768074b95747dca2266590e2185a Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 3 Sep 2022 20:20:12 +0200 Subject: [PATCH 13/20] Defer media session after audio context init. Seems to fix random media session issues --- docroot/js/mediaplayer.js | 71 ++++++++++++++++++++------------------- 1 file changed, 37 insertions(+), 34 deletions(-) diff --git a/docroot/js/mediaplayer.js b/docroot/js/mediaplayer.js index 21acc695..b2c7c3c2 100644 --- a/docroot/js/mediaplayer.js +++ b/docroot/js/mediaplayer.js @@ -42,8 +42,10 @@ LMS.mediaplayer = function () { document.addEventListener("click", _unlock); var _initAudioCtx = function() { - if (_audioIsInit) + if (_audioIsInit) { + _audioCtx.resume(); // not sure of this return; + } _audioIsInit = true; @@ -54,6 +56,21 @@ LMS.mediaplayer = function () { _gainNode.connect(_audioCtx.destination); _audioCtx.resume(); // not sure of this + if ("mediaSession" in navigator) { + navigator.mediaSession.setActionHandler("play", function() { + _playPause(); + }); + navigator.mediaSession.setActionHandler("pause", function() { + _playPause(); + }); + navigator.mediaSession.setActionHandler("previoustrack", function() { + _playPrevious(); + }); + navigator.mediaSession.setActionHandler("nexttrack", function() { + _playNext(); + }); + } + if (_pendingTrackParameters != null) { _applyAudioTrackParameters(_pendingTrackParameters, false); _pendingTrackParameters = null; @@ -68,10 +85,18 @@ LMS.mediaplayer = function () { if (_elems.audio.paused) { _elems.playpause.firstElementChild.classList.remove(pauseClass); _elems.playpause.firstElementChild.classList.add(playClass); + if ("mediaSession" in navigator) { + navigator.mediaSession.playbackState = "paused"; + console.log("paused"); + } } else { _elems.playpause.firstElementChild.classList.remove(playClass); _elems.playpause.firstElementChild.classList.add(pauseClass); + if ("mediaSession" in navigator) { + navigator.mediaSession.playbackState = "playing"; + console.log("playing"); + } } } @@ -107,16 +132,9 @@ LMS.mediaplayer = function () { } var _playTrack = function() { - var playPromise = _elems.audio.play(); - - if (playPromise !== undefined) { - playPromise.then(_ => { - // Automatic playback started - }) - .catch(error => { - // Auto-play was prevented - }); - } + _elems.audio.play() + .then(_ => {}) + .catch(error => { console.log("Cannot play audio: " + error); }); } var _playPause = function() { @@ -305,20 +323,6 @@ LMS.mediaplayer = function () { event.preventDefault(); }); - if ('mediaSession' in navigator) { - navigator.mediaSession.setActionHandler("play", function() { - _playPause(); - }); - navigator.mediaSession.setActionHandler("pause", function() { - _playPause(); - }); - navigator.mediaSession.setActionHandler("previoustrack", function() { - _playPrevious(); - }); - navigator.mediaSession.setActionHandler("nexttrack", function() { - _playNext(); - }); - } } var _removeAudioSources = function() { @@ -371,15 +375,6 @@ LMS.mediaplayer = function () { _elems.curtime.innerHTML = _durationToString(_offset); _elems.duration.innerHTML = _durationToString(_duration); - if ('mediaSession' in navigator) { - navigator.mediaSession.metadata = new MediaMetadata({ - title: params.title, - artist: params.artist, - album: params.release, - artwork: params.artwork, - }); - } - if (!_audioIsInit) { _pendingTrackParameters = params; return; @@ -394,6 +389,14 @@ LMS.mediaplayer = function () { var _applyAudioTrackParameters = function(params) { _setReplayGain(params.replayGain); + if ("mediaSession" in navigator) { + navigator.mediaSession.metadata = new MediaMetadata({ + title: params.title, + artist: params.artist, + album: params.release, + artwork: params.artwork, + }); + } } var stop = function() { From a6e2818a382733daa16feef5034e2d66638239a5 Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 3 Sep 2022 20:20:49 +0200 Subject: [PATCH 14/20] LMS prefix in title is actually annoying => removing it --- src/lms/ui/LmsApplication.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/lms/ui/LmsApplication.cpp b/src/lms/ui/LmsApplication.cpp index e458a367..ec7c476e 100644 --- a/src/lms/ui/LmsApplication.cpp +++ b/src/lms/ui/LmsApplication.cpp @@ -106,7 +106,6 @@ getOrCreateMessageBundle() return res; } -static constexpr const char* titleSeparator {" | "}; static constexpr const char* defaultPath {"/releases"}; std::unique_ptr @@ -627,7 +626,7 @@ LmsApplication::setTitle(const Wt::WString& title) if (title.empty()) WApplication::setTitle("LMS"); else - WApplication::setTitle(Wt::WString {"LMS"} + titleSeparator + title); + WApplication::setTitle(title); } void From 56e9b4f78749aed0c431e1b33788ce570d018842 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 4 Sep 2022 21:14:48 +0200 Subject: [PATCH 15/20] Better handle badly tagged multi-discs releases --- src/libs/av/impl/AudioFile.cpp | 2 +- src/libs/services/database/impl/Release.cpp | 16 ++++++- src/libs/services/database/impl/SqlQuery.cpp | 4 +- src/libs/services/database/impl/SqlQuery.hpp | 16 +++---- .../include/services/database/Release.hpp | 1 + src/libs/services/database/test/Common.cpp | 2 - src/libs/services/database/test/Release.cpp | 44 +++++++++++++++++++ src/lms/ui/explore/ReleaseView.cpp | 3 +- 8 files changed, 72 insertions(+), 16 deletions(-) diff --git a/src/libs/av/impl/AudioFile.cpp b/src/libs/av/impl/AudioFile.cpp index 7aead667..a90636ef 100644 --- a/src/libs/av/impl/AudioFile.cpp +++ b/src/libs/av/impl/AudioFile.cpp @@ -178,7 +178,7 @@ AudioFile::getBestStream() const } bool -AudioFile::hasAttachedPictures(void) const +AudioFile::hasAttachedPictures() const { for (std::size_t i = 0; i < _context->nb_streams; ++i) { diff --git a/src/libs/services/database/impl/Release.cpp b/src/libs/services/database/impl/Release.cpp index 1b2bf630..f6fc2b73 100644 --- a/src/libs/services/database/impl/Release.cpp +++ b/src/libs/services/database/impl/Release.cpp @@ -264,7 +264,7 @@ Release::find(Session& session, const FindParameters& params) } std::optional -Release::getTotalTrack(void) const +Release::getTotalTrack() const { assert(session()); @@ -276,7 +276,7 @@ Release::getTotalTrack(void) const } std::optional -Release::getTotalDisc(void) const +Release::getTotalDisc() const { assert(session()); @@ -287,6 +287,18 @@ Release::getTotalDisc(void) const return (res > 0) ? std::make_optional(res) : std::nullopt; } +std::size_t +Release::getDiscCount() const +{ + assert(session()); + int res {session()->query("SELECT COUNT(DISTINCT disc_number) FROM track t") + .join("release r ON r.id = t.release_id") + .where("r.id = ?") + .bind(getId())}; + + return res; +} + std::optional Release::getReleaseYear(bool original) const { diff --git a/src/libs/services/database/impl/SqlQuery.cpp b/src/libs/services/database/impl/SqlQuery.cpp index b08cd1ce..0be0511f 100644 --- a/src/libs/services/database/impl/SqlQuery.cpp +++ b/src/libs/services/database/impl/SqlQuery.cpp @@ -58,7 +58,7 @@ WhereClause::Or(const WhereClause& otherClause) } std::string -WhereClause::get(void) const +WhereClause::get() const { if (!_clause.empty()) return "WHERE " + _clause; @@ -176,7 +176,7 @@ FromClause::get() const } std::string -SqlQuery::get(void) const +SqlQuery::get() const { std::ostringstream oss; diff --git a/src/libs/services/database/impl/SqlQuery.hpp b/src/libs/services/database/impl/SqlQuery.hpp index bb15408a..21e20fee 100644 --- a/src/libs/services/database/impl/SqlQuery.hpp +++ b/src/libs/services/database/impl/SqlQuery.hpp @@ -36,7 +36,7 @@ class WhereClause WhereClause& bind(const std::string& arg); std::string get() const; - const std::list& getBindArgs(void) const {return _bindArgs;} + const std::list& getBindArgs() const {return _bindArgs;} private: std::string _clause; // WHERE clause @@ -101,15 +101,15 @@ class FromClause class SqlQuery { public: - SelectStatement& select(void) { return _selectStatement;} + SelectStatement& select() { return _selectStatement;} SelectStatement& select(const std::string& statement) { _selectStatement = SelectStatement(statement); return _selectStatement; } - FromClause& from(void) { return _fromClause; } + FromClause& from() { return _fromClause; } FromClause& from(const std::string& clause) { _whereClause = WhereClause(clause); return _fromClause; } - InnerJoinClause& innerJoin(void) { return _innerJoinClause; } - WhereClause& where(void) { return _whereClause; } - const WhereClause& where(void) const { return _whereClause; } - GroupByStatement& groupBy(void) { return _groupByStatement; } - const GroupByStatement& groupBy(void) const { return _groupByStatement; } + InnerJoinClause& innerJoin() { return _innerJoinClause; } + WhereClause& where() { return _whereClause; } + const WhereClause& where() const { return _whereClause; } + GroupByStatement& groupBy() { return _groupByStatement; } + const GroupByStatement& groupBy() const { return _groupByStatement; } std::string get() const; diff --git a/src/libs/services/database/include/services/database/Release.hpp b/src/libs/services/database/include/services/database/Release.hpp index 0bf62e43..2079fc41 100644 --- a/src/libs/services/database/include/services/database/Release.hpp +++ b/src/libs/services/database/include/services/database/Release.hpp @@ -107,6 +107,7 @@ class Release : public Object std::optional getMBID() const { return UUID::fromString(_MBID); } std::optional getTotalTrack() const; std::optional getTotalDisc() const; + std::size_t getDiscCount() const; // may not be total disc (if incomplete for example) std::chrono::milliseconds getDuration() const; Wt::WDateTime getLastWritten() const; diff --git a/src/libs/services/database/test/Common.cpp b/src/libs/services/database/test/Common.cpp index 033aa754..89a43da8 100644 --- a/src/libs/services/database/test/Common.cpp +++ b/src/libs/services/database/test/Common.cpp @@ -207,5 +207,3 @@ TEST_F(DatabaseFixture, Common_subRange) } } - - diff --git a/src/libs/services/database/test/Release.cpp b/src/libs/services/database/test/Release.cpp index 08c34068..450db6ca 100644 --- a/src/libs/services/database/test/Release.cpp +++ b/src/libs/services/database/test/Release.cpp @@ -464,3 +464,47 @@ TEST_F(DatabaseFixture, Release_artist) } } +TEST_F(DatabaseFixture, Release_getDiscCount) +{ + ScopedRelease release {session, "MyRelease"}; + ScopedTrack track {session, "MyTrack"}; + ScopedTrack track2 {session, "MyTrack2"}; + + { + auto transaction {session.createSharedTransaction()}; + EXPECT_EQ(release.get()->getDiscCount(), 0); + } + { + auto transaction {session.createUniqueTransaction()}; + track.get().modify()->setRelease(release.get()); + } + { + auto transaction {session.createSharedTransaction()}; + EXPECT_EQ(release.get()->getDiscCount(), 1); + } + { + auto transaction {session.createUniqueTransaction()}; + track.get().modify()->setDiscNumber(5); + } + { + auto transaction {session.createSharedTransaction()}; + EXPECT_EQ(release.get()->getDiscCount(), 1); + } + { + auto transaction {session.createUniqueTransaction()}; + track2.get().modify()->setRelease(release.get()); + track2.get().modify()->setDiscNumber(5); + } + { + auto transaction {session.createSharedTransaction()}; + EXPECT_EQ(release.get()->getDiscCount(), 1); + } + { + auto transaction {session.createUniqueTransaction()}; + track2.get().modify()->setDiscNumber(6); + } + { + auto transaction {session.createSharedTransaction()}; + EXPECT_EQ(release.get()->getDiscCount(), 2); + } +} diff --git a/src/lms/ui/explore/ReleaseView.cpp b/src/lms/ui/explore/ReleaseView.cpp index b4f38bf2..f003cebe 100644 --- a/src/lms/ui/explore/ReleaseView.cpp +++ b/src/lms/ui/explore/ReleaseView.cpp @@ -197,7 +197,8 @@ Release::refreshView() const bool variousArtists {release->hasVariousArtists()}; const auto totalDisc {release->getTotalDisc()}; - const bool isReleaseMultiDisc {totalDisc && *totalDisc > 1}; + const std::size_t discCount {release->getDiscCount()}; + const bool isReleaseMultiDisc {(discCount > 1) || (totalDisc && *totalDisc > 1)}; // Expect to be called in asc order std::map trackContainers; From aa3d226215ad330944df27d475853dbb71e78f31 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 4 Sep 2022 21:15:08 +0200 Subject: [PATCH 16/20] Remove annoying logs --- docroot/js/mediaplayer.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/docroot/js/mediaplayer.js b/docroot/js/mediaplayer.js index b2c7c3c2..57c7d81b 100644 --- a/docroot/js/mediaplayer.js +++ b/docroot/js/mediaplayer.js @@ -87,7 +87,6 @@ LMS.mediaplayer = function () { _elems.playpause.firstElementChild.classList.add(playClass); if ("mediaSession" in navigator) { navigator.mediaSession.playbackState = "paused"; - console.log("paused"); } } else { @@ -95,7 +94,6 @@ LMS.mediaplayer = function () { _elems.playpause.firstElementChild.classList.add(pauseClass); if ("mediaSession" in navigator) { navigator.mediaSession.playbackState = "playing"; - console.log("playing"); } } } From 915e1149337e4a0b2214d3beb5b1af415ba40dd1 Mon Sep 17 00:00:00 2001 From: emeric Date: Tue, 6 Sep 2022 12:48:52 +0200 Subject: [PATCH 17/20] Subsonic API: added getSong endpoint. ref #212 --- src/libs/subsonic/impl/SubsonicResource.cpp | 25 ++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/libs/subsonic/impl/SubsonicResource.cpp b/src/libs/subsonic/impl/SubsonicResource.cpp index aa283e98..d9140b89 100644 --- a/src/libs/subsonic/impl/SubsonicResource.cpp +++ b/src/libs/subsonic/impl/SubsonicResource.cpp @@ -872,6 +872,29 @@ handleGetAlbumRequest(RequestContext& context) return response; } +static +Response +handleGetSongRequest(RequestContext& context) +{ + // Mandatory params + TrackId id {getMandatoryParameterAs(context.parameters, "id")}; + + auto transaction {context.dbSession.createSharedTransaction()}; + + const Track::pointer track {Track::find(context.dbSession, id)}; + if (!track) + throw RequestedDataNotFoundError {}; + + User::pointer user {User::find(context.dbSession, context.userId)}; + if (!user) + throw UserNotAuthorizedError {}; + + Response response {Response::createOkResponse(context.serverProtocolVersion)}; + response.addNode("song", trackToResponseNode(track, context.dbSession, user)); + + return response; +} + static Response handleGetArtistRequest(RequestContext& context) @@ -1835,7 +1858,7 @@ static const std::unordered_map requestEntry {"/getArtists", {handleGetArtistsRequest}}, {"/getArtist", {handleGetArtistRequest}}, {"/getAlbum", {handleGetAlbumRequest}}, - {"/getSong", {handleNotImplemented}}, + {"/getSong", {handleGetSongRequest}}, {"/getVideos", {handleNotImplemented}}, {"/getArtistInfo", {handleGetArtistInfoRequest}}, {"/getArtistInfo2", {handleGetArtistInfo2Request}}, From 43aa2ece3ccf618d44dee3925894c25a4842737e Mon Sep 17 00:00:00 2001 From: emeric Date: Wed, 7 Sep 2022 18:35:15 +0200 Subject: [PATCH 18/20] Playing a track in the album view will now play the whole album at the selected track index (+ reworked play queue controller) --- src/lms/CMakeLists.txt | 1 + src/lms/ui/LmsApplication.cpp | 11 +- src/lms/ui/PlayQueue.cpp | 60 ++-- src/lms/ui/PlayQueue.hpp | 8 +- src/lms/ui/explore/ArtistView.cpp | 20 +- src/lms/ui/explore/ArtistView.hpp | 13 +- src/lms/ui/explore/Explore.cpp | 290 +++++------------- src/lms/ui/explore/Explore.hpp | 19 +- src/lms/ui/explore/PlayQueueController.cpp | 179 +++++++++++ .../PlayQueueController.hpp} | 41 ++- src/lms/ui/explore/ReleaseView.cpp | 24 +- src/lms/ui/explore/ReleaseView.hpp | 12 +- src/lms/ui/explore/ReleasesView.cpp | 14 +- src/lms/ui/explore/ReleasesView.hpp | 7 +- src/lms/ui/explore/SearchView.cpp | 14 +- src/lms/ui/explore/SearchView.hpp | 9 +- src/lms/ui/explore/TrackListHelpers.cpp | 13 +- src/lms/ui/explore/TrackListHelpers.hpp | 8 +- src/lms/ui/explore/TrackListView.cpp | 20 +- src/lms/ui/explore/TrackListView.hpp | 8 +- src/lms/ui/explore/TrackListsView.hpp | 3 - src/lms/ui/explore/TracksView.cpp | 20 +- src/lms/ui/explore/TracksView.hpp | 7 +- 23 files changed, 434 insertions(+), 367 deletions(-) create mode 100644 src/lms/ui/explore/PlayQueueController.cpp rename src/lms/ui/{PlayQueueAction.hpp => explore/PlayQueueController.hpp} (50%) diff --git a/src/lms/CMakeLists.txt b/src/lms/CMakeLists.txt index b627fe9f..7929bd44 100644 --- a/src/lms/CMakeLists.txt +++ b/src/lms/CMakeLists.txt @@ -32,6 +32,7 @@ add_executable(lms ui/explore/DatabaseCollectorBase.cpp ui/explore/Explore.cpp ui/explore/Filters.cpp + ui/explore/PlayQueueController.cpp ui/explore/ReleaseCollector.cpp ui/explore/ReleaseListHelpers.cpp ui/explore/ReleasesView.cpp diff --git a/src/lms/ui/LmsApplication.cpp b/src/lms/ui/LmsApplication.cpp index ec7c476e..c9992c5c 100644 --- a/src/lms/ui/LmsApplication.cpp +++ b/src/lms/ui/LmsApplication.cpp @@ -502,8 +502,9 @@ LmsApplication::createHome() Wt::WStackedWidget* mainStack {main->bindNew("contents")}; mainStack->setOverflow(Wt::Overflow::Visible); // wt makes it hidden by default - Explore* explore {mainStack->addNew(filters)}; - _playQueue = mainStack->addNew(); + std::unique_ptr playQueue {std::make_unique()}; + Explore* explore {mainStack->addNew(*filters, *playQueue)}; + _playQueue = mainStack->addWidget(std::move(playQueue)); mainStack->addNew(); searchEdit->enterPressed().connect([=] @@ -525,11 +526,7 @@ LmsApplication::createHome() mainStack->addNew(); } - explore->setMaxTrackCountForAction(_playQueue->getCapacity()); - explore->tracksAction.connect([this] (PlayQueueAction action, const std::vector& trackIds) - { - _playQueue->processTracks(action, trackIds); - }); + explore->getPlayQueueController().setMaxTrackCountToEnqueue(_playQueue->getCapacity()); // Events from MediaPlayer _mediaPlayer->playNext.connect([this] diff --git a/src/lms/ui/PlayQueue.cpp b/src/lms/ui/PlayQueue.cpp index a336e443..9731b291 100644 --- a/src/lms/ui/PlayQueue.cpp +++ b/src/lms/ui/PlayQueue.cpp @@ -416,38 +416,46 @@ PlayQueue::enqueueTracks(const std::vector& trackIds) } void -PlayQueue::processTracks(PlayQueueAction action, const std::vector& trackIds) +PlayQueue::play(const std::vector& trackIds) { - std::size_t nbAddedTracks {}; + playAtIndex(trackIds, 0); +} - switch (action) - { - case PlayQueueAction::PlayLast: - nbAddedTracks = enqueueTracks(trackIds); - if (!_trackPos) - loadTrack(0, true); - break; +void +PlayQueue::playShuffled(const std::vector& trackIds) +{ + clearTracks(); + std::vector shuffledTrackIds {trackIds}; + Random::shuffleContainer(shuffledTrackIds); + const std::size_t nbAddedTracks {enqueueTracks(shuffledTrackIds)}; + loadTrack(0, true); - case PlayQueueAction::Play: - clearTracks(); - nbAddedTracks = enqueueTracks(trackIds); - loadTrack(0, true); + notifyAddedTracks(nbAddedTracks); +} - break; +void +PlayQueue::playOrAddLast(const std::vector& trackIds) +{ + const std::size_t nbAddedTracks {enqueueTracks(trackIds)}; + if (!_trackPos) + loadTrack(0, true); - case PlayQueueAction::PlayShuffled: - { - clearTracks(); - { - std::vector shuffledTrackIds {trackIds}; - Random::shuffleContainer(shuffledTrackIds); - nbAddedTracks = enqueueTracks(shuffledTrackIds); - } - loadTrack(0, true); - break; - } - } + notifyAddedTracks(nbAddedTracks); +} +void +PlayQueue::playAtIndex(const std::vector& trackIds, std::size_t index) +{ + clearTracks(); + const std::size_t nbAddedTracks {enqueueTracks(trackIds)}; + loadTrack(index, true); + + notifyAddedTracks(nbAddedTracks); +} + +void +PlayQueue::notifyAddedTracks(std::size_t nbAddedTracks) const +{ if (nbAddedTracks > 0) LmsApp->notifyMsg(Notification::Type::Info, Wt::WString::tr("Lms.PlayQueue.playqueue"), Wt::WString::trn("Lms.PlayQueue.nb-tracks-added", nbAddedTracks).arg(nbAddedTracks), std::chrono::seconds {2}); diff --git a/src/lms/ui/PlayQueue.hpp b/src/lms/ui/PlayQueue.hpp index 2cd67e43..f6e21076 100644 --- a/src/lms/ui/PlayQueue.hpp +++ b/src/lms/ui/PlayQueue.hpp @@ -27,8 +27,8 @@ #include #include "services/database/Object.hpp" +#include "services/database/TrackId.hpp" #include "services/database/TrackListId.hpp" -#include "PlayQueueAction.hpp" #include "common/Template.hpp" @@ -53,7 +53,10 @@ class PlayQueue : public Template public: PlayQueue(); - void processTracks(PlayQueueAction action, const std::vector& trackIds); + void play(const std::vector& trackIds); + void playShuffled(const std::vector& trackIds); + void playOrAddLast(const std::vector& trackIds); // play if queue empty, otherwise just add last + void playAtIndex(const std::vector& trackIds, std::size_t index); // play the next track in the queue void playNext(); @@ -70,6 +73,7 @@ class PlayQueue : public Template constexpr std::size_t getCapacity() const { return _capacity; } private: + void notifyAddedTracks(std::size_t nbAddedTracks) const; Database::ObjectPtr getTrackList() const; bool isFull() const; diff --git a/src/lms/ui/explore/ArtistView.cpp b/src/lms/ui/explore/ArtistView.cpp index 778ca615..48f5cc9d 100644 --- a/src/lms/ui/explore/ArtistView.cpp +++ b/src/lms/ui/explore/ArtistView.cpp @@ -39,6 +39,7 @@ #include "Filters.hpp" #include "LmsApplication.hpp" #include "LmsApplicationException.hpp" +#include "PlayQueueController.hpp" #include "ReleaseListHelpers.hpp" #include "TrackListHelpers.hpp" #include "Utils.hpp" @@ -47,9 +48,10 @@ using namespace Database; namespace UserInterface { -Artist::Artist(Filters* filters) +Artist::Artist(Filters& filters, PlayQueueController& controller) : Template {Wt::WString::tr("Lms.Explore.Artist.template")} , _filters {filters} +, _playQueueController {controller} { addFunction("tr", &Wt::WTemplate::Functions::tr); addFunction("id", &Wt::WTemplate::Functions::id); @@ -59,7 +61,7 @@ Artist::Artist(Filters* filters) refreshView(); }); - filters->updated().connect([this] + filters.updated().connect([this] { refreshView(); }); @@ -140,7 +142,7 @@ Artist::refreshView() Wt::WInteractWidget* entry {clusterContainers->addWidget(Utils::createCluster(clusterId))}; entry->clicked().connect([=] { - _filters->add(clusterId); + _filters.add(clusterId); }); } } @@ -151,18 +153,18 @@ Artist::refreshView() bindNew("play-btn", Wt::WString::tr("Lms.Explore.play"), Wt::TextFormat::XHTML) ->clicked().connect([=] { - artistsAction.emit(PlayQueueAction::Play, {_artistId}); + _playQueueController.processCommand(PlayQueueController::Command::Play, {_artistId}); }); bindNew("play-shuffled", Wt::WString::tr("Lms.Explore.play-shuffled"), Wt::TextFormat::Plain) ->clicked().connect([=] { - artistsAction.emit(PlayQueueAction::PlayShuffled, {_artistId}); + _playQueueController.processCommand(PlayQueueController::Command::PlayShuffled, {_artistId}); }); bindNew("play-last", Wt::WString::tr("Lms.Explore.play-last"), Wt::TextFormat::Plain) ->clicked().connect([=] { - artistsAction.emit(PlayQueueAction::PlayLast, {_artistId}); + _playQueueController.processCommand(PlayQueueController::Command::PlayOrAddLast, {_artistId}); }); bindNew("download", Wt::WString::tr("Lms.Explore.download")) ->setLink(Wt::WLink {std::make_unique(*artistId)}); @@ -288,7 +290,7 @@ Artist::addSomeReleases(InfiniteScrollingContainer& releaseContainer, EnumSet(releaseContainer.getCount()), _releasesBatchSize}; Release::FindParameters params; - params.setClusters(_filters->getClusterIds()); + params.setClusters(_filters.getClusterIds()); params.setArtist(_artistId, linkTypes, excludedLinkTypes); params.setRange(range); params.setSortMethod(ReleaseSortMethod::DateDesc); @@ -315,7 +317,7 @@ Artist::addSomeNonReleaseTracks() const Range range {static_cast(_trackContainer->getCount()), _tracksBatchSize}; Track::FindParameters params; - params.setClusters(_filters->getClusterIds()); + params.setClusters(_filters.getClusterIds()); params.setArtist(_artistId); params.setRange(range); params.setSortMethod(TrackSortMethod::Name); @@ -333,7 +335,7 @@ Artist::addSomeNonReleaseTracks() } const Track::pointer track {Track::find(LmsApp->getDbSession(), trackId)}; - _trackContainer->add(TrackListHelpers::createEntry(track, tracksAction)); + _trackContainer->add(TrackListHelpers::createEntry(track, _playQueueController)); areTracksAdded = true; } diff --git a/src/lms/ui/explore/ArtistView.hpp b/src/lms/ui/explore/ArtistView.hpp index b1dd8cf5..d583a893 100644 --- a/src/lms/ui/explore/ArtistView.hpp +++ b/src/lms/ui/explore/ArtistView.hpp @@ -19,13 +19,12 @@ #pragma once +#include "services/database/ArtistId.hpp" #include "services/database/Object.hpp" #include "services/database/Types.hpp" #include "utils/EnumSet.hpp" #include "common/Template.hpp" -#include "PlayQueueAction.hpp" - namespace Database { class Artist; @@ -34,17 +33,14 @@ namespace Database namespace UserInterface { - class Filters; + class PlayQueueController; class InfiniteScrollingContainer; class Artist : public Template { public: - Artist(Filters* filters); - - PlayQueueActionArtistSignal artistsAction; - PlayQueueActionTrackSignal tracksAction; + Artist(Filters& filters, PlayQueueController& controller); private: void refreshView(); @@ -60,7 +56,8 @@ namespace UserInterface static constexpr std::size_t _tracksBatchSize {6}; static constexpr std::size_t _tracksMaxCount {160}; - Filters* _filters {}; + Filters& _filters; + PlayQueueController& _playQueueController; InfiniteScrollingContainer* _releaseContainer {}; InfiniteScrollingContainer* _appearsOnReleaseContainer {}; InfiniteScrollingContainer* _trackContainer {}; diff --git a/src/lms/ui/explore/Explore.cpp b/src/lms/ui/explore/Explore.cpp index be0a1ed2..1c0ed889 100644 --- a/src/lms/ui/explore/Explore.cpp +++ b/src/lms/ui/explore/Explore.cpp @@ -19,17 +19,8 @@ #include "Explore.hpp" +#include #include -#include -#include - -#include "services/database/Artist.hpp" -#include "services/database/Release.hpp" -#include "services/database/Session.hpp" -#include "services/database/Track.hpp" -#include "utils/Logger.hpp" - -#include "LmsApplication.hpp" #include "ArtistsView.hpp" #include "ArtistView.hpp" @@ -43,215 +34,96 @@ namespace UserInterface { -namespace { + namespace { -void -handleContentsPathChange(Wt::WStackedWidget* stack) -{ - enum Idx - { - IdxArtists = 0, - IdxArtist, - IdxTrackLists, - IdxTrackList, - IdxReleases, - IdxRelease, - IdxSearch, - IdxTracks, - }; - - static const std::map indexes = - { - { "/artists", IdxArtists }, - { "/artist", IdxArtist }, - { "/tracklists", IdxTrackLists }, - { "/tracklist", IdxTrackList }, - { "/releases", IdxReleases }, - { "/release", IdxRelease }, - { "/search", IdxSearch }, - { "/tracks", IdxTracks }, - }; - - for (const auto& index : indexes) - { - if (wApp->internalPathMatches(index.first)) + void + handleContentsPathChange(Wt::WStackedWidget* stack) { - stack->setCurrentIndex(index.second); - return; + enum Idx + { + IdxArtists = 0, + IdxArtist, + IdxTrackLists, + IdxTrackList, + IdxReleases, + IdxRelease, + IdxSearch, + IdxTracks, + }; + + static const std::map indexes = + { + { "/artists", IdxArtists }, + { "/artist", IdxArtist }, + { "/tracklists", IdxTrackLists }, + { "/tracklist", IdxTrackList }, + { "/releases", IdxReleases }, + { "/release", IdxRelease }, + { "/search", IdxSearch }, + { "/tracks", IdxTracks }, + }; + + for (const auto& index : indexes) + { + if (wApp->internalPathMatches(index.first)) + { + stack->setCurrentIndex(index.second); + return; + } + } } - } -} -} // namespace + } // namespace -Explore::Explore(Filters* filters) -: Wt::WTemplate {Wt::WString::tr("Lms.Explore.template")} -, _filters {filters} -{ - addFunction("tr", &Functions::tr); - - // Contents - Wt::WStackedWidget* contentsStack {bindNew("contents")}; - contentsStack->setOverflow(Wt::Overflow::Visible); // wt makes it hidden by default - - // same order as enum Idx - auto artists = std::make_unique(*_filters); - contentsStack->addWidget(std::move(artists)); - - auto artist = std::make_unique(_filters); - artist->artistsAction.connect(this, &Explore::handleArtistsAction); - artist->tracksAction.connect(this, &Explore::handleTracksAction); - contentsStack->addWidget(std::move(artist)); - - auto trackLists {std::make_unique(*_filters)}; - TrackLists* trackListsPtr {trackLists.get()}; - trackLists->trackListAction.connect(this, &Explore::handleTrackListAction); - contentsStack->addWidget(std::move(trackLists)); - - auto trackList {std::make_unique(*_filters)}; - TrackList* trackListPtr {trackList.get()}; - trackList->trackListAction.connect(this, &Explore::handleTrackListAction); - trackList->tracksAction.connect(this, &Explore::handleTracksAction); - contentsStack->addWidget(std::move(trackList)); - - trackListPtr->trackListDeleted.connect(trackListsPtr, &TrackLists::onTrackListDeleted); - - auto releases = std::make_unique(*_filters); - releases->releasesAction.connect(this, &Explore::handleReleasesAction); - contentsStack->addWidget(std::move(releases)); - - auto release = std::make_unique(_filters); - release->releasesAction.connect(this, &Explore::handleReleasesAction); - 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)); - - wApp->internalPathChanged().connect(this, [=] + Explore::Explore(Filters& filters, PlayQueue& playQueue) + : Wt::WTemplate {Wt::WString::tr("Lms.Explore.template")} + , _playQueueController {filters, playQueue} { + addFunction("tr", &Functions::tr); + + // Contents + Wt::WStackedWidget* contentsStack {bindNew("contents")}; + contentsStack->setOverflow(Wt::Overflow::Visible); // wt makes it hidden by default + + // same order as enum Idx + auto artists = std::make_unique(filters); + contentsStack->addWidget(std::move(artists)); + + auto artist = std::make_unique(filters, _playQueueController); + contentsStack->addWidget(std::move(artist)); + + auto trackLists {std::make_unique(filters)}; + contentsStack->addWidget(std::move(trackLists)); + + auto trackList {std::make_unique(filters, _playQueueController)}; + trackList->trackListDeleted.connect(trackLists.get(), &TrackLists::onTrackListDeleted); + contentsStack->addWidget(std::move(trackList)); + + auto releases = std::make_unique(filters, _playQueueController); + contentsStack->addWidget(std::move(releases)); + + auto release = std::make_unique(filters, _playQueueController); + contentsStack->addWidget(std::move(release)); + + auto search = std::make_unique(filters, _playQueueController); + _search = search.get(); + contentsStack->addWidget(std::move(search)); + + auto tracks = std::make_unique(filters, _playQueueController); + contentsStack->addWidget(std::move(tracks)); + + wApp->internalPathChanged().connect(this, [=] + { + handleContentsPathChange(contentsStack); + }); + handleContentsPathChange(contentsStack); - }); - - 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::vector& clusters, std::size_t maxTrackCount) -{ - assert(maxTrackCount); - - std::vector res; - - auto transaction {session.createSharedTransaction()}; - - for (const Database::ArtistId artistId : artistsId) - { - Database::Track::FindParameters params; - params.setArtist(artistId); - params.setSortMethod(Database::TrackSortMethod::DateDescAndRelease); - params.setClusters(clusters); - params.setRange({0, maxTrackCount - res.size()}); - - const auto tracks {Database::Track::find(session, params)}; - - res.reserve(res.size() + tracks.results.size()); - res.insert(std::end(res), std::cbegin(tracks.results), std::cend(tracks.results)); - - if (res.size() == maxTrackCount) - break; } - return res; -} - -static -std::vector -getReleasesTracks(Database::Session& session, const std::vector& releasesId, const std::vector& clusters, std::size_t maxTrackCount) -{ - using namespace Database; - assert(maxTrackCount); - - std::vector res; - - auto transaction {session.createSharedTransaction()}; - - for (const ReleaseId releaseId : releasesId) + void + Explore::search(const Wt::WString& searchText) { - Database::Track::FindParameters params; - params.setRelease(releaseId); - params.setSortMethod(Database::TrackSortMethod::Release); - params.setClusters(clusters); - params.setRange({0, maxTrackCount - res.size()}); - - const auto tracks {Database::Track::find(session, params)}; - - res.reserve(res.size() + tracks.results.size()); - res.insert(std::end(res), std::cbegin(tracks.results), std::cend(tracks.results)); - - if (res.size() == maxTrackCount) - break; + _search->refreshView(searchText); } - return res; -} - -static -std::vector -getTrackListTracks(Database::Session& session, Database::TrackListId trackListId, const std::vector& clusters, std::size_t maxTrackCount) -{ - using namespace Database; - assert(maxTrackCount); - - auto transaction {session.createSharedTransaction()}; - - Database::Track::FindParameters params; - params.setTrackList(trackListId); - params.setClusters(clusters); - params.setRange({0, maxTrackCount}); - params.setSortMethod(TrackSortMethod::TrackList); - params.setDistinct(false); - - return Database::Track::find(session, params).results; -} - -void -Explore::handleArtistsAction(PlayQueueAction action, const std::vector& artistsId) -{ - tracksAction.emit(action, getArtistsTracks(LmsApp->getDbSession(), artistsId, _filters->getClusterIds(), _maxTrackCount)); -} - -void -Explore::handleReleasesAction(PlayQueueAction action, const std::vector& releasesId) -{ - tracksAction.emit(action, getReleasesTracks(LmsApp->getDbSession(), releasesId, _filters->getClusterIds(), _maxTrackCount)); -} - -void -Explore::handleTrackListAction(PlayQueueAction action, Database::TrackListId trackListId) -{ - tracksAction.emit(action, getTrackListTracks(LmsApp->getDbSession(), trackListId, _filters->getClusterIds(), _maxTrackCount)); -} - -void -Explore::handleTracksAction(PlayQueueAction action, const std::vector& tracksId) -{ - // consider things are already filtered here, and _maxTrackCount honored playqueue side... - tracksAction.emit(action, tracksId); -} - } // namespace UserInterface - diff --git a/src/lms/ui/explore/Explore.hpp b/src/lms/ui/explore/Explore.hpp index 96362571..d06c7c48 100644 --- a/src/lms/ui/explore/Explore.hpp +++ b/src/lms/ui/explore/Explore.hpp @@ -20,34 +20,25 @@ #pragma once #include - -#include "services/database/Types.hpp" -#include "PlayQueueAction.hpp" +#include "PlayQueueController.hpp" namespace UserInterface { class Filters; class SearchView; + class PlayQueue; class Explore : public Wt::WTemplate { public: - Explore(Filters* filters); + Explore(Filters& filters, PlayQueue& playQueue); void search(const Wt::WString& searchText); - - PlayQueueActionTrackSignal tracksAction; - void setMaxTrackCountForAction(std::size_t maxTrackCount) { _maxTrackCount = maxTrackCount; } + PlayQueueController& getPlayQueueController() { return _playQueueController; } private: - void handleArtistsAction(PlayQueueAction action, const std::vector& artistsId); - void handleReleasesAction(PlayQueueAction action, const std::vector& releasesId); - void handleTrackListAction(PlayQueueAction action, Database::TrackListId trackiListId); - void handleTracksAction(PlayQueueAction action, const std::vector& tracksId); - - Filters* _filters {}; + PlayQueueController _playQueueController; SearchView* _search {}; - std::size_t _maxTrackCount {}; }; } // namespace UserInterface diff --git a/src/lms/ui/explore/PlayQueueController.cpp b/src/lms/ui/explore/PlayQueueController.cpp new file mode 100644 index 00000000..4f07d706 --- /dev/null +++ b/src/lms/ui/explore/PlayQueueController.cpp @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2022 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 "services/database/ClusterId.hpp" +#include "services/database/Release.hpp" +#include "services/database/Session.hpp" +#include "services/database/Track.hpp" +#include "explore/Filters.hpp" +#include "explore/PlayQueueController.hpp" +#include "PlayQueue.hpp" +#include "LmsApplication.hpp" + +namespace UserInterface +{ + + static + std::vector + getArtistsTracks(Database::Session& session, const std::vector& artistsId, const std::vector& clusters, std::size_t maxTrackCount) + { + assert(maxTrackCount); + + std::vector res; + + auto transaction {session.createSharedTransaction()}; + + for (const Database::ArtistId artistId : artistsId) + { + Database::Track::FindParameters params; + params.setArtist(artistId); + params.setSortMethod(Database::TrackSortMethod::DateDescAndRelease); + params.setClusters(clusters); + params.setRange({0, maxTrackCount - res.size()}); + + const auto tracks {Database::Track::find(session, params)}; + + res.reserve(res.size() + tracks.results.size()); + res.insert(std::end(res), std::cbegin(tracks.results), std::cend(tracks.results)); + + if (res.size() == maxTrackCount) + break; + } + + return res; + } + + static + std::vector + getReleasesTracks(Database::Session& session, const std::vector& releasesId, const std::vector& clusters, std::size_t maxTrackCount) + { + using namespace Database; + assert(maxTrackCount); + + std::vector res; + + auto transaction {session.createSharedTransaction()}; + + for (const ReleaseId releaseId : releasesId) + { + Database::Track::FindParameters params; + params.setRelease(releaseId); + params.setSortMethod(Database::TrackSortMethod::Release); + params.setClusters(clusters); + params.setRange({0, maxTrackCount - res.size()}); + + const auto tracks {Database::Track::find(session, params)}; + + res.reserve(res.size() + tracks.results.size()); + res.insert(std::end(res), std::cbegin(tracks.results), std::cend(tracks.results)); + + if (res.size() == maxTrackCount) + break; + } + + return res; + } + + static + std::vector + getTrackListTracks(Database::Session& session, Database::TrackListId trackListId, const std::vector& clusters, std::size_t maxTrackCount) + { + using namespace Database; + assert(maxTrackCount); + + auto transaction {session.createSharedTransaction()}; + + Database::Track::FindParameters params; + params.setTrackList(trackListId); + params.setClusters(clusters); + params.setRange({0, maxTrackCount}); + params.setSortMethod(TrackSortMethod::TrackList); + params.setDistinct(false); + + return Database::Track::find(session, params).results; + } + + PlayQueueController::PlayQueueController(Filters& filters, PlayQueue& playQueue) + : _filters {filters} + , _playQueue {playQueue} + { + } + + void + PlayQueueController::processCommand(Command command, const std::vector& artistIds) + { + const std::vector tracks {getArtistsTracks(LmsApp->getDbSession(), artistIds, _filters.getClusterIds(), _maxTrackCountToEnqueue)}; + processCommand(command, tracks); + + } + + void + PlayQueueController::processCommand(Command command, const std::vector& releaseIds) + { + const std::vector tracks {getReleasesTracks(LmsApp->getDbSession(), releaseIds, _filters.getClusterIds(), _maxTrackCountToEnqueue)}; + processCommand(command, tracks); + } + + void + PlayQueueController::processCommand(Command command, const std::vector& trackIds) + { + // consider things are already filtered here, and _maxTrackCount honored playqueue side... + switch (command) + { + case Command::Play: + _playQueue.play(trackIds); + break; + case Command::PlayShuffled: + _playQueue.playShuffled(trackIds); + break; + case Command::PlayOrAddLast: + _playQueue.playOrAddLast(trackIds); + break; + } + } + + void + PlayQueueController::processCommand(Command command, Database::TrackListId trackListId) + { + const std::vector tracks {getTrackListTracks(LmsApp->getDbSession(), trackListId, _filters.getClusterIds(), _maxTrackCountToEnqueue)}; + processCommand(command, tracks); + } + + void + PlayQueueController::playTrackInRelease(Database::TrackId trackId) + { + Database::ReleaseId releaseId; + { + auto transaction {LmsApp->getDbSession().createSharedTransaction()}; + const Database::Track::pointer track {Database::Track::find(LmsApp->getDbSession(), trackId)}; + if (!track || !track->getRelease()) + return; + + releaseId = track->getRelease()->getId(); + } + + const std::vector tracks {getReleasesTracks(LmsApp->getDbSession(), {releaseId}, _filters.getClusterIds(), _maxTrackCountToEnqueue)}; + auto itTrack {std::find(std::cbegin(tracks), std::cend(tracks), trackId)}; + if (itTrack == std::cend(tracks)) + return; + + const std::size_t index {static_cast(std::distance(std::cbegin(tracks), itTrack))}; + _playQueue.playAtIndex(tracks, index); + } +} // namespace UserInterface diff --git a/src/lms/ui/PlayQueueAction.hpp b/src/lms/ui/explore/PlayQueueController.hpp similarity index 50% rename from src/lms/ui/PlayQueueAction.hpp rename to src/lms/ui/explore/PlayQueueController.hpp index 5a3bc05e..6bb9baf7 100644 --- a/src/lms/ui/PlayQueueAction.hpp +++ b/src/lms/ui/explore/PlayQueueController.hpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Emeric Poupon + * Copyright (C) 2022 Emeric Poupon * * This file is part of LMS. * @@ -20,7 +20,6 @@ #pragma once #include -#include #include "services/database/ArtistId.hpp" #include "services/database/ReleaseId.hpp" @@ -29,16 +28,34 @@ namespace UserInterface { - enum class PlayQueueAction - { - Play, - PlayLast, - PlayShuffled, - }; + class Filters; + class PlayQueue; - using PlayQueueActionArtistSignal = Wt::Signal&>; - using PlayQueueActionReleaseSignal = Wt::Signal&>; - using PlayQueueActionTrackSignal = Wt::Signal&>; - using PlayQueueActionTrackListSignal = Wt::Signal; + // Used to interact with the play queue, using the current exploration filters + class PlayQueueController + { + public: + PlayQueueController(Filters& filters, PlayQueue& playQueue); + + enum class Command + { + Play, + PlayOrAddLast, + PlayShuffled, + }; + + void processCommand(Command command, const std::vector&); + void processCommand(Command command, const std::vector&); + void processCommand(Command command, const std::vector&); + void processCommand(Command command, Database::TrackListId); + void playTrackInRelease(Database::TrackId); + + void setMaxTrackCountToEnqueue(std::size_t maxTrackCount) { _maxTrackCountToEnqueue = maxTrackCount; } + + private: + Filters& _filters; + PlayQueue& _playQueue; + std::size_t _maxTrackCountToEnqueue {}; + }; } diff --git a/src/lms/ui/explore/ReleaseView.cpp b/src/lms/ui/explore/ReleaseView.cpp index f003cebe..3c0c6094 100644 --- a/src/lms/ui/explore/ReleaseView.cpp +++ b/src/lms/ui/explore/ReleaseView.cpp @@ -34,20 +34,22 @@ #include "common/Template.hpp" #include "resource/DownloadResource.hpp" -#include "Filters.hpp" +#include "explore/Filters.hpp" +#include "explore/PlayQueueController.hpp" +#include "explore/ReleaseListHelpers.hpp" #include "LmsApplication.hpp" #include "LmsApplicationException.hpp" #include "MediaPlayer.hpp" -#include "ReleaseListHelpers.hpp" #include "Utils.hpp" using namespace Database; namespace UserInterface { -Release::Release(Filters* filters) +Release::Release(Filters& filters, PlayQueueController& playQueueController) : Template {Wt::WString::tr("Lms.Explore.Release.template")} , _filters {filters} +, _playQueueController {playQueueController} { addFunction("tr", &Wt::WTemplate::Functions::tr); addFunction("id", &Wt::WTemplate::Functions::id); @@ -57,7 +59,7 @@ Release::Release(Filters* filters) refreshView(); }); - filters->updated().connect([this] + _filters.updated().connect([this] { refreshView(); }); @@ -147,7 +149,7 @@ Release::refreshView() Wt::WInteractWidget* entry {clusterContainers->addWidget(Utils::createCluster(clusterId))}; entry->clicked().connect([=] { - _filters->add(clusterId); + _filters.add(clusterId); }); } } @@ -156,19 +158,19 @@ Release::refreshView() bindNew("play-btn", Wt::WString::tr("Lms.Explore.play"), Wt::TextFormat::XHTML) ->clicked().connect([=] { - releasesAction.emit(PlayQueueAction::Play, {*releaseId}); + _playQueueController.processCommand(PlayQueueController::Command::Play, {*releaseId}); }); bindNew("play-shuffled", Wt::WString::tr("Lms.Explore.play-shuffled"), Wt::TextFormat::Plain) ->clicked().connect([=] { - releasesAction.emit(PlayQueueAction::PlayShuffled, {*releaseId}); + _playQueueController.processCommand(PlayQueueController::Command::PlayShuffled, {*releaseId}); }); bindNew("play-last", Wt::WString::tr("Lms.Explore.play-last"), Wt::TextFormat::Plain) ->clicked().connect([=] { - releasesAction.emit(PlayQueueAction::PlayLast, {*releaseId}); + _playQueueController.processCommand(PlayQueueController::Command::PlayOrAddLast, {*releaseId}); }); bindNew("download", Wt::WString::tr("Lms.Explore.download")) @@ -238,7 +240,7 @@ Release::refreshView() Database::Track::FindParameters params; params.setRelease(*releaseId); params.setSortMethod(Database::TrackSortMethod::Release); - params.setClusters(_filters->getClusterIds()); + params.setClusters(_filters.getClusterIds()); const auto tracks {Database::Track::find(LmsApp->getDbSession(), params)}; for (const Database::TrackId trackId : tracks.results) @@ -285,7 +287,7 @@ Release::refreshView() Wt::WPushButton* playBtn {entry->bindNew("play-btn", Wt::WString::tr("Lms.template.play-btn"), Wt::TextFormat::XHTML)}; playBtn->clicked().connect([=] { - tracksAction.emit(PlayQueueAction::Play, {trackId}); + _playQueueController.playTrackInRelease(trackId); }); { @@ -293,7 +295,7 @@ Release::refreshView() entry->bindNew("play-last", Wt::WString::tr("Lms.Explore.play-last")) ->clicked().connect([=] { - tracksAction.emit(PlayQueueAction::PlayLast, {trackId}); + _playQueueController.processCommand(PlayQueueController::Command::PlayOrAddLast, {trackId}); }); auto isStarred {[=] { return Service::get()->isStarred(LmsApp->getUserId(), trackId); }}; diff --git a/src/lms/ui/explore/ReleaseView.hpp b/src/lms/ui/explore/ReleaseView.hpp index e867874f..91964f33 100644 --- a/src/lms/ui/explore/ReleaseView.hpp +++ b/src/lms/ui/explore/ReleaseView.hpp @@ -20,8 +20,8 @@ #pragma once #include "services/database/Object.hpp" +#include "services/database/ReleaseId.hpp" #include "common/Template.hpp" -#include "PlayQueueAction.hpp" namespace Database { @@ -31,15 +31,12 @@ namespace Database namespace UserInterface { class Filters; - + class PlayQueueController; class Release : public Template { public: - Release(Filters* filters); - - PlayQueueActionReleaseSignal releasesAction; - PlayQueueActionTrackSignal tracksAction; + Release(Filters& filters, PlayQueueController& playQueueController); private: void refreshView(); @@ -48,7 +45,8 @@ namespace UserInterface void refreshLinks(const Database::ObjectPtr& release); void refreshSimilarReleases(const std::vector& similarReleasesId); - Filters* _filters {}; + Filters& _filters; + PlayQueueController& _playQueueController; }; } // namespace UserInterface diff --git a/src/lms/ui/explore/ReleasesView.cpp b/src/lms/ui/explore/ReleasesView.cpp index e70ceef2..4d3cd927 100644 --- a/src/lms/ui/explore/ReleasesView.cpp +++ b/src/lms/ui/explore/ReleasesView.cpp @@ -26,16 +26,18 @@ #include "common/InfiniteScrollingContainer.hpp" #include "common/Template.hpp" -#include "ReleaseListHelpers.hpp" -#include "Filters.hpp" +#include "explore/Filters.hpp" +#include "explore/PlayQueueController.hpp" +#include "explore/ReleaseListHelpers.hpp" #include "LmsApplication.hpp" using namespace Database; namespace UserInterface { -Releases::Releases(Filters& filters) +Releases::Releases(Filters& filters, PlayQueueController& playQueueController) : Template {Wt::WString::tr("Lms.Explore.Releases.template")} +, _playQueueController {playQueueController} , _releaseCollector {filters, _defaultMode, _maxCount} { addFunction("tr", &Wt::WTemplate::Functions::tr); @@ -69,18 +71,18 @@ Releases::Releases(Filters& filters) Wt::WPushButton* playBtn {bindNew("play-btn", Wt::WString::tr("Lms.Explore.play"), Wt::TextFormat::XHTML)}; playBtn->clicked().connect([this] { - releasesAction.emit(PlayQueueAction::Play, getAllReleases()); + _playQueueController.processCommand(PlayQueueController::Command::Play, getAllReleases()); }); bindNew("play-shuffled", Wt::WString::tr("Lms.Explore.play-shuffled"), Wt::TextFormat::Plain) ->clicked().connect([=] { - releasesAction.emit(PlayQueueAction::PlayShuffled, getAllReleases()); + _playQueueController.processCommand(PlayQueueController::Command::PlayShuffled, getAllReleases()); }); bindNew("play-last", Wt::WString::tr("Lms.Explore.play-last"), Wt::TextFormat::Plain) ->clicked().connect([=] { - releasesAction.emit(PlayQueueAction::PlayLast, getAllReleases()); + _playQueueController.processCommand(PlayQueueController::Command::PlayOrAddLast, getAllReleases()); }); _container = bindNew("releases", Wt::WString::tr("Lms.Explore.Releases.template.container")); diff --git a/src/lms/ui/explore/ReleasesView.hpp b/src/lms/ui/explore/ReleasesView.hpp index b085c307..116a1390 100644 --- a/src/lms/ui/explore/ReleasesView.hpp +++ b/src/lms/ui/explore/ReleasesView.hpp @@ -22,20 +22,18 @@ #include "services/database/Types.hpp" #include "common/Template.hpp" -#include "PlayQueueAction.hpp" #include "ReleaseCollector.hpp" namespace UserInterface { class Filters; class InfiniteScrollingContainer; + class PlayQueueController; class Releases : public Template { public: - Releases(Filters& filters); - - PlayQueueActionReleaseSignal releasesAction; + Releases(Filters& filters, PlayQueueController& playQueueController); private: void refreshView(); @@ -48,6 +46,7 @@ namespace UserInterface static constexpr std::size_t _batchSize {_maxItemsPerLine}; static constexpr std::size_t _maxCount {_maxItemsPerLine * 500}; + PlayQueueController& _playQueueController; Wt::WWidget* _currentActiveItem {}; InfiniteScrollingContainer* _container {}; ReleaseCollector _releaseCollector; diff --git a/src/lms/ui/explore/SearchView.cpp b/src/lms/ui/explore/SearchView.cpp index 42770b6d..b01030c5 100644 --- a/src/lms/ui/explore/SearchView.cpp +++ b/src/lms/ui/explore/SearchView.cpp @@ -35,12 +35,12 @@ using namespace Database; namespace UserInterface { - SearchView::SearchView(Filters* filters) + SearchView::SearchView(Filters& filters, PlayQueueController& playQueueController) : Wt::WTemplate {Wt::WString::tr("Lms.Explore.Search.template")} - , _filters {filters} - , _artistCollector {*filters, ArtistCollector::Mode::Search, getMaxCount(Mode::Artist)} - , _releaseCollector {*filters, ReleaseCollector::Mode::Search, getMaxCount(Mode::Release)} - , _trackCollector {*filters, TrackCollector::Mode::Search, getMaxCount(Mode::Track)} + , _playQueueController {playQueueController} + , _artistCollector {filters, ArtistCollector::Mode::Search, getMaxCount(Mode::Artist)} + , _releaseCollector {filters, ReleaseCollector::Mode::Search, getMaxCount(Mode::Release)} + , _trackCollector {filters, TrackCollector::Mode::Search, getMaxCount(Mode::Track)} { addFunction("tr", &Wt::WTemplate::Functions::tr); @@ -53,7 +53,7 @@ namespace UserInterface _tracks = bindNew("tracks"); _tracks->onRequestElements.connect([this] { addSomeTracks(); }); - _filters->updated().connect([=] + filters.updated().connect([=] { refreshView(); }); @@ -152,7 +152,7 @@ namespace UserInterface for (const TrackId trackId : trackIds.results) { const Track::pointer track {Track::find(LmsApp->getDbSession(), trackId)}; - _tracks->add(TrackListHelpers::createEntry(track, tracksAction)); + _tracks->add(TrackListHelpers::createEntry(track, _playQueueController)); } } diff --git a/src/lms/ui/explore/SearchView.hpp b/src/lms/ui/explore/SearchView.hpp index 36bd2653..9c22aa6e 100644 --- a/src/lms/ui/explore/SearchView.hpp +++ b/src/lms/ui/explore/SearchView.hpp @@ -27,20 +27,17 @@ #include "ArtistCollector.hpp" #include "ReleaseCollector.hpp" #include "TrackCollector.hpp" -#include "PlayQueueAction.hpp" namespace UserInterface { - class InfiniteScrollingContainer; class Filters; + class PlayQueueController; class SearchView : public Wt::WTemplate { public: - SearchView(Filters* filters); - - PlayQueueActionTrackSignal tracksAction; + SearchView(Filters& filters, PlayQueueController& playQueueController); void refreshView(const Wt::WString& searchText); @@ -74,7 +71,7 @@ namespace UserInterface void addSomeReleases(); void addSomeTracks(); - Filters* _filters {}; + PlayQueueController& _playQueueController; ArtistCollector _artistCollector; ReleaseCollector _releaseCollector; TrackCollector _trackCollector; diff --git a/src/lms/ui/explore/TrackListHelpers.cpp b/src/lms/ui/explore/TrackListHelpers.cpp index 71ad71e7..c296df60 100644 --- a/src/lms/ui/explore/TrackListHelpers.cpp +++ b/src/lms/ui/explore/TrackListHelpers.cpp @@ -20,7 +20,6 @@ #include "TrackListHelpers.hpp" #include -#include #include #include @@ -29,10 +28,10 @@ #include "services/scrobbling/IScrobblingService.hpp" #include "services/database/Session.hpp" #include "services/database/Track.hpp" -#include "utils/Logger.hpp" #include "utils/Service.hpp" #include "common/Template.hpp" +#include "explore/PlayQueueController.hpp" #include "resource/DownloadResource.hpp" #include "resource/CoverResource.hpp" #include "LmsApplication.hpp" @@ -44,7 +43,7 @@ using namespace Database; namespace UserInterface::TrackListHelpers { std::unique_ptr - createEntry(const Database::ObjectPtr& track, PlayQueueActionTrackSignal& tracksAction) + createEntry(const Database::ObjectPtr& track, PlayQueueController& playQueueController) { auto entry {std::make_unique