fix(ingest,proxy): pusta strona to nie koniec katalogu + miniaturki blokowane po IP

naughtymachinima (zgloszenie 83eb300d, "tylko 148 scen"): deep-crawl uznal katalog
za wyczerpany na stronie 8, bo trafil na pusta strone. Listing ma 1770 stron po 30
filmow, a ponowne wejscie na strone 9 oddalo 13 scen i ZERO bledow pobrania, czyli
tamto zero bylo chwilowym dlawieniem. Pusta strona bywa tez calkiem legalna, bo
bramka odrzuca klipy < 180 s, a to 2/3 tego katalogu. Konczymy wiec dopiero po trzech
pustych stronach pod rzad. Kursor odblokowany, kontrolny run przeszedl strony 9-14:
49 scen, 7 nowych, bez oznaczenia exhausted.

fullvideosporn (zgloszenie f4995691, "brak miniaturek"): adresy w bazie sa poprawne,
ale tn.sextu.com odrzuca nasz adres serwerowni: 429 przy kazdej probie, takze po
przerwie, a przez residential proxy 200 i 26 kB. Proxy obrazkow po odmowie
(401/403/429) ponawia teraz raz przez to proxy. Obrazek ma kilkadziesiat kB i klient
cachuje go dobe, wiec to nie jest proxowanie wideo. Sprawdzone: 6/6 miniaturek
laduje sie (15-116 kB), wczesniej 0/6.

Uwaga na przyszlosc: helper wladowal sie przy pierwszym podejsciu MIEDZY dekorator
trasy a proxy_image, wiec @router.get udekorowal jego, a /proxy/img/ oddawalo URL
proxy z loginem i haslem jako JSON. Naprawione, w docstringu ostrzezenie.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
goon-foss 2026-08-20 16:43:29 +02:00
parent fa7fb668b6
commit e26c78df24
2 changed files with 63 additions and 3 deletions

View file

@ -315,6 +315,19 @@ def sign_url(
return {"token": make_token(url, referer), "expires_in": TOKEN_TTL_SEC}
@functools.lru_cache(maxsize=1)
def _fallback_proxy_url() -> str | None:
"""Residential proxy, uzywany TYLKO gdy CDN odrzucil nasz adres IP.
NIE wstawiaj niczego miedzy dekorator trasy a `proxy_image`: przy pierwszym podejsciu
ta funkcja wladowala sie wlasnie tam i `@router.get` udekorowal JA, wiec /proxy/img/
oddawalo URL proxy razem z loginem i haslem jako JSON.
"""
from app.config import get_settings
return getattr(get_settings(), "brightdata_proxy_url", None) or None
@router.get("/img/{token}/{_basename:path}")
async def proxy_image(
token: str,
@ -365,6 +378,26 @@ async def proxy_image(
r = r2
except Exception as e:
log.info("img proxy retry z wlasnym originem padl %s: %s", target, e)
if r.status_code in (401, 403, 429):
# Ostatnia deska ratunku: CDN blokuje NASZ adres IP, nie zapytanie. Miniaturki
# fullvideosporn (tn.sextu.com) oddaja z serwerowni 429 przy kazdej probie
# (3/3, tez po przerwie), a przez residential proxy 200 i 26 kB. Stad puste
# kafelki mimo poprawnych adresow w bazie (zgloszenie f4995691). Obrazek ma
# kilkadziesiat kB i klient cachuje go na dobe, wiec to nie jest ten sam
# przypadek co proxowanie wideo. Sciagamy tylko wtedy, gdy host nas odrzucil.
proxy_url = _fallback_proxy_url()
if proxy_url:
try:
async with httpx.AsyncClient(
timeout=timeout, follow_redirects=True, proxy=proxy_url
) as pclient:
r3 = await pclient.get(target, headers=headers)
if r3.status_code < 400:
r = r3
else:
log.info("img proxy: proxy tez oddal %d dla %s", r3.status_code, target)
except Exception as e:
log.info("img proxy: proba przez proxy padla %s: %s", target, e)
if r.status_code >= 400:
# Upstream 4xx/5xx dla thumba, degraded zamiast raise (placeholder w mobile).
# GOON-5 (Cloudflare 523) i GOON-D, bezsensowny noise w Sentry, lepiej

View file

@ -108,6 +108,12 @@ def _pick_target(state: dict, targets: list[str]) -> str | None:
return live[0]
#: Ile pustych stron POD RZAD uznajemy za koniec katalogu. Jedna nie wystarcza, patrz
#: komentarz w petli: serwis moze chwilowo dlawic, a strona moze byc w calosci odrzucona
#: przez bramki jakosci.
_EMPTY_PAGES_BEFORE_END = 3
def run_deep_crawl(*, pages_per_run: int = 60, sitetags: list[str] | None = None) -> dict:
"""Jeden run: wybierz tube, crawl kolejne `pages_per_run` stron od kursora, ingest.
Zwraca podsumowanie (sitetag, zakres stron, counters, exhausted)."""
@ -142,6 +148,7 @@ def run_deep_crawl(*, pages_per_run: int = 60, sitetags: list[str] | None = None
t0 = time.time()
last_done = start - 1
exhausted = False
empty_streak = 0
budget_hit = False
st = state.setdefault(sitetag, {})
@ -168,10 +175,30 @@ def run_deep_crawl(*, pages_per_run: int = 60, sitetags: list[str] | None = None
# transient fetch-fail listingu, NIE awansuj kursora, następny run powtórzy
break
if not scenes:
log.info("deep-crawl %s: empty page %d → catalog end (exhausted)", sitetag, page)
exhausted = True
# Pusta strona to NIE dowod konca katalogu. Dwa powody, oba realne:
# 1. Serwis chwilowo dlawi ruch, wiec wszystkie fetche szczegolow z tej
# strony padaja i zostaje pusta lista.
# 2. Na stronie faktycznie nic nie przeszlo naszych bramek (np.
# naughtymachinima odrzuca klipy < 180 s, a to 2/3 jego katalogu).
# Naughtymachinima zostalo tak oznaczone jako `exhausted` na stronie 8,
# mimo ze listing ma 1770 stron po 30 filmow, a ponowne wejscie na
# strone 9 oddalo 13 scen i zero bledow pobrania. Zostalo 158 scen
# zamiast tysiecy (zgloszenie 83eb300d). Dlatego konczymy dopiero po
# kilku pustych stronach POD RZAD.
empty_streak += 1
last_done = page
_persist()
if empty_streak >= _EMPTY_PAGES_BEFORE_END:
log.info(
"deep-crawl %s: %d pustych stron pod rzad (do %d) → koniec katalogu",
sitetag, empty_streak, page,
)
exhausted = True
break
log.info("deep-crawl %s: pusta strona %d (%d/%d), lece dalej",
sitetag, page, empty_streak, _EMPTY_PAGES_BEFORE_END)
continue
empty_streak = 0
page_is_backfill = (page > _LATEST_PAGE_THRESHOLD) and not swept_once
for raw in scenes:
counters["seen"] += 1