video discover: lazy collection-id backfill so 'complete your collections' lights up

Already-matched movies predate the tmdb_collection_id column, so the collection gap rails
were empty (only newly-enriched movies got the id). Add a self-healing backfill: each
/discover/gaps load fills the franchise id for up to 20 owned movies missing it
(eng.movie_collection reuses the matcher's belongs_to_collection read), recording 0 for
movies with no franchise so they're not re-checked. The 'no-franchise' 0 is excluded from
the rails. Backfill is wrapped/isolated so it can never break the gap response. Over a few
Discover visits the whole library fills in and 'Complete the <franchise>' rails populate.
This commit is contained in:
BoulderBadgeDad 2026-06-22 23:48:45 -07:00
parent 8236b95db0
commit a68d9755a9
3 changed files with 60 additions and 1 deletions

View file

@ -100,6 +100,17 @@ def register_routes(bp):
db = get_video_db()
eng = get_video_enrichment_engine()
try:
# Lazy collection-id backfill: movies matched before the collection column
# exists have no franchise id. Fill a small batch each load (self-healing);
# isolated so a backfill hiccup never breaks the gap rails.
try:
for mv in db.movies_missing_collection(srv, limit=20):
coll = eng.movie_collection(mv["tmdb_id"])
if coll is not None:
db.set_movie_collection(mv["id"], coll.get("id"), coll.get("name"))
except Exception:
logger.exception("collection-id backfill batch failed")
owned = db.owned_movie_tmdb_ids(srv)
rails = []
# Complete your collections — top franchises you've started, missing entries.

View file

@ -436,6 +436,20 @@ class VideoEnrichmentEngine:
return []
return self._stamp_owned(items)
def movie_collection(self, tmdb_id) -> dict | None:
"""A movie's TMDB franchise membership {id, name} (belongs_to_collection), or
{id: None} when it belongs to no collection. Used to backfill already-matched
movies that predate the collection column."""
w = self.workers.get("tmdb")
if not w or not w.enabled or not tmdb_id:
return None
try:
meta = w.client.match("movie", None, None, known_id=tmdb_id) or {}
return {"id": meta.get("tmdb_collection_id"), "name": meta.get("tmdb_collection_name")}
except Exception:
logger.exception("movie_collection backfill failed for %s", tmdb_id)
return None
def trailer(self, kind, tmdb_id) -> dict | None:
"""Best YouTube trailer for a title (cached a day — trailers don't move)."""
w = self.workers.get("tmdb")

View file

@ -815,13 +815,47 @@ class VideoDatabase:
finally:
conn.close()
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}]."""
sql = ("SELECT id, tmdb_id FROM movies WHERE has_file=1 AND tmdb_id IS NOT NULL "
"AND tmdb_collection_id IS NULL")
args: list = []
if server_source:
sql += " AND server_source=?"
args.append(server_source)
sql += " LIMIT ?"
args.append(int(limit))
conn = self._get_connection()
try:
return [{"id": r["id"], "tmdb_id": r["tmdb_id"]} for r in conn.execute(sql, args)]
except sqlite3.Error:
return []
finally:
conn.close()
def set_movie_collection(self, movie_id: int, collection_id, name) -> None:
"""Persist a movie's TMDB collection id/name (backfill). collection_id may be
None recorded as 0 so the row isn't re-checked forever (a movie with no
franchise). 0 is excluded from the gap rails."""
conn = self._get_connection()
try:
conn.execute("UPDATE movies SET tmdb_collection_id=?, tmdb_collection_name=? WHERE id=?",
(int(collection_id) if collection_id else 0, name, int(movie_id)))
conn.commit()
except sqlite3.Error:
pass
finally:
conn.close()
def owned_movie_collections(self, server_source=None, limit: int = 12) -> list:
"""Franchises the user has STARTED (owns >=1 movie in), most-invested first —
drives the 'Complete your collections' gap rails. Returns
[{collection_id, name, owned_count}]."""
sql = ("SELECT tmdb_collection_id AS cid, "
"MAX(tmdb_collection_name) AS name, COUNT(*) AS c "
"FROM movies WHERE has_file=1 AND tmdb_collection_id IS NOT NULL")
"FROM movies WHERE has_file=1 AND tmdb_collection_id IS NOT NULL "
"AND tmdb_collection_id != 0")
args: list = []
if server_source:
sql += " AND server_source=?"