diff --git a/web/backend/src/MediaPlayer.ts b/web/backend/src/MediaPlayer.ts index 5b560ec..5018d61 100644 --- a/web/backend/src/MediaPlayer.ts +++ b/web/backend/src/MediaPlayer.ts @@ -37,6 +37,7 @@ enum UserEvent { FavoritesUpdate = "favorites_update", MetadataUpdate = "metadata_update", MPDUpdate = "mpd_update", + PlaybackError = "playback_error", } export interface Features { @@ -57,6 +58,9 @@ export class MediaPlayer { private dataBuffer: string = ''; private metadata: Map = new Map(); private bonjourInstance: Bonjour | null = null; + private playbackErrors: Map = new Map(); + private currentFile: string | null = null; + private lastLoadCandidate: string | null = null; constructor() { this.socket = this.tryRespawnPlayerProcess(); @@ -152,7 +156,8 @@ export class MediaPlayer { const playlist = response.data as PlaylistItem[]; return playlist.map((item: PlaylistItem) => ({ ...item, - metadata: this.metadata.get(item.filename) || {} + metadata: this.metadata.get(item.filename) || {}, + playbackError: this.playbackErrors.get(item.filename) })); }); } @@ -352,6 +357,8 @@ export class MediaPlayer { } private async loadFile(url: string, mode: string, fetchMetadata: boolean = true, options: string[] = []) { + this.lastLoadCandidate = url; + this.playbackErrors.delete(url); this.modify(UserEvent.PlaylistUpdate, () => this.writeCommand("loadfile", [url, mode, "-1", options.join(',')])); if (fetchMetadata) { @@ -470,6 +477,24 @@ export class MediaPlayer { this.pendingCommands.delete(response.request_id); } } else if (response.event) { + if (response.event === "start-file") { + // Clear any previous error for the file that is starting + const file = response.file || this.lastLoadCandidate; + if (file) { + this.currentFile = file; + this.playbackErrors.delete(file); + } + } else if (response.event === "end-file" && response.reason === "error") { + const file = response.file || this.currentFile || this.lastLoadCandidate || "Unknown file"; + const errorMessage = response.error || response["file-error"] || "Unknown playback error"; + + this.playbackErrors.set(file, errorMessage); + this.handleEvent(UserEvent.PlaybackError, { + filename: file, + error: errorMessage + }); + } + this.handleEvent(UserEvent.MPDUpdate, response); } else { console.log(response); diff --git a/web/backend/src/types.ts b/web/backend/src/types.ts index c88aee0..3630cdd 100644 --- a/web/backend/src/types.ts +++ b/web/backend/src/types.ts @@ -29,4 +29,5 @@ export interface PlaylistItem { playing?: boolean; current?: boolean; metadata?: LinkMetadata; -} + playbackError?: string; +} diff --git a/web/frontend/src/api/player.tsx b/web/frontend/src/api/player.tsx index f417904..7740df9 100644 --- a/web/frontend/src/api/player.tsx +++ b/web/frontend/src/api/player.tsx @@ -28,6 +28,7 @@ export interface PlaylistItem { id: number; playing: boolean | null; metadata?: Metadata; + playbackError?: string; } export const getDisplayTitle = (item: PlaylistItem): string => { @@ -62,6 +63,7 @@ export enum ServerEvent { FavoritesUpdate = "favorites_update", MetadataUpdate = "metadata_update", MPDUpdate = "mpd_update", + PlaybackError = "playback_error", ScreenShare = "screen_share", } diff --git a/web/frontend/src/components/App.tsx b/web/frontend/src/components/App.tsx index 5e56f69..5ef1e23 100644 --- a/web/frontend/src/components/App.tsx +++ b/web/frontend/src/components/App.tsx @@ -199,6 +199,7 @@ const App: React.FC = () => { case ServerEvent.NowPlayingUpdate: case ServerEvent.MetadataUpdate: case ServerEvent.MPDUpdate: + case ServerEvent.PlaybackError: fetchPlaylist(); fetchNowPlaying(); break; diff --git a/web/frontend/src/components/SongRow.tsx b/web/frontend/src/components/SongRow.tsx index 462336b..d2c29c8 100644 --- a/web/frontend/src/components/SongRow.tsx +++ b/web/frontend/src/components/SongRow.tsx @@ -1,6 +1,6 @@ import classNames from 'classnames'; import React, { useState, useRef, useEffect, ReactNode } from 'react'; -import { FaPlay, FaVolumeUp, FaVolumeOff } from 'react-icons/fa'; +import { FaPlay, FaVolumeUp, FaVolumeOff, FaExclamationTriangle } from 'react-icons/fa'; import { getDisplayTitle, PlaylistItem } from '../api/player'; export enum PlayState { @@ -19,6 +19,7 @@ export interface SongRowProps { const SongRow: React.FC = ({ song, auxControl, playState, onDelete, onPlay }) => { const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [showErrorDetails, setShowErrorDetails] = useState(false); const buttonRef = useRef(null); useEffect(() => { @@ -38,6 +39,7 @@ const SongRow: React.FC = ({ song, auxControl, playState, onDelete }, [showDeleteConfirm]); const displayTitle = getDisplayTitle(song); + const hasError = !!song.playbackError; return (
= ({ song, auxControl, playState, onDelete "bg-black/30": playState === PlayState.NotPlaying, })}>
- +
+ + + {hasError && showErrorDetails && ( +
+
Playback error
+
{song.playbackError}
+
+ )} +