Commit Graph

27 Commits

Author SHA1 Message Date
Simon
b48d7aa161 Reuse upstream connections, and stop sniffing what we already know
Two costs sat in front of every video: a TLS handshake per upstream request,
and a round trip spent asking the proxy what kind of file it was about to
play.

The session cache was a thread-local, which never once hit -- the server
gives each connection a fresh thread, so every request found empty storage
and built a session, and with it a new connection to the CDN. Instrumented,
that was one session per request; a video is dozens of range requests and an
HLS stream one per segment. Sessions now live in a shared pool, checked out
for a request and returned when its response closes (for a streamed body,
after the last byte), so the connection stays warm. Measured against a
nearby CDN: 32-45ms per request becomes 9-11ms.

The player then HEADed the proxy before playback to sniff a content type --
and that HEAD ran a full upstream GET server-side, so two connections were
opened before the first byte of video was asked for. It now sniffs only when
neither the URL's extension nor yt-dlp's `protocol` says what the source is,
which is nearly never; `protocol` is newly carried through /api/resolve for
exactly this. A HEAD that does still happen asks upstream for one byte and
restates the 206 as a 200 describing the whole resource.

`format_note` joins the resolved fields too: the quality menu has been
reading it since 52d7802, but the backend was dropping it, so no note could
ever have been shown.

Tests (scratchpad, headless): sessions reused across requests, never shared
by two at once, returned after a client aborts mid-stream; HEAD probes one
byte while ranged and plain GETs are byte-identical; no HEAD for an mp4 or a
protocol-bearing URL, and one for a URL with neither.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QBDkEXP4htyXTCZUwMLphd
2026-09-05 23:27:24 +00:00
Simon
d508263946 Don't re-resolve a favorite whose URL is already the media file
Some channels hand back the media URL itself as an item's url. Since the
favorites fix, opening one of those sent it to /api/resolve first, so
yt-dlp fetched the media just to report the URL we already had. On a
signed link (`?secure=<ts>-<token>`) that is a second request against
something that may be single-use or IP-bound, and the request that
matters -- the playback fetch -- is then refused. Such URLs now play
directly, with no resolve round trip, as they did before.

Alongside that, three things that make expiry survivable:

/api/stream, after its existing referer-less retry, now retries a 403
completely bare (Range only). Signed CDN links are routinely served to a
plain browser request and refused when it carries extras -- a
`Sec-Fetch-Mode: navigate` on a media subresource, say, which is what
yt-dlp's generic extractor hands back and no real player would send.

When every source fails, the player re-resolves once and retries instead
of giving up, since the likeliest cause is that signed URLs went stale in
a long-open tab rather than the video being gone. A manual quality pick
is dropped for that retry, as it names one of the URLs that just failed.

Favorites stored by older versions still carry a `meta` blob of resolved
formats, long expired; it's now stripped on read so nothing can reach for
one.

Verified: a favorite whose url is a .mp4 plays with zero /api/resolve
calls, straight from that URL; playback, prefetch, feed paging, HUD,
rotation, momentum and the version check all still pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QBDkEXP4htyXTCZUwMLphd
2026-09-05 20:41:31 +00:00
Simon
0f7e27fd77 Stamp asset URLs with their content hash
index.html is revalidated on every load, but the assets it names are not
under our control once they leave the origin: Cloudflare rewrites our
`Cache-Control: no-cache` on /static/* to `max-age=14400`, so a phone --
an iOS home-screen app above all, which keeps running whatever it has --
can execute four-hour-old JavaScript after a deploy.

The URLs now carry the file's content hash (static/js/main.js?v=<hash>),
reusing the manifest /api/version already computes, so every deploy asks
for URLs no cache can answer from an old copy. index.html itself gets an
explicit no-cache, must-revalidate.

Verified: served HTML carries per-file hashes, a changed file yields a
new URL, the app boots clean, and the refresh button's update path still
hot-swaps CSS and reloads for JS.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QBDkEXP4htyXTCZUwMLphd
2026-09-05 16:02:11 +00:00
Simon
d7086ead27 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
2026-09-05 14:26:15 +00:00
Simon
382a637b95 version updates 2026-06-24 20:54:16 +00:00
Simon
9fe7511b4d header bugfix 2026-06-24 20:08:08 +00:00
Simon
d6865d7c35 advanced probing 2026-06-23 12:44:13 +00:00
Simon
3d4b90b0e1 default impersonation on yt-dlp 2026-06-23 06:19:50 +00:00
Simon
b5b3e13dd0 live stream support 2026-06-22 12:34:47 +00:00
Simon
95b75bf999 yt-dlp impersonation tagets 2026-06-19 08:50:04 +00:00
Simon
5826ba6b8a install current version of yt-dlp with docker 2026-06-19 08:30:32 +00:00
Simon
988e11b159 fix missing headers in video get requests 2026-06-18 11:55:08 +00:00
Simon
f8072884b2 preferred quality setting 2026-06-17 15:44:20 +00:00
Simon
d73e413352 backend improvements 2026-02-12 17:40:45 +00:00
Simon
081493d13f request information with referer 2026-02-10 17:57:59 +00:00
Simon
c2872c1883 detect m3u8 is actually mp4 2026-02-09 18:35:39 +00:00
Simon
5baca567cb beeg fixed 2026-02-09 18:27:26 +00:00
Simon
7b90c05a29 load image fallback 2026-02-09 16:28:01 +00:00
Simon
6915da7f85 improved video play 2026-02-08 20:44:36 +00:00
Simon
407e3bf9c6 improved yt-dlp 2026-02-08 20:11:07 +00:00
Simon
313ba70fec improved video streams 2026-02-08 20:08:23 +00:00
Simon
1becdce9ff favicon 2026-02-08 17:16:48 +00:00
Simon
c67a5cde16 handle application/vnd.apple.mpegurl. 2026-02-08 15:23:01 +00:00
Simon
62c7bfd694 updated dependencies versions 2026-01-30 12:54:37 +00:00
Simon
273e7c61f3 basic functionality running 2026-01-30 11:24:19 +00:00
Simon
6762fb9513 requirements 2026-01-28 16:10:13 +00:00
Simon
3a9011690c first commit 2026-01-28 16:02:57 +00:00