video: fix detail reload (music router clobber) + reliably show missing episodes
Reload bug: music's router boots first, rewrites an unknown /video-detail/... URL to /dashboard, and my init read the already-changed URL (no restore) AND dispatched open-detail before video-detail.js was listening (empty page + stray back button). Fix: capture the path at SCRIPT-EVAL time (before music boots) and DEFER the restore to a macrotask so every DOMContentLoaded handler is registered and music's initial routing has run — then re-assert the real URL. Reload/deep-link now restore the exact item. Missing-episodes bug: the full-episode-list cascade only ran via the lazy refresh, which was gated on ART being missing — so a show that already had posters/logo never pulled its episode list (stayed owned-only). Added shows.episodes_synced (schema v5): the worker sets it after a full cascade; show_detail returns it; the lazy refresh now triggers when NOT synced, so owned + missing episodes populate.
This commit is contained in:
parent
0598f5fbda
commit
53391372c3
6 changed files with 49 additions and 11 deletions
|
|
@ -159,6 +159,10 @@ class VideoEnrichmentWorker:
|
|||
data.get("overview"), data.get("poster_url"))
|
||||
except Exception:
|
||||
logger.exception("episode backfill failed: show %s season %s", show_id, snum)
|
||||
try:
|
||||
self.db.mark_episodes_synced(show_id)
|
||||
except Exception:
|
||||
logger.exception("episode backfill: could not mark synced for show %s", show_id)
|
||||
|
||||
# ── status (same shape the music enrichment API returns) ──────────────────
|
||||
def get_stats(self) -> dict:
|
||||
|
|
|
|||
|
|
@ -29,7 +29,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 = 4
|
||||
SCHEMA_VERSION = 5
|
||||
|
||||
_DEFAULT_DB_PATH = "database/video_library.db"
|
||||
_SCHEMA_FILE = Path(__file__).resolve().parent / "video_schema.sql"
|
||||
|
|
@ -90,6 +90,7 @@ _COLUMN_MIGRATIONS = [
|
|||
("episodes", "rating", "REAL"),
|
||||
("movies", "logo_url", "TEXT"),
|
||||
("shows", "logo_url", "TEXT"),
|
||||
("shows", "episodes_synced", "INTEGER NOT NULL DEFAULT 0"),
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -327,6 +328,16 @@ class VideoDatabase:
|
|||
finally:
|
||||
conn.close()
|
||||
|
||||
def mark_episodes_synced(self, show_id: int) -> None:
|
||||
"""Flag that the show's FULL episode list has been pulled from metadata
|
||||
(so the lazy on-view refresh doesn't re-cascade every visit)."""
|
||||
conn = self._get_connection()
|
||||
try:
|
||||
conn.execute("UPDATE shows SET episodes_synced=1 WHERE id=?", (show_id,))
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def show_season_numbers(self, show_id: int) -> list:
|
||||
conn = self._get_connection()
|
||||
try:
|
||||
|
|
@ -958,6 +969,7 @@ class VideoDatabase:
|
|||
"tmdb_id": show["tmdb_id"], "tvdb_id": show["tvdb_id"], "imdb_id": show["imdb_id"],
|
||||
"has_poster": bool(show["poster_url"]), "has_backdrop": bool(show["backdrop_url"]),
|
||||
"logo": show["logo_url"],
|
||||
"episodes_synced": bool(show["episodes_synced"]),
|
||||
"monitored": bool(show["monitored"]),
|
||||
"season_count": len(out_seasons),
|
||||
"episode_total": total, "episode_owned": owned_total,
|
||||
|
|
|
|||
|
|
@ -125,6 +125,7 @@ CREATE TABLE IF NOT EXISTS shows (
|
|||
poster_url TEXT,
|
||||
backdrop_url TEXT,
|
||||
logo_url TEXT, -- transparent title logo (clearlogo)
|
||||
episodes_synced INTEGER NOT NULL DEFAULT 0, -- full episode list pulled from metadata?
|
||||
monitored INTEGER NOT NULL DEFAULT 1, -- "following" (watchlist)
|
||||
quality_profile_id INTEGER REFERENCES quality_profiles(id) ON DELETE SET NULL,
|
||||
root_folder_id INTEGER REFERENCES root_folders(id) ON DELETE SET NULL,
|
||||
|
|
|
|||
|
|
@ -626,6 +626,13 @@ def test_backfill_inserts_missing_episodes_as_unowned(db):
|
|||
assert by[2]["title"] == "Two" and by[2]["air_date"] == "2020-01-08"
|
||||
|
||||
|
||||
def test_episodes_synced_flag_drives_lazy_refresh(db):
|
||||
sid = db.upsert_show_tree("plex", {"server_id": "s1", "title": "S", "seasons": []})
|
||||
assert db.show_detail(sid)["episodes_synced"] is False # → lazy refresh will run
|
||||
db.mark_episodes_synced(sid)
|
||||
assert db.show_detail(sid)["episodes_synced"] is True # → won't re-cascade
|
||||
|
||||
|
||||
def test_backfill_creates_fully_missing_season(db):
|
||||
sid = db.upsert_show_tree("plex", {"server_id": "s1", "title": "S", "seasons": [
|
||||
{"season_number": 1, "episodes": [{"episode_number": 1}]}]})
|
||||
|
|
|
|||
|
|
@ -517,7 +517,10 @@
|
|||
// it (once per show), then re-render. Sidesteps "already matched, never re-runs".
|
||||
function maybeRefreshArt(id) {
|
||||
if (artAttemptedFor === id || !data || data.id !== id) return;
|
||||
var needs = !data.logo || (data.seasons || []).some(function (s) { return !s.has_poster; });
|
||||
// Trigger if the full episode list hasn't been pulled yet (so missing
|
||||
// episodes show up), or any art is still missing.
|
||||
var needs = !data.episodes_synced || !data.logo
|
||||
|| (data.seasons || []).some(function (s) { return !s.has_poster; });
|
||||
if (!needs) return;
|
||||
artAttemptedFor = id;
|
||||
fetch(DETAIL_URL + 'show/' + id + '/refresh-art',
|
||||
|
|
|
|||
|
|
@ -16,6 +16,11 @@
|
|||
(function () {
|
||||
'use strict';
|
||||
|
||||
// Captured at SCRIPT-EVAL time — before music's router boots (on
|
||||
// DOMContentLoaded) and may rewrite an unknown /video-detail/... URL to
|
||||
// /dashboard. This is the real path the user reloaded/deep-linked.
|
||||
var BOOT_PATH = window.location.pathname;
|
||||
|
||||
var SIDE_KEY = 'soulsync_side';
|
||||
var MUSIC_SUBTITLE = 'Music Sync & Manager';
|
||||
var VIDEO_SUBTITLE = 'Movies, TV & YouTube';
|
||||
|
|
@ -196,8 +201,9 @@
|
|||
}
|
||||
|
||||
function init() {
|
||||
// Capture a deep-linked detail path BEFORE applySide can clear it.
|
||||
var bootDetail = parseDetailPath(window.location.pathname);
|
||||
// Deep-linked detail path captured at eval time (music may already have
|
||||
// rewritten window.location to /dashboard by now).
|
||||
var bootDetail = parseDetailPath(BOOT_PATH);
|
||||
|
||||
var toggleButtons = document.querySelectorAll('.side-toggle-btn');
|
||||
for (var i = 0; i < toggleButtons.length; i++) {
|
||||
|
|
@ -265,14 +271,19 @@
|
|||
|
||||
applySide(bootDetail ? 'video' : readSide());
|
||||
|
||||
// Deep link / reload straight to a detail URL → restore it (applySide above
|
||||
// may have cleared the URL, so re-assert it first).
|
||||
// Deep link / reload straight to a detail URL → restore it. Deferred to a
|
||||
// macrotask so EVERY script's DOMContentLoaded handler has registered
|
||||
// first (video-detail.js loads after us and must be listening for the
|
||||
// open-detail event), and so it lands AFTER music's initial routing — then
|
||||
// we re-assert the real URL it clobbered.
|
||||
if (bootDetail) {
|
||||
var path = buildDetailPath(bootDetail.source, bootDetail.kind, bootDetail.id);
|
||||
try {
|
||||
history.replaceState({ videoDetail: bootDetail }, '', path);
|
||||
} catch (e) { /* ignore */ }
|
||||
restoreDetail(bootDetail);
|
||||
setTimeout(function () {
|
||||
var path = buildDetailPath(bootDetail.source, bootDetail.kind, bootDetail.id);
|
||||
try {
|
||||
history.replaceState({ videoDetail: bootDetail }, '', path);
|
||||
} catch (e) { /* ignore */ }
|
||||
restoreDetail(bootDetail);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue