fix(channel): recover a throttled channel banner instead of leaving it broken

The banner URL is valid, but googleusercontent intermittently drops the banner
request when the channel page fires the banner + avatar + ~16 thumbnails at once;
the browser then negative-caches the miss, so the banner stays broken with no
auto-retry (even though the same URL loads fine on a fresh request). On error we
now retry a few times with a ?r= cache-buster (a fresh URL that bypasses the
negative cache), and only fall back to the blank bar if it keeps failing (a
genuinely dead URL). Fixes the broken-banner on channel pages like BASSHUNTER.
This commit is contained in:
npeter83 2026-07-12 16:54:55 +02:00
parent eef0e870d4
commit 69bbdb15d2

View file

@ -68,12 +68,20 @@ export default function ChannelPage({
const qc = useQueryClient();
const confirm = useConfirm();
const [tab, setTab] = useState<"videos" | "about">("videos");
// The banner URL is valid, but googleusercontent intermittently throttles it when the channel
// page fires the banner + avatar + ~16 thumbnails in one burst — the request is dropped and the
// browser then NEGATIVE-CACHES the miss, so the banner stays broken even though a retry would
// succeed. So on error we retry a few times with a cache-buster (forcing a fresh request), and
// only fall back to the blank bar if it keeps failing (a genuinely dead URL — rare).
const MAX_BANNER_RETRIES = 3;
const [bannerAttempt, setBannerAttempt] = useState(0);
const detail = useQuery({
queryKey: ["channel", channelId],
queryFn: () => api.channelDetail(channelId),
});
const ch = detail.data;
useEffect(() => setBannerAttempt(0), [ch?.banner_url]); // fresh channel/banner → try again
// Background auto-ingest ("explore") of an un-subscribed channel's uploads.
const [nextToken, setNextToken] = useState<string | null | undefined>(undefined);
@ -197,7 +205,7 @@ export default function ChannelPage({
<main className="flex-1 min-w-0 overflow-y-auto [scrollbar-gutter:stable]">
{/* Banner + back — OPTION C: inset, rounded card (not full-bleed) */}
<div className="relative px-4 sm:px-6 pt-3">
{ch?.banner_url ? (
{ch?.banner_url && bannerAttempt <= MAX_BANNER_RETRIES ? (
// Match YouTube's banner: the stored bannerExternalUrl is the full 16:9 template at a
// low default size, so request a crisp wide version (=w1707) and object-cover the
// desktop "safe area" — the centre 2560×423 (~6:1) band.
@ -206,9 +214,17 @@ export default function ChannelPage({
style={{ aspectRatio: "2560 / 423", maxHeight: "150px" }}
>
<img
src={`${ch.banner_url}=w1707`}
// key forces a fresh <img> per attempt; the ?r= cache-buster on a retry bypasses the
// browser's negative cache (googleusercontent accepts an extra query param).
key={bannerAttempt}
src={`${ch.banner_url}=w1707${bannerAttempt ? `?r=${bannerAttempt}` : ""}`}
alt=""
className="w-full h-full object-cover object-center"
onError={() => {
if (bannerAttempt <= MAX_BANNER_RETRIES) {
window.setTimeout(() => setBannerAttempt((a) => a + 1), 500 * (bannerAttempt + 1));
}
}}
/>
</div>
) : (