init
This commit is contained in:
180
backend/app/db.py
Normal file
180
backend/app/db.py
Normal file
@@ -0,0 +1,180 @@
|
||||
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
|
||||
Reference in New Issue
Block a user