feat(sxyprn): on-demand thumbnail resolver (live posters, ~1h-TTL workaround)
trafficdeposit poster tokens live ~1h (hour-bucketed), so stored URLs can't persist.
New GET /proxy/sxyprn-thumb/{post_id}: resolves the current og:image from the live
/post/<id> page (cache resolved poster URL ~40min), streams bytes with Referer +
long client Cache-Control (URL is stable per post_id → client disk-caches the image,
backend fetches each post ~once). Deleted posts ("Post Not Found") → 404.
Scene grid now emits /proxy/sxyprn-thumb/<id> for sxyprn sources (derived from
page_url) instead of the dead stored trafficdeposit URL. Verified: live post → 200
image, deleted → 404, grid emits resolver URL. Backend-only, no OTA.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
f7670963df
commit
08079787da
2 changed files with 80 additions and 1 deletions
|
|
@ -442,6 +442,19 @@ def get_scene(
|
||||||
return _build_scene_out(session, scene, device_id=device_id)
|
return _build_scene_out(session, scene, device_id=device_id)
|
||||||
|
|
||||||
|
|
||||||
|
_SXYPRN_POST_RE = re.compile(r"sxyprn\.com/post/([0-9a-f]{6,40})", re.IGNORECASE)
|
||||||
|
|
||||||
|
|
||||||
|
def _sxyprn_thumb_url(page_url: str | None) -> str | None:
|
||||||
|
"""Dla źródła sxyprn zwraca STABILNY endpoint on-demand resolvera
|
||||||
|
(`/proxy/sxyprn-thumb/<id>`) zamiast martwego trafficdeposit URL — token żyje ~1h,
|
||||||
|
więc poster resolvujemy przy serwowaniu (bug 2026-06-10)."""
|
||||||
|
if not page_url:
|
||||||
|
return None
|
||||||
|
m = _SXYPRN_POST_RE.search(page_url)
|
||||||
|
return f"/proxy/sxyprn-thumb/{m.group(1)}" if m else None
|
||||||
|
|
||||||
|
|
||||||
def _is_rotting_thumb(url: str) -> bool:
|
def _is_rotting_thumb(url: str) -> bool:
|
||||||
"""sxyprn/trafficdeposit miniaturki są czasowo podpisane i rotują (asset 404 po
|
"""sxyprn/trafficdeposit miniaturki są czasowo podpisane i rotują (asset 404 po
|
||||||
~tygodniach, nie odświeżalne server-side; bug 2026-06-10). De-prioritize je w wyborze
|
~tygodniach, nie odświeżalne server-side; bug 2026-06-10). De-prioritize je w wyborze
|
||||||
|
|
@ -573,7 +586,12 @@ def _build_scenes_out_batch(
|
||||||
thumb_fallback: dict = {}
|
thumb_fallback: dict = {}
|
||||||
anim_by_scene: dict = {}
|
anim_by_scene: dict = {}
|
||||||
for sid, thumb, anim, page_url in pb_light:
|
for sid, thumb, anim, page_url in pb_light:
|
||||||
if thumb:
|
sxy = _sxyprn_thumb_url(page_url)
|
||||||
|
if sxy:
|
||||||
|
# sxyprn → żywy on-demand resolver (martwy stored URL ignorujemy),
|
||||||
|
# tier fallback: użyty tylko gdy scena nie ma stabilniejszej miniatury.
|
||||||
|
thumb_fallback.setdefault(sid, (sxy, page_url))
|
||||||
|
elif thumb:
|
||||||
if _is_rotting_thumb(thumb):
|
if _is_rotting_thumb(thumb):
|
||||||
thumb_fallback.setdefault(sid, (thumb, page_url))
|
thumb_fallback.setdefault(sid, (thumb, page_url))
|
||||||
elif sid not in thumb_by_scene:
|
elif sid not in thumb_by_scene:
|
||||||
|
|
|
||||||
|
|
@ -335,6 +335,67 @@ async def proxy_image(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# sxyprn on-demand thumbnail resolve (bug 2026-06-10). trafficdeposit poster token
|
||||||
|
# żyje ~1h (bucket godzinowy), więc URL-i NIE da się przechować — resolvujemy bieżący
|
||||||
|
# og:image ze strony /post/<id> przy serwowaniu. Cache resolved poster URL ~40min
|
||||||
|
# (< 1h TTL). Klient dostaje STABILNY /proxy/sxyprn-thumb/<id> → cache'uje bajty na
|
||||||
|
# stałe (treść postera niezmienna), więc fetchujemy stronę post ~raz per post.
|
||||||
|
_SXYPRN_POSTER_CACHE: dict[str, tuple[str, float]] = {}
|
||||||
|
_SXYPRN_POSTER_TTL = 2400
|
||||||
|
_OG_IMG_RE = re.compile(r"og:image[\"'][^>]*content=[\"']([^\"']+)", re.IGNORECASE)
|
||||||
|
_OG_IMG_RE2 = re.compile(r"content=[\"']([^\"']+)[\"'][^>]*property=[\"']og:image", re.IGNORECASE)
|
||||||
|
_VID_POSTER_RE = re.compile(r"<video[^>]*poster=[\"']([^\"']+)", re.IGNORECASE)
|
||||||
|
_SXYPRN_PID_RE = re.compile(r"^[0-9a-f]{6,40}$")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/sxyprn-thumb/{post_id}")
|
||||||
|
async def sxyprn_thumb(post_id: str) -> Response:
|
||||||
|
"""On-demand poster sxyprn. URL stabilny per post_id (klient cache'uje bajty);
|
||||||
|
backend resolvuje bieżący og:image (token ~1h) i streamuje z Refererem."""
|
||||||
|
pid = post_id.split(".")[0] # zdejmij ewentualne .jpg
|
||||||
|
if not _SXYPRN_PID_RE.match(pid):
|
||||||
|
raise HTTPException(status_code=400, detail="bad post_id")
|
||||||
|
now = time.time()
|
||||||
|
cached = _SXYPRN_POSTER_CACHE.get(pid)
|
||||||
|
poster = cached[0] if (cached and cached[1] > now) else None
|
||||||
|
timeout = httpx.Timeout(connect=10.0, read=20.0, write=10.0, pool=5.0)
|
||||||
|
async with httpx.AsyncClient(timeout=timeout, follow_redirects=True) as client:
|
||||||
|
if poster is None:
|
||||||
|
try:
|
||||||
|
r = await client.get(
|
||||||
|
f"https://sxyprn.com/post/{pid}.html", headers=_build_headers(None)
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
log.info("sxyprn-thumb page fetch failed %s: %s", pid, e)
|
||||||
|
return Response(content=b"", status_code=502, media_type="image/jpeg")
|
||||||
|
html = r.text
|
||||||
|
if "Post Not Found" in html:
|
||||||
|
return Response(content=b"", status_code=404, media_type="image/jpeg")
|
||||||
|
m = _OG_IMG_RE.search(html) or _OG_IMG_RE2.search(html) or _VID_POSTER_RE.search(html)
|
||||||
|
if not m:
|
||||||
|
return Response(content=b"", status_code=404, media_type="image/jpeg")
|
||||||
|
poster = m.group(1).strip()
|
||||||
|
if poster.startswith("//"):
|
||||||
|
poster = "https:" + poster
|
||||||
|
_SXYPRN_POSTER_CACHE[pid] = (poster, now + _SXYPRN_POSTER_TTL)
|
||||||
|
if len(_SXYPRN_POSTER_CACHE) > 8000:
|
||||||
|
for k in [k for k, v in list(_SXYPRN_POSTER_CACHE.items()) if v[1] < now]:
|
||||||
|
_SXYPRN_POSTER_CACHE.pop(k, None)
|
||||||
|
try:
|
||||||
|
pr = await client.get(poster, headers=_build_headers("https://sxyprn.com/"))
|
||||||
|
except Exception as e:
|
||||||
|
log.info("sxyprn-thumb poster fetch failed %s: %s", pid, e)
|
||||||
|
return Response(content=b"", status_code=502, media_type="image/jpeg")
|
||||||
|
if pr.status_code >= 400:
|
||||||
|
_SXYPRN_POSTER_CACHE.pop(pid, None) # stale token → re-resolve next time
|
||||||
|
return Response(content=b"", status_code=502, media_type="image/jpeg")
|
||||||
|
return Response(
|
||||||
|
content=pr.content,
|
||||||
|
media_type=pr.headers.get("content-type", "image/jpeg"),
|
||||||
|
headers={"Cache-Control": "public, max-age=604800"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def _refetch_mixdrop_url(session: "AsyncSession", embed_url: str) -> str | None:
|
async def _refetch_mixdrop_url(session: "AsyncSession", embed_url: str) -> str | None:
|
||||||
"""Re-fetch mixdrop embed, decode P.A.C.K.E.R., extract fresh MDCore.wurl.
|
"""Re-fetch mixdrop embed, decode P.A.C.K.E.R., extract fresh MDCore.wurl.
|
||||||
Cookies persist w session, użytkowane potem do mp4 GET (same-session bind).
|
Cookies persist w session, użytkowane potem do mp4 GET (same-session bind).
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue