From e26c78df24c69e9a77b6d00b13e5bab0a924bd61 Mon Sep 17 00:00:00 2001 From: goon-foss Date: Thu, 20 Aug 2026 16:43:29 +0200 Subject: [PATCH] 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 --- app/api/stream_proxy.py | 33 +++++++++++++++++++++++++++++++++ app/scheduler/deep_crawl.py | 33 ++++++++++++++++++++++++++++++--- 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/app/api/stream_proxy.py b/app/api/stream_proxy.py index ede282e..6c6cf7e 100644 --- a/app/api/stream_proxy.py +++ b/app/api/stream_proxy.py @@ -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 diff --git a/app/scheduler/deep_crawl.py b/app/scheduler/deep_crawl.py index b2310de..955cc15 100644 --- a/app/scheduler/deep_crawl.py +++ b/app/scheduler/deep_crawl.py @@ -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 - break + _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