fix(movies): obsada i studio z motywu "vid", pandamovies z powrotem
Some checks are pending
Backend tests / test (push) Waiting to run

Mangoporn, streamporn.li i pandamovies przeszly w pazdzierniku na nowy motyw:
obsada tylko w JSON-LD, studio, rok i gatunki w chipach bez rel="tag" (czasem
bez koncowego ukosnika). Parser po cichu przestal czytac obsade i studio, a
mangoporn dostawal jedynie tag "Porn Movies". Pandamovies dziala znowu pod
pandamovies.org, wiec wraca do ingestu. Skrypty: pelne zaciagniecie zrodla
po przerwie i uzupelnienie metadanych filmow z pazdziernika.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
goon-foss 2026-10-16 10:57:32 +02:00
parent bf45339c97
commit 37f8eb1762
4 changed files with 156 additions and 14 deletions

View file

@ -35,6 +35,7 @@ def get_movie_connectors() -> list[tuple[str, type]]:
""" """
from app.connectors.dooplay import ( from app.connectors.dooplay import (
MangopornConnector, MangopornConnector,
PandamoviesConnector,
StreampornVipConnector, StreampornVipConnector,
) )
from app.connectors.freeomovie import FreeoMovieConnector from app.connectors.freeomovie import FreeoMovieConnector
@ -55,17 +56,11 @@ def get_movie_connectors() -> list[tuple[str, type]]:
("paradisehill", ParadisehillConnector), ("paradisehill", ParadisehillConnector),
("mangoporn", MangopornConnector), ("mangoporn", MangopornConnector),
("streampornvip", StreampornVipConnector), ("streampornvip", StreampornVipConnector),
# pandamovies WYLACZONY 2026-08-20: serwis padl, zostal im tylko zapamietany # pandamovies: wylaczony 2026-08-20 (serwis padl, kazda sciezka poza strona
# fronton. Strona glowna oddaje 291 kB i wypisuje 56 filmow, ale KAZDA inna # glowna dawala 404), wlaczony z powrotem 2026-10-16 po przeprowadzce na
# sciezka to 404: listing /movies/ (ten, ktory czytal konektor), /movies/page/2/ # pandamovies.org: listing /movies/ 27 filmow na strone, 7 hosterow na film,
# linkowana przez ich wlasny szablon, /genre/* i /release-year/* z ich menu, # dziala wprost z VPS bez proxy. Ten sam motyw "vid" co mangoporn i streamporn.li.
# sitemapa, a przede wszystkim same strony filmow (0 na 6 dzialajacych). ("pandamovies", PandamoviesConnector),
# To nie blokada na nasze IP: przez residential proxy wychodzi identyczne 404
# (te same 796 bajtow). Konektor konczyl runy jako success z zerem rekordow,
# co falszowalo obraz w monitoringu. Playback tego, co juz mamy, dziala dalej,
# bo zrodla wskazuja na hostery (doply, luluvid, seekplayer), nie na ten serwis.
# Klasa zostaje w dooplay.py dla backrefu origins `pandamovies:*`.
# ("pandamovies", PandamoviesConnector),
# freeomovie.to (bestia theme, nie dooplay), mirror: świeże filmy DVD, # freeomovie.to (bestia theme, nie dooplay), mirror: świeże filmy DVD,
# title-trigram attach do canonical + playback z TABS. Ocena 2026-07-02. # title-trigram attach do canonical + playback z TABS. Ocena 2026-07-02.
("freeomovie", FreeoMovieConnector), ("freeomovie", FreeoMovieConnector),

View file

@ -191,9 +191,22 @@ _ST_DURATION_RE = re.compile(
_ST_DATE_RE = re.compile( _ST_DATE_RE = re.compile(
r'class="st st--date"[^>]*>.*?<time\s+datetime="(\d{4}-\d{2}-\d{2})', re.IGNORECASE | re.DOTALL r'class="st st--date"[^>]*>.*?<time\s+datetime="(\d{4}-\d{2}-\d{2})', re.IGNORECASE | re.DOTALL
) )
_YEAR_LOOSE_RE = re.compile(r'/(?:year|release-year)/(\d{4})/"', re.IGNORECASE) # pandamovies.org 2026-10: chip `/release-year/2026` bez koncowego ukosnika.
_YEAR_LOOSE_RE = re.compile(r'/(?:year|release-year)/(\d{4})/?"', re.IGNORECASE)
# Motyw "vid" (mangoporn, pandamovies, streamporn.li, 2026-10): studio w chipie bez
# rel="tag", pod /studios/, /movies/studios/ albo /director/, czasem bez ukosnika.
_STUDIO_CHIP_RE = re.compile(
r'<a\s+class="chip"\s+href="https?://[^/]+/(?:movies/)?(?:studios?|director)/([a-z0-9-]+)/?"[^>]*>([^<]+)</a>',
re.IGNORECASE,
)
# Ten sam motyw nie ma juz sekcji obsady z rel="tag"; obsada filmu jest w JSON-LD
# (`"actor":[{"name":..., "url":".../actor/<slug>"}]`). Linki do aktorow w HTML bywaja
# tez w widzetach bocznych, wiec bierzemy JSON-LD, ktory opisuje tylko ten film.
_JSONLD_ACTORS_RE = re.compile(r'"actor"\s*:\s*(\[[^\]]*\])')
# mangoporn ma gatunki pod /movies/genre/, pandamovies bez koncowego ukosnika, a chip
# kategorii /genres/porn-movies/ to nie gatunek (byl jedynym tagiem filmow mangoporn).
_GENRE_CHIP_RE = re.compile( _GENRE_CHIP_RE = re.compile(
r'<a\s+class="chip"\s+href="https?://[^/]+/genres?/([a-z0-9-]+)/"[^>]*>([^<]+)</a>', r'<a\s+class="chip"\s+href="https?://[^/]+/(?:movies/)?genres?/(?!porn-movies/?")([a-z0-9-]+)/?"[^>]*>([^<]+)</a>',
re.IGNORECASE, re.IGNORECASE,
) )
_DATA_SERVERS_RE = re.compile(r'data-servers="([^"]*)"', re.IGNORECASE) _DATA_SERVERS_RE = re.compile(r'data-servers="([^"]*)"', re.IGNORECASE)
@ -348,6 +361,30 @@ class DooplayConnector(BaseMovieConnector):
) )
def _jsonld_performers(html: str, source_name: str) -> list[RawPerformer]:
"""Obsada z JSON-LD filmu (motyw "vid" 2026-10). Id jak dawniej: slug z adresu aktora,
wiec te same osoby nie dubluja sie wzgledem filmow sprzed zmiany motywu."""
m = _JSONLD_ACTORS_RE.search(html)
if not m:
return []
try:
people = json.loads(m.group(1))
except ValueError:
return []
out: list[RawPerformer] = []
seen: set[str] = set()
for p in people:
if not isinstance(p, dict):
continue
name = _decode_html((p.get("name") or "").strip())
slug = (p.get("url") or "").rstrip("/").rsplit("/", 1)[-1].lower()
if not name or not slug or slug in seen:
continue
seen.add(slug)
out.append(RawPerformer(external_id=f"{source_name}:{slug}", name=name))
return out
def _parse_dooplay_detail( def _parse_dooplay_detail(
*, slug: str, html: str, source_name: str, base_url: str, page_url: str | None = None *, slug: str, html: str, source_name: str, base_url: str, page_url: str | None = None
) -> RawMovie | None: ) -> RawMovie | None:
@ -381,7 +418,7 @@ def _parse_dooplay_detail(
release_year = int(m_year.group(1)) if m_year else vid_year release_year = int(m_year.group(1)) if m_year else vid_year
studio: RawStudio | None = None studio: RawStudio | None = None
m_studio = _STUDIO_RE.search(html) m_studio = _STUDIO_RE.search(html) or _STUDIO_CHIP_RE.search(html)
if m_studio: if m_studio:
studio_slug = m_studio.group(1) studio_slug = m_studio.group(1)
studio_name = _decode_html(m_studio.group(2).strip()) studio_name = _decode_html(m_studio.group(2).strip())
@ -468,6 +505,8 @@ def _parse_dooplay_detail(
) )
for m in _PORNSTAR_RE.finditer(html) for m in _PORNSTAR_RE.finditer(html)
] ]
if not performers:
performers = _jsonld_performers(html, source_name)
tags = [ tags = [
RawTag( RawTag(

View file

@ -0,0 +1,88 @@
"""Uzupelnia obsade, studio i tagi filmow z serwisow dooplay po zmianie motywu (2026-10).
Mangoporn, streamporn.li i pandamovies przeszly na motyw "vid": obsada tylko w JSON-LD,
studio i rok w chipach bez rel="tag". Parser tego nie czytal, wiec filmy dodane od ok.
5.10 przyszly bez obsady i czesto bez studia (cichy fail parsera, ingest konczyl sie
"errors: 0"). Skrypt pobiera ich strony na nowo i puszcza przez resolve_movie, ktory przy
znanym refie tylko uzupelnia puste pola i dopina obsade/tagi. Strony przez proxy, bo
mangoporn z VPS bywa niedostepny (timeout, blad TLS).
python3 /srv/scripts/backfill_dooplay_metadata.py # dry-run: tylko liczy
python3 /srv/scripts/backfill_dooplay_metadata.py --apply [--since 2026-10-01]
"""
from __future__ import annotations
import argparse
import functools
import logging
import app.connectors.dooplay as dp
from app.config import get_settings
dp.browser_get = functools.partial(dp.browser_get, proxy=get_settings().brightdata_proxy_url)
from sqlalchemy import text # noqa: E402
from app.db import session_scope # noqa: E402
from app.normalize.movies import normalize_movie # noqa: E402
from app.resolve.movie_resolver import resolve_movie # noqa: E402
_CONNECTORS = {
"mangoporn": dp.MangopornConnector,
"streampornvip": dp.StreampornVipConnector,
"pandamovies": dp.PandamoviesConnector,
}
_REFS_SQL = """
SELECT r.movie_id, r.external_id, r.url, so.id AS source_id, so.name
FROM movie_external_refs r
JOIN sources so ON so.id = r.source_id
JOIN movies m ON m.id = r.movie_id
WHERE so.name = ANY(:names) AND m.created_at >= :since AND r.url IS NOT NULL
AND (m.studio_id IS NULL
OR NOT EXISTS (SELECT 1 FROM movie_performers p WHERE p.movie_id = m.id))
ORDER BY m.created_at DESC
"""
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--apply", action="store_true")
ap.add_argument("--since", default="2026-10-01")
args = ap.parse_args()
logging.basicConfig(level=logging.WARNING)
with session_scope() as s:
refs = s.execute(text(_REFS_SQL), {"names": list(_CONNECTORS), "since": args.since}).all()
print(f"refow do uzupelnienia: {len(refs)}", flush=True)
if not args.apply:
return
conns = {name: cls() for name, cls in _CONNECTORS.items()}
done = failed = with_cast = 0
for n, ref in enumerate(refs, 1):
try:
raw = conns[ref.name]._fetch_detail(ref.url)
except Exception as e: # noqa: BLE001
failed += 1
print(f" blad {ref.name} {ref.url}: {str(e)[:100]}", flush=True)
continue
if raw is None:
failed += 1
continue
norm = normalize_movie(raw)
if norm.external_id != ref.external_id:
failed += 1
print(f" inny id {ref.external_id} -> {norm.external_id}, pomijam", flush=True)
continue
with session_scope() as s:
resolve_movie(s, norm=norm, source_id=ref.source_id)
done += 1
with_cast += bool(raw.performers)
if n % 25 == 0:
print(f"... {n}/{len(refs)}, uzupelnione {done}, z obsada {with_cast}, bledy {failed}", flush=True)
print(f"uzupelnione: {done}, z obsada: {with_cast}, bledy: {failed}", flush=True)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,20 @@
"""Pelne zaciagniecie filmow z jednego zrodla (bez delty), np. po przerwie w ingescie.
Codzienny _job_movie_ingest idzie w trybie przyrostowym (dooplay: 3 strony listy), wiec
po kilku tygodniach przestoju zrodla zaleglosci same sie nie nadrobia. Pelny tryb dooplay
czyta do 50 stron. Uzycie (z pliku, w osobnym kontenerze):
docker compose run -d --name X worker python3 /srv/scripts/ingest_movies_backfill.py pandamovies [limit]
"""
import logging
import sys
from app.connectors import get_movie_connectors
from app.ingest import ingest_movies_from_connector
logging.basicConfig(level=logging.INFO)
name = sys.argv[1]
limit = int(sys.argv[2]) if len(sys.argv) > 2 else None
connectors = dict(get_movie_connectors())
if name not in connectors:
sys.exit(f"nie ma konektora filmow {name!r}, sa: {sorted(connectors)}")
print(ingest_movies_from_connector(connectors[name](), limit=limit, use_delta=False))