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:
parent
eef0e870d4
commit
69bbdb15d2
1 changed files with 18 additions and 2 deletions
|
|
@ -68,12 +68,20 @@ export default function ChannelPage({
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
const [tab, setTab] = useState<"videos" | "about">("videos");
|
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({
|
const detail = useQuery({
|
||||||
queryKey: ["channel", channelId],
|
queryKey: ["channel", channelId],
|
||||||
queryFn: () => api.channelDetail(channelId),
|
queryFn: () => api.channelDetail(channelId),
|
||||||
});
|
});
|
||||||
const ch = detail.data;
|
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.
|
// Background auto-ingest ("explore") of an un-subscribed channel's uploads.
|
||||||
const [nextToken, setNextToken] = useState<string | null | undefined>(undefined);
|
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]">
|
<main className="flex-1 min-w-0 overflow-y-auto [scrollbar-gutter:stable]">
|
||||||
{/* Banner + back — OPTION C: inset, rounded card (not full-bleed) */}
|
{/* Banner + back — OPTION C: inset, rounded card (not full-bleed) */}
|
||||||
<div className="relative px-4 sm:px-6 pt-3">
|
<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
|
// 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
|
// low default size, so request a crisp wide version (=w1707) and object-cover the
|
||||||
// desktop "safe area" — the centre 2560×423 (~6:1) band.
|
// desktop "safe area" — the centre 2560×423 (~6:1) band.
|
||||||
|
|
@ -206,9 +214,17 @@ export default function ChannelPage({
|
||||||
style={{ aspectRatio: "2560 / 423", maxHeight: "150px" }}
|
style={{ aspectRatio: "2560 / 423", maxHeight: "150px" }}
|
||||||
>
|
>
|
||||||
<img
|
<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=""
|
alt=""
|
||||||
className="w-full h-full object-cover object-center"
|
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>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue