goon/scripts/studio_retrofix.py
goon-foss a4486980a4 style: myslniki em/en z komentarzy i docstringow (calosc repo)
1745 wystapien w 291 plikach (backend, scripts, mobile). Dominowal jeden ksztalt,
' — ' laczace zdania, wiec poszedl przecinek; myslnik otwierajacy mysl zdjety;
zakresy 0–5 i A–Z na dywiz.

Podzial komentarz/kod robil skaner stanow, nie regex, zeby nie ruszyc myslnika
stojacego w stringu albo w wyrazeniu regularnym. Potwierdzone maszynowo: po wycieciu
komentarzy z wersji HEAD i z dysku obie sa identyczne we wszystkich 291 plikach,
czyli poza komentarzami nie zmienilo sie nic. 233 pliki .py parsuja sie bez bledu,
tsc ma te same 11 bledow w tych samych 3 plikach co przed zmiana.

Dwie rzeczy wymagaly poprawki po automacie: wyrownane listy definicji, gdzie myslnik
byl separatorem kolumny (37 miejsc, kolumna przywrocona), i trzy przeniesienia mysli
do nowej linii, gdzie zostawal wiszacy przecinek na poczatku (poprawione recznie).

Bundla nie republikuje: Hermes i tak nie niesie komentarzy, wiec dla uzytkownika
to zero zmiany.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 11:51:57 +02:00

167 lines
6.9 KiB
Python

"""Retro-fix studio_id dla istniejących pornapp scen z aggregator-WordPress source'ów.
Cel: skleić orphan factories (porndish, xmoviesforyou, watchporn, hdporn92) z canonical
scenami w TPDB/StashDB. Te tube'y mają w tytule `[Studio]` lub `Studio - Perf - Title`
ale `studios.name` jest ustawione na nazwę source'a (`PornDish`, `Watch.Porn`).
Pipeline per scena:
1. Parse `[Studio]` lub `Studio - ...` z `scene.title`
2. Lookup canonical studio w `studios` table (po slugify name match, czas też
prosty substring fallback)
3. Jeśli match → update scene.studio_id (commit per-batch)
4. Po update, scena MAY auto-merge przy następnym ingest run gdy resolver path 4
blocking po studio+date znajdzie kandydatów
**Usage:**
docker compose exec -T worker python scripts/studio_retrofix.py --dry-run
docker compose exec -T worker python scripts/studio_retrofix.py --commit
**ETA:** ~10 min dla ~67k scen (porndish 4.5k + xmoviesforyou 1.8k + watchporn 4k +
hdporn92 31k = ~41k; reszta to słabo parsujące się). Read-only DB do dry-run.
"""
from __future__ import annotations
import argparse
import logging
import re
import sys
from sqlalchemy import text
from app.db import session_scope
from app.normalize.text import slugify
from app.resolve.studio_title_parser import parse_title
log = logging.getLogger("studio_retrofix")
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s")
# Sitetagi z parseable title formats. Reszta pornapp scen (xhamster/xvideos/etc.)
# nie ma `[Studio]` w tytule.
TARGET_SITETAGS = ["porndishcom", "xmoviesforyoucom", "watchporn", "hdporn92com"]
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--commit", action="store_true", help="Zapisz zmiany (default: dry-run)")
ap.add_argument("--limit", type=int, default=None, help="Limit scen (debug)")
ap.add_argument("--sitetags", nargs="+", default=TARGET_SITETAGS)
args = ap.parse_args()
# Load studios index (slug → studio_id, name) once
log.info("loading studios index...")
with session_scope() as session:
rows = session.execute(text("SELECT id, name FROM studios")).all()
studio_by_slug: dict[str, tuple[str, str]] = {}
for r in rows:
sl = slugify(r.name)
studio_by_slug[sl] = (str(r.id), r.name)
log.info(" %d studios loaded", len(studio_by_slug))
# Find candidate scenes per sitetag
stats = {
"scanned": 0,
"no_parse": 0,
"studio_no_canonical": 0,
"already_correct": 0,
"would_fix_studio": 0,
"would_fix_date": 0,
"fixed_studio": 0,
"fixed_date": 0,
}
no_match_studios: dict[str, int] = {} # parsed studio name → count
for sitetag in args.sitetags:
log.info("--- %s ---", sitetag)
with session_scope() as session:
rows = session.execute(
text("""
SELECT sc.id, sc.title, sc.studio_id, sc.release_date, st.name AS curr_studio
FROM scene_external_refs ser
JOIN scenes sc ON sc.id = ser.scene_id
JOIN sources s ON s.id = ser.source_id
LEFT JOIN studios st ON st.id = sc.studio_id
WHERE s.name = 'tube-scraper'
AND ser.external_id LIKE :prefix
ORDER BY sc.id
LIMIT :lim
"""),
{"prefix": f"{sitetag}:%", "lim": args.limit or 10_000_000},
).all()
log.info(" candidates: %d scenes", len(rows))
# Process in batches of 500
BATCH = 500
for i in range(0, len(rows), BATCH):
batch = rows[i : i + BATCH]
# (scene_id, studio_id_or_None, release_date_or_None)
updates: list[tuple[str, str | None, str | None]] = []
for row in batch:
stats["scanned"] += 1
parsed = parse_title(row.title)
if parsed.studio is None:
stats["no_parse"] += 1
continue
parsed_slug = slugify(parsed.studio)
target = studio_by_slug.get(parsed_slug)
if not target:
fallback = re.sub(r'(?:ll?|hd|xxx|com|tv|tube|video|videos)$', '',
parsed_slug)
target = studio_by_slug.get(fallback) if fallback != parsed_slug else None
if not target:
stats["studio_no_canonical"] += 1
no_match_studios[parsed.studio] = no_match_studios.get(parsed.studio, 0) + 1
continue
target_id, target_name = target
new_studio_id = None if str(row.studio_id) == target_id else target_id
new_date = (
parsed.release_date.isoformat()
if parsed.release_date is not None and row.release_date is None
else None
)
if new_studio_id is None and new_date is None:
stats["already_correct"] += 1
continue
if new_studio_id:
stats["would_fix_studio"] += 1
if new_date:
stats["would_fix_date"] += 1
updates.append((str(row.id), new_studio_id, new_date))
if args.commit and updates:
with session_scope() as session:
for scene_id, studio_id, rel_date in updates:
sets = []
params: dict = {"id": scene_id}
if studio_id:
sets.append("studio_id = :sid")
params["sid"] = studio_id
stats["fixed_studio"] += 1
if rel_date:
sets.append("release_date = :rd")
params["rd"] = rel_date
stats["fixed_date"] += 1
sets.append("updated_at = NOW()")
session.execute(
text(f"UPDATE scenes SET {', '.join(sets)} WHERE id = :id"),
params,
)
log.info(" batch %d/%d: committed %d updates",
i // BATCH + 1, (len(rows) + BATCH - 1) // BATCH, len(updates))
log.info("===== STATS =====")
for k, v in stats.items():
log.info(" %s: %d", k, v)
# Top 30 unparsed studios (na case'y do dodania do studios table jak TPDB nie dostarczy)
log.info("===== Top 30 parsed studios NOT in canonical (parse OK, lookup miss) =====")
for studio, n in sorted(no_match_studios.items(), key=lambda x: -x[1])[:30]:
log.info(" %s: %d scenes", studio, n)
log.info("dry_run=%s. Use --commit to apply.", not args.commit)
return 0
if __name__ == "__main__":
sys.exit(main())