video discover phase 4 (backend): ignore list / 'Not interested'
Movie/show-level ignore list (episodes can't be individually ignored): - video_ignored table (kind, tmdb_id, title/year/poster snapshot) + SCHEMA_VERSION 18->19. - add_ignored / remove_ignored / list_ignored / ignored_keys DB methods. - _stamp_owned (the choke point all discover results pass through) drops ignored titles, so every surface hides them uniformly + always-fresh (tiny indexed query). Person-gap rails (which bypass _stamp_owned) filtered in the gaps endpoint. - GET/POST /discover/ignore (list / add / remove).
This commit is contained in:
parent
d019e3aca1
commit
3f344a8cd9
4 changed files with 100 additions and 1 deletions
|
|
@ -137,6 +137,7 @@ def register_routes(bp):
|
|||
logger.exception("collection-id backfill batch failed")
|
||||
|
||||
owned = db.owned_movie_tmdb_ids(srv)
|
||||
ignored = db.ignored_keys()
|
||||
rails = []
|
||||
# Complete your collections — top franchises you've started, missing entries.
|
||||
for coll in db.owned_movie_collections(srv, limit=8):
|
||||
|
|
@ -152,6 +153,9 @@ def register_routes(bp):
|
|||
continue
|
||||
missing = filmography_gaps(owned, p.get("credits") or [],
|
||||
kinds=("movie",), min_vote_count=50, limit=30)
|
||||
if ignored:
|
||||
missing = [m for m in missing
|
||||
if f"{m.get('kind')}:{m.get('tmdb_id')}" not in ignored]
|
||||
if len(missing) >= 3:
|
||||
rails.append({"title": "More from " + person["name"], "kind": "person",
|
||||
"items": missing})
|
||||
|
|
@ -176,6 +180,26 @@ def register_routes(bp):
|
|||
tr = None
|
||||
return jsonify({"trailer": tr or None})
|
||||
|
||||
@bp.route("/discover/ignore", methods=["GET", "POST"])
|
||||
def video_discover_ignore():
|
||||
"""The Discover 'Not interested' list. GET -> {items}. POST
|
||||
{action:'add'|'remove', kind, tmdb_id, title?, year?, poster?}."""
|
||||
from . import get_video_db
|
||||
db = get_video_db()
|
||||
try:
|
||||
if request.method == "GET":
|
||||
return jsonify({"items": db.list_ignored()})
|
||||
body = request.get_json(silent=True) or {}
|
||||
kind, tmdb_id = body.get("kind"), body.get("tmdb_id")
|
||||
if body.get("action") == "remove":
|
||||
db.remove_ignored(kind, tmdb_id)
|
||||
return jsonify({"success": True})
|
||||
ok = db.add_ignored(kind, tmdb_id, body.get("title"), body.get("year"), body.get("poster"))
|
||||
return jsonify({"success": ok})
|
||||
except Exception:
|
||||
logger.exception("discover ignore failed")
|
||||
return jsonify({"success": False, "items": []})
|
||||
|
||||
@bp.route("/discover/languages", methods=["GET", "POST"])
|
||||
def video_discover_languages():
|
||||
"""Get/set the preferred original-languages for general rails (ISO-639-1 codes).
|
||||
|
|
|
|||
|
|
@ -343,6 +343,15 @@ class VideoEnrichmentEngine:
|
|||
r["library_id"] = maps.get(r["kind"], {}).get(int(r["tmdb_id"]))
|
||||
except (TypeError, ValueError):
|
||||
r["library_id"] = None
|
||||
# Drop titles the user marked 'Not interested' — one tiny indexed query, always
|
||||
# fresh, so every discover surface (rails, recs, collection gaps) hides them uniformly.
|
||||
try:
|
||||
ignored = self.db.ignored_keys()
|
||||
except Exception:
|
||||
ignored = None
|
||||
if ignored:
|
||||
items = [r for r in (items or [])
|
||||
if f"{r.get('kind')}:{r.get('tmdb_id')}" not in ignored]
|
||||
return items
|
||||
|
||||
def discover_curated(self, key, page=1) -> list:
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ logger = get_logger("video_database")
|
|||
|
||||
# Bump when video_schema.sql changes in a way worth recording. Stored in
|
||||
# PRAGMA user_version as a backstop indicator (nothing gates on it yet).
|
||||
SCHEMA_VERSION = 18
|
||||
SCHEMA_VERSION = 19
|
||||
|
||||
_DEFAULT_DB_PATH = "database/video_library.db"
|
||||
_SCHEMA_FILE = Path(__file__).resolve().parent / "video_schema.sql"
|
||||
|
|
@ -815,6 +815,58 @@ class VideoDatabase:
|
|||
finally:
|
||||
conn.close()
|
||||
|
||||
# ── Discover ignore list ("Not interested") ──────────────────────────────
|
||||
def add_ignored(self, kind: str, tmdb_id, title=None, year=None, poster_url=None) -> bool:
|
||||
"""Hide a title from Discover (movie/show level). Idempotent."""
|
||||
if kind not in ("movie", "show") or tmdb_id is None:
|
||||
return False
|
||||
try:
|
||||
with self._get_connection() as conn:
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO video_ignored (kind, tmdb_id, title, year, poster_url) "
|
||||
"VALUES (?, ?, ?, ?, ?)",
|
||||
(kind, int(tmdb_id), title, int(year) if year else None, poster_url),
|
||||
)
|
||||
conn.commit()
|
||||
return True
|
||||
except (sqlite3.Error, TypeError, ValueError) as e:
|
||||
logger.debug(f"add_ignored failed: {e}")
|
||||
return False
|
||||
|
||||
def remove_ignored(self, kind: str, tmdb_id) -> bool:
|
||||
"""Un-hide a title."""
|
||||
try:
|
||||
with self._get_connection() as conn:
|
||||
conn.execute("DELETE FROM video_ignored WHERE kind=? AND tmdb_id=?",
|
||||
(kind, int(tmdb_id)))
|
||||
conn.commit()
|
||||
return True
|
||||
except (sqlite3.Error, TypeError, ValueError) as e:
|
||||
logger.debug(f"remove_ignored failed: {e}")
|
||||
return False
|
||||
|
||||
def list_ignored(self) -> list:
|
||||
"""All ignored titles, most-recently-added first — drives the manage modal."""
|
||||
try:
|
||||
with self._get_connection() as conn:
|
||||
rows = conn.execute(
|
||||
"SELECT kind, tmdb_id, title, year, poster_url FROM video_ignored "
|
||||
"ORDER BY added_at DESC"
|
||||
).fetchall()
|
||||
return [{"kind": r["kind"], "tmdb_id": r["tmdb_id"], "title": r["title"],
|
||||
"year": r["year"], "poster": r["poster_url"]} for r in rows]
|
||||
except sqlite3.Error:
|
||||
return []
|
||||
|
||||
def ignored_keys(self) -> set:
|
||||
"""Set of 'kind:tmdb_id' strings for fast Discover filtering."""
|
||||
try:
|
||||
with self._get_connection() as conn:
|
||||
return {f"{r['kind']}:{r['tmdb_id']}"
|
||||
for r in conn.execute("SELECT kind, tmdb_id FROM video_ignored")}
|
||||
except sqlite3.Error:
|
||||
return set()
|
||||
|
||||
def movies_missing_collection(self, server_source=None, limit: int = 20) -> list:
|
||||
"""Owned, TMDB-matched movies whose franchise (tmdb_collection_id) hasn't been
|
||||
backfilled yet — drives the lazy collection-id backfill. Returns [{id, tmdb_id}]."""
|
||||
|
|
|
|||
|
|
@ -231,6 +231,20 @@ CREATE INDEX IF NOT EXISTS idx_credits_movie ON credits(movie_id);
|
|||
CREATE INDEX IF NOT EXISTS idx_credits_show ON credits(show_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_credits_person ON credits(person_id);
|
||||
|
||||
-- ── Discover: "Not interested" / ignore list ────────────────────────────────
|
||||
-- Titles the user has hidden from Discover (movie/show level only — episodes can't be
|
||||
-- individually ignored). Keyed by (kind, tmdb_id); a denormalised title/poster snapshot so
|
||||
-- the manage-modal renders without a TMDB round-trip.
|
||||
CREATE TABLE IF NOT EXISTS video_ignored (
|
||||
kind TEXT NOT NULL, -- 'movie' | 'show'
|
||||
tmdb_id INTEGER NOT NULL,
|
||||
title TEXT,
|
||||
year INTEGER,
|
||||
poster_url TEXT,
|
||||
added_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (kind, tmdb_id)
|
||||
);
|
||||
|
||||
-- ── Content: YouTube (channels → videos) ────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS channels (
|
||||
id INTEGER PRIMARY KEY,
|
||||
|
|
|
|||
Loading…
Reference in a new issue