From a5f94b608b186683deb198b398601520ad572b00 Mon Sep 17 00:00:00 2001 From: Piper McCorkle Date: Fri, 27 Jun 2025 19:31:49 -0500 Subject: [PATCH] ListenBrainzScrobbler: Report more info to ListenBrainz Report music service, URL, and Spotify ID to ListenBrainz. ListenBrainz accepts the music service in listen reports, in both a canonical domain format and a human-readable display name format. This commit makes Strawberry report both, for maximum flexibility. I've also set it up to report a shareable track URL for supported streaming services. I am already using this data in my homepage's "Now Playing" widget. Fixes #1768 --- src/core/song.cpp | 33 +++++++++++++++++++++++++ src/core/song.h | 4 +++ src/scrobbler/listenbrainzscrobbler.cpp | 15 +++++++++++ src/scrobbler/scrobblemetadata.cpp | 4 +++ src/scrobbler/scrobblemetadata.h | 4 +++ src/scrobbler/scrobblercache.cpp | 16 ++++++++++++ 6 files changed, 76 insertions(+) diff --git a/src/core/song.cpp b/src/core/song.cpp index cc556f2bf..d1ce021cf 100644 --- a/src/core/song.cpp +++ b/src/core/song.cpp @@ -1182,6 +1182,22 @@ QIcon Song::IconForSource(const Source source) { } +// Convert a source to a music service domain name, for ListenBrainz. +// See the "Music service names" note on https://listenbrainz.readthedocs.io/en/latest/users/json.html. + +QString Song::DomainForSource(const Source source) { + + switch (source) { + case Song::Source::Tidal: return u"tidal.com"_s; + case Song::Source::Qobuz: return u"qobuz.com"_s; + case Song::Source::SomaFM: return u"somafm.com"_s; + case Song::Source::RadioParadise: return u"radioparadise.com"_s; + case Song::Source::Spotify: return u"spotify.com"_s; + default: return QString(); + } + +} + QString Song::TextForFiletype(const FileType filetype) { switch (filetype) { @@ -1282,6 +1298,23 @@ QIcon Song::IconForFiletype(const FileType filetype) { } +// Get a URL usable for sharing this song with another user. +// This is only applicable when streaming from a streaming service, since we can't link to local content. +// Returns a web URL which points to the current streaming track or live stream, or an empty string if that is not applicable. + +QString Song::ShareURL() const { + + switch (source()) { + case Song::Source::Stream: + case Song::Source::SomaFM: return url().toString(); + case Song::Source::Tidal: return "https://tidal.com/track/%1"_L1.arg(song_id()); + case Song::Source::Qobuz: return "https://open.qobuz.com/track/%1"_L1.arg(song_id()); + case Song::Source::Spotify: return "https://open.spotify.com/track/%1"_L1.arg(song_id()); + default: return QString(); + } + +} + bool Song::IsFileLossless() const { switch (filetype()) { diff --git a/src/core/song.h b/src/core/song.h index 7b4317217..0ebdb99e8 100644 --- a/src/core/song.h +++ b/src/core/song.h @@ -453,6 +453,7 @@ class Song { static QString DescriptionForSource(const Source source); static Source SourceFromText(const QString &source); static QIcon IconForSource(const Source source); + static QString DomainForSource(const Source source); static QString TextForFiletype(const FileType filetype); static QString ExtensionForFiletype(const FileType filetype); static QIcon IconForFiletype(const FileType filetype); @@ -460,9 +461,12 @@ class Song { QString TextForSource() const { return TextForSource(source()); } QString DescriptionForSource() const { return DescriptionForSource(source()); } QIcon IconForSource() const { return IconForSource(source()); } + QString DomainForSource() const { return DomainForSource(source()); } QString TextForFiletype() const { return TextForFiletype(filetype()); } QIcon IconForFiletype() const { return IconForFiletype(filetype()); } + QString ShareURL() const; + bool IsFileLossless() const; static FileType FiletypeByMimetype(const QString &mimetype); static FileType FiletypeByDescription(const QString &text); diff --git a/src/scrobbler/listenbrainzscrobbler.cpp b/src/scrobbler/listenbrainzscrobbler.cpp index af1ddb7a5..64946f3ab 100644 --- a/src/scrobbler/listenbrainzscrobbler.cpp +++ b/src/scrobbler/listenbrainzscrobbler.cpp @@ -235,6 +235,21 @@ QJsonObject ListenBrainzScrobbler::JsonTrackMetadata(const ScrobbleMetadata &met object_additional_info.insert("work_mbids"_L1, array_musicbrainz_work_id); } + if (!metadata.music_service.isEmpty()) { + object_additional_info.insert("music_service"_L1, metadata.music_service); + } + if (!metadata.music_service_name.isEmpty()) { + object_additional_info.insert("music_service_name"_L1, metadata.music_service_name); + } + + if (!metadata.share_url.isEmpty()) { + object_additional_info.insert("origin_url"_L1, metadata.share_url); + } + + if (!metadata.spotify_id.isEmpty()) { + object_additional_info.insert("spotify_id"_L1, metadata.spotify_id); + } + object_track_metadata.insert("additional_info"_L1, object_additional_info); return object_track_metadata; diff --git a/src/scrobbler/scrobblemetadata.cpp b/src/scrobbler/scrobblemetadata.cpp index 9b146e113..98c5f05ab 100644 --- a/src/scrobbler/scrobblemetadata.cpp +++ b/src/scrobbler/scrobblemetadata.cpp @@ -38,4 +38,8 @@ ScrobbleMetadata::ScrobbleMetadata(const Song &song) musicbrainz_disc_id(song.musicbrainz_disc_id()), musicbrainz_release_group_id(song.musicbrainz_release_group_id()), musicbrainz_work_id(song.musicbrainz_work_id()), + music_service(song.is_stream() ? song.DomainForSource() : QString()), + music_service_name(song.is_stream() ? song.DescriptionForSource() : QString()), + share_url(song.ShareURL()), + spotify_id(song.source() == Song::Source::Spotify ? song.song_id() : QString()), length_nanosec(song.length_nanosec()) {} diff --git a/src/scrobbler/scrobblemetadata.h b/src/scrobbler/scrobblemetadata.h index 89c6541af..9a72be3e3 100644 --- a/src/scrobbler/scrobblemetadata.h +++ b/src/scrobbler/scrobblemetadata.h @@ -45,6 +45,10 @@ class ScrobbleMetadata { QString musicbrainz_disc_id; QString musicbrainz_release_group_id; // release_group_mbid QString musicbrainz_work_id; // work_mbids + QString music_service; + QString music_service_name; + QString share_url; + QString spotify_id; qint64 length_nanosec; QString effective_albumartist() const { return albumartist.isEmpty() ? artist : albumartist; } diff --git a/src/scrobbler/scrobblercache.cpp b/src/scrobbler/scrobblercache.cpp index 4b2993a46..f4cce0759 100644 --- a/src/scrobbler/scrobblercache.cpp +++ b/src/scrobbler/scrobblercache.cpp @@ -180,6 +180,18 @@ void ScrobblerCache::ReadCache() { if (json_obj_track.contains("musicbrainz_work_id"_L1)) { metadata.musicbrainz_work_id = json_obj_track["musicbrainz_work_id"_L1].toString(); } + if (json_obj_track.contains("music_service"_L1)) { + metadata.music_service = json_obj_track["music_service"_L1].toString(); + } + if (json_obj_track.contains("music_service_name"_L1)) { + metadata.music_service_name = json_obj_track["music_service_name"_L1].toString(); + } + if (json_obj_track.contains("share_url"_L1)) { + metadata.share_url = json_obj_track["share_url"_L1].toString(); + } + if (json_obj_track.contains("spotify_id"_L1)) { + metadata.spotify_id = json_obj_track["spotify_id"_L1].toString(); + } ScrobblerCacheItemPtr cache_item = make_shared(metadata, timestamp); scrobbler_cache_ << cache_item; @@ -220,6 +232,10 @@ void ScrobblerCache::WriteCache() { object.insert("musicbrainz_disc_id"_L1, QJsonValue::fromVariant(cache_item->metadata.musicbrainz_disc_id)); object.insert("musicbrainz_release_group_id"_L1, QJsonValue::fromVariant(cache_item->metadata.musicbrainz_release_group_id)); object.insert("musicbrainz_work_id"_L1, QJsonValue::fromVariant(cache_item->metadata.musicbrainz_work_id)); + object.insert("music_service"_L1, QJsonValue::fromVariant(cache_item->metadata.music_service)); + object.insert("music_service_name"_L1, QJsonValue::fromVariant(cache_item->metadata.music_service_name)); + object.insert("share_url"_L1, QJsonValue::fromVariant(cache_item->metadata.share_url)); + object.insert("spotify_id"_L1, QJsonValue::fromVariant(cache_item->metadata.spotify_id)); object.insert("length_nanosec"_L1, QJsonValue::fromVariant(cache_item->metadata.length_nanosec)); array.append(QJsonValue::fromVariant(object)); }