From 019fff137d27d4c197f7275dfaa383cf89df3a8e Mon Sep 17 00:00:00 2001 From: deluan Date: Mon, 23 Feb 2026 23:13:30 -0500 Subject: [PATCH] refactor: update status display logic and improve Spotify URL handling --- README.md | 1 - main.go | 9 +++++---- rpc.go | 12 ++++++++---- spotify.go | 30 +++++++++++++----------------- spotify_test.go | 19 +++++++------------ 5 files changed, 33 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 1f8d9c8..738f2de 100644 --- a/README.md +++ b/README.md @@ -208,7 +208,6 @@ Resolved URLs are cached (30 days for direct track links, 4 hours for search fal | [rpc.go](rpc.go) | Discord gateway communication, WebSocket handling, activity management | | [coverart.go](coverart.go) | Artwork URL handling and optional uguu.se image hosting | | [manifest.json](manifest.json) | Plugin metadata and permission declarations | -| [navidrome.webp](navidrome.webp) | Navidrome logo used as the album art overlay badge | | [Makefile](Makefile) | Build automation | ## Building diff --git a/main.go b/main.go index 3e0f69d..9aebd0b 100644 --- a/main.go +++ b/main.go @@ -152,17 +152,18 @@ func (p *discordPlugin) NowPlaying(input scrobbler.NowPlayingRequest) error { // Resolve the activity name based on configuration activityName := "Navidrome" - statusDisplayType := 0 + statusDisplayType := statusDisplayListening activityNameOption, _ := pdk.GetConfig(activityNameKey) switch activityNameOption { case activityNameTrack: activityName = input.Track.Title + statusDisplayType = statusDisplayDefault case activityNameAlbum: activityName = input.Track.Album + statusDisplayType = statusDisplayDefault case activityNameArtist: activityName = input.Track.Artist - default: - statusDisplayType = 2 + statusDisplayType = statusDisplayDefault } // Resolve Spotify URLs if enabled @@ -170,7 +171,7 @@ func (p *discordPlugin) NowPlaying(input scrobbler.NowPlayingRequest) error { spotifyLinksOption, _ := pdk.GetConfig(spotifyLinksKey) if spotifyLinksOption == "true" { spotifyURL = resolveSpotifyURL(input.Track) - artistSearchURL = spotifySearch(input.Track.Artist) + artistSearchURL = spotifySearchURL(input.Track.Artist) } // Send activity update diff --git a/rpc.go b/rpc.go index 2a18690..d710c57 100644 --- a/rpc.go +++ b/rpc.go @@ -6,8 +6,6 @@ package main import ( - "crypto/md5" - "encoding/hex" "encoding/json" "fmt" "strings" @@ -24,6 +22,13 @@ const ( presenceOpCode = 3 // Presence update operation code ) +// Discord status_display_type values control how the activity name is shown. +// Type 0 renders the name as-is; type 2 renders the name with a "Listening to" prefix. +const ( + statusDisplayDefault = 0 // Show activity name as-is (e.g. track title) + statusDisplayListening = 2 // Show "Listening to " (used for "Navidrome") +) + const heartbeatInterval = 41 // Heartbeat interval in seconds // Image cache TTL constants @@ -132,8 +137,7 @@ func (r *discordRPC) processImage(imageURL, clientID, token string, ttl int64) ( } // Check cache first - h := md5.Sum([]byte(imageURL)) - cacheKey := "discord.image." + hex.EncodeToString(h[:8]) + cacheKey := "discord.image." + hashKey(imageURL) cachedValue, exists, err := host.CacheGetString(cacheKey) if err == nil && exists { pdk.Log(pdk.LogDebug, fmt.Sprintf("Cache hit for image URL: %s", imageURL)) diff --git a/spotify.go b/spotify.go index 8f618bd..20cec67 100644 --- a/spotify.go +++ b/spotify.go @@ -14,6 +14,12 @@ import ( "github.com/navidrome/navidrome/plugins/pdk/go/scrobbler" ) +// hashKey returns a hex-encoded MD5 hash of s, for use as a cache key suffix. +func hashKey(s string) string { + h := md5.Sum([]byte(s)) + return hex.EncodeToString(h[:]) +} + const ( spotifyCacheTTLHit int64 = 30 * 24 * 60 * 60 // 30 days for resolved track IDs spotifyCacheTTLMiss int64 = 4 * 60 * 60 // 4 hours for misses (retry later) @@ -25,29 +31,19 @@ type listenBrainzResult struct { SpotifyTrackIDs []string `json:"spotify_track_ids"` } -// buildSpotifySearchURL constructs a Spotify search URL using artist and title. -// Used as the ultimate fallback when ListenBrainz resolution fails. -func buildSpotifySearchURL(artist, title string) string { - query := strings.TrimSpace(strings.Join([]string{artist, title}, " ")) +// spotifySearchURL builds a Spotify search URL from one or more terms. +// Empty terms are ignored. Returns "" if all terms are empty. +func spotifySearchURL(terms ...string) string { + query := strings.TrimSpace(strings.Join(terms, " ")) if query == "" { - return "https://open.spotify.com/search/" - } - return fmt.Sprintf("https://open.spotify.com/search/%s", url.PathEscape(query)) -} - -// spotifySearch builds a Spotify search URL for a single search term. -func spotifySearch(term string) string { - term = strings.TrimSpace(term) - if term == "" { return "" } - return "https://open.spotify.com/search/" + url.PathEscape(term) + return "https://open.spotify.com/search/" + url.PathEscape(query) } // spotifyCacheKey returns a deterministic cache key for a track's Spotify URL. func spotifyCacheKey(artist, title, album string) string { - h := md5.Sum([]byte(strings.ToLower(artist) + "\x00" + strings.ToLower(title) + "\x00" + strings.ToLower(album))) - return "spotify.url." + hex.EncodeToString(h[:8]) + return "spotify.url." + hashKey(strings.ToLower(artist)+"\x00"+strings.ToLower(title)+"\x00"+strings.ToLower(album)) } // trySpotifyFromMBID calls the ListenBrainz spotify-id-from-mbid endpoint. @@ -158,7 +154,7 @@ func resolveSpotifyURL(track scrobbler.TrackInfo) string { } // 3. Fallback to search URL - searchURL := buildSpotifySearchURL(track.Artist, track.Title) + searchURL := spotifySearchURL(track.Artist, track.Title) _ = host.CacheSetString(cacheKey, searchURL, spotifyCacheTTLMiss) pdk.Log(pdk.LogInfo, fmt.Sprintf("Spotify resolution missed, falling back to search URL for %q - %q: %s", primary, track.Title, searchURL)) return searchURL diff --git a/spotify_test.go b/spotify_test.go index d825dc3..99719ec 100644 --- a/spotify_test.go +++ b/spotify_test.go @@ -31,20 +31,15 @@ var _ = Describe("Spotify", func() { ) }) - Describe("buildSpotifySearchURL", func() { + Describe("spotifySearchURL", func() { DescribeTable("constructs Spotify search URL", - func(artist, title, expectedSubstring string) { - url := buildSpotifySearchURL(artist, title) - Expect(url).To(HavePrefix("https://open.spotify.com/search/")) - if expectedSubstring != "" { - Expect(url).To(ContainSubstring(expectedSubstring)) - } + func(expectedURL string, terms ...string) { + Expect(spotifySearchURL(terms...)).To(Equal(expectedURL)) }, - Entry("artist and title", "Rick Astley", "Never Gonna Give You Up", "Rick%20Astley"), - Entry("another track", "Radiohead", "Karma Police", "Radiohead"), - Entry("empty artist", "", "Only Title", "Only%20Title"), - Entry("empty title", "Solo Artist", "", "Solo%20Artist"), - Entry("both empty", "", "", ""), + Entry("artist and title", "https://open.spotify.com/search/Rick%20Astley%20Never%20Gonna%20Give%20You%20Up", "Rick Astley", "Never Gonna Give You Up"), + Entry("single term", "https://open.spotify.com/search/Radiohead", "Radiohead"), + Entry("empty terms", "", "", ""), + Entry("one empty term", "https://open.spotify.com/search/Solo%20Artist", "Solo Artist", ""), ) })