diff --git a/frontend/src/components/ConfigPanel.tsx b/frontend/src/components/ConfigPanel.tsx index 2198268..70f2b3c 100644 --- a/frontend/src/components/ConfigPanel.tsx +++ b/frontend/src/components/ConfigPanel.tsx @@ -194,6 +194,14 @@ export default function ConfigPanel() { version: plexResult.version ?? "", })}

+ {plexResult.media_check?.checked && + (plexResult.media_check.ok ? ( +

{t("config.plexMediaOk")}

+ ) : ( +

+ {t("config.plexMediaMissing", { path: plexResult.media_check.local_path ?? "" })} +

+ ))}

{t("config.plexPickLibraries")}

{plexResult.sections.map((s) => ( diff --git a/frontend/src/components/PlexPlayer.tsx b/frontend/src/components/PlexPlayer.tsx index 083f28a..9ecbfe6 100644 --- a/frontend/src/components/PlexPlayer.tsx +++ b/frontend/src/components/PlexPlayer.tsx @@ -56,6 +56,7 @@ export default function PlexPlayer({ itemId, onClose }: Props) { const [muted, setMuted] = useState(false); const [fs, setFs] = useState(false); const [ready, setReady] = useState(false); + const [loadError, setLoadError] = useState(null); const [uiVisible, setUiVisible] = useState(true); const hideTimer = useRef(null); // Selected audio / subtitle stream ordinals (null audio = default first; null subtitle = off). @@ -83,10 +84,21 @@ export default function PlexPlayer({ itemId, onClose }: Props) { const video = videoRef.current; if (!video) return; setReady(false); + setLoadError(null); let sess; try { sess = await api.plexSession(id, startAt, audioRef.current, subRef.current); - } catch { + } catch (e: any) { + // Surface WHY playback can't start instead of an eternal spinner. 404 = the physical file + // isn't reachable (missing media bind-mount or a wrong plex_path_map); 501 = the codec + // needs full transcoding (a later phase). + setLoadError( + e?.status === 501 + ? t("plex.player.errTranscode") + : e?.status === 404 + ? t("plex.player.errNotFound") + : t("plex.player.errGeneric"), + ); return; } sessionStartRef.current = sess.start_s; @@ -112,6 +124,11 @@ export default function PlexPlayer({ itemId, onClose }: Props) { setReady(true); video.play().catch(() => {}); }); + // A fatal HLS error (e.g. the remux session died / the file became unreadable) would + // otherwise leave the spinner up forever — surface it instead. + hls.on(Hls.Events.ERROR, (_e, data) => { + if (data.fatal) setLoadError(t("plex.player.errGeneric")); + }); } else { // direct file (or Safari native HLS) video.src = sess.url; @@ -125,7 +142,7 @@ export default function PlexPlayer({ itemId, onClose }: Props) { video.addEventListener("loadedmetadata", onMeta); } }, - [id], + [id, t], ); // Start playback when the detail arrives (resume from the saved position unless already watched). @@ -407,8 +424,12 @@ export default function PlexPlayer({ itemId, onClose }: Props) { /> {!ready && ( -
- {t("plex.player.loading")} +
+ {loadError ? ( + {loadError} + ) : ( + {t("plex.player.loading")} + )}
)} diff --git a/frontend/src/i18n/locales/de/config.json b/frontend/src/i18n/locales/de/config.json index 21c34bf..ee35d7f 100644 --- a/frontend/src/i18n/locales/de/config.json +++ b/frontend/src/i18n/locales/de/config.json @@ -194,6 +194,8 @@ "plexTestOk": "Verbunden mit {{name}}", "plexTestFailed": "Plex-Verbindung fehlgeschlagen — prüfe URL und Token", "plexConnected": "Verbunden mit {{name}} {{version}}", + "plexMediaOk": "Medieneinbindung OK — eine Beispieldatei wurde auf der Festplatte gefunden.", + "plexMediaMissing": "Medien unter {{path}} nicht lesbar — prüfe, ob die Plex-Medien in den Container eingebunden sind und die Pfadzuordnung stimmt. Die Wiedergabe schlägt fehl, bis dies behoben ist.", "plexPickLibraries": "Anzuzeigende Bibliotheken:", "plexPickHint": "Keine auszuwählen entspricht dem Anzeigen aller Bibliotheken. Nicht vergessen zu speichern.", "plexMovies": "Filme", diff --git a/frontend/src/i18n/locales/de/plex.json b/frontend/src/i18n/locales/de/plex.json index 89c013c..9a6ab69 100644 --- a/frontend/src/i18n/locales/de/plex.json +++ b/frontend/src/i18n/locales/de/plex.json @@ -47,7 +47,10 @@ "tracks": "Audio & Untertitel", "audio": "Audio", "subtitles": "Untertitel", - "subOff": "Aus" + "subOff": "Aus", + "errNotFound": "Diese Datei kann nicht abgespielt werden — die Medien sind auf dem Server nicht erreichbar (die Plex-Medieneinbindung fehlt oder die Pfadzuordnung ist falsch). Bitte den Administrator, die Plex-Konfiguration zu prüfen.", + "errTranscode": "Das Format dieser Datei erfordert eine Transkodierung, die noch nicht unterstützt wird.", + "errGeneric": "Die Wiedergabe konnte nicht gestartet werden. Bitte erneut versuchen." }, "playable": { "direct": "Spielt direkt im Browser", diff --git a/frontend/src/i18n/locales/en/config.json b/frontend/src/i18n/locales/en/config.json index dc4242a..ac00434 100644 --- a/frontend/src/i18n/locales/en/config.json +++ b/frontend/src/i18n/locales/en/config.json @@ -194,6 +194,8 @@ "plexTestOk": "Connected to {{name}}", "plexTestFailed": "Plex connection failed — check the URL and token", "plexConnected": "Connected to {{name}} {{version}}", + "plexMediaOk": "Media mount OK — a sample file was found on disk.", + "plexMediaMissing": "Media not readable at {{path}} — check that the Plex media is bind-mounted into the container and that the path map is correct. Playback will fail until this is fixed.", "plexPickLibraries": "Libraries to expose:", "plexPickHint": "Unchecking all is the same as exposing every library. Remember to Save.", "plexMovies": "Movies", diff --git a/frontend/src/i18n/locales/en/plex.json b/frontend/src/i18n/locales/en/plex.json index 1570159..d99d03e 100644 --- a/frontend/src/i18n/locales/en/plex.json +++ b/frontend/src/i18n/locales/en/plex.json @@ -47,7 +47,10 @@ "tracks": "Audio & subtitles", "audio": "Audio", "subtitles": "Subtitles", - "subOff": "Off" + "subOff": "Off", + "errNotFound": "This file can't be played — the media isn't reachable on the server (the Plex media mount is missing or the path map is wrong). Ask the admin to check the Plex configuration.", + "errTranscode": "This file's format needs transcoding, which isn't supported yet.", + "errGeneric": "Playback couldn't start. Please try again." }, "playable": { "direct": "Plays directly in the browser", diff --git a/frontend/src/i18n/locales/hu/config.json b/frontend/src/i18n/locales/hu/config.json index 2bcee67..f4e165a 100644 --- a/frontend/src/i18n/locales/hu/config.json +++ b/frontend/src/i18n/locales/hu/config.json @@ -194,6 +194,8 @@ "plexTestOk": "Kapcsolódva: {{name}}", "plexTestFailed": "A Plex-kapcsolat sikertelen — ellenőrizd az URL-t és a tokent", "plexConnected": "Kapcsolódva: {{name}} {{version}}", + "plexMediaOk": "A media-mount rendben — egy mintafájl megtalálható a lemezen.", + "plexMediaMissing": "A média nem olvasható itt: {{path}} — ellenőrizd, hogy a Plex média be van-e mountolva a konténerbe, és hogy helyes-e az útvonal-leképezés. A lejátszás addig nem fog működni, amíg ez nincs javítva.", "plexPickLibraries": "Megjelenítendő könyvtárak:", "plexPickHint": "Ha egyiket sem jelölöd be, az az összes könyvtár megjelenítésével egyenértékű. Ne feledj menteni.", "plexMovies": "Filmek", diff --git a/frontend/src/i18n/locales/hu/plex.json b/frontend/src/i18n/locales/hu/plex.json index 490e3ed..60c58ae 100644 --- a/frontend/src/i18n/locales/hu/plex.json +++ b/frontend/src/i18n/locales/hu/plex.json @@ -47,7 +47,10 @@ "tracks": "Audió és felirat", "audio": "Audió", "subtitles": "Felirat", - "subOff": "Ki" + "subOff": "Ki", + "errNotFound": "Ez a fájl nem játszható le — a média nem érhető el a szerveren (hiányzik a Plex media-mount, vagy hibás az útvonal-leképezés). Kérd meg az adminisztrátort, hogy ellenőrizze a Plex beállításait.", + "errTranscode": "Ennek a fájlnak a formátuma transzkódolást igényel, ami még nem támogatott.", + "errGeneric": "A lejátszást nem sikerült elindítani. Próbáld újra." }, "playable": { "direct": "Közvetlenül játszható a böngészőben", diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 492da09..7a8df7d 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -622,6 +622,14 @@ export interface PlexTestResult { server_name: string; version?: string; sections: PlexSection[]; + // Whether a sample media file resolved + was readable through the local mount (bind-mount + + // path-map check). checked=false when the library is empty (nothing to probe yet). + media_check?: { + checked: boolean; + ok?: boolean; + plex_path?: string; + local_path?: string; + }; } // Plex browse/search/drill-down (the mirrored catalog rendered as feed-style cards).