""" MediaTrack - OMDB Poster Repair ================================ Scarica i JPG mancanti per tutti i record OmdbData che hanno url_poster ma non hanno il file corrispondente su disco. Per i poster con URL scaduto (404), ri-interroga l'API OMDB per ottenere un URL fresco, aggiorna il DB e poi scarica il JPG. Usage: python omdb_poster_repair.py [--db-path PATH] [--posters-dir PATH] [--delay 0.3] [--no-refresh] [--dry-run] Opzioni: --no-refresh Salta la ri-chiamata a OMDB per URL scaduti (solo download diretto) --dry-run Mostra quanti poster mancano senza scaricare nulla """ import os import sys import time import argparse import sqlite3 import requests from pathlib import Path from datetime import datetime DEFAULT_DB_PATH = r"I:\SOFTWARE\PYTHON_SRV\sync_db\mediatrack.db" DEFAULT_POSTERS_DIR = Path(r"I:\SOFTWARE\PYTHON_SRV\PYTHON_LOCAL\MyICR_Suite\local_db\posters") OMDB_API_KEY = os.environ.get('OMDB_API_KEY', '123637e0') # Numero di tentativi per errori di rete transitori RETRIES = 2 # --------------------------------------------------------------------------- # DB helpers # --------------------------------------------------------------------------- def get_missing_posters(db_path: str, posters_dir: Path) -> list[dict]: """Restituisce tutti i record OmdbData con url_poster (o NULL) ma senza JPG su disco.""" con = sqlite3.connect(db_path, timeout=30) con.row_factory = sqlite3.Row cur = con.cursor() # Include anche record con url_poster NULL: hanno titolo OMDB ma poster mai scaricato cur.execute(""" SELECT codice_imdb, url_poster FROM OmdbData WHERE titolo_omdb IS NOT NULL AND ( url_poster IS NULL OR (url_poster != 'N/A' AND url_poster != '') OR (url_poster = 'N/A' AND (scaricato_il IS NULL OR scaricato_il < datetime('now', '-90 day'))) ) ORDER BY codice_imdb """) rows = [dict(r) for r in cur.fetchall()] con.close() # Carica l'elenco dei file esistenti in memoria una volta sola # (evita migliaia di .exists() su I:\, che sono lentissime su rete) existing = {f.stem for f in posters_dir.iterdir() if f.suffix == '.jpg'} if posters_dir.exists() else set() missing = [r for r in rows if r['codice_imdb'] not in existing] return missing def update_poster_url(db_path: str, codice_imdb: str, new_url: str) -> None: """Aggiorna url_poster in OmdbData con l'URL fresco ottenuto da OMDB.""" con = sqlite3.connect(db_path, timeout=30) cur = con.cursor() cur.execute("UPDATE OmdbData SET url_poster = ? WHERE codice_imdb = ?", (new_url, codice_imdb)) con.commit() con.close() def mark_poster_unavailable(db_path: str, codice_imdb: str) -> None: """Marca url_poster = 'N/A' e aggiorna scaricato_il per i titoli senza poster recuperabile. Il repair li ritetera' dopo 90 giorni usando scaricato_il come data di riferimento.""" con = sqlite3.connect(db_path, timeout=30) cur = con.cursor() cur.execute( "UPDATE OmdbData SET url_poster = 'N/A', scaricato_il = CURRENT_TIMESTAMP WHERE codice_imdb = ?", (codice_imdb,) ) con.commit() con.close() def update_scaricato_il(db_path: str, codice_imdb: str) -> None: con = sqlite3.connect(db_path, timeout=30) cur = con.cursor() cur.execute( "UPDATE OmdbData SET scaricato_il = CURRENT_TIMESTAMP WHERE codice_imdb = ?", (codice_imdb,) ) con.commit() con.close() # --------------------------------------------------------------------------- # OMDB API # --------------------------------------------------------------------------- def fetch_fresh_poster_url(codice_imdb: str) -> str | None: """ Ri-interroga l'API OMDB per ottenere un URL poster fresco. Restituisce l'URL se presente e valido, altrimenti None. """ try: r = requests.get( f"http://www.omdbapi.com/?i={codice_imdb}&apikey={OMDB_API_KEY}", timeout=10 ) r.raise_for_status() data = r.json() url = data.get('Poster') return url if url and url != 'N/A' else None except Exception: return None # --------------------------------------------------------------------------- # Download # --------------------------------------------------------------------------- def download_jpg(url: str, dest: Path) -> tuple[str, str]: """ Scarica il JPG da url e lo salva in dest. Restituisce (stato, dettaglio): 'ok' — scaricato con successo '404' — URL non trovato (CDN scaduto) 'error' — errore di rete/IO transitorio Riprova RETRIES volte su errori transitori. """ for attempt in range(1, RETRIES + 1): try: r = requests.get(url, 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}/{RETRIES})' except requests.exceptions.ConnectionError as e: detail = f'connection error (attempt {attempt}/{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 < RETRIES: time.sleep(2) return 'error', detail # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- def main(): parser = argparse.ArgumentParser(description='OMDB Poster Repair — scarica JPG mancanti') parser.add_argument('--db-path', default=DEFAULT_DB_PATH) parser.add_argument('--posters-dir', default=None) parser.add_argument('--delay', type=float, default=0.5, help='Secondi tra un download e il successivo (default: 0.5)') parser.add_argument('--no-refresh', action='store_true', help='Non ri-interrogare OMDB per URL scaduti (solo download diretto)') parser.add_argument('--dry-run', action='store_true', help='Mostra quanti poster mancano senza scaricare nulla') args = parser.parse_args() db_path = args.db_path posters_dir = Path(args.posters_dir) if args.posters_dir else DEFAULT_POSTERS_DIR do_refresh = not args.no_refresh start_time = datetime.now() print(f"[Poster Repair] Avvio : {start_time.strftime('%Y-%m-%d %H:%M:%S')}") print(f"[Poster Repair] DB : {db_path}") print(f"[Poster Repair] Dir : {posters_dir}") print(f"[Poster Repair] Refresh URL: {'si' if do_refresh else 'no'}") if not Path(db_path).exists(): print(f"[Poster Repair] ERRORE: DB non trovato: {db_path}") sys.exit(1) missing = get_missing_posters(db_path, posters_dir) print(f"[Poster Repair] Poster mancanti: {len(missing)}\n") if not missing: print("[Poster Repair] Nessun poster mancante. Uscita.") return if args.dry_run: print("[Poster Repair] DRY-RUN — nessun download effettuato.") return posters_dir.mkdir(parents=True, exist_ok=True) ok = 0 # scaricato con URL esistente refreshed_ok = 0 # 404 → nuovo URL da OMDB → scaricato permanent_404 = 0 # 404 anche dopo refresh (o refresh disabilitato) no_poster = 0 # OMDB non ha un poster per questo titolo errors = 0 error_log = [] for i, record in enumerate(missing, 1): codice = record['codice_imdb'] url = record.get('url_poster') or '' dest = posters_dir / f"{codice}.jpg" # --- Primo tentativo con URL esistente --- if url and url != 'N/A': stato, dettaglio = download_jpg(url, dest) else: stato = '404' # nessun URL salvato — trattalo come 404 per forzare il refresh dettaglio = '' # --- Se 404 e refresh abilitato: ri-chiede URL fresco a OMDB --- if stato == '404' and do_refresh: fresh_url = fetch_fresh_poster_url(codice) time.sleep(0.3) # rispetta rate limit OMDB if fresh_url: update_poster_url(db_path, codice, fresh_url) stato, dettaglio = download_jpg(fresh_url, dest) if stato == 'ok': refreshed_ok += 1 update_scaricato_il(db_path, codice) print(f" [{i:>5}/{len(missing)}] REFRESH OK {codice} ({dettaglio})") elif stato == '404': permanent_404 += 1 mark_poster_unavailable(db_path, codice) print(f" [{i:>5}/{len(missing)}] 404 perm. {codice}") else: errors += 1 error_log.append(f"{codice}: {dettaglio}") print(f" [{i:>5}/{len(missing)}] ERR {codice} -> {dettaglio}") else: no_poster += 1 mark_poster_unavailable(db_path, codice) print(f" [{i:>5}/{len(missing)}] NO POSTER {codice} (OMDB non ha immagine)") elif stato == 'ok': ok += 1 update_scaricato_il(db_path, codice) print(f" [{i:>5}/{len(missing)}] OK {codice} ({dettaglio})") elif stato == '404': # refresh disabilitato permanent_404 += 1 print(f" [{i:>5}/{len(missing)}] 404 {codice}") else: errors += 1 error_log.append(f"{codice}: {dettaglio}") print(f" [{i:>5}/{len(missing)}] ERR {codice} -> {dettaglio}") if i < len(missing): time.sleep(args.delay) elapsed = (datetime.now() - start_time).total_seconds() print(f"\n[Poster Repair] === RIEPILOGO ===") print(f"[Poster Repair] Scaricati (URL ok) : {ok}") print(f"[Poster Repair] Scaricati (dopo refresh): {refreshed_ok}") print(f"[Poster Repair] 404 permanenti : {permanent_404}") print(f"[Poster Repair] Nessun poster su OMDB : {no_poster}") print(f"[Poster Repair] Errori rete/IO : {errors}") print(f"[Poster Repair] Durata : {elapsed:.0f}s") if error_log: print(f"\n[Poster Repair] Dettaglio errori:") for e in error_log: print(f" - {e}") if errors > 0 and (ok + refreshed_ok) == 0: sys.exit(1) if __name__ == '__main__': main()