Files
horror/backend/app/db.py
2026-08-12 19:30:41 +02:00

181 lines
5.1 KiB
Python

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