From f7d1b725d776c721f5d7b0562a7e3fe5407ba8b1 Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Mon, 15 Jun 2026 13:50:19 -0700 Subject: [PATCH] video: resolve the video server independently of the music active pointer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The video side now uses a configured Plex/Jellyfin on its own (resolve_video_ server), not config_manager.get_active_media_server(). So a music-only server (Navidrome/SoulSync) never applies to video, and 'Navidrome for music + Plex for video' works. Order: explicit video pick (video_server setting) → music-active if video-capable → the single configured one → Plex if both → None. Seam tests cover each case incl. the mixed setup. --- api/video/libraries.py | 11 ++--- core/video/sources.py | 59 +++++++++++++++++++++++---- tests/test_video_server_resolution.py | 52 +++++++++++++++++++++++ 3 files changed, 110 insertions(+), 12 deletions(-) create mode 100644 tests/test_video_server_resolution.py diff --git a/api/video/libraries.py b/api/video/libraries.py index 686d968c..a6ead6df 100644 --- a/api/video/libraries.py +++ b/api/video/libraries.py @@ -20,10 +20,9 @@ def register_routes(bp): def video_libraries(): from . import get_video_db try: - from core.video.sources import list_video_libraries - from config.settings import config_manager + from core.video.sources import list_video_libraries, resolve_video_server libs = list_video_libraries() or {"server": None, "movies": [], "tv": []} - server = libs.get("server") or config_manager.get_active_media_server() + server = libs.get("server") or resolve_video_server() libs["selected"] = (get_video_db().get_library_selection(server) if server else {"movies": None, "tv": None}) return jsonify(libs) @@ -35,9 +34,11 @@ def register_routes(bp): def save_video_libraries(): from . import get_video_db try: - from config.settings import config_manager + from core.video.sources import resolve_video_server body = request.get_json(silent=True) or {} - server = config_manager.get_active_media_server() + server = resolve_video_server() + if not server: + return jsonify({"error": "no video server"}), 400 get_video_db().set_library_selection(server, body.get("movies"), body.get("tv")) return jsonify({"status": "saved", "server": server}) except Exception: diff --git a/core/video/sources.py b/core/video/sources.py index 214cd806..251ca5c7 100644 --- a/core/video/sources.py +++ b/core/video/sources.py @@ -63,17 +63,61 @@ def _parse_jf_providers(item) -> dict: } +def resolve_video_server(db=None): + """The server the VIDEO side uses — a configured Plex/Jellyfin, resolved + INDEPENDENTLY of the music 'active server' pointer (so e.g. Navidrome-for-music + + Plex-for-video works, and music-only servers never apply here). Returns + 'plex' | 'jellyfin' | None. Order: explicit video pick → the music-active one if + it's video-capable → the single configured one → Plex if both → None.""" + try: + from config.settings import config_manager + except Exception: + return None + plex_ok = bool((config_manager.get_plex_config() or {}).get("base_url")) + jelly_ok = bool((config_manager.get_jellyfin_config() or {}).get("base_url")) + + pref = None + if db is None: + try: + from database.video_database import VideoDatabase + db = VideoDatabase() + except Exception: + db = None + if db is not None: + try: + pref = db.get_setting("video_server") + except Exception: + pref = None + if pref == "plex" and plex_ok: + return "plex" + if pref == "jellyfin" and jelly_ok: + return "jellyfin" + + active = config_manager.get_active_media_server() + if active == "plex" and plex_ok: + return "plex" + if active == "jellyfin" and jelly_ok: + return "jellyfin" + if plex_ok and not jelly_ok: + return "plex" + if jelly_ok and not plex_ok: + return "jellyfin" + if plex_ok and jelly_ok: + return "plex" + return None + + def _build_source(movies_lib=None, tv_lib=None): - """Build a media source for the active server, restricted to the named - Movies/TV libraries when given. Reuses the SHARED connection config — but - Plex gets a dedicated long-timeout connection for the bulk scan.""" + """Build a media source for the VIDEO server (see resolve_video_server), + restricted to the named Movies/TV libraries when given. Reuses the SHARED + connection config — but Plex gets a dedicated long-timeout connection.""" try: from config.settings import config_manager except Exception: logger.exception("video sources: config unavailable") return None - server = config_manager.get_active_media_server() + server = resolve_video_server() if server == "plex": cfg = config_manager.get_plex_config() or {} @@ -103,11 +147,12 @@ def _build_source(movies_lib=None, tv_lib=None): def _load_selection(): - """The user's Movies/TV library choice for the active server (or {}).""" + """The user's Movies/TV library choice for the VIDEO server (or {}).""" try: - from config.settings import config_manager from database.video_database import VideoDatabase - server = config_manager.get_active_media_server() + server = resolve_video_server() + if not server: + return {} return VideoDatabase().get_library_selection(server) except Exception: logger.exception("video sources: could not load library selection") diff --git a/tests/test_video_server_resolution.py b/tests/test_video_server_resolution.py new file mode 100644 index 00000000..697e1667 --- /dev/null +++ b/tests/test_video_server_resolution.py @@ -0,0 +1,52 @@ +"""Seam tests: the video side resolves its server (Plex/Jellyfin) INDEPENDENTLY +of the music 'active server' pointer — so music-only servers never apply and a +mixed setup (Navidrome music + Plex video) works.""" + +from __future__ import annotations + +import config.settings as cs +import pytest + +from core.video.sources import resolve_video_server +from database.video_database import VideoDatabase + + +def _set_cm(monkeypatch, plex, jelly, active): + class CM: + def get_plex_config(self): return {"base_url": "http://p", "token": "t"} if plex else {} + def get_jellyfin_config(self): return {"base_url": "http://j"} if jelly else {} + def get_active_media_server(self): return active + monkeypatch.setattr(cs, "config_manager", CM()) + + +@pytest.fixture() +def vdb(tmp_path): + return VideoDatabase(database_path=str(tmp_path / "v.db")) + + +def test_plex_only(monkeypatch, vdb): + _set_cm(monkeypatch, True, False, "plex") + assert resolve_video_server(vdb) == "plex" + + +def test_jellyfin_only(monkeypatch, vdb): + _set_cm(monkeypatch, False, True, "jellyfin") + assert resolve_video_server(vdb) == "jellyfin" + + +def test_none_when_only_a_music_server(monkeypatch, vdb): + _set_cm(monkeypatch, False, False, "navidrome") + assert resolve_video_server(vdb) is None + + +def test_independent_of_music_active(monkeypatch, vdb): + # Navidrome is the music server, but Plex is configured → video uses Plex. + _set_cm(monkeypatch, True, False, "navidrome") + assert resolve_video_server(vdb) == "plex" + + +def test_both_configured_default_then_explicit_pick(monkeypatch, vdb): + _set_cm(monkeypatch, True, True, "plex") + assert resolve_video_server(vdb) == "plex" # falls back to the active one + vdb.set_setting("video_server", "jellyfin") + assert resolve_video_server(vdb) == "jellyfin" # explicit video pick wins