From 135f6b9ea1882dd49a1aa31e2b29ec796fe50bff Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Wed, 22 Apr 2026 20:07:28 -0700 Subject: [PATCH] playStatsTrack: fall back to streaming sources when not in library MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Top Tracks sidebar play button on the artist-detail page (and the same buttons on the Stats page) called /api/stats/resolve-track and gave up with a 'Track not found in library' toast on a miss. Now when the library lookup misses, falls through to /api/enhanced- search/stream-track — the same Soulseek/YouTube/streaming-source pipeline the search-results play button uses. So Last.fm popular tracks, recent plays, and stats artist top tracks all play even if you don't own the track yet. Library hit still wins (faster, full quality). Only on miss does it escalate to streaming. Final error toast updated to reflect both paths having been tried. --- webui/static/stats-automations.js | 59 ++++++++++++++++++++++++------- 1 file changed, 47 insertions(+), 12 deletions(-) diff --git a/webui/static/stats-automations.js b/webui/static/stats-automations.js index 88fbf2a3..7b48c34c 100644 --- a/webui/static/stats-automations.js +++ b/webui/static/stats-automations.js @@ -464,6 +464,7 @@ function _renderDbStorageChart(tables, totalFileSize, method) { } async function playStatsTrack(title, artist, album) { + // 1. Try the library first — fastest and best quality if owned. try { const resp = await fetch('/api/stats/resolve-track', { method: 'POST', @@ -471,22 +472,56 @@ async function playStatsTrack(title, artist, album) { body: JSON.stringify({ title, artist }), }); const data = await resp.json(); - if (!data.success || !data.track) { - showToast(data.error || 'Track not found in library', 'error'); + if (data.success && data.track) { + const t = data.track; + playLibraryTrack({ + id: t.id, + title: t.title, + file_path: t.file_path, + bitrate: t.bitrate, + artist_id: t.artist_id, + album_id: t.album_id, + _stats_image: t.image_url || null, + }, t.album_title || album || '', t.artist_name || artist || ''); return; } - const t = data.track; - playLibraryTrack({ - id: t.id, - title: t.title, - file_path: t.file_path, - bitrate: t.bitrate, - artist_id: t.artist_id, - album_id: t.album_id, - _stats_image: t.image_url || null, - }, t.album_title || album || '', t.artist_name || artist || ''); } catch (e) { + console.debug('Library resolve failed, will try streaming fallback:', e); + } + + // 2. Library miss — fall back to streaming via the enhanced-search streamer + // (Soulseek → YouTube → other configured sources, same pipeline used by + // the search results' play button). + if (typeof showLoadingOverlay === 'function') { + showLoadingOverlay(`Searching for ${title}...`); + } + try { + const streamResp = await fetch('/api/enhanced-search/stream-track', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + track_name: title, + artist_name: artist, + album_name: album || '', + duration_ms: 0, + }), + }); + const streamData = await streamResp.json(); + if (typeof hideLoadingOverlay === 'function') hideLoadingOverlay(); + + if (streamData.success && streamData.result) { + if (typeof startStream === 'function') { + await startStream(streamData.result); + } else { + showToast('Streaming not available', 'error'); + } + } else { + showToast(streamData.error || 'Track not found in library or any source', 'error'); + } + } catch (e) { + if (typeof hideLoadingOverlay === 'function') hideLoadingOverlay(); showToast('Failed to play track', 'error'); + console.error('Stream fallback failed:', e); } }