181 lines
5.1 KiB
Python
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
|