""" MediaTrack - OMDB Batch Enrichment =================================== Scarica dati OMDB (locandina, sinossi, cast, rating...) per i prodotti che hanno codice_imdb ma non hanno ancora dati in OmdbData. Fase 1 — Nuovi record: prodotti senza OmdbData o stub piu' vecchi di 30 giorni. Fase 2 — Retry poster: record con dati OK ma poster mancante/N/A da piu' di 45 giorni. Re-scarica dati + poster da OMDB. Aggiorna scaricato_il cosi' il retry successivo avviene dopo altri 45 giorni. Chiamato da Frank come job schedulato (job_type: 'batch' via bat wrapper). Opera direttamente sul DB server — indipendente dal config locale. Usage: python omdb_batch_enrich.py [--db-path PATH] [--posters-dir PATH] [--batch-size 500] [--delay 5.0] [--dry-run] Frank job config (via bat wrapper): executable_path : C:\\PYTHON\\omdb_enrich\\omdb_batch_enrich.bat job_type : batch schedule : es. ogni notte alle 02:00 (cron: 0 2 * * *) """ import sys import time import sqlite3 import argparse import threading import requests from pathlib import Path from datetime import datetime, timedelta, timezone # Root di MyICR_Suite — necessario per importare database_logic sys.path.insert(0, r"C:\PYTHON\MyICR_Suite") from app.modules.mediatrack.backend import database_logic # Cartella di default dove vengono salvati i JPG delle locandine DEFAULT_POSTERS_DIR = Path(r"I:\SOFTWARE\PYTHON_SRV\PYTHON_LOCAL\MyICR_Suite\local_db\posters") # Path DB di default DEFAULT_DB_PATH = r"I:\SOFTWARE\PYTHON_SRV\sync_db\mediatrack.db" # Numero di tentativi per errori di rete transitori (non per 404) POSTER_RETRIES = 2 # Giorni dopo cui ritentare il poster per record con url_poster mancante/N/A POSTER_RETRY_DAYS = 45 def download_poster(codice_imdb: str, url_poster: str, posters_dir: Path) -> tuple[str, str]: """ Scarica il JPG della locandina e lo salva in posters_dir/{codice_imdb}.jpg. Restituisce (stato, dettaglio): 'ok' — scaricato con successo 'skip' — file gia' presente su disco 'no_url'— url_poster assente o N/A '404' — URL non trovato (Amazon CDN scaduto) 'error' — errore di rete/IO transitorio """ if not url_poster or url_poster == 'N/A': return 'no_url', '' dest = posters_dir / f"{codice_imdb}.jpg" if dest.exists(): return 'skip', '' posters_dir.mkdir(parents=True, exist_ok=True) for attempt in range(1, POSTER_RETRIES + 1): try: r = requests.get(url_poster, timeout=15) if r.status_code == 404: return '404', '' r.raise_for_status() if r.content[:3] != b'\xff\xd8\xff': return 'error', 'downloaded content is not a valid JPEG' dest.write_bytes(r.content) return 'ok', f"{len(r.content)} bytes" except requests.exceptions.Timeout: detail = f'timeout (attempt {attempt}/{POSTER_RETRIES})' except requests.exceptions.ConnectionError as e: detail = f'connection error (attempt {attempt}/{POSTER_RETRIES}): {e}' except requests.exceptions.HTTPError as e: return 'error', str(e) except OSError as e: return 'error', f'write error: {e}' except Exception as e: detail = f'unexpected: {e}' if attempt < POSTER_RETRIES: time.sleep(2) return 'error', detail def get_prodotti_poster_da_refreshare(db_path: str, limit: int) -> list[dict]: """ Fase 2 — Restituisce i record con dati OMDB presenti ma poster mancante o N/A da piu' di POSTER_RETRY_DAYS giorni. Query diretta su SQLite, indipendente da database_logic, cosi' non serve modificare MyICR Suite. """ con = sqlite3.connect(db_path, timeout=30) con.row_factory = sqlite3.Row cur = con.cursor() cutoff = (datetime.now(timezone.utc) - timedelta(days=POSTER_RETRY_DAYS)).strftime('%Y-%m-%d %H:%M:%S') cur.execute(""" SELECT A.id_media, A.titolo_ufficiale, A.codice_imdb FROM AnagraficaMedia A JOIN OmdbData O ON A.id_media = O.id_media_fk WHERE O.titolo_omdb IS NOT NULL AND (O.url_poster IS NULL OR O.url_poster NOT LIKE 'http%') AND (O.scaricato_il IS NULL OR O.scaricato_il < ?) ORDER BY O.scaricato_il ASC LIMIT ? """, (cutoff, limit)) rows = [dict(r) for r in cur.fetchall()] con.close() return rows def _run_phase(prodotti: list[dict], delay: float, posters_dir: Path) -> dict: """Esegue il loop di enrichment su una lista di prodotti. Restituisce i contatori.""" done = errors = not_found = poster_ok = poster_404 = poster_err = 0 error_log = [] total = len(prodotti) not_found_phrases = ("error getting data", "movie not found", "series not found", "incorrect imdb id", "not found!") for i, p in enumerate(prodotti, 1): id_media = p['id_media'] codice_imdb = p['codice_imdb'] titolo = p['titolo_ufficiale'] success, msg = database_logic.importa_e_salva_da_omdb(id_media, codice_imdb) if success: done += 1 url_poster = database_logic.get_poster_url(id_media) stato, dettaglio = download_poster(codice_imdb, url_poster, posters_dir) if url_poster else ('no_url', '') if stato in ('ok', 'skip'): poster_ok += 1 poster_tag = "[P]" elif stato == '404': poster_404 += 1 poster_tag = "[!]" elif stato == 'no_url': poster_tag = " " else: poster_err += 1 poster_tag = "[X]" error_log.append(f"POSTER {codice_imdb}: {dettaglio}") print(f" [{i:>4}/{total}] OK {poster_tag} {titolo[:50]:<50} ({codice_imdb})") else: if any(phrase in msg.lower() for phrase in not_found_phrases): database_logic.mark_omdb_not_found(id_media, codice_imdb) not_found += 1 print(f" [{i:>4}/{total}] N/F {titolo[:50]:<50} ({codice_imdb})") else: errors += 1 error_log.append(f"{titolo} ({codice_imdb}): {msg}") print(f" [{i:>4}/{total}] ERR {titolo[:50]:<50} ({codice_imdb}) -> {msg}") if i < total: time.sleep(delay) return { 'done': done, 'errors': errors, 'not_found': not_found, 'poster_ok': poster_ok, 'poster_404': poster_404, 'poster_err': poster_err, 'error_log': error_log, } def main(): parser = argparse.ArgumentParser(description='OMDB Batch Enrichment per MediaTrack') parser.add_argument('--db-path', type=str, default=DEFAULT_DB_PATH) parser.add_argument('--posters-dir', type=str, default=None) parser.add_argument('--batch-size', type=int, default=500, help='Prodotti per fase per run (default: 500)') parser.add_argument('--delay', type=float, default=5.0, help='Secondi tra richieste OMDB (default: 5.0)') parser.add_argument('--distributor', type=str, default=None) parser.add_argument('--dry-run', action='store_true') args = parser.parse_args() database_logic.NOME_DATABASE = args.db_path posters_dir = Path(args.posters_dir) if args.posters_dir else DEFAULT_POSTERS_DIR start_time = datetime.now() print(f"[OMDB Batch] Avvio : {start_time.strftime('%Y-%m-%d %H:%M:%S')}") print(f"[OMDB Batch] DB : {args.db_path}") print(f"[OMDB Batch] Poster : {posters_dir}") print(f"[OMDB Batch] Param : batch_size={args.batch_size}, delay={args.delay}s, dry_run={args.dry_run}") result = [None] def _check(): result[0] = Path(args.db_path).exists() t = threading.Thread(target=_check, daemon=True) t.start() t.join(timeout=10) if result[0] is None: print(f"[OMDB Batch] Rete non raggiungibile (timeout 10s) — skip") sys.exit(0) if not result[0]: print(f"[OMDB Batch] DB non trovato: {args.db_path} — skip") sys.exit(0) if args.distributor: print(f"[OMDB Batch] Filtro distributore: {args.distributor}") # ----------------------------------------------------------------------- # FASE 1 — Nuovi record e stub scaduti # ----------------------------------------------------------------------- total_in_coda = database_logic.count_prodotti_da_arricchire() print(f"\n[OMDB Batch] FASE 1 — Nuovi record in coda: {total_in_coda}") r1 = {'done': 0, 'errors': 0, 'not_found': 0, 'poster_ok': 0, 'poster_404': 0, 'poster_err': 0, 'error_log': []} if total_in_coda > 0: if args.dry_run: prodotti = database_logic.get_prodotti_da_arricchire(args.batch_size, args.distributor) print(f"[OMDB Batch] DRY-RUN Fase 1: {len(prodotti)} prodotti da processare.") else: prodotti = database_logic.get_prodotti_da_arricchire(args.batch_size, args.distributor) print(f"[OMDB Batch] Batch caricato: {len(prodotti)} prodotti\n") r1 = _run_phase(prodotti, args.delay, posters_dir) else: print(f"[OMDB Batch] Nessun nuovo record.") # ----------------------------------------------------------------------- # FASE 2 — Retry poster scaduti (url_poster mancante/N/A da > 45 giorni) # ----------------------------------------------------------------------- prodotti_f2 = get_prodotti_poster_da_refreshare(args.db_path, args.batch_size) print(f"\n[OMDB Batch] FASE 2 — Poster da refreshare (>{POSTER_RETRY_DAYS}gg): {len(prodotti_f2)}") r2 = {'done': 0, 'errors': 0, 'not_found': 0, 'poster_ok': 0, 'poster_404': 0, 'poster_err': 0, 'error_log': []} if prodotti_f2: if args.dry_run: print(f"[OMDB Batch] DRY-RUN Fase 2: {len(prodotti_f2)} prodotti da processare.") else: print() r2 = _run_phase(prodotti_f2, args.delay, posters_dir) # ----------------------------------------------------------------------- # RIEPILOGO # ----------------------------------------------------------------------- rimasti = database_logic.count_prodotti_da_arricchire() elapsed = (datetime.now() - start_time).total_seconds() print(f"\n[OMDB Batch] === RIEPILOGO ===") print(f"[OMDB Batch] {'FASE 1':>8} {'FASE 2':>8}") print(f"[OMDB Batch] Completati : {r1['done']:>8} {r2['done']:>8}") print(f"[OMDB Batch] Poster OK : {r1['poster_ok']:>8} {r2['poster_ok']:>8}") print(f"[OMDB Batch] Poster 404 : {r1['poster_404']:>8} {r2['poster_404']:>8}") print(f"[OMDB Batch] Poster ERR : {r1['poster_err']:>8} {r2['poster_err']:>8}") print(f"[OMDB Batch] Non su OMDB : {r1['not_found']:>8} {r2['not_found']:>8}") print(f"[OMDB Batch] Errori OMDB : {r1['errors']:>8} {r2['errors']:>8}") print(f"[OMDB Batch] Rimasti F1 : {rimasti}") print(f"[OMDB Batch] Durata : {elapsed:.0f}s") all_errors = r1['error_log'] + r2['error_log'] if all_errors: print(f"\n[OMDB Batch] Dettaglio errori:") for e in all_errors: print(f" - {e}") if (r1['errors'] + r2['errors']) > 0 and (r1['done'] + r2['done']) == 0: sys.exit(1) if __name__ == '__main__': main()