From 5bfa7bd94a430efe472e287c3260434509aebc63 Mon Sep 17 00:00:00 2001 From: Nikhil Gohil Date: Thu, 3 Sep 2026 13:24:08 -0700 Subject: [PATCH 1/2] fix(navidrome): invalidate song cache on publish and add artist-title fallback Add a 30-second TTL and invalidateIndexedSongsCache method to NavidromeClient, called prior to publishing playlists. Add clean artist and title string fallback matching in findSong when path or MBID matching fails across volume mounts. --- backend/services/navidrome.js | 35 +++++++++++++++---- .../playback/navidromePlaybackDestination.js | 3 ++ 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/backend/services/navidrome.js b/backend/services/navidrome.js index 0e4434c7..02a7ee26 100644 --- a/backend/services/navidrome.js +++ b/backend/services/navidrome.js @@ -138,10 +138,26 @@ export class NavidromeClient { if (relativeMatches.length === 1) return relativeMatches[0]; const mbid = String(track.mbid || "").trim().toLowerCase(); - if (!mbid) return null; - return indexedSongs.find( - (song) => String(song.musicBrainzId || "").trim().toLowerCase() === mbid, - ) || null; + if (mbid) { + const mbidMatch = indexedSongs.find( + (song) => String(song.musicBrainzId || "").trim().toLowerCase() === mbid, + ); + if (mbidMatch) return mbidMatch; + } + + const cleanTitle = String(_title || track.title || track.trackName || "").trim().toLowerCase(); + const cleanArtist = String(_artist || track.artist || track.artistName || "").trim().toLowerCase(); + if (cleanTitle && cleanArtist) { + return ( + indexedSongs.find( + (song) => + String(song.title || "").trim().toLowerCase() === cleanTitle && + String(song.artist || "").trim().toLowerCase() === cleanArtist, + ) || null + ); + } + + return null; } async searchSongsByArtist(artistName, limit = 5) { @@ -365,8 +381,15 @@ export class NavidromeClient { } } - async _getIndexedSongs() { - if (!this._indexedSongsPromise) { + invalidateIndexedSongsCache() { + this._indexedSongsPromise = null; + this._indexedSongsAt = 0; + } + + async _getIndexedSongs(force = false) { + const now = Date.now(); + if (force || !this._indexedSongsPromise || now - (this._indexedSongsAt || 0) > 30000) { + this._indexedSongsAt = now; this._indexedSongsPromise = (async () => { const songs = []; for (let start = 0; ; ) { diff --git a/backend/services/playback/navidromePlaybackDestination.js b/backend/services/playback/navidromePlaybackDestination.js index 09427119..2f2be3e6 100644 --- a/backend/services/playback/navidromePlaybackDestination.js +++ b/backend/services/playback/navidromePlaybackDestination.js @@ -424,6 +424,9 @@ export class NavidromePlaybackDestination { pointer = { ...pointer, title: current }; navidromePlaylistPointerStore.setPointer(snapshot.entityId, targetKey, pointer); } + if (typeof this.client?.invalidateIndexedSongsCache === "function") { + this.client.invalidateIndexedSongsCache(); + } const songs = []; for (let index = 0; index < snapshot.tracks.length; index += SONG_LOOKUP_BATCH_SIZE) { const batch = snapshot.tracks.slice(index, index + SONG_LOOKUP_BATCH_SIZE); From 51e4bead363169585662f1c98e5892d017bc9316 Mon Sep 17 00:00:00 2001 From: Nikhil Gohil Date: Thu, 3 Sep 2026 13:46:54 -0700 Subject: [PATCH 2/2] fix(navidrome): disambiguate fallback song matches by album and uniqueness Avoid arbitrarily picking the first title/artist match by requiring a unique match or disambiguating by album name when multiple recordings exist. --- backend/services/navidrome.js | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/backend/services/navidrome.js b/backend/services/navidrome.js index 02a7ee26..cd7a7905 100644 --- a/backend/services/navidrome.js +++ b/backend/services/navidrome.js @@ -148,13 +148,21 @@ export class NavidromeClient { const cleanTitle = String(_title || track.title || track.trackName || "").trim().toLowerCase(); const cleanArtist = String(_artist || track.artist || track.artistName || "").trim().toLowerCase(); if (cleanTitle && cleanArtist) { - return ( - indexedSongs.find( - (song) => - String(song.title || "").trim().toLowerCase() === cleanTitle && - String(song.artist || "").trim().toLowerCase() === cleanArtist, - ) || null + const candidateMatches = indexedSongs.filter( + (song) => + String(song.title || "").trim().toLowerCase() === cleanTitle && + String(song.artist || "").trim().toLowerCase() === cleanArtist, ); + if (candidateMatches.length === 1) return candidateMatches[0]; + if (candidateMatches.length > 1) { + const cleanAlbum = String(track.album || track.albumName || "").trim().toLowerCase(); + if (cleanAlbum) { + const albumMatches = candidateMatches.filter( + (song) => String(song.album || "").trim().toLowerCase() === cleanAlbum, + ); + if (albumMatches.length === 1) return albumMatches[0]; + } + } } return null;