You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
SoulSync/api/video/libraries.py

183 lines
8.7 KiB

"""Video library mapping endpoints.
GET /api/video/libraries -> discover the active server's Movies/TV libraries
+ the user's current selection.
POST /api/video/libraries -> save {movies, tv} (library titles) for the active
server. The scanner then reads only those.
"""
from __future__ import annotations
from flask import jsonify, request
from utils.logging_config import get_logger
logger = get_logger("video_api.libraries")
def register_routes(bp):
@bp.route("/libraries", methods=["GET"])
def video_libraries():
from . import get_video_db
try:
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 resolve_video_server()
libs["selected"] = (get_video_db().get_library_selection(server)
if server else {"movies": None, "tv": None})
return jsonify(libs)
except Exception:
logger.exception("Failed to list video libraries")
return jsonify({"error": "Failed to list video libraries"}), 500
@bp.route("/server", methods=["GET"])
def video_server_status():
"""Which server the video side uses + which of Plex/Jellyfin are configured
(so the UI can show a picker, or a 'connect a server' message)."""
try:
from core.video.sources import (resolve_video_server,
video_plex_config, video_jellyfin_config)
plex = bool(video_plex_config().get("base_url"))
jelly = bool(video_jellyfin_config().get("base_url"))
return jsonify({"server": resolve_video_server(), "plex": plex, "jellyfin": jelly})
except Exception:
logger.exception("video server status failed")
return jsonify({"server": None, "plex": False, "jellyfin": False})
@bp.route("/server", methods=["POST"])
def video_server_set():
"""Set the explicit video-side server pick (only meaningful when both Plex
and Jellyfin are configured)."""
from . import get_video_db
body = request.get_json(silent=True) or {}
choice = body.get("server")
if choice not in ("plex", "jellyfin"):
return jsonify({"error": "bad server"}), 400
get_video_db().set_setting("video_server", choice)
return jsonify({"status": "saved", "server": choice})
@bp.route("/server-config", methods=["GET"])
def video_server_config_get():
"""The video side's OWN server connection — its stored creds when set, else
the values INHERITED (read-only) from music. 'inherited' flags tell the UI a
field is a placeholder it can override; tokens/keys are returned masked."""
try:
from core.video.sources import video_plex_config, video_jellyfin_config
p, j = video_plex_config(), video_jellyfin_config()
def mask(v):
v = v or ""
return ("" * 12) if v else ""
return jsonify({
"plex": {"base_url": p.get("base_url") or "", "token": mask(p.get("token")),
"has_token": bool(p.get("token")), "inherited": p.get("source") == "music"},
"jellyfin": {"base_url": j.get("base_url") or "", "api_key": mask(j.get("api_key")),
"has_key": bool(j.get("api_key")), "inherited": j.get("source") == "music"},
})
except Exception:
logger.exception("video server-config get failed")
return jsonify({"plex": {}, "jellyfin": {}})
@bp.route("/server-config", methods=["POST"])
def video_server_config_set():
"""Save the video side's OWN Plex/Jellyfin creds to video.db (NEVER the music
config). An empty/blank field clears that override → video falls back to
inheriting music's value. A masked token (all •) is left untouched."""
from . import get_video_db
body = request.get_json(silent=True) or {}
db = get_video_db()
def is_mask(v):
return bool(v) and set(str(v)) == {""}
def put(key, val):
if is_mask(val):
return # unchanged masked secret — keep what's stored
db.set_setting(key, (val or "").strip())
plex = body.get("plex") or {}
jelly = body.get("jellyfin") or {}
if "base_url" in plex:
put("video_plex_url", plex.get("base_url"))
if "token" in plex:
put("video_plex_token", plex.get("token"))
if "base_url" in jelly:
put("video_jellyfin_url", jelly.get("base_url"))
if "api_key" in jelly:
put("video_jellyfin_key", jelly.get("api_key"))
return jsonify({"status": "saved"})
@bp.route("/server-config/test", methods=["POST"])
def video_server_config_test():
"""Test the video side's effective connection for one server, using its OWN
stored/inherited creds (so it verifies exactly what the video scan will use)."""
body = request.get_json(silent=True) or {}
which = body.get("server")
if which not in ("plex", "jellyfin"):
return jsonify({"success": False, "error": "bad server"}), 400
try:
if which == "plex":
from core.video.sources import video_plex_config, PLEX_SCAN_TIMEOUT
cfg = video_plex_config()
if not cfg.get("base_url") or not cfg.get("token"):
return jsonify({"success": False, "error": "Plex URL/token not set"})
from plexapi.server import PlexServer
srv = PlexServer(cfg["base_url"], cfg["token"], timeout=PLEX_SCAN_TIMEOUT)
return jsonify({"success": True, "message": "Connected to " + (srv.friendlyName or "Plex")})
from core.video.sources import video_jellyfin_config, video_jellyfin_test
ok, message = video_jellyfin_test(video_jellyfin_config())
if ok:
return jsonify({"success": True, "message": message})
return jsonify({"success": False, "error": message})
except Exception as e:
return jsonify({"success": False, "error": str(e) or "connection failed"})
@bp.route("/jellyfin/users", methods=["GET"])
def video_jellyfin_users():
"""List the Jellyfin server's users so the video side can pick one (its
libraries are scoped to that user) — mirrors the music user picker. Uses
video's own effective Jellyfin creds."""
from . import get_video_db
try:
from core.video.sources import video_jellyfin_config
cfg = video_jellyfin_config()
base = (cfg.get("base_url") or "").rstrip("/")
key = cfg.get("api_key") or ""
if not base or not key:
return jsonify({"success": False, "users": []})
import requests
r = requests.get(base + "/Users", headers={"X-Emby-Token": key}, timeout=8)
if r.status_code != 200:
return jsonify({"success": False, "users": [], "error": "HTTP %d" % r.status_code})
users = r.json() or []
out = [{"id": u.get("Id"), "name": u.get("Name"),
"admin": bool((u.get("Policy") or {}).get("IsAdministrator"))}
for u in users if u.get("Id")]
selected = get_video_db().get_setting("video_jellyfin_user") or ""
return jsonify({"success": True, "users": out, "selected": selected})
except Exception as e:
return jsonify({"success": False, "users": [], "error": str(e)})
@bp.route("/jellyfin/user", methods=["POST"])
def video_jellyfin_user_set():
"""Persist the chosen Jellyfin user (its Id) for the video side."""
from . import get_video_db
body = request.get_json(silent=True) or {}
get_video_db().set_setting("video_jellyfin_user", (body.get("user") or "").strip())
return jsonify({"status": "saved"})
@bp.route("/libraries", methods=["POST"])
def save_video_libraries():
from . import get_video_db
try:
from core.video.sources import resolve_video_server
body = request.get_json(silent=True) or {}
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:
logger.exception("Failed to save video library selection")
return jsonify({"error": "Failed to save selection"}), 500