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

@@ -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) {