mirror of https://github.com/Nezreka/SoulSync.git
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.
183 lines
8.7 KiB
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
|