Wishlist: real season posters (not the show poster) + extend backfill
Season tiles showed the show poster because the wishlist had no season art. Now stored + used: new video_wishlist.season_poster_url (SCHEMA_VERSION 11 + migration); the get-modal captures each season's poster at add-time (owned -> /poster/season proxy, tmdb -> direct); query_wishlist exposes season.poster_url; tiles render the real season poster (falling back to show poster, then placeholder). Backfill generalized: /wishlist/backfill-stills -> /wishlist/backfill-art fills BOTH stills and season posters from the same cached tmdb_season call (it already returns the season poster). The page fires it once when either is missing. Tests updated/added: art backfill targets + season-poster set + endpoint. 104 passed.
This commit is contained in:
parent
2985514627
commit
e5cd5cf08c
7 changed files with 78 additions and 45 deletions
|
|
@ -108,21 +108,23 @@ def register_routes(bp):
|
||||||
logger.exception("Failed to remove from video wishlist")
|
logger.exception("Failed to remove from video wishlist")
|
||||||
return jsonify({"success": False, "error": "Failed"}), 500
|
return jsonify({"success": False, "error": "Failed"}), 500
|
||||||
|
|
||||||
@bp.route("/wishlist/backfill-stills", methods=["POST"])
|
@bp.route("/wishlist/backfill-art", methods=["POST"])
|
||||||
def video_wishlist_backfill_stills():
|
def video_wishlist_backfill_art():
|
||||||
"""Fill episode stills for rows that predate still-capture. One tmdb_season
|
"""Fill episode stills + season posters for rows that predate art-capture.
|
||||||
call per (show, season); best-effort. Returns how many rows were filled."""
|
One tmdb_season call per (show, season); best-effort. Returns rows filled."""
|
||||||
from . import get_video_db
|
from . import get_video_db
|
||||||
from core.video.enrichment.engine import get_video_enrichment_engine
|
from core.video.enrichment.engine import get_video_enrichment_engine
|
||||||
db = get_video_db()
|
db = get_video_db()
|
||||||
eng = get_video_enrichment_engine()
|
eng = get_video_enrichment_engine()
|
||||||
updated = 0
|
updated = 0
|
||||||
try:
|
try:
|
||||||
for grp in db.wishlist_still_backfill_targets():
|
for grp in db.wishlist_art_backfill_targets():
|
||||||
try:
|
try:
|
||||||
se = eng.tmdb_season(grp["tmdb_id"], grp["season_number"]) or {}
|
se = eng.tmdb_season(grp["tmdb_id"], grp["season_number"]) or {}
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
|
if se.get("poster_url"):
|
||||||
|
updated += db.set_wishlist_season_poster(grp["tmdb_id"], grp["season_number"], se["poster_url"])
|
||||||
for ep in (se.get("episodes") or []):
|
for ep in (se.get("episodes") or []):
|
||||||
if ep.get("still_url") and ep.get("episode_number") is not None:
|
if ep.get("still_url") and ep.get("episode_number") is not None:
|
||||||
if db.set_wishlist_still(grp["tmdb_id"], grp["season_number"],
|
if db.set_wishlist_still(grp["tmdb_id"], grp["season_number"],
|
||||||
|
|
@ -130,7 +132,7 @@ def register_routes(bp):
|
||||||
updated += 1
|
updated += 1
|
||||||
return jsonify({"success": True, "updated": updated})
|
return jsonify({"success": True, "updated": updated})
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("wishlist still backfill failed")
|
logger.exception("wishlist art backfill failed")
|
||||||
return jsonify({"success": False, "updated": updated})
|
return jsonify({"success": False, "updated": updated})
|
||||||
|
|
||||||
@bp.route("/wishlist/check", methods=["POST"])
|
@bp.route("/wishlist/check", methods=["POST"])
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ logger = get_logger("video_database")
|
||||||
|
|
||||||
# Bump when video_schema.sql changes in a way worth recording. Stored in
|
# 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).
|
# PRAGMA user_version as a backstop indicator (nothing gates on it yet).
|
||||||
SCHEMA_VERSION = 10
|
SCHEMA_VERSION = 11
|
||||||
|
|
||||||
_DEFAULT_DB_PATH = "database/video_library.db"
|
_DEFAULT_DB_PATH = "database/video_library.db"
|
||||||
_SCHEMA_FILE = Path(__file__).resolve().parent / "video_schema.sql"
|
_SCHEMA_FILE = Path(__file__).resolve().parent / "video_schema.sql"
|
||||||
|
|
@ -100,6 +100,7 @@ _COLUMN_MIGRATIONS = [
|
||||||
("shows", "airs_time", "TEXT"), # TVDB show air time, e.g. "21:00" (network local)
|
("shows", "airs_time", "TEXT"), # TVDB show air time, e.g. "21:00" (network local)
|
||||||
("video_watchlist", "state", "TEXT NOT NULL DEFAULT 'follow'"), # follow | mute (tombstone)
|
("video_watchlist", "state", "TEXT NOT NULL DEFAULT 'follow'"), # follow | mute (tombstone)
|
||||||
("video_wishlist", "still_url", "TEXT"), # episode still thumbnail (captured at add time)
|
("video_wishlist", "still_url", "TEXT"), # episode still thumbnail (captured at add time)
|
||||||
|
("video_wishlist", "season_poster_url", "TEXT"), # the episode's season poster
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1518,17 +1519,19 @@ class VideoDatabase:
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"""INSERT INTO video_wishlist
|
"""INSERT INTO video_wishlist
|
||||||
(kind, tmdb_id, title, poster_url, season_number, episode_number,
|
(kind, tmdb_id, title, poster_url, season_number, episode_number,
|
||||||
episode_title, still_url, air_date, library_id, server_source)
|
episode_title, still_url, season_poster_url, air_date, library_id, server_source)
|
||||||
VALUES ('episode', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES ('episode', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
ON CONFLICT(tmdb_id, season_number, episode_number) WHERE kind='episode' DO UPDATE SET
|
ON CONFLICT(tmdb_id, season_number, episode_number) WHERE kind='episode' DO UPDATE SET
|
||||||
title=excluded.title,
|
title=excluded.title,
|
||||||
poster_url=COALESCE(excluded.poster_url, video_wishlist.poster_url),
|
poster_url=COALESCE(excluded.poster_url, video_wishlist.poster_url),
|
||||||
episode_title=COALESCE(excluded.episode_title, video_wishlist.episode_title),
|
episode_title=COALESCE(excluded.episode_title, video_wishlist.episode_title),
|
||||||
still_url=COALESCE(excluded.still_url, video_wishlist.still_url),
|
still_url=COALESCE(excluded.still_url, video_wishlist.still_url),
|
||||||
|
season_poster_url=COALESCE(excluded.season_poster_url, video_wishlist.season_poster_url),
|
||||||
air_date=COALESCE(excluded.air_date, video_wishlist.air_date),
|
air_date=COALESCE(excluded.air_date, video_wishlist.air_date),
|
||||||
library_id=COALESCE(excluded.library_id, video_wishlist.library_id)""",
|
library_id=COALESCE(excluded.library_id, video_wishlist.library_id)""",
|
||||||
(int(show_tmdb_id), show_title, poster_url, int(sn), int(en),
|
(int(show_tmdb_id), show_title, poster_url, int(sn), int(en),
|
||||||
e.get("title"), e.get("still_url"), e.get("air_date"), library_id, server_source))
|
e.get("title"), e.get("still_url"), e.get("season_poster_url"),
|
||||||
|
e.get("air_date"), library_id, server_source))
|
||||||
n += 1
|
n += 1
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return n
|
return n
|
||||||
|
|
@ -1628,16 +1631,20 @@ class VideoDatabase:
|
||||||
items = []
|
items = []
|
||||||
for sr in show_rows:
|
for sr in show_rows:
|
||||||
eps = conn.execute(
|
eps = conn.execute(
|
||||||
"SELECT season_number, episode_number, episode_title, still_url, air_date, status "
|
"SELECT season_number, episode_number, episode_title, still_url, "
|
||||||
|
"season_poster_url, air_date, status "
|
||||||
"FROM video_wishlist WHERE kind='episode' AND tmdb_id=? "
|
"FROM video_wishlist WHERE kind='episode' AND tmdb_id=? "
|
||||||
"ORDER BY season_number, episode_number", (sr["tmdb_id"],)).fetchall()
|
"ORDER BY season_number, episode_number", (sr["tmdb_id"],)).fetchall()
|
||||||
by_season: dict = {}
|
by_season: dict = {}
|
||||||
|
season_poster: dict = {}
|
||||||
for e in eps:
|
for e in eps:
|
||||||
by_season.setdefault(e["season_number"], []).append({
|
by_season.setdefault(e["season_number"], []).append({
|
||||||
"episode_number": e["episode_number"], "title": e["episode_title"],
|
"episode_number": e["episode_number"], "title": e["episode_title"],
|
||||||
"still_url": e["still_url"], "air_date": e["air_date"], "status": e["status"]})
|
"still_url": e["still_url"], "air_date": e["air_date"], "status": e["status"]})
|
||||||
seasons = [{"season_number": sn, "episodes": by_season[sn]}
|
if e["season_poster_url"] and e["season_number"] not in season_poster:
|
||||||
for sn in sorted(by_season)]
|
season_poster[e["season_number"]] = e["season_poster_url"]
|
||||||
|
seasons = [{"season_number": sn, "poster_url": season_poster.get(sn),
|
||||||
|
"episodes": by_season[sn]} for sn in sorted(by_season)]
|
||||||
items.append({"kind": "show", "tmdb_id": sr["tmdb_id"], "title": sr["title"],
|
items.append({"kind": "show", "tmdb_id": sr["tmdb_id"], "title": sr["title"],
|
||||||
"poster_url": sr["poster_url"], "library_id": sr["library_id"],
|
"poster_url": sr["poster_url"], "library_id": sr["library_id"],
|
||||||
"wanted": sr["wanted"], "done": sr["done"] or 0, "seasons": seasons})
|
"wanted": sr["wanted"], "done": sr["done"] or 0, "seasons": seasons})
|
||||||
|
|
@ -1669,16 +1676,17 @@ class VideoDatabase:
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
def wishlist_still_backfill_targets(self) -> list:
|
def wishlist_art_backfill_targets(self) -> list:
|
||||||
"""Distinct (show_tmdb_id, season) with episode rows missing a still — one
|
"""Distinct (show_tmdb_id, season) with episode rows missing a still OR a
|
||||||
tmdb_season fetch per group fills them all (cheap backfill for rows added
|
season poster — one tmdb_season fetch per group fills both (cheap backfill
|
||||||
before still-capture existed)."""
|
for rows added before art-capture existed)."""
|
||||||
conn = self._get_connection()
|
conn = self._get_connection()
|
||||||
try:
|
try:
|
||||||
rows = conn.execute(
|
rows = conn.execute(
|
||||||
"SELECT DISTINCT tmdb_id, season_number FROM video_wishlist "
|
"SELECT DISTINCT tmdb_id, season_number FROM video_wishlist "
|
||||||
"WHERE kind='episode' AND (still_url IS NULL OR still_url='') "
|
"WHERE kind='episode' AND tmdb_id IS NOT NULL AND season_number IS NOT NULL "
|
||||||
"AND tmdb_id IS NOT NULL AND season_number IS NOT NULL").fetchall()
|
"AND ((still_url IS NULL OR still_url='') OR "
|
||||||
|
" (season_poster_url IS NULL OR season_poster_url=''))").fetchall()
|
||||||
return [{"tmdb_id": r["tmdb_id"], "season_number": r["season_number"]} for r in rows]
|
return [{"tmdb_id": r["tmdb_id"], "season_number": r["season_number"]} for r in rows]
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
@ -1698,6 +1706,21 @@ class VideoDatabase:
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
def set_wishlist_season_poster(self, show_tmdb_id, season_number, poster_url) -> int:
|
||||||
|
"""Fill the season poster on every episode row of a season that lacks one."""
|
||||||
|
if not poster_url:
|
||||||
|
return 0
|
||||||
|
conn = self._get_connection()
|
||||||
|
try:
|
||||||
|
cur = conn.execute(
|
||||||
|
"UPDATE video_wishlist SET season_poster_url=? WHERE kind='episode' AND tmdb_id=? "
|
||||||
|
"AND season_number=? AND (season_poster_url IS NULL OR season_poster_url='')",
|
||||||
|
(poster_url, int(show_tmdb_id), int(season_number)))
|
||||||
|
conn.commit()
|
||||||
|
return cur.rowcount
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
def wishlist_state(self, *, movie_ids=None, show_tmdb_id=None) -> dict:
|
def wishlist_state(self, *, movie_ids=None, show_tmdb_id=None) -> dict:
|
||||||
"""Hydration: which of ``movie_ids`` are wishlisted, and which episode
|
"""Hydration: which of ``movie_ids`` are wishlisted, and which episode
|
||||||
keys ('S_E') of ``show_tmdb_id`` are. Returns {movies:set, episodes:set}."""
|
keys ('S_E') of ``show_tmdb_id`` are. Returns {movies:set, episodes:set}."""
|
||||||
|
|
|
||||||
|
|
@ -368,6 +368,7 @@ CREATE TABLE IF NOT EXISTS video_wishlist (
|
||||||
episode_number INTEGER, -- episode rows
|
episode_number INTEGER, -- episode rows
|
||||||
episode_title TEXT, -- episode rows
|
episode_title TEXT, -- episode rows
|
||||||
still_url TEXT, -- episode still thumbnail (episode rows)
|
still_url TEXT, -- episode still thumbnail (episode rows)
|
||||||
|
season_poster_url TEXT, -- the episode's SEASON poster (episode rows)
|
||||||
air_date TEXT, -- episode rows (already aired by the time it's here)
|
air_date TEXT, -- episode rows (already aired by the time it's here)
|
||||||
status TEXT NOT NULL DEFAULT 'wanted', -- wanted|searching|downloading|downloaded|failed
|
status TEXT NOT NULL DEFAULT 'wanted', -- wanted|searching|downloading|downloaded|failed
|
||||||
library_id INTEGER, -- owned movies.id/shows.id when re-downloading
|
library_id INTEGER, -- owned movies.id/shows.id when re-downloading
|
||||||
|
|
|
||||||
|
|
@ -497,16 +497,16 @@ def test_wishlist_check_by_show(tmp_path):
|
||||||
assert res["by_show"]["1396"] == ["1_1"] and "1399" not in res["by_show"]
|
assert res["by_show"]["1396"] == ["1_1"] and "1399" not in res["by_show"]
|
||||||
|
|
||||||
|
|
||||||
def test_wishlist_backfill_stills_endpoint(tmp_path, monkeypatch):
|
def test_wishlist_backfill_art_endpoint(tmp_path, monkeypatch):
|
||||||
client, vapi = _make_client(tmp_path)
|
client, vapi = _make_client(tmp_path)
|
||||||
db = vapi._video_db
|
db = vapi._video_db
|
||||||
db.add_episodes_to_wishlist(1396, "BB", [{"season_number": 1, "episode_number": 1}]) # no still
|
db.add_episodes_to_wishlist(1396, "BB", [{"season_number": 1, "episode_number": 1}]) # no art
|
||||||
import core.video.enrichment.engine as eng_mod
|
import core.video.enrichment.engine as eng_mod
|
||||||
|
|
||||||
class FakeEng:
|
class FakeEng:
|
||||||
def tmdb_season(self, tv, sn): return {"episodes": [{"episode_number": 1, "still_url": "/s.jpg"}]}
|
def tmdb_season(self, tv, sn):
|
||||||
|
return {"poster_url": "/s1.jpg", "episodes": [{"episode_number": 1, "still_url": "/s.jpg"}]}
|
||||||
monkeypatch.setattr(eng_mod, "get_video_enrichment_engine", lambda: FakeEng())
|
monkeypatch.setattr(eng_mod, "get_video_enrichment_engine", lambda: FakeEng())
|
||||||
r = client.post("/api/video/wishlist/backfill-stills").get_json()
|
assert client.post("/api/video/wishlist/backfill-art").get_json()["success"] is True
|
||||||
assert r["success"] and r["updated"] == 1
|
season = db.query_wishlist("show")["items"][0]["seasons"][0]
|
||||||
ep = db.query_wishlist("show")["items"][0]["seasons"][0]["episodes"][0]
|
assert season["poster_url"] == "/s1.jpg" and season["episodes"][0]["still_url"] == "/s.jpg"
|
||||||
assert ep["still_url"] == "/s.jpg"
|
|
||||||
|
|
|
||||||
|
|
@ -956,14 +956,15 @@ def test_wishlist_episode_still_roundtrips(db):
|
||||||
assert by[1]["still_url"] == "https://img/e1.jpg" and by[2]["still_url"] is None
|
assert by[1]["still_url"] == "https://img/e1.jpg" and by[2]["still_url"] is None
|
||||||
|
|
||||||
|
|
||||||
def test_wishlist_still_backfill_targets_and_set(db):
|
def test_wishlist_art_backfill_targets_and_set(db):
|
||||||
db.add_episodes_to_wishlist(1396, "BB", [
|
db.add_episodes_to_wishlist(1396, "BB", [
|
||||||
{"season_number": 1, "episode_number": 1, "still_url": "/have.jpg"},
|
{"season_number": 1, "episode_number": 1, "still_url": "/have.jpg", "season_poster_url": "/s1.jpg"},
|
||||||
{"season_number": 1, "episode_number": 2},
|
{"season_number": 1, "episode_number": 2},
|
||||||
{"season_number": 2, "episode_number": 1}])
|
{"season_number": 2, "episode_number": 1}])
|
||||||
tset = {(t["tmdb_id"], t["season_number"]) for t in db.wishlist_still_backfill_targets()}
|
tset = {(t["tmdb_id"], t["season_number"]) for t in db.wishlist_art_backfill_targets()}
|
||||||
assert (1396, 1) in tset and (1396, 2) in tset
|
assert (1396, 1) in tset and (1396, 2) in tset # S1 missing a still, S2 missing both
|
||||||
assert db.set_wishlist_still(1396, 1, 2, "/new.jpg") is True
|
assert db.set_wishlist_still(1396, 1, 2, "/new.jpg") is True
|
||||||
assert db.set_wishlist_still(1396, 1, 1, "/x.jpg") is False # won't clobber an existing still
|
assert db.set_wishlist_still(1396, 1, 1, "/x.jpg") is False # won't clobber an existing still
|
||||||
eps = {e["episode_number"]: e for e in db.query_wishlist("show")["items"][0]["seasons"][0]["episodes"]}
|
assert db.set_wishlist_season_poster(1396, 2, "/s2.jpg") == 1 # fills the season's episodes
|
||||||
assert eps[1]["still_url"] == "/have.jpg" and eps[2]["still_url"] == "/new.jpg"
|
by_season = {s["season_number"]: s for s in db.query_wishlist("show")["items"][0]["seasons"]}
|
||||||
|
assert by_season[1]["poster_url"] == "/s1.jpg" and by_season[2]["poster_url"] == "/s2.jpg"
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,8 @@
|
||||||
modalState.sel.forEach(function (key) {
|
modalState.sel.forEach(function (key) {
|
||||||
var p = key.split('_'), m = (modalState.epMeta || {})[key] || {};
|
var p = key.split('_'), m = (modalState.epMeta || {})[key] || {};
|
||||||
eps.push({ season_number: parseInt(p[0], 10), episode_number: parseInt(p[1], 10),
|
eps.push({ season_number: parseInt(p[0], 10), episode_number: parseInt(p[1], 10),
|
||||||
title: m.title, air_date: m.air_date, still_url: m.still });
|
title: m.title, air_date: m.air_date, still_url: m.still,
|
||||||
|
season_poster_url: (modalState.seasonPoster || {})[p[0]] });
|
||||||
});
|
});
|
||||||
if (!eps.length) { if (btn) btn.disabled = false; toast('Select at least one episode', 'info'); return; }
|
if (!eps.length) { if (btn) btn.disabled = false; toast('Select at least one episode', 'info'); return; }
|
||||||
|
|
||||||
|
|
@ -313,12 +314,16 @@
|
||||||
// they load per-season on expand (like the full detail page). Stash the tv
|
// they load per-season on expand (like the full detail page). Stash the tv
|
||||||
// id so the lazy loader can fetch them.
|
// id so the lazy loader can fetch them.
|
||||||
var tvId = (o && o.source === 'tmdb') ? parseInt(o.id, 10) : null;
|
var tvId = (o && o.source === 'tmdb') ? parseInt(o.id, 10) : null;
|
||||||
modalState = { kind: 'show', sel: new Set(), tvId: tvId, today: today, epMeta: {} };
|
modalState = { kind: 'show', sel: new Set(), tvId: tvId, today: today, epMeta: {}, seasonPoster: {} };
|
||||||
var totalMissing = 0, anyLazy = false;
|
var totalMissing = 0, anyLazy = false;
|
||||||
var html = '<div class="vgm-eps-head"><span class="vgm-eps-h">Episodes</span>' +
|
var html = '<div class="vgm-eps-head"><span class="vgm-eps-h">Episodes</span>' +
|
||||||
'<label class="vgm-eps-toggle"><input type="checkbox" data-vgm-missing-only checked>' +
|
'<label class="vgm-eps-toggle"><input type="checkbox" data-vgm-missing-only checked>' +
|
||||||
'<span>Missing only</span></label></div><div class="vgm-eps-list">';
|
'<span>Missing only</span></label></div><div class="vgm-eps-list">';
|
||||||
d.seasons.forEach(function (s) {
|
d.seasons.forEach(function (s) {
|
||||||
|
// Capture this season's poster (owned → proxy by season id, tmdb → direct).
|
||||||
|
modalState.seasonPoster[String(s.season_number)] = (o && o.source === 'library')
|
||||||
|
? (s.has_poster && s.id != null ? '/api/video/poster/season/' + s.id : null)
|
||||||
|
: (s.poster_url || null);
|
||||||
var eps = s.episodes || [];
|
var eps = s.episodes || [];
|
||||||
// Lazy: a tmdb season with a known count but no episodes shipped yet.
|
// Lazy: a tmdb season with a known count but no episodes shipped yet.
|
||||||
if (!eps.length && (s.episode_total || 0) > 0 && tvId) {
|
if (!eps.length && (s.episode_total || 0) > 0 && tvId) {
|
||||||
|
|
|
||||||
|
|
@ -79,8 +79,9 @@
|
||||||
: '<div class="wl-orb-initials">' + esc(initials(sh.title)) + '</div>';
|
: '<div class="wl-orb-initials">' + esc(initials(sh.title)) + '</div>';
|
||||||
var tiles = (sh.seasons || []).map(function (se) {
|
var tiles = (sh.seasons || []).map(function (se) {
|
||||||
var n = se.episodes.length;
|
var n = se.episodes.length;
|
||||||
// #2: stamp the season number over the art so tiles read as distinct seasons
|
// real season poster (falls back to the show poster, then a placeholder)
|
||||||
var inner = sh.poster_url ? '<img src="' + esc(sh.poster_url) + '" alt="">' : '<div class="wl-album-tile-fallback">📺</div>';
|
var posterUrl = se.poster_url || sh.poster_url || null;
|
||||||
|
var inner = posterUrl ? '<img src="' + esc(posterUrl) + '" alt="">' : '<div class="wl-album-tile-fallback">📺</div>';
|
||||||
var art = '<div class="wl-album-tile-art">' + inner + '<span class="vwsh-season-tag">S' + se.season_number + '</span></div>';
|
var art = '<div class="wl-album-tile-art">' + inner + '<span class="vwsh-season-tag">S' + se.season_number + '</span></div>';
|
||||||
var tracks = (se.episodes || []).map(function (e) {
|
var tracks = (se.episodes || []).map(function (e) {
|
||||||
var t = e.title || ('Episode ' + e.episode_number);
|
var t = e.title || ('Episode ' + e.episode_number);
|
||||||
|
|
@ -204,24 +205,24 @@
|
||||||
render(d.items || []);
|
render(d.items || []);
|
||||||
updatePagination(p);
|
updatePagination(p);
|
||||||
updateEmpty(p.total_count);
|
updateEmpty(p.total_count);
|
||||||
maybeBackfillStills(d.items || []);
|
maybeBackfillArt(d.items || []);
|
||||||
})
|
})
|
||||||
.catch(function () { if (ld) ld.classList.add('hidden'); render([]); updatePagination(null); updateEmpty(0); });
|
.catch(function () { if (ld) ld.classList.add('hidden'); render([]); updatePagination(null); updateEmpty(0); });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Episodes added before still-capture have no image — fetch them once (cheap:
|
// Rows added before art-capture have no episode still / season poster — fetch
|
||||||
// one tmdb_season call per show/season server-side), then reload to show them.
|
// them once (cheap: one tmdb_season call per show/season server-side), reload.
|
||||||
var stillsBackfilled = false;
|
var artBackfilled = false;
|
||||||
function maybeBackfillStills(items) {
|
function maybeBackfillArt(items) {
|
||||||
if (state.tab !== 'show' || stillsBackfilled) return;
|
if (state.tab !== 'show' || artBackfilled) return;
|
||||||
var missing = (items || []).some(function (sh) {
|
var missing = (items || []).some(function (sh) {
|
||||||
return (sh.seasons || []).some(function (se) {
|
return (sh.seasons || []).some(function (se) {
|
||||||
return (se.episodes || []).some(function (e) { return !e.still_url; });
|
return !se.poster_url || (se.episodes || []).some(function (e) { return !e.still_url; });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
if (!missing) return;
|
if (!missing) return;
|
||||||
stillsBackfilled = true;
|
artBackfilled = true;
|
||||||
fetch('/api/video/wishlist/backfill-stills', { method: 'POST', headers: { Accept: 'application/json' } })
|
fetch('/api/video/wishlist/backfill-art', { method: 'POST', headers: { Accept: 'application/json' } })
|
||||||
.then(function (r) { return r.ok ? r.json() : null; })
|
.then(function (r) { return r.ok ? r.json() : null; })
|
||||||
.then(function (res) { if (res && res.updated > 0) load(); })
|
.then(function (res) { if (res && res.updated > 0) load(); })
|
||||||
.catch(function () { /* best-effort */ });
|
.catch(function () { /* best-effort */ });
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue