From 76290768bbe4c03c0a587734debf734ee4ba43ea Mon Sep 17 00:00:00 2001 From: emeric Date: Thu, 12 Oct 2023 22:38:06 +0200 Subject: [PATCH 01/16] OpenSubsonic API: return OS fields on error --- src/libs/subsonic/impl/SubsonicResponse.cpp | 54 ++++++++++----------- src/libs/subsonic/impl/SubsonicResponse.hpp | 1 + 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/src/libs/subsonic/impl/SubsonicResponse.cpp b/src/libs/subsonic/impl/SubsonicResponse.cpp index 95952e2e..3f177a6d 100644 --- a/src/libs/subsonic/impl/SubsonicResponse.cpp +++ b/src/libs/subsonic/impl/SubsonicResponse.cpp @@ -82,7 +82,7 @@ namespace API::Subsonic void Response::Node::createEmptyArrayValue(std::string_view key) { - assert (!_value); + assert(!_value); _childrenValues.emplace(key, ValuesType{}); } @@ -97,7 +97,7 @@ namespace API::Subsonic void Response::Node::addArrayValue(std::string_view key, long long value) { assert(!_value); - auto& values {_childrenValues[std::string{ key }]}; + auto& values{ _childrenValues[std::string{ key }] }; values.push_back(value); assert(std::all_of(std::cbegin(values) + 1, std::cend(values), [&](const ValueType& value) {return value.index() == values.front().index();})); } @@ -120,13 +120,30 @@ namespace API::Subsonic } Response Response::createOkResponse(ProtocolVersion protocolVersion) + { + return createResponseCommon(protocolVersion); + } + + Response Response::createFailedResponse(ProtocolVersion protocolVersion, const Error& error) + { + return createResponseCommon(protocolVersion, &error); + } + + Response Response::createResponseCommon(ProtocolVersion protocolVersion, const Error* error) { Response response; Node& responseNode{ response._root.createChild("subsonic-response") }; - responseNode.setAttribute("status", "ok"); + responseNode.setAttribute("status", error ? "failed" : "ok"); responseNode.setVersionAttribute(protocolVersion); + if (error) + { + Node& errorNode{ responseNode.createChild("error") }; + errorNode.setAttribute("code", static_cast(error->getCode())); + errorNode.setAttribute("message", error->getMessage()); + } + // OpenSubsonic mandatory fields responseNode.setAttribute("type", "lms"); responseNode.setAttribute("serverVersion", serverVersion); @@ -135,22 +152,6 @@ namespace API::Subsonic return response; } - Response Response::createFailedResponse(ProtocolVersion protocolVersion, const Error& error) - { - Response response; - Node& responseNode{ response._root.createChild("subsonic-response") }; - - 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", static_cast(error.getCode())); - errorNode.setAttribute("message", error.getMessage()); - - return response; - } - void Response::addNode(const std::string& key, Node node) { return _root._children["subsonic-response"].front().addChild(key, std::move(node)); @@ -198,15 +199,15 @@ namespace API::Subsonic } auto valueToPropertyTree = [](const Node::ValueType& value) - { - boost::property_tree::ptree res; - std::visit([&](const auto& rawValue) { - res.put_value(rawValue); - }, value); + boost::property_tree::ptree res; + std::visit([&](const auto& rawValue) + { + res.put_value(rawValue); + }, value); - return res; - }; + return res; + }; if (node._value) { @@ -300,4 +301,3 @@ namespace API::Subsonic } } // namespace - diff --git a/src/libs/subsonic/impl/SubsonicResponse.hpp b/src/libs/subsonic/impl/SubsonicResponse.hpp index 1d04fd6e..64046ac0 100644 --- a/src/libs/subsonic/impl/SubsonicResponse.hpp +++ b/src/libs/subsonic/impl/SubsonicResponse.hpp @@ -249,6 +249,7 @@ namespace API::Subsonic void write(std::ostream& os, ResponseFormat format); private: + static Response createResponseCommon(ProtocolVersion protocolVersion, const Error* error = nullptr); void writeJSON(std::ostream& os); void writeXML(std::ostream& os); From d2956df4ccb242f7a73557d1c606291ffb1b96a4 Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 14 Oct 2023 19:22:13 +0200 Subject: [PATCH 02/16] Handle artist display names --- SUBSONIC.md | 3 + approot/release.xml | 1 - approot/releases.xml | 5 +- src/libs/metadata/impl/TagLibParser.cpp | 3 + .../metadata/include/metadata/IParser.hpp | 2 + src/libs/services/database/impl/Migration.cpp | 13 + src/libs/services/database/impl/Migration.hpp | 2 +- .../include/services/database/Release.hpp | 5 +- .../include/services/database/Track.hpp | 4 + .../scanner/impl/ScanStepScanFiles.cpp | 5 +- src/libs/subsonic/impl/responses/Album.cpp | 2 + src/libs/subsonic/impl/responses/Song.cpp | 13 +- src/lms/ui/PlayQueue.cpp | 4 +- src/lms/ui/Utils.cpp | 393 ++++++++++-------- src/lms/ui/Utils.hpp | 5 +- src/lms/ui/explore/ReleaseHelpers.cpp | 184 ++++---- src/lms/ui/explore/ReleaseHelpers.hpp | 12 +- src/lms/ui/explore/ReleaseView.cpp | 30 +- src/lms/ui/explore/TrackListHelpers.cpp | 6 +- src/tools/metadata/LmsMetadata.cpp | 6 + 20 files changed, 389 insertions(+), 309 deletions(-) diff --git a/SUBSONIC.md b/SUBSONIC.md index 7bb4b93c..ba3ba36f 100644 --- a/SUBSONIC.md +++ b/SUBSONIC.md @@ -14,6 +14,7 @@ The following extra fields are implemented: * `musicBrainzId` * `genres` * `artists` + * `displayArtist` * `releaseTypes` * `moods` * `originalReleaseDate` @@ -23,7 +24,9 @@ The following extra fields are implemented: * `musicBrainzId`: note this is actually the recording MBID when this response refers to a song * `genres` * `artists` + * `displayArtist` * `albumArtists` + * `displayAlbumArtist` * `contributors` * `moods` * `replayGain` diff --git a/approot/release.xml b/approot/release.xml index e836e793..992f97eb 100644 --- a/approot/release.xml +++ b/approot/release.xml @@ -14,7 +14,6 @@

${name}

${}${artists}${} - ${}
${tr:Lms.Explore.various-artists}
${
}
${}${year} · ${}${duration}
${clusters class="mb-2"}
diff --git a/approot/releases.xml b/approot/releases.xml index 2bcc0871..c66f3577 100644 --- a/approot/releases.xml +++ b/approot/releases.xml @@ -40,12 +40,11 @@
-
+
${cover class="shadow-sm"}
${release-name class="d-block text-truncate text-nowrap text-decoration-none link-success"} - ${}${artist-name class="d-block text-truncate text-nowrap text-decoration-none link-secondary"}${} - ${}
${tr:Lms.Explore.various-artists}
${
} + ${}${artist-name class="d-block text-truncate text-nowrap"}${} ${}
${year}
${
}
diff --git a/src/libs/metadata/impl/TagLibParser.cpp b/src/libs/metadata/impl/TagLibParser.cpp index d5fb733b..cfcaaeb7 100644 --- a/src/libs/metadata/impl/TagLibParser.cpp +++ b/src/libs/metadata/impl/TagLibParser.cpp @@ -210,6 +210,7 @@ getRelease(const TagMap& tags) release.emplace(); release->name = std::move(*releaseName); + release->artistDisplayName = getPropertyValueAs(tags, "ALBUMARTIST").value_or(""); release->mbid = getPropertyValueFirstMatchAs(tags, {"MUSICBRAINZ_ALBUMID", "MUSICBRAINZ ALBUM ID", "MUSICBRAINZ/ALBUM ID"}); release->artists = getArtists(tags, {"ALBUMARTISTS", "ALBUMARTIST"}, {"ALBUMARTISTSSORT", "ALBUMARTISTSORT"}, {"MUSICBRAINZ_ALBUMARTISTID", "MUSICBRAINZ ALBUM ARTIST ID", "MUSICBRAINZ/ALBUM ARTIST ID"}); release->mediumCount = getPropertyValueAs(tags, "DISCTOTAL"); @@ -353,6 +354,8 @@ TagLibParser::processTag(Track& track, const std::string& tag, const std::vector track.copyrightURL = value; else if (tag == "REPLAYGAIN_TRACK_GAIN") track.replayGain = StringUtils::readAs(value); + else if (tag == "ARTIST") + track.artistDisplayName = value; else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end()) { std::set clusterNames; diff --git a/src/libs/metadata/include/metadata/IParser.hpp b/src/libs/metadata/include/metadata/IParser.hpp index 15b72282..bb2b0274 100644 --- a/src/libs/metadata/include/metadata/IParser.hpp +++ b/src/libs/metadata/include/metadata/IParser.hpp @@ -79,6 +79,7 @@ namespace MetaData std::optional mbid; std::string name; + std::string artistDisplayName; std::vector artists; std::optional mediumCount; std::optional primaryType; @@ -117,6 +118,7 @@ namespace MetaData std::string copyright; std::string copyrightURL; std::optional replayGain; + std::string artistDisplayName; std::vector artists; std::vector conductorArtists; std::vector composerArtists; diff --git a/src/libs/services/database/impl/Migration.cpp b/src/libs/services/database/impl/Migration.cpp index 28b22dd8..52bc4fb4 100644 --- a/src/libs/services/database/impl/Migration.cpp +++ b/src/libs/services/database/impl/Migration.cpp @@ -665,6 +665,18 @@ CREATE TABLE IF NOT EXISTS "track_backup" ( ScanSettings::get(session).modify()->incScanVersion(); } + static + void + migrateFromV40(Session& session) + { + // add artist_display_name in Release and Track + session.getDboSession().execute("ALTER TABLE release ADD artist_display_name TEXT"); + session.getDboSession().execute("ALTER TABLE track ADD artist_display_name TEXT"); + + // 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) { @@ -711,6 +723,7 @@ CREATE TABLE IF NOT EXISTS "track_backup" ( {37, migrateFromV37}, {38, migrateFromV38}, {39, migrateFromV39}, + {40, migrateFromV40}, }; while (1) diff --git a/src/libs/services/database/impl/Migration.hpp b/src/libs/services/database/impl/Migration.hpp index 2c4f1896..b20c7680 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 {40}; + static constexpr Version LMS_DATABASE_VERSION {41}; class VersionInfo { public: diff --git a/src/libs/services/database/include/services/database/Release.hpp b/src/libs/services/database/include/services/database/Release.hpp index 810a4067..a9c39b07 100644 --- a/src/libs/services/database/include/services/database/Release.hpp +++ b/src/libs/services/database/include/services/database/Release.hpp @@ -113,6 +113,7 @@ namespace Database Wt::WDateTime getLastWritten() const; std::optional getPrimaryType() const { return _primaryType; } EnumSet getSecondaryTypes() const { return _secondaryTypes; } + std::string_view getArtistDisplayName() const { return _artistDisplayName; } std::size_t getTracksCount() const; // Setters @@ -121,6 +122,7 @@ namespace Database void setTotalDisc(std::optional totalDisc) { _totalDisc = totalDisc; } void setPrimaryType(std::optional type) { _primaryType = type; } void setSecondaryTypes(EnumSet types) { _secondaryTypes = types; } + void setArtistDisplayName(std::string_view name) { _artistDisplayName = name; } // Get the artists of this release std::vector> getArtists(TrackArtistLinkType type = TrackArtistLinkType::Artist) const; @@ -137,7 +139,7 @@ namespace Database Wt::Dbo::field(a, _totalDisc, "total_disc"); Wt::Dbo::field(a, _primaryType, "primary_type"); Wt::Dbo::field(a, _secondaryTypes, "secondary_types"); - + Wt::Dbo::field(a, _artistDisplayName, "artist_display_name"); Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToOne, "release"); } @@ -155,6 +157,7 @@ namespace Database std::optional _totalDisc{}; std::optional _primaryType; EnumSet _secondaryTypes; + std::string _artistDisplayName; Wt::Dbo::collection> _tracks; // Tracks in the release }; diff --git a/src/libs/services/database/include/services/database/Track.hpp b/src/libs/services/database/include/services/database/Track.hpp index 1f944f75..94401a61 100644 --- a/src/libs/services/database/include/services/database/Track.hpp +++ b/src/libs/services/database/include/services/database/Track.hpp @@ -138,6 +138,7 @@ class Track final : public Object void setCopyrightURL(const std::string& copyrightURL) { _copyrightURL = std::string(copyrightURL, 0, _maxCopyrightURLLength); } void setTrackReplayGain(std::optional replayGain) { _trackReplayGain = replayGain; } void setReleaseReplayGain(std::optional replayGain) { _releaseReplayGain = replayGain; } // may be by disc! + void setArtistDisplayName(std::string_view name) { _artistDisplayName = name; } void clearArtistLinks(); void addArtistLink(const ObjectPtr& artistLink); void setRelease(ObjectPtr release) { _release = getDboPtr(release); } @@ -163,6 +164,7 @@ class Track final : public Object std::optional getCopyrightURL() const; std::optional getTrackReplayGain() const { return _trackReplayGain; } std::optional getReleaseReplayGain() const { return _releaseReplayGain; } + std::string_view getArtistDisplayName() const { return _artistDisplayName; } // no artistLinkTypes means get all std::vector> getArtists(EnumSet artistLinkTypes) const; // no type means all std::vector getArtistIds(EnumSet artistLinkTypes) const; // no type means all @@ -195,6 +197,7 @@ class Track final : public Object Wt::Dbo::field(a, _copyrightURL, "copyright_url"); Wt::Dbo::field(a, _trackReplayGain, "track_replay_gain"); Wt::Dbo::field(a, _releaseReplayGain, "release_replay_gain"); // here in Track since Release does not have concept of "disc" (yet?) + Wt::Dbo::field(a, _artistDisplayName, "artist_display_name"); Wt::Dbo::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade); Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "track"); Wt::Dbo::hasMany(a, _clusters, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade); @@ -228,6 +231,7 @@ class Track final : public Object std::string _copyrightURL; std::optional _trackReplayGain; std::optional _releaseReplayGain; + std::string _artistDisplayName; Wt::Dbo::ptr _release; Wt::Dbo::collection> _trackArtistLinks; diff --git a/src/libs/services/scanner/impl/ScanStepScanFiles.cpp b/src/libs/services/scanner/impl/ScanStepScanFiles.cpp index f1b9b5d9..76d29439 100644 --- a/src/libs/services/scanner/impl/ScanStepScanFiles.cpp +++ b/src/libs/services/scanner/impl/ScanStepScanFiles.cpp @@ -189,9 +189,11 @@ namespace if (release->getPrimaryType() != primaryType) release.modify()->setPrimaryType(primaryType); } - const EnumSet secondaryTypes {convertReleaseTypesSecondary(releaseInfo.secondaryTypes)}; + const EnumSet secondaryTypes{ convertReleaseTypesSecondary(releaseInfo.secondaryTypes) }; if (release->getSecondaryTypes() != secondaryTypes) release.modify()->setSecondaryTypes(secondaryTypes); + if (release->getArtistDisplayName() != releaseInfo.artistDisplayName) + release.modify()->setArtistDisplayName(releaseInfo.artistDisplayName); } Release::pointer @@ -525,5 +527,6 @@ namespace Scanner track.modify()->setCopyright(trackInfo->copyright); track.modify()->setCopyrightURL(trackInfo->copyrightURL); track.modify()->setTrackReplayGain(trackInfo->replayGain); + track.modify()->setArtistDisplayName(trackInfo->artistDisplayName); } } diff --git a/src/libs/subsonic/impl/responses/Album.cpp b/src/libs/subsonic/impl/responses/Album.cpp index 78bfb6c6..5e93c6ff 100644 --- a/src/libs/subsonic/impl/responses/Album.cpp +++ b/src/libs/subsonic/impl/responses/Album.cpp @@ -186,6 +186,8 @@ namespace API::Subsonic for (const Artist::pointer& artist : release->getReleaseArtists()) albumNode.addArrayChild("artists", createArtistNode(artist)); + albumNode.setAttribute("displayArtist", release->getArtistDisplayName()); + { const Wt::WDate originalReleaseDate{ release->getOriginalReleaseDate() }; albumNode.setAttribute("originalReleaseDate", originalReleaseDate.isValid() ? StringUtils::toISO8601String(originalReleaseDate) : ""); diff --git a/src/libs/subsonic/impl/responses/Song.cpp b/src/libs/subsonic/impl/responses/Song.cpp index 73eeb5d7..9a7c6653 100644 --- a/src/libs/subsonic/impl/responses/Song.cpp +++ b/src/libs/subsonic/impl/responses/Song.cpp @@ -134,11 +134,12 @@ namespace API::Subsonic trackResponse.setAttribute("artistId", idToString(artists.front()->getId())); } - if (track->getRelease()) + Release::pointer release{ track->getRelease() }; + if (release) { - trackResponse.setAttribute("album", track->getRelease()->getName()); - trackResponse.setAttribute("albumId", idToString(track->getRelease()->getId())); - trackResponse.setAttribute("parent", idToString(track->getRelease()->getId())); + trackResponse.setAttribute("album", release->getName()); + trackResponse.setAttribute("albumId", idToString(release->getId())); + trackResponse.setAttribute("parent", idToString(release->getId())); } trackResponse.setAttribute("duration", std::chrono::duration_cast(track->getDuration()).count()); @@ -195,7 +196,11 @@ namespace API::Subsonic } }; addArtistLinks("artists", TrackArtistLinkType::Artist); + trackResponse.setAttribute("displayArtist", track->getArtistDisplayName()); + addArtistLinks("albumartists", TrackArtistLinkType::ReleaseArtist); + if (release) + trackResponse.setAttribute("displayAlbumArtist", release->getArtistDisplayName()); auto addClusters{ [&](std::string_view field, std::string_view clusterTypeName) { diff --git a/src/lms/ui/PlayQueue.cpp b/src/lms/ui/PlayQueue.cpp index e1e5d946..1df4bc79 100644 --- a/src/lms/ui/PlayQueue.cpp +++ b/src/lms/ui/PlayQueue.cpp @@ -535,8 +535,8 @@ PlayQueue::addEntry(const Database::TrackListEntry::pointer& tracklistEntry) if (!artists.empty()) { entry->setCondition("if-has-artists", true); - entry->bindWidget("artists", Utils::createArtistContainer(artists)); - entry->bindWidget("artists-md", Utils::createArtistContainer(artists)); + entry->bindWidget("artists", Utils::createArtistAnchorList(artists)); + entry->bindWidget("artists-md", Utils::createArtistAnchorList(artists)); } const auto release {track->getRelease()}; diff --git a/src/lms/ui/Utils.cpp b/src/lms/ui/Utils.cpp index 26da3f58..0ea27ebf 100644 --- a/src/lms/ui/Utils.cpp +++ b/src/lms/ui/Utils.cpp @@ -38,204 +38,269 @@ namespace UserInterface::Utils { - std::string - durationToString(std::chrono::milliseconds msDuration) - { - const std::chrono::seconds duration {std::chrono::duration_cast(msDuration)}; + std::string durationToString(std::chrono::milliseconds msDuration) + { + const std::chrono::seconds duration{ std::chrono::duration_cast(msDuration) }; - std::ostringstream oss; + std::ostringstream oss; - if (duration.count() >= 3600) - { - oss << (duration.count() / 3600) - << ":" - << std::setfill('0') << std::setw(2) << (duration.count() % 3600) / 60 - << ":" - << std::setfill('0') << std::setw(2) << duration.count() % 60; - } - else - { - oss << (duration.count() / 60) - << ":" - << std::setfill('0') << std::setw(2) << duration.count() % 60; - } + if (duration.count() >= 3600) + { + oss << (duration.count() / 3600) + << ":" + << std::setfill('0') << std::setw(2) << (duration.count() % 3600) / 60 + << ":" + << std::setfill('0') << std::setw(2) << duration.count() % 60; + } + else + { + oss << (duration.count() / 60) + << ":" + << std::setfill('0') << std::setw(2) << duration.count() % 60; + } - return oss.str(); - } + return oss.str(); + } - std::unique_ptr - createCover(Database::ReleaseId releaseId, CoverResource::Size size) - { - auto cover {std::make_unique()}; - cover->setImageLink(LmsApp->getCoverResource()->getReleaseUrl(releaseId, size)); - cover->setStyleClass("Lms-cover img-fluid"); // HACK - cover->setAttributeValue("onload", LmsApp->javaScriptClass() + ".onLoadCover(this)"); // HACK - return cover; - } + std::unique_ptr createCover(Database::ReleaseId releaseId, CoverResource::Size size) + { + auto cover{ std::make_unique() }; + cover->setImageLink(LmsApp->getCoverResource()->getReleaseUrl(releaseId, size)); + cover->setStyleClass("Lms-cover img-fluid"); // HACK + cover->setAttributeValue("onload", LmsApp->javaScriptClass() + ".onLoadCover(this)"); // HACK + return cover; + } - std::unique_ptr - createCover(Database::TrackId trackId, CoverResource::Size size) - { - auto cover {std::make_unique()}; - cover->setImageLink(LmsApp->getCoverResource()->getTrackUrl(trackId, size)); - cover->setStyleClass("Lms-cover img-fluid"); // HACK - cover->setAttributeValue("onload", LmsApp->javaScriptClass() + ".onLoadCover(this)"); // HACK - return cover; - } + std::unique_ptr createCover(Database::TrackId trackId, CoverResource::Size size) + { + auto cover{ std::make_unique() }; + cover->setImageLink(LmsApp->getCoverResource()->getTrackUrl(trackId, size)); + cover->setStyleClass("Lms-cover img-fluid"); // HACK + cover->setAttributeValue("onload", LmsApp->javaScriptClass() + ".onLoadCover(this)"); // HACK + return cover; + } - std::unique_ptr - createCluster(Database::ClusterId clusterId, bool canDelete) - { - auto transaction {LmsApp->getDbSession().createSharedTransaction()}; + std::unique_ptr createCluster(Database::ClusterId clusterId, bool canDelete) + { + auto transaction{ LmsApp->getDbSession().createSharedTransaction() }; - const Database::Cluster::pointer cluster {Database::Cluster::find(LmsApp->getDbSession(), clusterId)}; - if (!cluster) - return {}; + const Database::Cluster::pointer cluster{ Database::Cluster::find(LmsApp->getDbSession(), clusterId) }; + if (!cluster) + return {}; - auto getStyleClass {[](const Database::Cluster::pointer& cluster) -> const char* - { - switch (cluster->getType()->getId().getValue() % 8) - { - case 0: return "bg-primary"; - case 1: return "bg-secondary"; - case 2: return "bg-success"; - case 3: return "bg-danger"; - case 4: return "bg-warning text-dark"; - case 5: return "bg-info text-dark"; - case 6: return "bg-light text-dark"; - case 7: return "bg-dark"; - } + auto getStyleClass{ [](const Database::Cluster::pointer& cluster) -> const char* + { + switch (cluster->getType()->getId().getValue() % 8) + { + case 0: return "bg-primary"; + case 1: return "bg-secondary"; + case 2: return "bg-success"; + case 3: return "bg-danger"; + case 4: return "bg-warning text-dark"; + case 5: return "bg-info text-dark"; + case 6: return "bg-light text-dark"; + case 7: return "bg-dark"; + } - return "bg-primary"; - }}; + return "bg-primary"; + } }; - const std::string styleClass {getStyleClass(cluster)}; - auto res {std::make_unique(std::string {} + (canDelete ? " " : "") + Wt::WString::fromUTF8(cluster->getName()), Wt::TextFormat::UnsafeXHTML)}; + const std::string styleClass{ getStyleClass(cluster) }; + auto res{ std::make_unique(std::string {} + (canDelete ? " " : "") + Wt::WString::fromUTF8(cluster->getName()), Wt::TextFormat::UnsafeXHTML) }; - res->setStyleClass("Lms-badge-cluster badge me-1 " + styleClass); // HACK - res->setToolTip(cluster->getType()->getName(), Wt::TextFormat::Plain); - res->setInline(true); + res->setStyleClass("Lms-badge-cluster badge me-1 " + styleClass); // HACK + res->setToolTip(cluster->getType()->getName(), Wt::TextFormat::Plain); + res->setInline(true); - return res; - } + return res; + } - std::unique_ptr - createArtistContainer(const std::vector& artistIds) - { - using namespace Database; + std::unique_ptr createArtistAnchorList(const std::vector& artistIds, std::string_view cssAnchorClass) + { + using namespace Database; - std::unique_ptr artistContainer {std::make_unique()}; + std::unique_ptr artistContainer{ std::make_unique() }; - bool firstArtist {true}; + bool firstArtist{ true }; - auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - for (const ArtistId artistId : artistIds) - { - const Artist::pointer artist {Artist::find(LmsApp->getDbSession(), artistId)}; - if (!artist) - continue; + auto transaction{ LmsApp->getDbSession().createSharedTransaction() }; + for (const ArtistId artistId : artistIds) + { + const Artist::pointer artist{ Artist::find(LmsApp->getDbSession(), artistId) }; + if (!artist) + continue; - if (!firstArtist) - artistContainer->addNew(" · "); + if (!firstArtist) + artistContainer->addNew(" · "); - auto anchor {createArtistAnchor(artist)}; - anchor->addStyleClass("link-success text-decoration-none"); // hack - artistContainer->addWidget(std::move(anchor)); - firstArtist = false; - } + auto anchor{ createArtistAnchor(artist) }; + anchor->addStyleClass("text-decoration-none"); // hack + anchor->addStyleClass(std::string{ cssAnchorClass }); + artistContainer->addWidget(std::move(anchor)); + firstArtist = false; + } - return artistContainer; - } + return artistContainer; + } - Wt::WLink - createArtistLink(Database::Artist::pointer artist) - { - if (const auto mbid {artist->getMBID()}) - return Wt::WLink {Wt::LinkType::InternalPath, "/artist/mbid/" + std::string {mbid->getAsString()}}; - else - return Wt::WLink {Wt::LinkType::InternalPath, "/artist/" + artist->getId().toString()}; - } + std::unique_ptr createArtistDisplayNameWithAnchors(std::string_view displayName, const std::vector& artistIds, std::string_view cssAnchorClass) + { + using namespace Database; - std::unique_ptr - createArtistAnchor(Database::Artist::pointer artist, bool setText) - { - auto res = std::make_unique(createArtistLink(artist)); + std::size_t matchCount{}; + std::string_view::size_type currentOffset{}; - if (setText) - { - res->setTextFormat(Wt::TextFormat::Plain); - res->setText(Wt::WString::fromUTF8(artist->getName())); - res->setToolTip(Wt::WString::fromUTF8(artist->getName()), Wt::TextFormat::Plain); - } + auto result{ std::make_unique() }; + auto transaction{ LmsApp->getDbSession().createSharedTransaction() }; - return res; - } + // consider order is guaranteed + we will likely succeed + for (const ArtistId artistId : artistIds) + { + const Artist::pointer artist{ Artist::find(LmsApp->getDbSession(), artistId) }; + if (!artist) + break; - Wt::WLink - createReleaseLink(Database::Release::pointer release) - { - if (const auto mbid {release->getMBID()}) - return Wt::WLink {Wt::LinkType::InternalPath, "/release/mbid/" + std::string {mbid->getAsString()}}; - else - return Wt::WLink {Wt::LinkType::InternalPath, "/release/" + release->getId().toString()}; - } + const auto pos{ displayName.find(artist->getName(), currentOffset) }; + if (pos == std::string_view::npos) + break; - std::unique_ptr - createReleaseAnchor(Database::Release::pointer release, bool setText) - { - auto res = std::make_unique(createReleaseLink(release)); + assert(pos >= currentOffset); + if (pos != currentOffset) + result->addNew(std::string{ displayName.substr(currentOffset, pos - currentOffset) }, Wt::TextFormat::Plain); - if (setText) - { - res->setTextFormat(Wt::TextFormat::Plain); - res->setText(Wt::WString::fromUTF8(release->getName())); - res->setToolTip(Wt::WString::fromUTF8(release->getName()), Wt::TextFormat::Plain); - } + auto anchor{ createArtistAnchor(artist) }; + anchor->addStyleClass("text-decoration-none"); // hack + anchor->addStyleClass(std::string{ cssAnchorClass }); // hack + result->addWidget(std::move(anchor)); + currentOffset = pos + artist->getName().size(); + matchCount += 1; + } - return res; - } + if (matchCount != artistIds.size()) + return createArtistAnchorList(artistIds, cssAnchorClass); - std::unique_ptr - createTrackListAnchor(Database::TrackList::pointer trackList, bool setText) - { - Wt::WLink link {Wt::LinkType::InternalPath, "/tracklist/" + trackList->getId().toString()}; - auto res {std::make_unique(link)}; + return result; + } - if (setText) - { - const Wt::WString name {Wt::WString::fromUTF8(std::string {trackList->getName()})}; - res->setTextFormat(Wt::TextFormat::Plain); - res->setText(name); - res->setToolTip(name, Wt::TextFormat::Plain); - } + std::unique_ptr createArtistsAnchorsForRelease(Database::ObjectPtr release, Database::ArtistId omitIfMatchThisArtist, std::string_view cssAnchorClass) + { + using namespace Database; - return res; - } + Artist::FindParameters params; + params.setRelease(release->getId()); + params.setLinkType(TrackArtistLinkType::ReleaseArtist); - std::unique_ptr - createClustersForTrack(Database::Track::pointer track, Filters& filters) - { - using namespace Database; + if (const auto releaseArtists{ Artist::find(LmsApp->getDbSession(), params) }; !releaseArtists.results.empty()) + { + if (releaseArtists.results.size() == 1 && releaseArtists.results.front() == omitIfMatchThisArtist) + return {}; - std::unique_ptr clusterContainer {std::make_unique()}; + return Utils::createArtistDisplayNameWithAnchors(release->getArtistDisplayName(), releaseArtists.results, cssAnchorClass); + } - const auto clusterTypes {ScanSettings::get(LmsApp->getDbSession())->getClusterTypes()}; - const auto clusterGroups {track->getClusterGroups(clusterTypes, 3)}; + params.setLinkType(TrackArtistLinkType::Artist); + const auto artists{ Artist::find(LmsApp->getDbSession(), params) }; + if (artists.results.size() == 1) + { + if (artists.results.front() == omitIfMatchThisArtist) + return {}; - for (const auto& clusters : clusterGroups) - { - for (const Cluster::pointer& cluster : clusters) - { - const ClusterId clusterId {cluster->getId()}; - Wt::WInteractWidget* entry {clusterContainer->addWidget(Utils::createCluster(clusterId))}; - entry->clicked().connect([&filters, clusterId] - { - filters.add(clusterId); - }); - } - } + return Utils::createArtistAnchorList({ artists.results.front() }, cssAnchorClass); + } - return clusterContainer; - } + if (artists.results.size() > 1) + { + auto res{ std::make_unique() }; + res->addNew(Wt::WString::tr("Lms.Explore.various-artists")); + return res; + } + + return {}; + } + + Wt::WLink createArtistLink(Database::Artist::pointer artist) + { + if (const auto mbid{ artist->getMBID() }) + return Wt::WLink{ Wt::LinkType::InternalPath, "/artist/mbid/" + std::string {mbid->getAsString()} }; + else + return Wt::WLink{ Wt::LinkType::InternalPath, "/artist/" + artist->getId().toString() }; + } + + std::unique_ptr createArtistAnchor(Database::Artist::pointer artist, bool setText) + { + auto res = std::make_unique(createArtistLink(artist)); + + if (setText) + { + res->setTextFormat(Wt::TextFormat::Plain); + res->setText(Wt::WString::fromUTF8(artist->getName())); + res->setToolTip(Wt::WString::fromUTF8(artist->getName()), Wt::TextFormat::Plain); + } + + return res; + } + + Wt::WLink createReleaseLink(Database::Release::pointer release) + { + if (const auto mbid{ release->getMBID() }) + return Wt::WLink{ Wt::LinkType::InternalPath, "/release/mbid/" + std::string {mbid->getAsString()} }; + else + return Wt::WLink{ Wt::LinkType::InternalPath, "/release/" + release->getId().toString() }; + } + + std::unique_ptr createReleaseAnchor(Database::Release::pointer release, bool setText) + { + auto res = std::make_unique(createReleaseLink(release)); + + if (setText) + { + res->setTextFormat(Wt::TextFormat::Plain); + res->setText(Wt::WString::fromUTF8(release->getName())); + res->setToolTip(Wt::WString::fromUTF8(release->getName()), Wt::TextFormat::Plain); + } + + return res; + } + + std::unique_ptr createTrackListAnchor(Database::TrackList::pointer trackList, bool setText) + { + Wt::WLink link{ Wt::LinkType::InternalPath, "/tracklist/" + trackList->getId().toString() }; + auto res{ std::make_unique(link) }; + + if (setText) + { + const Wt::WString name{ Wt::WString::fromUTF8(std::string {trackList->getName()}) }; + res->setTextFormat(Wt::TextFormat::Plain); + res->setText(name); + res->setToolTip(name, Wt::TextFormat::Plain); + } + + return res; + } + + std::unique_ptr createClustersForTrack(Database::Track::pointer track, Filters& filters) + { + using namespace Database; + + std::unique_ptr clusterContainer{ std::make_unique() }; + + const auto clusterTypes{ ScanSettings::get(LmsApp->getDbSession())->getClusterTypes() }; + const auto clusterGroups{ track->getClusterGroups(clusterTypes, 3) }; + + for (const auto& clusters : clusterGroups) + { + for (const Cluster::pointer& cluster : clusters) + { + const ClusterId clusterId{ cluster->getId() }; + Wt::WInteractWidget* entry{ clusterContainer->addWidget(Utils::createCluster(clusterId)) }; + entry->clicked().connect([&filters, clusterId] + { + filters.add(clusterId); + }); + } + } + + return clusterContainer; + } } diff --git a/src/lms/ui/Utils.hpp b/src/lms/ui/Utils.hpp index 70ad067c..3a14af09 100644 --- a/src/lms/ui/Utils.hpp +++ b/src/lms/ui/Utils.hpp @@ -21,6 +21,7 @@ #include #include +#include #include #include @@ -57,7 +58,9 @@ namespace UserInterface::Utils std::unique_ptr createCluster(Database::ClusterId clusterId, bool canDelete = false); - std::unique_ptr createArtistContainer(const std::vector& artists); + std::unique_ptr createArtistAnchorList(const std::vector& artistIds, std::string_view cssAnchorClass = "link-success"); + std::unique_ptr createArtistDisplayNameWithAnchors(std::string_view displayName, const std::vector& artistIds, std::string_view cssAnchorClass = "link-success"); + std::unique_ptr createArtistsAnchorsForRelease(Database::ObjectPtr release, Database::ArtistId omitIfMatchThisArtist = {}, std::string_view cssAnchorClass = "link-success"); Wt::WLink createArtistLink(Database::ObjectPtr artist); std::unique_ptr createArtistAnchor(Database::ObjectPtr artist, bool setText = true); diff --git a/src/lms/ui/explore/ReleaseHelpers.cpp b/src/lms/ui/explore/ReleaseHelpers.cpp index 57a91efe..4d2e6499 100644 --- a/src/lms/ui/explore/ReleaseHelpers.cpp +++ b/src/lms/ui/explore/ReleaseHelpers.cpp @@ -32,122 +32,110 @@ using namespace Database; namespace UserInterface::ReleaseListHelpers { - static - std::unique_ptr - createEntryInternal(const Release::pointer& release, const std::string& templateKey, const Artist::pointer& artist, const bool showYear) - { - auto entry {std::make_unique(Wt::WString::tr(templateKey))}; + namespace + { + std::unique_ptr createEntryInternal(const Release::pointer& release, const std::string& templateKey, const Artist::pointer& artist, const bool showYear) + { + auto entry{ std::make_unique(Wt::WString::tr(templateKey)) }; - entry->bindWidget("release-name", Utils::createReleaseAnchor(release)); - entry->addFunction("tr", &Wt::WTemplate::Functions::tr); + entry->bindWidget("release-name", Utils::createReleaseAnchor(release)); + entry->addFunction("tr", &Wt::WTemplate::Functions::tr); - { - Wt::WAnchor* anchor {entry->bindWidget("cover", Utils::createReleaseAnchor(release, false))}; - auto cover {Utils::createCover(release->getId(), CoverResource::Size::Large)}; - cover->addStyleClass("Lms-cover-release Lms-cover-anchor"); - anchor->setImage(std::move(cover)); - } + { + Wt::WAnchor* anchor{ entry->bindWidget("cover", Utils::createReleaseAnchor(release, false)) }; + auto cover{ Utils::createCover(release->getId(), CoverResource::Size::Large) }; + cover->addStyleClass("Lms-cover-release Lms-cover-anchor"); + anchor->setImage(std::move(cover)); + } - auto artists {release->getReleaseArtists()}; - if (artists.empty()) - artists = release->getArtists(); + auto artistAnchors{ Utils::createArtistsAnchorsForRelease(release, artist ? artist->getId() : ArtistId{}, "link-secondary") }; + if (artistAnchors) + { + entry->setCondition("if-has-artist", true); + entry->bindWidget("artist-name", std::move(artistAnchors)); + } - const bool isSameArtist {(std::find(std::cbegin(artists), std::cend(artists), artist) != artists.end())}; + if (showYear) + { + Wt::WString year{ ReleaseHelpers::buildReleaseYearString(release->getReleaseDate(), release->getOriginalReleaseDate()) }; + if (!year.empty()) + { + entry->setCondition("if-has-year", true); + entry->bindString("year", year, Wt::TextFormat::Plain); + } + } - if (artists.size() > 1) - { - entry->setCondition("if-has-various-artists", true); - } - else if (artists.size() == 1 && !isSameArtist) - { - entry->setCondition("if-has-artist", true); - entry->bindWidget("artist-name", Utils::createArtistAnchor(artists.front())); - } + return entry; + } + } - if (showYear) - { - Wt::WString year {ReleaseHelpers::buildReleaseYearString(release->getReleaseDate(), release->getOriginalReleaseDate())}; - if (!year.empty()) - { - entry->setCondition("if-has-year", true); - entry->bindString("year", year, Wt::TextFormat::Plain); - } - } + std::unique_ptr createEntry(const Release::pointer& release, const Artist::pointer& artist, bool showYear) + { + return createEntryInternal(release, "Lms.Explore.Releases.template.entry-grid", artist, showYear); + } - return entry; - } + std::unique_ptr createEntry(const Release::pointer& release) + { + return createEntry(release, Artist::pointer{}, false /*year*/); + } - std::unique_ptr - createEntry(const Release::pointer& release, const Artist::pointer& artist, bool showYear) - { - return createEntryInternal(release, "Lms.Explore.Releases.template.entry-grid", artist, showYear); - } - - std::unique_ptr - createEntry(const Release::pointer& release) - { - return createEntry(release, Artist::pointer {}, false /*year*/); - } - - std::unique_ptr - createEntryForArtist(const Database::Release::pointer& release, const Database::Artist::pointer& artist) - { - return createEntry(release, artist, true); - } + std::unique_ptr createEntryForArtist(const Database::Release::pointer& release, const Database::Artist::pointer& artist) + { + return createEntry(release, artist, true); + } } // namespace UserInterface namespace UserInterface::ReleaseHelpers { - Wt::WString - buildReleaseTypeString(ReleaseTypePrimary primaryType, EnumSet secondaryTypes) - { - Wt::WString res; + Wt::WString buildReleaseTypeString(ReleaseTypePrimary primaryType, EnumSet secondaryTypes) + { + Wt::WString res; - switch (primaryType) - { - case ReleaseTypePrimary::Album: res = Wt::WString::tr("Lms.Explore.Release.type-primary-album"); break; - case ReleaseTypePrimary::Broadcast: res = Wt::WString::tr("Lms.Explore.Release.type-primary-broadcast"); break; - case ReleaseTypePrimary::EP: res = Wt::WString::tr("Lms.Explore.Release.type-primary-ep"); break; - case ReleaseTypePrimary::Single: res = Wt::WString::tr("Lms.Explore.Release.type-primary-single"); break; - case ReleaseTypePrimary::Other: res = Wt::WString::tr("Lms.Explore.Release.type-primary-other"); break; - } + switch (primaryType) + { + case ReleaseTypePrimary::Album: res = Wt::WString::tr("Lms.Explore.Release.type-primary-album"); break; + case ReleaseTypePrimary::Broadcast: res = Wt::WString::tr("Lms.Explore.Release.type-primary-broadcast"); break; + case ReleaseTypePrimary::EP: res = Wt::WString::tr("Lms.Explore.Release.type-primary-ep"); break; + case ReleaseTypePrimary::Single: res = Wt::WString::tr("Lms.Explore.Release.type-primary-single"); break; + case ReleaseTypePrimary::Other: res = Wt::WString::tr("Lms.Explore.Release.type-primary-other"); break; + } - for (ReleaseTypeSecondary secondaryType : secondaryTypes) - { - res += Wt::WString {" · "}; + for (ReleaseTypeSecondary secondaryType : secondaryTypes) + { + res += Wt::WString{ " · " }; - switch (secondaryType) - { - case ReleaseTypeSecondary::Compilation: res += Wt::WString::tr("Lms.Explore.Release.type-secondary-compilation"); break; - case ReleaseTypeSecondary::Spokenword: res += Wt::WString::tr("Lms.Explore.Release.type-secondary-spokenword"); break; - case ReleaseTypeSecondary::Soundtrack: res += Wt::WString::tr("Lms.Explore.Release.type-secondary-soundtrack"); break; - case ReleaseTypeSecondary::Interview: res += Wt::WString::tr("Lms.Explore.Release.type-secondary-interview"); break; - case ReleaseTypeSecondary::Audiobook: res += Wt::WString::tr("Lms.Explore.Release.type-secondary-audiobook"); break; - case ReleaseTypeSecondary::AudioDrama: res += Wt::WString::tr("Lms.Explore.Release.type-secondary-audiodrama"); break; - case ReleaseTypeSecondary::Live: res += Wt::WString::tr("Lms.Explore.Release.type-secondary-live"); break; - case ReleaseTypeSecondary::Remix: res += Wt::WString::tr("Lms.Explore.Release.type-secondary-remix"); break; - case ReleaseTypeSecondary::DJMix: res += Wt::WString::tr("Lms.Explore.Release.type-secondary-djmix"); break; - case ReleaseTypeSecondary::Mixtape_Street: res += Wt::WString::tr("Lms.Explore.Release.type-secondary-mixtape-street"); break; - case ReleaseTypeSecondary::Demo: res += Wt::WString::tr("Lms.Explore.Release.type-secondary-demo"); break; - } - } + switch (secondaryType) + { + case ReleaseTypeSecondary::Compilation: res += Wt::WString::tr("Lms.Explore.Release.type-secondary-compilation"); break; + case ReleaseTypeSecondary::Spokenword: res += Wt::WString::tr("Lms.Explore.Release.type-secondary-spokenword"); break; + case ReleaseTypeSecondary::Soundtrack: res += Wt::WString::tr("Lms.Explore.Release.type-secondary-soundtrack"); break; + case ReleaseTypeSecondary::Interview: res += Wt::WString::tr("Lms.Explore.Release.type-secondary-interview"); break; + case ReleaseTypeSecondary::Audiobook: res += Wt::WString::tr("Lms.Explore.Release.type-secondary-audiobook"); break; + case ReleaseTypeSecondary::AudioDrama: res += Wt::WString::tr("Lms.Explore.Release.type-secondary-audiodrama"); break; + case ReleaseTypeSecondary::Live: res += Wt::WString::tr("Lms.Explore.Release.type-secondary-live"); break; + case ReleaseTypeSecondary::Remix: res += Wt::WString::tr("Lms.Explore.Release.type-secondary-remix"); break; + case ReleaseTypeSecondary::DJMix: res += Wt::WString::tr("Lms.Explore.Release.type-secondary-djmix"); break; + case ReleaseTypeSecondary::Mixtape_Street: res += Wt::WString::tr("Lms.Explore.Release.type-secondary-mixtape-street"); break; + case ReleaseTypeSecondary::Demo: res += Wt::WString::tr("Lms.Explore.Release.type-secondary-demo"); break; + } + } - return res; - } + return res; + } - Wt::WString buildReleaseYearString(const Wt::WDate& releaseDate, const Wt::WDate& originalReleaseDate) - { - Wt::WString res; + Wt::WString buildReleaseYearString(const Wt::WDate& releaseDate, const Wt::WDate& originalReleaseDate) + { + Wt::WString res; - // Year can be here, but originalYear can't be here without year (enforced by scanner) - if (!releaseDate.isValid()) - return res; + // Year can be here, but originalYear can't be here without year (enforced by scanner) + if (!releaseDate.isValid()) + return res; - if (originalReleaseDate.isValid() && originalReleaseDate != releaseDate) - res = std::to_string(originalReleaseDate.year()) + " (" + std::to_string(releaseDate.year()) + ")"; - else - res = std::to_string(releaseDate.year()); + if (originalReleaseDate.isValid() && originalReleaseDate != releaseDate) + res = std::to_string(originalReleaseDate.year()) + " (" + std::to_string(releaseDate.year()) + ")"; + else + res = std::to_string(releaseDate.year()); - return res; - } + return res; + } } // namespace UserInterface::ReleaseHelpers} diff --git a/src/lms/ui/explore/ReleaseHelpers.hpp b/src/lms/ui/explore/ReleaseHelpers.hpp index 3bdef46b..2f67c79a 100644 --- a/src/lms/ui/explore/ReleaseHelpers.hpp +++ b/src/lms/ui/explore/ReleaseHelpers.hpp @@ -31,18 +31,18 @@ namespace Database { - class Artist; - class Release; + class Artist; + class Release; } namespace UserInterface::ReleaseListHelpers { - std::unique_ptr createEntry(const Database::ObjectPtr& release); - std::unique_ptr createEntryForArtist(const Database::ObjectPtr& release, const Database::ObjectPtr& artist); + std::unique_ptr createEntry(const Database::ObjectPtr& release); + std::unique_ptr createEntryForArtist(const Database::ObjectPtr& release, const Database::ObjectPtr& artist); } // namespace UserInterface namespace UserInterface::ReleaseHelpers { - Wt::WString buildReleaseTypeString(Database::ReleaseTypePrimary primaryType, EnumSet secondaryTypes); - Wt::WString buildReleaseYearString(const Wt::WDate& releaseDate, const Wt::WDate& originalReleaseDate); + Wt::WString buildReleaseTypeString(Database::ReleaseTypePrimary primaryType, EnumSet secondaryTypes); + Wt::WString buildReleaseYearString(const Wt::WDate& releaseDate, const Wt::WDate& originalReleaseDate); } diff --git a/src/lms/ui/explore/ReleaseView.cpp b/src/lms/ui/explore/ReleaseView.cpp index de03fe76..4b5770ee 100644 --- a/src/lms/ui/explore/ReleaseView.cpp +++ b/src/lms/ui/explore/ReleaseView.cpp @@ -128,7 +128,7 @@ showReleaseInfoModal(Database::ReleaseId releaseId) for (const auto& [role, artistIds] : artistMap) { - std::unique_ptr artistContainer {Utils::createArtistContainer(std::vector (std::cbegin(artistIds), std::cend(artistIds)))}; + std::unique_ptr artistContainer {Utils::createArtistAnchorList(std::vector (std::cbegin(artistIds), std::cend(artistIds)))}; auto artistsEntry {std::make_unique