chore(seekplayer): wygaszona cala rodzina dla filmow + reaper zgodny z resolverem

Na prosbe Jana wygaszone hurtem 338 904 zrodla rodziny seekplayer (easyvidplayer,
embedseek, seekplayer, upns, player4me, rpmplay) na 26 248 filmach.

Dowody, ktore to uzasadnily, trzy niezalezne:
1. Swieze identyfikatory wziete PROSTO ze strony serwisu tego samego dnia zachowuja sie
   tak samo jak nasze z bazy: brak adresu albo 404. Czyli to NIE jest nasza staroc,
   tylko hoster nie serwuje (przy galaxyporn ten sam test wychodzil odwrotnie).
2. Pomiar per marka, po 6 probek na kazda: 0 grajacych na 36.
3. Rozkodowany ich wlasny player: komunikat "Media source is not playable" pada dopiero
   po wyczerpaniu WSZYSTKICH czterech sciezek CDN (Ce.shift() az do pustej listy). Czyli
   ich klient tez sie poddaje.

Zabezpieczenie: NIE wygaszono zrodel filmu, ktory nie ma nic innego (4 zrodla, 1 film),
zeby nic nie zniknelo z katalogu. Po operacji 0 filmow stracilo wszystkie zrodla, a te,
ktore mialy ta rodzine, maja srednio 10,6 zywych zrodel.

Odwracalne: ustawiamy dead_at + czytelny dead_reason, nie kasujemy wierszy.

Skrypt: `_probe` deleguje teraz do `seekplayer_engine.extract`, zeby werdykt byl TEN SAM
co u resolvera aplikacji. Wczesniej mial wlasna, luzniejsza regule (sa pola = zywe) i
uznawal za zywe zrodla z adresem do usunietego pliku, na ktorych CDN oddaje 502.
Usuniete nieuzywane importy i akapit docstringu, ktory po tej zmianie mowil cos
przeciwnego niz kod.
This commit is contained in:
goon-foss 2026-08-27 11:53:45 +02:00
parent 38eee03902
commit 1fd1a750e3

View file

@ -7,14 +7,12 @@ w odpowiedzi nie ma pola `source`. Ich własny player w tej sytuacji pisze wpros
„Video is not ready yet", co potwierdziłem w przeglądarce z łącza domowego, i to samo
widać z VPS oraz z brytyjskiego proxy, więc nie chodzi o blokadę po IP ani po kraju.
Skala (próbka 45 losowych źródeł, 2026-08-24): 26 razy HTTP 404, 18 razy odpowiedź bez
strumienia, 1 raz działający strumień. Czyli około 2% katalogu na tym silniku jeszcze
gra, reszty hoster już nie ma. Sam protokół jest bez zmian, dlatego NIE kasujemy tych
źródeł hurtem, tylko pytamy o każde z osobna i zostawiamy te, które odpowiadają.
Skala (próbka 45 źródeł, 2026-08-24): 26 razy HTTP 404, 18 razy odpowiedź bez strumienia,
1 raz działający strumień. Powtórka 2026-08-27 na sześciu markach: 0 grających na 36.
Bezpieczeństwo: filmów zależnych WYŁĄCZNIE od tej rodziny jest w bazie dokładnie
jeden, pozostałe 26 223 mają inne źródło. Sprzątanie nie chowa więc katalogu, tylko
przestaje podsuwać wybór, który i tak kończy się czarnym ekranem.
Bezpieczeństwo: filmów zależnych WYŁĄCZNIE od tej rodziny jest w bazie jeden, pozostałe
mają inne źródło. Sprzątanie nie chowa więc katalogu, tylko przestaje podsuwać wybór,
który i tak kończy się czarnym ekranem.
Oszczędność: domeny bliźniacze (`p.` i `vip.easyvidplayer.com`, `my.` i
`vip.player4me`) trzymają ten sam identyfikator wideo, więc pytamy raz na parę
@ -23,6 +21,13 @@ Oszczędność: domeny bliźniacze (`p.` i `vip.easyvidplayer.com`, `my.` i
Wznawianie za darmo: bierzemy tylko żywe źródła, więc kolejne odpalenie samo pomija
to, co już oznaczone. Żadnego pliku stanu.
STAN NA 2026-08-27: cała rodzina została wygaszona hurtowo dla FILMÓW (338 904 źródeł),
bo dowody były jednoznaczne: świeże identyfikatory prosto ze strony serwisu też nic nie
zwracają, 0 grających na 36 próbek w sześciu markach, a ich własny player kończy
komunikatem „Media source is not playable", który pada dopiero po wyczerpaniu WSZYSTKICH
czterech ścieżek CDN. Zostawiono 4 źródła jednego filmu, który nie miał nic innego, żeby
nic nie zniknęło z katalogu. Ten skrypt zostaje do punktowego sprawdzania i do scen.
Użycie (w kontenerze workera):
python3 /srv/scripts/seekplayer_reap.py --limit 300 --dry-run
python3 /srv/scripts/seekplayer_reap.py
@ -31,7 +36,6 @@ from __future__ import annotations
import argparse
import collections
import json
import logging
import re
import sys
@ -42,8 +46,7 @@ from urllib.parse import urlparse
from sqlalchemy import text
from app.db import session_scope
from app.extractors._fetch import _DEFAULT_UA, browser_get
from app.extractors.hosters.seekplayer_engine import _HOST_RE, _decrypt
from app.extractors.hosters.seekplayer_engine import _HOST_RE
log = logging.getLogger("seekplayer-reap")
@ -52,12 +55,6 @@ _FAMILY_SQL = (
"easyvidplayer|embedseek|seekplayer|upns|player4me|rpmplay"
"|4meplayer|seekplays|ezplayer|seeks"
)
# Pola, w których silnik podaje adres strumienia. Nazwy wzięte z ich własnego bundla
# (`{Cloudflare: cf, Tiktok: hlsVideoTiktok, Google: hlsVideoGoogle, "In-House": source}`),
# a warunek „nie ma żadnego z nich" to dokładnie ten sam warunek, po którym ich player
# pokazuje „Video is not ready yet".
_STREAM_FIELDS = ("source", "cf", "hlsVideoGoogle", "hlsVideoTiktok", "cfNative", "p2pStream")
_CONCURRENCY = 3
# Odstęp przed każdym pobraniem. Bez niego przy pięciu wątkach 38% odpowiedzi to było
# HTTP 429, a limit nie jest dowodem na nic, więc takie źródło trzeba zostawić i wrócić
@ -75,38 +72,28 @@ def _brand(hostname: str) -> str:
def _probe(item: tuple) -> tuple:
"""((brand, hash), verdict, detail). Sieć i parsowanie, bez dotykania bazy."""
"""((brand, hash), verdict, detail). Sieć i parsowanie, bez dotykania bazy.
Deleguje do `seekplayer_engine.extract`, ŻEBY WERDYKT BYŁ TEN SAM co u resolvera,
z którego korzysta aplikacja. Wcześniej skrypt miał własną, luźniejszą regułę
(są pola = żywe), przez co uznawał za żywe źródła z adresem do usuniętego pliku,
na których CDN oddaje 502. Silnik sprawdza teraz każdą ścieżkę naprawdę.
"""
key, url = item
time.sleep(_SPACING_SEC)
p = urlparse(url)
host = f"{p.scheme}://{p.hostname}"
api = f"{host}/api/v1/video?id={p.fragment}&w=1920&h=1080&r="
headers = {"User-Agent": _DEFAULT_UA, "Accept": "*/*", "Referer": host + "/"}
for attempt in (1, 2):
from app.extractors._models import HosterDead
from app.extractors.hosters import seekplayer_engine
try:
r = browser_get(api, headers=headers, timeout=_TIMEOUT)
out = seekplayer_engine.extract(url, timeout=_TIMEOUT)
except HosterDead as e:
return (key, _DEAD, str(e)[-80:])
except Exception as e:
return (key, _UNKNOWN, f"{type(e).__name__}")
if r.status_code in (404, 410):
return (key, _DEAD, f"HTTP {r.status_code}")
if r.status_code == 429:
# Rate limit nie jest dowodem na cokolwiek, więc jedna przerwa i koniec.
if attempt == 1:
time.sleep(5)
continue
return (key, _UNKNOWN, "HTTP 429")
if r.status_code != 200 or not r.text:
return (key, _UNKNOWN, f"HTTP {r.status_code}")
try:
data = json.loads(_decrypt(r.text))
except Exception as e:
return (key, _UNKNOWN, f"dekod {type(e).__name__}")
if data.get("error"):
return (key, _DEAD, str(data["error"])[:80])
if any(data.get(k) for k in _STREAM_FIELDS):
return (key, _ALIVE, "")
return (key, _DEAD, "brak pola source (Video is not ready yet)")
return (key, _UNKNOWN, "429")
# Rate limit i błędy sieci NIE są dowodem na nic: zostawiamy nietknięte.
return (key, _UNKNOWN, type(e).__name__)
if out:
return (key, _ALIVE, (out[0].raw or {}).get("sciezka") or "")
return (key, _UNKNOWN, "brak zrodla bez wyjatku")
def _load_work() -> tuple[dict, dict, dict]: