refactor: update status display logic and improve Spotify URL handling
This commit is contained in:
@@ -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 |
|
| [rpc.go](rpc.go) | Discord gateway communication, WebSocket handling, activity management |
|
||||||
| [coverart.go](coverart.go) | Artwork URL handling and optional uguu.se image hosting |
|
| [coverart.go](coverart.go) | Artwork URL handling and optional uguu.se image hosting |
|
||||||
| [manifest.json](manifest.json) | Plugin metadata and permission declarations |
|
| [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 |
|
| [Makefile](Makefile) | Build automation |
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|||||||
@@ -152,17 +152,18 @@ func (p *discordPlugin) NowPlaying(input scrobbler.NowPlayingRequest) error {
|
|||||||
|
|
||||||
// Resolve the activity name based on configuration
|
// Resolve the activity name based on configuration
|
||||||
activityName := "Navidrome"
|
activityName := "Navidrome"
|
||||||
statusDisplayType := 0
|
statusDisplayType := statusDisplayListening
|
||||||
activityNameOption, _ := pdk.GetConfig(activityNameKey)
|
activityNameOption, _ := pdk.GetConfig(activityNameKey)
|
||||||
switch activityNameOption {
|
switch activityNameOption {
|
||||||
case activityNameTrack:
|
case activityNameTrack:
|
||||||
activityName = input.Track.Title
|
activityName = input.Track.Title
|
||||||
|
statusDisplayType = statusDisplayDefault
|
||||||
case activityNameAlbum:
|
case activityNameAlbum:
|
||||||
activityName = input.Track.Album
|
activityName = input.Track.Album
|
||||||
|
statusDisplayType = statusDisplayDefault
|
||||||
case activityNameArtist:
|
case activityNameArtist:
|
||||||
activityName = input.Track.Artist
|
activityName = input.Track.Artist
|
||||||
default:
|
statusDisplayType = statusDisplayDefault
|
||||||
statusDisplayType = 2
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve Spotify URLs if enabled
|
// Resolve Spotify URLs if enabled
|
||||||
@@ -170,7 +171,7 @@ func (p *discordPlugin) NowPlaying(input scrobbler.NowPlayingRequest) error {
|
|||||||
spotifyLinksOption, _ := pdk.GetConfig(spotifyLinksKey)
|
spotifyLinksOption, _ := pdk.GetConfig(spotifyLinksKey)
|
||||||
if spotifyLinksOption == "true" {
|
if spotifyLinksOption == "true" {
|
||||||
spotifyURL = resolveSpotifyURL(input.Track)
|
spotifyURL = resolveSpotifyURL(input.Track)
|
||||||
artistSearchURL = spotifySearch(input.Track.Artist)
|
artistSearchURL = spotifySearchURL(input.Track.Artist)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send activity update
|
// Send activity update
|
||||||
|
|||||||
@@ -6,8 +6,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/md5"
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -24,6 +22,13 @@ const (
|
|||||||
presenceOpCode = 3 // Presence update operation code
|
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 <name>" (used for "Navidrome")
|
||||||
|
)
|
||||||
|
|
||||||
const heartbeatInterval = 41 // Heartbeat interval in seconds
|
const heartbeatInterval = 41 // Heartbeat interval in seconds
|
||||||
|
|
||||||
// Image cache TTL constants
|
// Image cache TTL constants
|
||||||
@@ -132,8 +137,7 @@ func (r *discordRPC) processImage(imageURL, clientID, token string, ttl int64) (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check cache first
|
// Check cache first
|
||||||
h := md5.Sum([]byte(imageURL))
|
cacheKey := "discord.image." + hashKey(imageURL)
|
||||||
cacheKey := "discord.image." + hex.EncodeToString(h[:8])
|
|
||||||
cachedValue, exists, err := host.CacheGetString(cacheKey)
|
cachedValue, exists, err := host.CacheGetString(cacheKey)
|
||||||
if err == nil && exists {
|
if err == nil && exists {
|
||||||
pdk.Log(pdk.LogDebug, fmt.Sprintf("Cache hit for image URL: %s", imageURL))
|
pdk.Log(pdk.LogDebug, fmt.Sprintf("Cache hit for image URL: %s", imageURL))
|
||||||
|
|||||||
+13
-17
@@ -14,6 +14,12 @@ import (
|
|||||||
"github.com/navidrome/navidrome/plugins/pdk/go/scrobbler"
|
"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 (
|
const (
|
||||||
spotifyCacheTTLHit int64 = 30 * 24 * 60 * 60 // 30 days for resolved track IDs
|
spotifyCacheTTLHit int64 = 30 * 24 * 60 * 60 // 30 days for resolved track IDs
|
||||||
spotifyCacheTTLMiss int64 = 4 * 60 * 60 // 4 hours for misses (retry later)
|
spotifyCacheTTLMiss int64 = 4 * 60 * 60 // 4 hours for misses (retry later)
|
||||||
@@ -25,29 +31,19 @@ type listenBrainzResult struct {
|
|||||||
SpotifyTrackIDs []string `json:"spotify_track_ids"`
|
SpotifyTrackIDs []string `json:"spotify_track_ids"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildSpotifySearchURL constructs a Spotify search URL using artist and title.
|
// spotifySearchURL builds a Spotify search URL from one or more terms.
|
||||||
// Used as the ultimate fallback when ListenBrainz resolution fails.
|
// Empty terms are ignored. Returns "" if all terms are empty.
|
||||||
func buildSpotifySearchURL(artist, title string) string {
|
func spotifySearchURL(terms ...string) string {
|
||||||
query := strings.TrimSpace(strings.Join([]string{artist, title}, " "))
|
query := strings.TrimSpace(strings.Join(terms, " "))
|
||||||
if query == "" {
|
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 ""
|
||||||
}
|
}
|
||||||
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.
|
// spotifyCacheKey returns a deterministic cache key for a track's Spotify URL.
|
||||||
func spotifyCacheKey(artist, title, album string) string {
|
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." + hashKey(strings.ToLower(artist)+"\x00"+strings.ToLower(title)+"\x00"+strings.ToLower(album))
|
||||||
return "spotify.url." + hex.EncodeToString(h[:8])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// trySpotifyFromMBID calls the ListenBrainz spotify-id-from-mbid endpoint.
|
// trySpotifyFromMBID calls the ListenBrainz spotify-id-from-mbid endpoint.
|
||||||
@@ -158,7 +154,7 @@ func resolveSpotifyURL(track scrobbler.TrackInfo) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3. Fallback to search URL
|
// 3. Fallback to search URL
|
||||||
searchURL := buildSpotifySearchURL(track.Artist, track.Title)
|
searchURL := spotifySearchURL(track.Artist, track.Title)
|
||||||
_ = host.CacheSetString(cacheKey, searchURL, spotifyCacheTTLMiss)
|
_ = 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))
|
pdk.Log(pdk.LogInfo, fmt.Sprintf("Spotify resolution missed, falling back to search URL for %q - %q: %s", primary, track.Title, searchURL))
|
||||||
return searchURL
|
return searchURL
|
||||||
|
|||||||
+7
-12
@@ -31,20 +31,15 @@ var _ = Describe("Spotify", func() {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
Describe("buildSpotifySearchURL", func() {
|
Describe("spotifySearchURL", func() {
|
||||||
DescribeTable("constructs Spotify search URL",
|
DescribeTable("constructs Spotify search URL",
|
||||||
func(artist, title, expectedSubstring string) {
|
func(expectedURL string, terms ...string) {
|
||||||
url := buildSpotifySearchURL(artist, title)
|
Expect(spotifySearchURL(terms...)).To(Equal(expectedURL))
|
||||||
Expect(url).To(HavePrefix("https://open.spotify.com/search/"))
|
|
||||||
if expectedSubstring != "" {
|
|
||||||
Expect(url).To(ContainSubstring(expectedSubstring))
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
Entry("artist and title", "Rick Astley", "Never Gonna Give You Up", "Rick%20Astley"),
|
Entry("artist and title", "https://open.spotify.com/search/Rick%20Astley%20Never%20Gonna%20Give%20You%20Up", "Rick Astley", "Never Gonna Give You Up"),
|
||||||
Entry("another track", "Radiohead", "Karma Police", "Radiohead"),
|
Entry("single term", "https://open.spotify.com/search/Radiohead", "Radiohead"),
|
||||||
Entry("empty artist", "", "Only Title", "Only%20Title"),
|
Entry("empty terms", "", "", ""),
|
||||||
Entry("empty title", "Solo Artist", "", "Solo%20Artist"),
|
Entry("one empty term", "https://open.spotify.com/search/Solo%20Artist", "Solo Artist", ""),
|
||||||
Entry("both empty", "", "", ""),
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user