import json import sqlite3 from contextlib import contextmanager from .config import DB_PATH SCHEMA = """ CREATE TABLE IF NOT EXISTS movies ( id INTEGER PRIMARY KEY, title TEXT NOT NULL, original_title TEXT, release_date TEXT, year INTEGER, overview TEXT, teaser TEXT, poster_path TEXT, backdrop_path TEXT, vote_average REAL, vote_count INTEGER, runtime INTEGER, genres TEXT, images TEXT, trailer_key TEXT, imdb_id TEXT, netflix_id TEXT, netflix_url TEXT, on_netflix INTEGER DEFAULT 0, canon_group INTEGER, canon_pos INTEGER, collection_id INTEGER, collection_name TEXT, synced_at TEXT ); CREATE TABLE IF NOT EXISTS meta ( key TEXT PRIMARY KEY, value TEXT ); -- Fingerabdrücke der Backdrops, damit spätere Syncs die Bilder nicht -- erneut herunterladen müssen -- hash als BLOB: der dHash ist vorzeichenlos 64 Bit und passt nicht in -- SQLites vorzeichenbehaftetes INTEGER CREATE TABLE IF NOT EXISTS image_prints ( file_path TEXT PRIMARY KEY, hash BLOB NOT NULL, histogram BLOB NOT NULL ); """ JSON_FIELDS = ("genres", "images") @contextmanager def connect(): DB_PATH.parent.mkdir(parents=True, exist_ok=True) conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row try: yield conn conn.commit() finally: conn.close() def init(): with connect() as conn: conn.executescript(SCHEMA) # Spalten, die erst später dazukamen — die Teaser in einer bestehenden # Datenbank sind zu teuer, um sie für ein Schema-Update wegzuwerfen existing = {row["name"] for row in conn.execute("PRAGMA table_info(movies)")} for column, kind in ( ("collection_id", "INTEGER"), ("collection_name", "TEXT"), ("netflix_id", "TEXT"), ("imdb_id", "TEXT"), ("on_netflix", "INTEGER DEFAULT 0"), ("canon_group", "INTEGER"), ("canon_pos", "INTEGER"), ): if column not in existing: conn.execute(f"ALTER TABLE movies ADD COLUMN {column} {kind}") def row_to_dict(row): movie = dict(row) for field in JSON_FIELDS: movie[field] = json.loads(movie[field] or "[]") return movie def upsert(conn, movie): payload = dict(movie) for field in JSON_FIELDS: payload[field] = json.dumps(payload.get(field) or [], ensure_ascii=False) columns = ", ".join(payload) placeholders = ", ".join(f":{c}" for c in payload) conn.execute( f"INSERT OR REPLACE INTO movies ({columns}) VALUES ({placeholders})", payload ) def existing_teasers(conn): rows = conn.execute( "SELECT id, teaser FROM movies WHERE teaser IS NOT NULL AND teaser != ''" ).fetchall() return {row["id"]: row["teaser"] for row in rows} def existing_netflix_ids(conn): rows = conn.execute( "SELECT id, netflix_id FROM movies WHERE netflix_id IS NOT NULL" ).fetchall() return {row["id"]: row["netflix_id"] for row in rows} def drop_missing(conn, keep_ids): """Was weder auf Netflix läuft noch im Kanon steht, fliegt raus.""" current = {r["id"] for r in conn.execute("SELECT id FROM movies").fetchall()} gone = current - set(keep_ids) if gone: conn.executemany("DELETE FROM movies WHERE id = ?", [(i,) for i in gone]) return len(gone) def all_movies(): with connect() as conn: rows = conn.execute( "SELECT * FROM movies ORDER BY vote_count DESC, title ASC" ).fetchall() return [row_to_dict(r) for r in rows] def get_movie(movie_id): with connect() as conn: row = conn.execute("SELECT * FROM movies WHERE id = ?", (movie_id,)).fetchone() return row_to_dict(row) if row else None def known_prints(conn, paths): """SQLite begrenzt die Parameterzahl, also in Blöcken abfragen.""" found = {} paths = list(paths) for start in range(0, len(paths), 500): chunk = paths[start : start + 500] marks = ", ".join("?" * len(chunk)) rows = conn.execute( f"SELECT file_path, hash, histogram FROM image_prints " f"WHERE file_path IN ({marks})", chunk, ).fetchall() for row in rows: found[row["file_path"]] = ( int.from_bytes(row["hash"], "big"), row["histogram"], ) return found def store_prints(conn, prints): conn.executemany( "INSERT OR REPLACE INTO image_prints (file_path, hash, histogram) " "VALUES (?, ?, ?)", [ (path, value[0].to_bytes(8, "big"), value[1]) for path, value in prints.items() ], ) def set_meta(conn, key, value): conn.execute( "INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)", (key, str(value)) ) def get_meta(key): with connect() as conn: row = conn.execute("SELECT value FROM meta WHERE key = ?", (key,)).fetchone() return row["value"] if row else None