feat(plex): surface playback errors + media-mount status in the UI

PlexPlayer: when the stream session can't start, show why instead of an eternal
spinner — 404 (physical file unreachable: missing media mount / wrong path map),
501 (format needs transcoding), or a generic failure; also catch fatal hls.js
errors. ConfigPanel: render the Test-connection media_check (mount OK, or a
warning naming the unreadable local path). New plex/config i18n in en/hu/de.
This commit is contained in:
npeter83 2026-07-05 06:58:34 +02:00
parent 2138f3c357
commit fdd39ef3e4
9 changed files with 59 additions and 7 deletions

View file

@ -194,6 +194,14 @@ export default function ConfigPanel() {
version: plexResult.version ?? "", version: plexResult.version ?? "",
})} })}
</p> </p>
{plexResult.media_check?.checked &&
(plexResult.media_check.ok ? (
<p className="text-[11px] text-emerald-500 mb-2">{t("config.plexMediaOk")}</p>
) : (
<p className="text-[11px] text-red-400 mb-2">
{t("config.plexMediaMissing", { path: plexResult.media_check.local_path ?? "" })}
</p>
))}
<p className="text-[11px] text-muted mb-1.5">{t("config.plexPickLibraries")}</p> <p className="text-[11px] text-muted mb-1.5">{t("config.plexPickLibraries")}</p>
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
{plexResult.sections.map((s) => ( {plexResult.sections.map((s) => (

View file

@ -56,6 +56,7 @@ export default function PlexPlayer({ itemId, onClose }: Props) {
const [muted, setMuted] = useState(false); const [muted, setMuted] = useState(false);
const [fs, setFs] = useState(false); const [fs, setFs] = useState(false);
const [ready, setReady] = useState(false); const [ready, setReady] = useState(false);
const [loadError, setLoadError] = useState<string | null>(null);
const [uiVisible, setUiVisible] = useState(true); const [uiVisible, setUiVisible] = useState(true);
const hideTimer = useRef<number | null>(null); const hideTimer = useRef<number | null>(null);
// Selected audio / subtitle stream ordinals (null audio = default first; null subtitle = off). // 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; const video = videoRef.current;
if (!video) return; if (!video) return;
setReady(false); setReady(false);
setLoadError(null);
let sess; let sess;
try { try {
sess = await api.plexSession(id, startAt, audioRef.current, subRef.current); 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; return;
} }
sessionStartRef.current = sess.start_s; sessionStartRef.current = sess.start_s;
@ -112,6 +124,11 @@ export default function PlexPlayer({ itemId, onClose }: Props) {
setReady(true); setReady(true);
video.play().catch(() => {}); 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 { } else {
// direct file (or Safari native HLS) // direct file (or Safari native HLS)
video.src = sess.url; video.src = sess.url;
@ -125,7 +142,7 @@ export default function PlexPlayer({ itemId, onClose }: Props) {
video.addEventListener("loadedmetadata", onMeta); video.addEventListener("loadedmetadata", onMeta);
} }
}, },
[id], [id, t],
); );
// Start playback when the detail arrives (resume from the saved position unless already watched). // 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 && ( {!ready && (
<div className="absolute inset-0 grid place-items-center text-white/70 text-sm pointer-events-none"> <div className="absolute inset-0 grid place-items-center px-6 text-center text-sm pointer-events-none">
{t("plex.player.loading")} {loadError ? (
<span className="max-w-md text-red-300">{loadError}</span>
) : (
<span className="text-white/70">{t("plex.player.loading")}</span>
)}
</div> </div>
)} )}

View file

@ -194,6 +194,8 @@
"plexTestOk": "Verbunden mit {{name}}", "plexTestOk": "Verbunden mit {{name}}",
"plexTestFailed": "Plex-Verbindung fehlgeschlagen — prüfe URL und Token", "plexTestFailed": "Plex-Verbindung fehlgeschlagen — prüfe URL und Token",
"plexConnected": "Verbunden mit {{name}} {{version}}", "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:", "plexPickLibraries": "Anzuzeigende Bibliotheken:",
"plexPickHint": "Keine auszuwählen entspricht dem Anzeigen aller Bibliotheken. Nicht vergessen zu speichern.", "plexPickHint": "Keine auszuwählen entspricht dem Anzeigen aller Bibliotheken. Nicht vergessen zu speichern.",
"plexMovies": "Filme", "plexMovies": "Filme",

View file

@ -47,7 +47,10 @@
"tracks": "Audio & Untertitel", "tracks": "Audio & Untertitel",
"audio": "Audio", "audio": "Audio",
"subtitles": "Untertitel", "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": { "playable": {
"direct": "Spielt direkt im Browser", "direct": "Spielt direkt im Browser",

View file

@ -194,6 +194,8 @@
"plexTestOk": "Connected to {{name}}", "plexTestOk": "Connected to {{name}}",
"plexTestFailed": "Plex connection failed — check the URL and token", "plexTestFailed": "Plex connection failed — check the URL and token",
"plexConnected": "Connected to {{name}} {{version}}", "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:", "plexPickLibraries": "Libraries to expose:",
"plexPickHint": "Unchecking all is the same as exposing every library. Remember to Save.", "plexPickHint": "Unchecking all is the same as exposing every library. Remember to Save.",
"plexMovies": "Movies", "plexMovies": "Movies",

View file

@ -47,7 +47,10 @@
"tracks": "Audio & subtitles", "tracks": "Audio & subtitles",
"audio": "Audio", "audio": "Audio",
"subtitles": "Subtitles", "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": { "playable": {
"direct": "Plays directly in the browser", "direct": "Plays directly in the browser",

View file

@ -194,6 +194,8 @@
"plexTestOk": "Kapcsolódva: {{name}}", "plexTestOk": "Kapcsolódva: {{name}}",
"plexTestFailed": "A Plex-kapcsolat sikertelen — ellenőrizd az URL-t és a tokent", "plexTestFailed": "A Plex-kapcsolat sikertelen — ellenőrizd az URL-t és a tokent",
"plexConnected": "Kapcsolódva: {{name}} {{version}}", "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:", "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.", "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", "plexMovies": "Filmek",

View file

@ -47,7 +47,10 @@
"tracks": "Audió és felirat", "tracks": "Audió és felirat",
"audio": "Audió", "audio": "Audió",
"subtitles": "Felirat", "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": { "playable": {
"direct": "Közvetlenül játszható a böngészőben", "direct": "Közvetlenül játszható a böngészőben",

View file

@ -622,6 +622,14 @@ export interface PlexTestResult {
server_name: string; server_name: string;
version?: string; version?: string;
sections: PlexSection[]; 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). // Plex browse/search/drill-down (the mirrored catalog rendered as feed-style cards).