From c2d9e7b5a9fb3294663fd48a1181af8b26baefb8 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Tue, 16 Jun 2026 10:45:57 +0200 Subject: [PATCH 1/3] 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" } From de4d657d42c3a119b90d73b55d49020598bfb8f8 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Tue, 16 Jun 2026 11:58:57 +0200 Subject: [PATCH 2/3] fix(player): make the unavailable overlay fully opaque The fallback used a translucent background and left the iframe mounted, so YouTube's own 'Video unavailable' screen bled through and overlapped our message. Use a solid background and hide the iframe while the error shows. --- frontend/src/components/PlayerModal.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/PlayerModal.tsx b/frontend/src/components/PlayerModal.tsx index c0df596..1d38fed 100644 --- a/frontend/src/components/PlayerModal.tsx +++ b/frontend/src/components/PlayerModal.tsx @@ -437,9 +437,11 @@ export default function PlayerModal({ onClick={(e) => e.stopPropagation()} >
-
+ {/* Hide the iframe entirely on error so YouTube's own error screen can't bleed + through our overlay. */} +
{playerError != null && ( -
+

{t("player.unavailableTitle")}

From e6c53b3f96cc640f438daf84090020e1331f05a1 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Tue, 16 Jun 2026 12:02:38 +0200 Subject: [PATCH 3/3] =?UTF-8?q?chore(release):=20v0.5.1=20=E2=80=94=20grac?= =?UTF-8?q?eful=20fallback=20for=20non-embeddable=20videos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- VERSION | 2 +- frontend/src/lib/releaseNotes.ts | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 8f0916f..4b9fcbe 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.5.0 +0.5.1 diff --git a/frontend/src/lib/releaseNotes.ts b/frontend/src/lib/releaseNotes.ts index b01edfa..ae13403 100644 --- a/frontend/src/lib/releaseNotes.ts +++ b/frontend/src/lib/releaseNotes.ts @@ -14,6 +14,14 @@ export interface ReleaseEntry { } export const RELEASE_NOTES: ReleaseEntry[] = [ + { + version: "0.5.1", + date: "2026-06-16", + summary: "A clearer message when a video can't play in the in-app player.", + fixes: [ + "When a video can't be played here — some music tracks and other videos have embedding disabled by their owner — the player now shows a clear note and an “Open on YouTube” button instead of a blank error screen.", + ], + }, { version: "0.5.0", date: "2026-06-16",