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