From 373ee8f0bef9428015b497fcbd741e58182c606d Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 28 Feb 2025 18:14:00 +0100 Subject: [PATCH 01/31] Subsonic API: added version in album responses --- SUBSONIC.md | 1 + src/libs/subsonic/impl/responses/Album.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/SUBSONIC.md b/SUBSONIC.md index 0ef54e21..1781a5f9 100644 --- a/SUBSONIC.md +++ b/SUBSONIC.md @@ -36,6 +36,7 @@ The following extra fields are implemented: * `recordLabels` * `releaseTypes` * `userRating` + * `version` * `Child` response: * `albumArtists` * `artists` diff --git a/src/libs/subsonic/impl/responses/Album.cpp b/src/libs/subsonic/impl/responses/Album.cpp index 7132e011..55fe3d82 100644 --- a/src/libs/subsonic/impl/responses/Album.cpp +++ b/src/libs/subsonic/impl/responses/Album.cpp @@ -145,6 +145,7 @@ namespace lms::api::subsonic return albumNode; // OpenSubsonic specific fields (must always be set) + albumNode.setAttribute("version", release->getComment()); albumNode.setAttribute("sortName", release->getSortName()); albumNode.setAttribute("mediaType", "album"); From 3ecd63fea0a329ba1cd05c4568dbeec2638b6838 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 2 Mar 2025 11:48:27 +0100 Subject: [PATCH 02/31] Added a way to probe images from a buffer --- src/libs/image/impl/graphicsmagick/Image.cpp | 23 ++++++++++++++++++++ src/libs/image/impl/stb/Image.cpp | 18 +++++++++++++++ src/libs/image/include/image/Image.hpp | 1 + 3 files changed, 42 insertions(+) diff --git a/src/libs/image/impl/graphicsmagick/Image.cpp b/src/libs/image/impl/graphicsmagick/Image.cpp index 8f4c7605..53e04095 100644 --- a/src/libs/image/impl/graphicsmagick/Image.cpp +++ b/src/libs/image/impl/graphicsmagick/Image.cpp @@ -77,6 +77,29 @@ namespace lms::image } } + ImageProperties probeImage(std::span encodedData) + { + LMS_SCOPED_TRACE_DETAILED("Image", "ProbeBuffer"); + + try + { + Magick::Image image; + Magick::Blob blob{ encodedData.data(), encodedData.size() }; + image.ping(blob); + + ImageProperties properties; + properties.width = image.size().width(); + properties.height = image.size().height(); + + return properties; + } + catch (Magick::Exception& e) + { + LMS_LOG(COVER, ERROR, "Caught Magick exception: " << e.what()); + throw Exception{ std::string{ "Magick probe error: " } + e.what() }; + } + } + std::unique_ptr decodeImage(std::span encodedData) { LMS_SCOPED_TRACE_DETAILED("Image", "DecodeBuffer"); diff --git a/src/libs/image/impl/stb/Image.cpp b/src/libs/image/impl/stb/Image.cpp index ec9432bf..5638cb69 100644 --- a/src/libs/image/impl/stb/Image.cpp +++ b/src/libs/image/impl/stb/Image.cpp @@ -60,6 +60,24 @@ namespace lms::image return properties; } + ImageProperties probeImage(std::span encodedData) + { + LMS_SCOPED_TRACE_DETAILED("Image", "ProbeBuffer"); + + int x{}; + int y{}; + int comp{}; + + if (::stbi_info_from_memory(reinterpret_cast(encodedData.data()), static_cast(encodedData.size()), &x, &y, &comp) == 0) + throw StbiException{ "Probe failed" }; + + ImageProperties properties; + properties.width = x; + properties.height = y; + + return properties; + } + std::unique_ptr decodeImage(std::span encodedData) { LMS_SCOPED_TRACE_DETAILED("Image", "DecodeBuffer"); diff --git a/src/libs/image/include/image/Image.hpp b/src/libs/image/include/image/Image.hpp index 594d78fd..31e46ba8 100644 --- a/src/libs/image/include/image/Image.hpp +++ b/src/libs/image/include/image/Image.hpp @@ -33,6 +33,7 @@ namespace lms::image // All these methods may throw Exception ImageProperties probeImage(const std::filesystem::path& path); + ImageProperties probeImage(std::span encodedData); std::unique_ptr decodeImage(std::span encodedData); std::unique_ptr decodeImage(const std::filesystem::path& path); From 6608063d615a72a087c62f8b2b639fffba1e82c6 Mon Sep 17 00:00:00 2001 From: Daniel Thirion Date: Mon, 3 Mar 2025 23:14:37 +0100 Subject: [PATCH 03/31] Added initial implementation of inline remixers First implementation is a bit of a hardcode; working on making it work with all artist relations. Will then add a config flag or something for it to be user-toggleable. --- approot/release.xml | 3 +++ src/lms/ui/explore/ReleaseView.cpp | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/approot/release.xml b/approot/release.xml index 46b190f8..ac5f4009 100644 --- a/approot/release.xml +++ b/approot/release.xml @@ -123,6 +123,9 @@ + ${}
+ Remixer — ${remixers class="d-inline d-md-inline"} +
${
} diff --git a/src/lms/ui/explore/ReleaseView.cpp b/src/lms/ui/explore/ReleaseView.cpp index d6b6eff0..c4127b66 100644 --- a/src/lms/ui/explore/ReleaseView.cpp +++ b/src/lms/ui/explore/ReleaseView.cpp @@ -437,6 +437,13 @@ namespace lms::ui entry->bindWidget("artists-md", utils::createArtistDisplayNameWithAnchors(track->getArtistDisplayName(), artists)); } + const auto remixers{ track->getArtistIds({ TrackArtistLinkType::Remixer }) }; + if (!remixers.empty()) + { + entry->setCondition("if-has-remixers", true); + entry->bindWidget("remixers", utils::createArtistDisplayNameWithAnchors(track->getArtistDisplayName(), remixers)); + } + auto trackNumber{ track->getTrackNumber() }; if (trackNumber) { From 2e5c9e921880404705406172531d6f71f5945c65 Mon Sep 17 00:00:00 2001 From: Daniel Thirion Date: Tue, 4 Mar 2025 10:43:51 +0100 Subject: [PATCH 04/31] Implement inline artist credits For now, it's always shown. Soon, I will be implementing a setting to chose whether to show everything, remixers only, or nothing. --- approot/release.xml | 6 ++--- src/lms/ui/explore/ReleaseView.cpp | 43 ++++++++++++++++++++++++++---- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/approot/release.xml b/approot/release.xml index ac5f4009..91f5c97c 100644 --- a/approot/release.xml +++ b/approot/release.xml @@ -123,9 +123,9 @@ - ${}
- Remixer — ${remixers class="d-inline d-md-inline"} -
${
} + ${} + ${artist-links} + ${}
diff --git a/src/lms/ui/explore/ReleaseView.cpp b/src/lms/ui/explore/ReleaseView.cpp index c4127b66..4221b87e 100644 --- a/src/lms/ui/explore/ReleaseView.cpp +++ b/src/lms/ui/explore/ReleaseView.cpp @@ -437,11 +437,44 @@ namespace lms::ui entry->bindWidget("artists-md", utils::createArtistDisplayNameWithAnchors(track->getArtistDisplayName(), artists)); } - const auto remixers{ track->getArtistIds({ TrackArtistLinkType::Remixer }) }; - if (!remixers.empty()) - { - entry->setCondition("if-has-remixers", true); - entry->bindWidget("remixers", utils::createArtistDisplayNameWithAnchors(track->getArtistDisplayName(), remixers)); + { // Generate all artist link widgets + // TODO: add config to chose whether to only show Remixer or show All. + std::vector>> artistLinksList; + auto createTrackArtistLink = [&](TrackArtistLinkType linkType, const std::string& trkey) { + const auto artists{ track->getArtistIds({ linkType }) }; + const auto text = Wt::WString::trn(trkey, artists.size()).arg(artists.size()); + auto anchors{ utils::createArtistAnchorList(std::vector(std::cbegin(artists), std::cend(artists))) }; + if (!artists.empty()) + { + auto myPair = std::make_pair(text, std::move(anchors)); + artistLinksList.push_back(std::move(myPair)); + } + }; + createTrackArtistLink(TrackArtistLinkType::Remixer, "Lms.Explore.Artists.linktype-remixer"); + createTrackArtistLink(TrackArtistLinkType::Producer, "Lms.Explore.Artists.linktype-producer"); + createTrackArtistLink(TrackArtistLinkType::Performer, "Lms.Explore.Artists.linktype-performer"); + createTrackArtistLink(TrackArtistLinkType::Composer, "Lms.Explore.Artists.linktype-composer"); + createTrackArtistLink(TrackArtistLinkType::Conductor, "Lms.Explore.Artists.linktype-conductor"); + createTrackArtistLink(TrackArtistLinkType::Lyricist, "Lms.Explore.Artists.linktype-lyricist"); + createTrackArtistLink(TrackArtistLinkType::Mixer, "Lms.Explore.Artists.linktype-mixer"); + + if (!artistLinksList.empty()) + { + entry->setCondition("if-has-artist-links", true); + Wt::WContainerWidget* artistLinksContainer = entry->bindNew("artist-links"); + + for (auto& [role, artistWidget] : artistLinksList) + { + auto* currContainer = artistLinksContainer->addNew(); + currContainer->setStyleClass("overflow-hidden ms-5 pb-2 px-2 text-small d-flex"); + + auto* roleText = currContainer->addNew(role + Wt::WString::fromUTF8(" — ")); + roleText->setStyleClass("d-inline"); + + auto* newArtistWidget = currContainer->addWidget(std::move(artistWidget)); + newArtistWidget->setInline(true); + } + } } auto trackNumber{ track->getTrackNumber() }; From 1ef48880c38fb1cc3a22b6c1035292c89d3f7ea4 Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 8 Mar 2025 12:07:26 +0100 Subject: [PATCH 05/31] Some UI tweaks + display roles for performers --- approot/release.xml | 14 +++--- src/lms/ui/explore/ReleaseView.cpp | 60 +++++++++---------------- src/lms/ui/explore/TrackListHelpers.cpp | 30 ++++++++----- src/lms/ui/explore/TrackListHelpers.hpp | 5 +++ 4 files changed, 52 insertions(+), 57 deletions(-) diff --git a/approot/release.xml b/approot/release.xml index 91f5c97c..b103fc6d 100644 --- a/approot/release.xml +++ b/approot/release.xml @@ -94,13 +94,14 @@ ${}${track-number}${}
-
+
${name} ${}${artists class="d-block d-md-none"}${} + ${}${artist-links}${}
-
+
${}${artists-md class="d-none d-md-block"}${}
@@ -123,9 +124,12 @@
- ${} - ${artist-links} - ${} +
+ + +
+ ${role} — ${anchors class="ms-1"} +
diff --git a/src/lms/ui/explore/ReleaseView.cpp b/src/lms/ui/explore/ReleaseView.cpp index 4221b87e..f6f2a059 100644 --- a/src/lms/ui/explore/ReleaseView.cpp +++ b/src/lms/ui/explore/ReleaseView.cpp @@ -185,6 +185,24 @@ namespace lms::ui return core::stringUtils::readAs(wApp->internalPathNextPart("/release/")); } + + void fillTrackArtistLinks(Wt::WTemplate* trackEntry, const db::Track::pointer& track) + { + const std::map> artistsByRole{ TrackListHelpers::getArtistsByRole(track->getId()) }; + + if (artistsByRole.empty()) + return; + + trackEntry->setCondition("if-has-artist-links", true); + Wt::WContainerWidget* artistLinksContainer = trackEntry->bindNew("artist-links"); + + for (auto& [role, artists] : artistsByRole) + { + Wt::WTemplate* artistLinkEntry{ artistLinksContainer->addNew(Wt::WString::tr("Lms.Explore.Release.template.artist-links-entry")) }; + artistLinkEntry->bindString("role", role, Wt::TextFormat::Plain); + artistLinkEntry->bindWidget("anchors", utils::createArtistAnchorList(std::vector(std::cbegin(artists), std::cend(artists)))); + } + } } // namespace Release::Release(Filters& filters, PlayQueueController& playQueueController) @@ -415,7 +433,7 @@ namespace lms::ui const db::TrackId trackId{ track->getId() }; const auto discNumber{ track->getDiscNumber() }; - Wt::WContainerWidget* container; + Wt::WContainerWidget* container{}; if (useSubtitleContainers && discNumber) container = getOrAddDiscContainer(*discNumber, track->getDiscSubtitle()); else if (hasDiscSubtitle && !discNumber) @@ -437,45 +455,7 @@ namespace lms::ui entry->bindWidget("artists-md", utils::createArtistDisplayNameWithAnchors(track->getArtistDisplayName(), artists)); } - { // Generate all artist link widgets - // TODO: add config to chose whether to only show Remixer or show All. - std::vector>> artistLinksList; - auto createTrackArtistLink = [&](TrackArtistLinkType linkType, const std::string& trkey) { - const auto artists{ track->getArtistIds({ linkType }) }; - const auto text = Wt::WString::trn(trkey, artists.size()).arg(artists.size()); - auto anchors{ utils::createArtistAnchorList(std::vector(std::cbegin(artists), std::cend(artists))) }; - if (!artists.empty()) - { - auto myPair = std::make_pair(text, std::move(anchors)); - artistLinksList.push_back(std::move(myPair)); - } - }; - createTrackArtistLink(TrackArtistLinkType::Remixer, "Lms.Explore.Artists.linktype-remixer"); - createTrackArtistLink(TrackArtistLinkType::Producer, "Lms.Explore.Artists.linktype-producer"); - createTrackArtistLink(TrackArtistLinkType::Performer, "Lms.Explore.Artists.linktype-performer"); - createTrackArtistLink(TrackArtistLinkType::Composer, "Lms.Explore.Artists.linktype-composer"); - createTrackArtistLink(TrackArtistLinkType::Conductor, "Lms.Explore.Artists.linktype-conductor"); - createTrackArtistLink(TrackArtistLinkType::Lyricist, "Lms.Explore.Artists.linktype-lyricist"); - createTrackArtistLink(TrackArtistLinkType::Mixer, "Lms.Explore.Artists.linktype-mixer"); - - if (!artistLinksList.empty()) - { - entry->setCondition("if-has-artist-links", true); - Wt::WContainerWidget* artistLinksContainer = entry->bindNew("artist-links"); - - for (auto& [role, artistWidget] : artistLinksList) - { - auto* currContainer = artistLinksContainer->addNew(); - currContainer->setStyleClass("overflow-hidden ms-5 pb-2 px-2 text-small d-flex"); - - auto* roleText = currContainer->addNew(role + Wt::WString::fromUTF8(" — ")); - roleText->setStyleClass("d-inline"); - - auto* newArtistWidget = currContainer->addWidget(std::move(artistWidget)); - newArtistWidget->setInline(true); - } - } - } + fillTrackArtistLinks(entry, track); auto trackNumber{ track->getTrackNumber() }; if (trackNumber) diff --git a/src/lms/ui/explore/TrackListHelpers.cpp b/src/lms/ui/explore/TrackListHelpers.cpp index 572b4e80..3dada888 100644 --- a/src/lms/ui/explore/TrackListHelpers.cpp +++ b/src/lms/ui/explore/TrackListHelpers.cpp @@ -50,18 +50,8 @@ namespace lms::ui::TrackListHelpers { using namespace db; - void showTrackInfoModal(db::TrackId trackId, Filters& filters) + std::map> getArtistsByRole(db::TrackId trackId) { - auto transaction{ LmsApp->getDbSession().createReadTransaction() }; - - const db::Track::pointer track{ Track::find(LmsApp->getDbSession(), trackId) }; - if (!track) - return; - - auto trackInfo{ std::make_unique