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:
parent
2138f3c357
commit
fdd39ef3e4
9 changed files with 59 additions and 7 deletions
|
|
@ -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) => (
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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).
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue