From ef4bdee1f7b78f75204f412c5b61b983d68d848a Mon Sep 17 00:00:00 2001 From: npeter83 Date: Tue, 16 Jun 2026 10:45:57 +0200 Subject: [PATCH] fix(player): friendly fallback when a video can't be embedded The in-app IFrame player showed YouTube's bare 'Video unavailable' screen with no way out when a video couldn't be embedded (auto-generated Topic art-tracks disable embedding -> error 101/150; removed/private -> 100). Catch the IFrame onError and overlay a clear message + 'Open on YouTube' button, with a tailored note for embedding-disabled videos. --- frontend/src/components/PlayerModal.tsx | 35 ++++++++++++++++++++++-- frontend/src/i18n/locales/de/player.json | 6 +++- frontend/src/i18n/locales/en/player.json | 6 +++- frontend/src/i18n/locales/hu/player.json | 6 +++- 4 files changed, 48 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/PlayerModal.tsx b/frontend/src/components/PlayerModal.tsx index 5f48ee3..c0df596 100644 --- a/frontend/src/components/PlayerModal.tsx +++ b/frontend/src/components/PlayerModal.tsx @@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next"; import type { TFunction } from "i18next"; import { createPortal } from "react-dom"; import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { ArrowLeft, Check, CheckCheck, SkipBack, SkipForward, X } from "lucide-react"; +import { AlertTriangle, ArrowLeft, Check, CheckCheck, ExternalLink, SkipBack, SkipForward, X } from "lucide-react"; import Avatar from "./Avatar"; import AddToPlaylist from "./AddToPlaylist"; import { api, type Video } from "../lib/api"; @@ -237,12 +237,17 @@ export default function PlayerModal({ const navigated = currentVideoId !== active.id; // Title/author of a navigated-to video, read from the player (free, no API call). const [liveData, setLiveData] = useState<{ title?: string; author?: string } | null>(null); + // IFrame Player API error code, when the current video can't play in the embed (e.g. an + // auto-generated Topic "Art Track" with embedding disabled → 101/150, or removed/private → + // 100). We overlay a friendly message + "Open on YouTube" instead of YouTube's bare error. + const [playerError, setPlayerError] = useState(null); const loadVideo = (id: string, start: number | null) => { const p = playerRef.current; if (!p || typeof p.loadVideoById !== "function") return; // Explicit start wins; otherwise resume the active item, start others at 0. const startSeconds = start != null ? start : id === active.id ? resumeAt : 0; + setPlayerError(null); p.loadVideoById({ videoId: id, startSeconds: startSeconds || 0 }); currentIdRef.current = id; setCurrentVideoId(id); @@ -330,6 +335,7 @@ export default function PlayerModal({ let cancelled = false; const id = active.id; autoMarkedRef.current = false; + setPlayerError(null); // Auto-watch only applies to the active item — not to other videos the player // navigated to via description links. @@ -388,6 +394,9 @@ export default function PlayerModal({ if (queue && index < queue.length - 1) setPlayingId(queue[index + 1].id); } }, + // The embed couldn't play this video (embedding disabled, removed, private…). + // Surface our own message + an "Open on YouTube" escape hatch. + onError: (e: any) => setPlayerError(typeof e?.data === "number" ? e.data : -1), }, }); }); @@ -427,8 +436,30 @@ export default function PlayerModal({ className="glass-card relative w-full max-w-4xl max-h-full overflow-y-auto rounded-2xl shadow-2xl" onClick={(e) => e.stopPropagation()} > -
+
+ {playerError != null && ( +
+
+ +

{t("player.unavailableTitle")}

+

+ {playerError === 101 || playerError === 150 + ? t("player.embedDisabledBody") + : t("player.unavailableBody")} +

+ + + {t("player.openOnYoutube")} + +
+
+ )}
{hasQueue && ( diff --git a/frontend/src/i18n/locales/de/player.json b/frontend/src/i18n/locales/de/player.json index 1acf7b8..ebe909e 100644 --- a/frontend/src/i18n/locales/de/player.json +++ b/frontend/src/i18n/locales/de/player.json @@ -15,5 +15,9 @@ "markWatched": "Als angesehen markieren", "watchedUnmark": "Angesehen — zum Aufheben klicken", "jumpToTime": "Zu dieser Stelle springen", - "playInApp": "Im integrierten Player abspielen" + "playInApp": "Im integrierten Player abspielen", + "unavailableTitle": "Hier nicht abspielbar", + "unavailableBody": "Dieses Video ist nicht verfügbar — möglicherweise privat, entfernt oder regional gesperrt.", + "embedDisabledBody": "Der Eigentümer hat die Wiedergabe auf anderen Seiten deaktiviert (häufig bei automatisch generierten „Topic“-Musiktiteln). Auf YouTube ist es weiterhin abspielbar.", + "openOnYoutube": "Auf YouTube öffnen" } diff --git a/frontend/src/i18n/locales/en/player.json b/frontend/src/i18n/locales/en/player.json index aba13f2..e71f155 100644 --- a/frontend/src/i18n/locales/en/player.json +++ b/frontend/src/i18n/locales/en/player.json @@ -15,5 +15,9 @@ "markWatched": "Mark watched", "watchedUnmark": "Watched — click to unmark", "jumpToTime": "Jump to this time", - "playInApp": "Play in the in-app player" + "playInApp": "Play in the in-app player", + "unavailableTitle": "Can't play this here", + "unavailableBody": "This video isn't available — it may be private, removed, or region-restricted.", + "embedDisabledBody": "The owner disabled playback on other sites (common for auto-generated “Topic” music tracks). It still plays on YouTube.", + "openOnYoutube": "Open on YouTube" } diff --git a/frontend/src/i18n/locales/hu/player.json b/frontend/src/i18n/locales/hu/player.json index c0e1ec2..acce7d8 100644 --- a/frontend/src/i18n/locales/hu/player.json +++ b/frontend/src/i18n/locales/hu/player.json @@ -15,5 +15,9 @@ "markWatched": "Megtekintettnek jelöl", "watchedUnmark": "Megtekintve — kattints a jelölés visszavonásához", "jumpToTime": "Ugrás erre az időpontra", - "playInApp": "Lejátszás a beépített lejátszóban" + "playInApp": "Lejátszás a beépített lejátszóban", + "unavailableTitle": "Ez itt nem játszható le", + "unavailableBody": "Ez a videó nem elérhető — lehet privát, eltávolított vagy régiókorlátozott.", + "embedDisabledBody": "A tulajdonos letiltotta a lejátszást más oldalakon (gyakori az auto-generált „Topic” zenei sávoknál). A YouTube-on viszont lejátszható.", + "openOnYoutube": "Megnyitás YouTube-on" }