diff --git a/apps/mukke/apps/web/src/lib/components/MiniPlayer.svelte b/apps/mukke/apps/web/src/lib/components/MiniPlayer.svelte index d3409057e..595325845 100644 --- a/apps/mukke/apps/web/src/lib/components/MiniPlayer.svelte +++ b/apps/mukke/apps/web/src/lib/components/MiniPlayer.svelte @@ -13,6 +13,37 @@ {#if playerStore.currentSong}
+ + {#if playerStore.error} +
+ + + + {playerStore.error} + +
+ {/if} +
diff --git a/apps/mukke/apps/web/src/lib/stores/library.svelte.ts b/apps/mukke/apps/web/src/lib/stores/library.svelte.ts index 60442b087..380415d0b 100644 --- a/apps/mukke/apps/web/src/lib/stores/library.svelte.ts +++ b/apps/mukke/apps/web/src/lib/stores/library.svelte.ts @@ -104,7 +104,9 @@ function createLibraryStore() { }, async loadCoverUrls(paths: string[]) { - const uncached = paths.filter((p) => p && !state.coverUrls[p]); + // Filter out non-image paths (e.g. .mp3 storagePaths stored as coverArtPath by mistake) + const imageExtensions = /\.(jpg|jpeg|png|gif|webp|avif|svg)$/i; + const uncached = paths.filter((p) => p && !state.coverUrls[p] && imageExtensions.test(p)); if (uncached.length === 0) return; try { const data = await fetchApi<{ urls: Record }>('/library/cover-urls', { diff --git a/apps/mukke/apps/web/src/lib/stores/player.svelte.ts b/apps/mukke/apps/web/src/lib/stores/player.svelte.ts index 3d3379026..1627a5a53 100644 --- a/apps/mukke/apps/web/src/lib/stores/player.svelte.ts +++ b/apps/mukke/apps/web/src/lib/stores/player.svelte.ts @@ -16,6 +16,7 @@ interface PlayerState { currentIndex: number; showFullPlayer: boolean; showQueue: boolean; + error: string | null; } function getBackendUrl(): string { @@ -55,6 +56,7 @@ function createPlayerStore() { currentIndex: 0, showFullPlayer: false, showQueue: false, + error: null, }); let audio: HTMLAudioElement | null = null; @@ -70,6 +72,18 @@ function createPlayerStore() { audio.addEventListener('ended', () => { handleNext(); }); + audio.addEventListener('error', () => { + const mediaError = audio!.error; + const msg = + mediaError?.code === MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED + ? 'Audio format not supported' + : mediaError?.code === MediaError.MEDIA_ERR_NETWORK + ? 'Network error while loading audio' + : 'Failed to load audio file'; + console.warn(`[Mukke Player] ${msg} for song: ${state.currentSong?.title}`); + state.error = msg; + state.isPlaying = false; + }); } async function fetchApi(path: string, options: RequestInit = {}): Promise { @@ -152,6 +166,7 @@ function createPlayerStore() { state.currentSong = song; state.currentTime = 0; state.duration = 0; + state.error = null; try { const url = await getDownloadUrl(song.id); @@ -160,8 +175,18 @@ function createPlayerStore() { state.isPlaying = true; updateMediaSession(song); } catch (e) { - console.error('Failed to play song:', e); + console.warn(`[Mukke Player] Failed to play "${song.title}":`, e); state.isPlaying = false; + if (!state.error) { + state.error = 'Failed to play song. The file may be missing.'; + } + // Auto-skip to next song after a short delay + setTimeout(() => { + if (state.error && state.queue.length > 1) { + state.error = null; + handleNext(); + } + }, 2000); } } @@ -215,6 +240,9 @@ function createPlayerStore() { get showQueue() { return state.showQueue; }, + get error() { + return state.error; + }, async playSong(song: Song, queue?: Song[], startIndex?: number) { if (queue) { @@ -322,6 +350,10 @@ function createPlayerStore() { state.showQueue = !state.showQueue; }, + clearError() { + state.error = null; + }, + clearQueue() { if (audio) { audio.pause(); @@ -336,6 +368,7 @@ function createPlayerStore() { state.currentIndex = 0; state.showFullPlayer = false; state.showQueue = false; + state.error = null; }, removeFromQueue(index: number) { diff --git a/apps/mukke/apps/web/src/routes/(app)/library/+page.svelte b/apps/mukke/apps/web/src/routes/(app)/library/+page.svelte index 488f73251..97ab73e58 100644 --- a/apps/mukke/apps/web/src/routes/(app)/library/+page.svelte +++ b/apps/mukke/apps/web/src/routes/(app)/library/+page.svelte @@ -3,12 +3,14 @@ import { goto } from '$app/navigation'; import { libraryStore } from '$lib/stores/library.svelte'; import { authStore } from '$lib/stores/auth.svelte'; + import { playerStore } from '$lib/stores/player.svelte'; import SongEditor from '$lib/components/SongEditor.svelte'; import type { Song } from '@mukke/shared'; const tabs = ['songs', 'albums', 'artists', 'genres'] as const; let editingSong = $state(null); + let failedCovers = $state>(new Set()); function getBackendUrl(): string { let baseUrl = 'http://localhost:3010'; @@ -58,6 +60,10 @@ editingSong = song; } + function handlePlaySong(song: Song, index: number) { + playerStore.playSong(song, libraryStore.songs, index); + } + async function openInEditor(songId: string, e: Event) { e.preventDefault(); e.stopPropagation(); @@ -148,22 +154,40 @@
- {#each libraryStore.songs as song} + {#each libraryStore.songs as song, index}
handlePlaySong(song, index)} + onkeydown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handlePlaySong(song, index); + } + }} + class="grid grid-cols-[40px_1fr_1fr_1fr_80px_40px_40px_40px] gap-4 px-4 py-3 hover:bg-background transition-colors items-center cursor-pointer group {playerStore + .currentSong?.id === song.id + ? 'bg-primary/5' + : ''}" >
- {#if song.coverArtPath && libraryStore.coverUrls[song.coverArtPath]} + {#if song.coverArtPath && libraryStore.coverUrls[song.coverArtPath] && !failedCovers.has(song.id)} { + failedCovers = new Set([...failedCovers, song.id]); + }} /> {:else} {/if} + + {#if playerStore.currentSong?.id === song.id && playerStore.isPlaying} +
+ + + +
+ {:else} + + {/if}
- {song.title} + {song.title} {song.artist ?? 'Unknown'} {song.album ?? 'Unknown'} @@ -254,11 +300,15 @@
- {#if album.coverArtPath && libraryStore.coverUrls[album.coverArtPath]} + {#if album.coverArtPath && libraryStore.coverUrls[album.coverArtPath] && !failedCovers.has(album.coverArtPath)} {album.album} { + if (album.coverArtPath) + failedCovers = new Set([...failedCovers, album.coverArtPath]); + }} /> {:else}