fix(movies): obsada i studio z motywu "vid", pandamovies z powrotem
Some checks are pending
Backend tests / test (push) Waiting to run
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:
parent
bf45339c97
commit
37f8eb1762
4 changed files with 156 additions and 14 deletions
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
88
scripts/backfill_dooplay_metadata.py
Normal file
88
scripts/backfill_dooplay_metadata.py
Normal 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()
|
||||||
20
scripts/ingest_movies_backfill.py
Normal file
20
scripts/ingest_movies_backfill.py
Normal 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))
|
||||||
Loading…
Add table
Reference in a new issue