perf: keep-alive upstream sessions, TTL cache on upstream data, /stream LRU, cache hls.min.js
- shared thread-local curl_cffi sessions (no TLS handshake per call) - in-memory TTL cache: search 5min, details/episodes/languages/ playlist-URLs 1h; api_stream retries once with a cold cache on 502 (recovers rotated playlist URLs) - /stream LRU (24 entries) serves re-seeks and playlist re-fetches from memory - hls.min.js: Cache-Control public max-age=30d (was no-store -> 500KB re-download per page load on mobile) Measured (click->play chain, cold -> warm): search 0.45s -> 0.00s detail 0.46s -> 0.00s episodes 0.40s -> 0.00s stream 1.06s -> 0.00s m3u8 x2 0.64s -> 0.00s segment 0.20s -> 0.00s
This commit is contained in:
1 parent
ca86daf9a6
commit
19e878800c
3 files changed
+84
-14
No files matched your search
+34
-7
@@ -9,6 +9,8 @@ import json
|
||||
import os
|
||||
import time
|
||||
from pathlib import Path
|
||||
from base64 import urlsafe_b64encode, urlsafe_b64decode
|
||||
from collections import OrderedDict
|
||||
from urllib.parse import quote, urljoin
|
||||
|
||||
from fastapi import Body, FastAPI, HTTPException, Query, Request
|
||||
@@ -179,9 +181,13 @@ def _upstream(fn, *a, **kw):
|
||||
|
||||
@app.middleware("http")
|
||||
async def no_cache(request, call_next):
|
||||
# ponytail: no-store on everything except /stream (segments should cache)
|
||||
# ponytail: no-store on everything except /stream (immutable HLS
|
||||
# data) and the vendored hls.min.js (immutable, 500KB — re-downloading
|
||||
# it on every page load hurt on mobile)
|
||||
r = await call_next(request)
|
||||
if not request.url.path.startswith("/stream"):
|
||||
if request.url.path == "/hls.min.js":
|
||||
r.headers["Cache-Control"] = "public, max-age=2592000"
|
||||
elif not request.url.path.startswith("/stream"):
|
||||
r.headers["Cache-Control"] = "no-store"
|
||||
return r
|
||||
|
||||
@@ -203,11 +209,21 @@ def api_episodes(anime_id: int):
|
||||
|
||||
@app.get("/api/episode/{ep_id}/stream")
|
||||
def api_stream(ep_id: int, lang: str = "eng"):
|
||||
master = _upstream(upstream.master_m3u8, ep_id, lang)
|
||||
if not master:
|
||||
raise HTTPException(404, f"no {lang} stream for episode {ep_id}")
|
||||
return {"token": _b64(master),
|
||||
"qualities": _upstream(upstream.master_variants, master)}
|
||||
def go():
|
||||
master = _upstream(upstream.master_m3u8, ep_id, lang)
|
||||
if not master:
|
||||
raise HTTPException(404, f"no {lang} stream for episode {ep_id}")
|
||||
return {"token": _b64(master),
|
||||
"qualities": _upstream(upstream.master_variants, master)}
|
||||
try:
|
||||
return go()
|
||||
except HTTPException as e:
|
||||
# one-shot retry with a cold cache: recovers rotated/expired
|
||||
# playlist URLs from the TTL cache
|
||||
if e.status_code == 502:
|
||||
upstream.clear_cache()
|
||||
return go()
|
||||
raise
|
||||
|
||||
|
||||
def _rewrite_m3u8(text: str, base: str) -> str:
|
||||
@@ -219,14 +235,25 @@ def _rewrite_m3u8(text: str, base: str) -> str:
|
||||
for line in text.splitlines()) + "\n"
|
||||
|
||||
|
||||
_STREAM_CACHE = OrderedDict() # ponytail: LRU of proxied HLS data (immutable);
|
||||
_STREAM_MAX = 24 # segments ~1-2MB -> <= ~48MB
|
||||
|
||||
|
||||
@app.get("/stream/{token}")
|
||||
def stream(token: str):
|
||||
# ponytail: buffers whole response in memory; .ts segments are small
|
||||
url = _url(token)
|
||||
hit = _STREAM_CACHE.get(url)
|
||||
if hit:
|
||||
_STREAM_CACHE.move_to_end(url)
|
||||
return Response(hit[1], media_type=hit[0])
|
||||
r = _upstream(upstream._get, url)
|
||||
body = r.content
|
||||
if url.endswith(".m3u8") or "mpegurl" in r.headers.get("content-type", ""):
|
||||
body = _rewrite_m3u8(body.decode(), url).encode()
|
||||
_STREAM_CACHE[url] = (r.headers.get("content-type", "application/octet-stream"), body)
|
||||
while len(_STREAM_CACHE) > _STREAM_MAX:
|
||||
_STREAM_CACHE.popitem(last=False)
|
||||
return Response(body, media_type=r.headers.get("content-type", "application/octet-stream"))
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user