Play favorites from freshly resolved formats, not the page URL

Streaming a page URL makes /api/stream re-run yt-dlp on every request --
slow, and a 500 on some sites -- so the player and the reels feed now
wait for App.videos.ensureFormats() when an item has no formats
(favorites, or a card clicked before its hover-resolve landed) and play a
real media URL with the extractor's headers, the same path a hovered card
takes. Favorites' download does the same. The page URL survives only as a
last resort when resolution yields nothing.

Two things in the proxy kept this site broken either way:

heavyfetish serves media from paths with a trailing slash
(/get_file/.../11097_720p.mp4/), which missed every extension test in
stream_video and sent even a resolved media URL down the yt-dlp branch --
a full extraction per request, including every seek.

Its CDN (st17.heavyfetish.com) also serves a certificate that expired
2026-02-16, so the upstream fetch failed verification and returned 500.
A browser can't play such a host at all, which is much of why this proxy
exists, so impersonate_get() now retries once without verification, logs
it, and remembers the host so the doomed handshake isn't repeated for
every range request. STREAM_TLS_VERIFY_ONLY=1 restores the hard failure.

Verified in headless Chrome against the real site: a favorite holding
only the page URL now resolves, streams (206, video/mp4, duration
3167.8s, readyState 4, no error) and shows "720p mp4 | 480p mp4" in the
quality menu.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QBDkEXP4htyXTCZUwMLphd
This commit is contained in:
Simon
2026-09-05 14:26:15 +00:00
parent 25dad88ed9
commit d7086ead27
4 changed files with 89 additions and 21 deletions

View File

@@ -187,8 +187,10 @@ App.favorites = App.favorites || {};
if (downloadBtn) {
downloadBtn.onclick = (event) => {
event.stopPropagation();
App.videos.downloadVideo(item);
App.videos.closeAllMenus();
// Same as playback: resolve a real media URL first rather
// than pointing the download at the page URL.
App.videos.ensureFormats(item).then(() => App.videos.downloadVideo(item));
};
}
const uploaderBtn = card.querySelector('.uploader-link');

View File

@@ -331,6 +331,21 @@ App.feed = App.feed || {};
}
return;
}
// A slide whose formats haven't been resolved yet carries only a page
// URL, and handing that to /api/stream makes the backend re-run yt-dlp
// per request (slow, and a hard failure on some sites). Resolve once,
// then load for real -- `_awaitingFormats` keeps a failed resolve from
// looping, so we still fall back to the page URL as a last resort.
const meta = videoData && (videoData.meta || videoData);
const hasFormats = !!(meta && Array.isArray(meta.formats) && meta.formats.length);
if (!hasFormats && !slide._awaitingFormats && App.videos && typeof App.videos.ensureFormats === 'function') {
slide._awaitingFormats = true;
App.videos.ensureFormats(videoData).then(() => {
if (slide._videoData === videoData) loadSlideSource(slide, videoData, autoplay);
});
return;
}
slide.classList.add('is-loaded');
const resolved = slide._formatOverride

View File

@@ -815,19 +815,28 @@ App.player = App.player || {};
});
let destroyFormatMenu = bindFormats();
addCleanup(() => destroyFormatMenu());
const meta = (source && typeof source === 'object') ? (source.meta || source) : null;
const hasFormats = !!(meta && Array.isArray(meta.formats) && meta.formats.length);
// Items that arrive without formats -- a listing card clicked before its
// hover-resolve finished, or a favorite (which deliberately stores no
// resolved formats, since their URLs expire) -- would otherwise show an
// empty quality menu. Playback already starts from the page URL via the
// proxy, so resolve in the background and rebuild the menu with the real
// qualities as soon as they land.
if (App.videos && typeof App.videos.ensureFormats === 'function') {
App.videos.ensureFormats(source).then((meta) => {
// A later open() may have taken over in the meantime; its own
// bindFormats() owns the menu then.
if (!meta || cp.source !== source) return;
destroyFormatMenu();
destroyFormatMenu = bindFormats();
// resolved formats, since their URLs expire) -- carry only a page URL,
// and they'd also show an empty quality menu. Resolve them first: the
// page-URL fallback makes /api/stream re-run yt-dlp on every single
// request, which is slow and fails outright on some sites, whereas a
// resolved format is a real media URL with the extractor's headers --
// the same path a hovered card plays through.
let deferredStart = false;
if (!hasFormats && App.videos && typeof App.videos.ensureFormats === 'function') {
deferredStart = true;
App.videos.ensureFormats(source).then((resolved) => {
// A later open() (or a close) may have taken over in the
// meantime; that session owns the player now.
if (cp.source !== source) return;
if (resolved) {
destroyFormatMenu();
destroyFormatMenu = bindFormats();
}
playSources(source, { originEl: cp.originEl });
});
}
bindGestures(cp.video);
@@ -840,7 +849,9 @@ App.player = App.player || {};
document.body.style.overflow = 'hidden';
wakeHud();
playSources(source, { originEl: cp.originEl });
// Already-resolved sources start immediately; unresolved ones start from
// the ensureFormats() callback above (the spinner is already up).
if (!deferredStart) playSources(source, { originEl: cp.originEl });
};
App.player.close = function(opts) {