From 4183cef4f1c405f4224e23a05c9c5df55e24875a Mon Sep 17 00:00:00 2001 From: Julien Maille Date: Wed, 24 Dec 2025 13:48:39 +0100 Subject: [PATCH] fix: resolve multiple bugs including playback loops and search race conditions --- js/api.js | 20 ++++++++++++-------- js/player.js | 4 ++++ js/ui.js | 16 ++++++++++++---- 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/js/api.js b/js/api.js index 2a14ea2..ab66413 100644 --- a/js/api.js +++ b/js/api.js @@ -219,12 +219,12 @@ export class LosslessAPI { } } - async searchTracks(query) { + async searchTracks(query, options = {}) { const cached = await this.cache.get('search_tracks', query); if (cached) return cached; try { - const response = await this.fetchWithRetry(`/search/?s=${encodeURIComponent(query)}`); + const response = await this.fetchWithRetry(`/search/?s=${encodeURIComponent(query)}`, options); const data = await response.json(); const normalized = this.normalizeSearchResponse(data, 'tracks'); const result = { @@ -235,17 +235,18 @@ export class LosslessAPI { await this.cache.set('search_tracks', query, result); return result; } catch (error) { + if (error.name === 'AbortError') throw error; console.error('Track search failed:', error); return { items: [], limit: 0, offset: 0, totalNumberOfItems: 0 }; } } - async searchArtists(query) { + async searchArtists(query, options = {}) { const cached = await this.cache.get('search_artists', query); if (cached) return cached; try { - const response = await this.fetchWithRetry(`/search/?a=${encodeURIComponent(query)}`); + const response = await this.fetchWithRetry(`/search/?a=${encodeURIComponent(query)}`, options); const data = await response.json(); const normalized = this.normalizeSearchResponse(data, 'artists'); const result = { @@ -256,17 +257,18 @@ export class LosslessAPI { await this.cache.set('search_artists', query, result); return result; } catch (error) { + if (error.name === 'AbortError') throw error; console.error('Artist search failed:', error); return { items: [], limit: 0, offset: 0, totalNumberOfItems: 0 }; } } - async searchAlbums(query) { + async searchAlbums(query, options = {}) { const cached = await this.cache.get('search_albums', query); if (cached) return cached; try { - const response = await this.fetchWithRetry(`/search/?al=${encodeURIComponent(query)}`); + const response = await this.fetchWithRetry(`/search/?al=${encodeURIComponent(query)}`, options); const data = await response.json(); const normalized = this.normalizeSearchResponse(data, 'albums'); const result = { @@ -277,17 +279,18 @@ export class LosslessAPI { await this.cache.set('search_albums', query, result); return result; } catch (error) { + if (error.name === 'AbortError') throw error; console.error('Album search failed:', error); return { items: [], limit: 0, offset: 0, totalNumberOfItems: 0 }; } } - async searchPlaylists(query) { + async searchPlaylists(query, options = {}) { const cached = await this.cache.get('search_playlists', query); if (cached) return cached; try { - const response = await this.fetchWithRetry(`/search/?p=${encodeURIComponent(query)}`); + const response = await this.fetchWithRetry(`/search/?p=${encodeURIComponent(query)}`, options); const data = await response.json(); const normalized = this.normalizeSearchResponse(data, 'playlists'); const result = { @@ -298,6 +301,7 @@ export class LosslessAPI { await this.cache.set('search_playlists', query, result); return result; } catch (error) { + if (error.name === 'AbortError') throw error; console.error('Playlist search failed:', error); return { items: [], limit: 0, offset: 0, totalNumberOfItems: 0 }; } diff --git a/js/player.js b/js/player.js index 8cb5090..93532ab 100644 --- a/js/player.js +++ b/js/player.js @@ -143,6 +143,9 @@ export class Player { if (this.preloadAbortController.signal.aborted) break; this.preloadCache.set(track.id, streamUrl); + + // Warm connection/cache + fetch(streamUrl, { method: 'HEAD', signal: this.preloadAbortController.signal }).catch(() => {}); } catch (error) { if (error.name !== 'AbortError') { console.debug('Failed to get stream URL for preload:', trackTitle); @@ -250,6 +253,7 @@ export class Player { if (this.audio.paused) { this.audio.play().catch(e => { + if (e.name === 'NotAllowedError' || e.name === 'AbortError') return; console.error("Play failed, reloading track:", e); if (this.currentTrack) { this.playTrackFromQueue(); diff --git a/js/ui.js b/js/ui.js index 78c067c..b0cfb45 100644 --- a/js/ui.js +++ b/js/ui.js @@ -6,6 +6,7 @@ export class UIRenderer { constructor(api) { this.api = api; this.currentTrack = null; + this.searchAbortController = null; } setCurrentTrack(track) { @@ -371,12 +372,18 @@ export class UIRenderer { albumsContainer.innerHTML = this.createSkeletonCards(6, false); playlistsContainer.innerHTML = this.createSkeletonCards(6, false); + if (this.searchAbortController) { + this.searchAbortController.abort(); + } + this.searchAbortController = new AbortController(); + const signal = this.searchAbortController.signal; + try { const [tracksResult, artistsResult, albumsResult, playlistsResult] = await Promise.all([ - this.api.searchTracks(query), - this.api.searchArtists(query), - this.api.searchAlbums(query), - this.api.searchPlaylists(query) + this.api.searchTracks(query, { signal }), + this.api.searchArtists(query, { signal }), + this.api.searchAlbums(query, { signal }), + this.api.searchPlaylists(query, { signal }) ]); let finalTracks = tracksResult.items; @@ -430,6 +437,7 @@ export class UIRenderer { : createPlaceholder('No playlists found.'); } catch (error) { + if (error.name === 'AbortError') return; console.error("Search failed:", error); const errorMsg = createPlaceholder(`Error during search. ${error.message}`); tracksContainer.innerHTML = errorMsg;