Merge: promote dev to prod
Some checks failed
CI / frontend (push) Failing after 32s
CI / backend (push) Failing after 34s

This commit is contained in:
npeter83 2026-06-16 12:02:38 +02:00
commit a2c3053a46
6 changed files with 60 additions and 7 deletions

View file

@ -1 +1 @@
0.5.0 0.5.1

View file

@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next";
import type { TFunction } from "i18next"; import type { TFunction } from "i18next";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import { useQuery, useQueryClient } from "@tanstack/react-query"; 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 Avatar from "./Avatar";
import AddToPlaylist from "./AddToPlaylist"; import AddToPlaylist from "./AddToPlaylist";
import { api, type Video } from "../lib/api"; import { api, type Video } from "../lib/api";
@ -237,12 +237,17 @@ export default function PlayerModal({
const navigated = currentVideoId !== active.id; const navigated = currentVideoId !== active.id;
// Title/author of a navigated-to video, read from the player (free, no API call). // 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); 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<number | null>(null);
const loadVideo = (id: string, start: number | null) => { const loadVideo = (id: string, start: number | null) => {
const p = playerRef.current; const p = playerRef.current;
if (!p || typeof p.loadVideoById !== "function") return; if (!p || typeof p.loadVideoById !== "function") return;
// Explicit start wins; otherwise resume the active item, start others at 0. // Explicit start wins; otherwise resume the active item, start others at 0.
const startSeconds = start != null ? start : id === active.id ? resumeAt : 0; const startSeconds = start != null ? start : id === active.id ? resumeAt : 0;
setPlayerError(null);
p.loadVideoById({ videoId: id, startSeconds: startSeconds || 0 }); p.loadVideoById({ videoId: id, startSeconds: startSeconds || 0 });
currentIdRef.current = id; currentIdRef.current = id;
setCurrentVideoId(id); setCurrentVideoId(id);
@ -330,6 +335,7 @@ export default function PlayerModal({
let cancelled = false; let cancelled = false;
const id = active.id; const id = active.id;
autoMarkedRef.current = false; autoMarkedRef.current = false;
setPlayerError(null);
// Auto-watch only applies to the active item — not to other videos the player // Auto-watch only applies to the active item — not to other videos the player
// navigated to via description links. // navigated to via description links.
@ -388,6 +394,9 @@ export default function PlayerModal({
if (queue && index < queue.length - 1) setPlayingId(queue[index + 1].id); 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,32 @@ export default function PlayerModal({
className="glass-card relative w-full max-w-4xl max-h-full overflow-y-auto rounded-2xl shadow-2xl" className="glass-card relative w-full max-w-4xl max-h-full overflow-y-auto rounded-2xl shadow-2xl"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
<div className="aspect-video w-full bg-black rounded-t-2xl overflow-hidden"> <div className="relative aspect-video w-full bg-black rounded-t-2xl overflow-hidden">
<div ref={mountRef} className="w-full h-full" /> {/* Hide the iframe entirely on error so YouTube's own error screen can't bleed
through our overlay. */}
<div ref={mountRef} className={`w-full h-full ${playerError != null ? "invisible" : ""}`} />
{playerError != null && (
<div className="absolute inset-0 grid place-items-center bg-bg p-6 text-center">
<div className="max-w-sm">
<AlertTriangle className="w-8 h-8 text-amber-500 mx-auto mb-3" />
<h3 className="text-base font-semibold">{t("player.unavailableTitle")}</h3>
<p className="text-sm text-muted mt-1.5 mb-4">
{playerError === 101 || playerError === 150
? t("player.embedDisabledBody")
: t("player.unavailableBody")}
</p>
<a
href={`https://www.youtube.com/watch?v=${currentVideoId}`}
target="_blank"
rel="noreferrer"
className="inline-flex items-center justify-center gap-2 px-4 py-2 rounded-xl font-semibold bg-accent text-accent-fg hover:opacity-90 transition"
>
<ExternalLink className="w-4 h-4" />
{t("player.openOnYoutube")}
</a>
</div>
</div>
)}
</div> </div>
{hasQueue && ( {hasQueue && (

View file

@ -15,5 +15,9 @@
"markWatched": "Als angesehen markieren", "markWatched": "Als angesehen markieren",
"watchedUnmark": "Angesehen — zum Aufheben klicken", "watchedUnmark": "Angesehen — zum Aufheben klicken",
"jumpToTime": "Zu dieser Stelle springen", "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"
} }

View file

@ -15,5 +15,9 @@
"markWatched": "Mark watched", "markWatched": "Mark watched",
"watchedUnmark": "Watched — click to unmark", "watchedUnmark": "Watched — click to unmark",
"jumpToTime": "Jump to this time", "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"
} }

View file

@ -15,5 +15,9 @@
"markWatched": "Megtekintettnek jelöl", "markWatched": "Megtekintettnek jelöl",
"watchedUnmark": "Megtekintve — kattints a jelölés visszavonásához", "watchedUnmark": "Megtekintve — kattints a jelölés visszavonásához",
"jumpToTime": "Ugrás erre az időpontra", "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"
} }

View file

@ -14,6 +14,14 @@ export interface ReleaseEntry {
} }
export const RELEASE_NOTES: 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", version: "0.5.0",
date: "2026-06-16", date: "2026-06-16",