video enrichment 1c: isolated /api/video/enrichment blueprint
Mirrors the music enrichment API so the shared Manage-Workers modal can drive video workers by pointing at /api/video/...: - GET services; GET <service>/status (worker.get_stats); POST pause/resume; GET breakdown; GET unmatched (paged, kind/status/q); POST retry. - Unknown service -> 404. Engine via the lazy singleton; DB queries via the isolated video DB. 6 API tests (services/status/breakdown/unmatched/pause/ resume/retry/404) with an injected engine + fake clients.
This commit is contained in:
parent
c17138bf7d
commit
80679b02ba
3 changed files with 134 additions and 0 deletions
|
|
@ -42,10 +42,12 @@ def create_video_blueprint() -> Blueprint:
|
||||||
from .library import register_routes as reg_library
|
from .library import register_routes as reg_library
|
||||||
from .libraries import register_routes as reg_libraries
|
from .libraries import register_routes as reg_libraries
|
||||||
from .poster import register_routes as reg_poster
|
from .poster import register_routes as reg_poster
|
||||||
|
from .enrichment import register_routes as reg_enrichment
|
||||||
reg_dashboard(bp)
|
reg_dashboard(bp)
|
||||||
reg_scan(bp)
|
reg_scan(bp)
|
||||||
reg_library(bp)
|
reg_library(bp)
|
||||||
reg_libraries(bp)
|
reg_libraries(bp)
|
||||||
reg_poster(bp)
|
reg_poster(bp)
|
||||||
|
reg_enrichment(bp)
|
||||||
|
|
||||||
return bp
|
return bp
|
||||||
|
|
|
||||||
89
api/video/enrichment.py
Normal file
89
api/video/enrichment.py
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
"""Video enrichment API — mirrors the music enrichment endpoints so the shared
|
||||||
|
Manage-Workers modal can drive video workers by pointing at /api/video/...
|
||||||
|
|
||||||
|
GET /api/video/enrichment/services
|
||||||
|
GET /api/video/enrichment/<service>/status
|
||||||
|
POST /api/video/enrichment/<service>/pause | /resume
|
||||||
|
GET /api/video/enrichment/<service>/breakdown
|
||||||
|
GET /api/video/enrichment/<service>/unmatched?kind=&status=&q=&limit=&offset=
|
||||||
|
POST /api/video/enrichment/<service>/retry {kind, scope, item_id}
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from flask import jsonify, request
|
||||||
|
|
||||||
|
from utils.logging_config import get_logger
|
||||||
|
|
||||||
|
logger = get_logger("video_api.enrichment")
|
||||||
|
|
||||||
|
|
||||||
|
def _int(val, default):
|
||||||
|
try:
|
||||||
|
return int(val)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def register_routes(bp):
|
||||||
|
def engine():
|
||||||
|
from core.video.enrichment.engine import get_video_enrichment_engine
|
||||||
|
return get_video_enrichment_engine()
|
||||||
|
|
||||||
|
@bp.route("/enrichment/services", methods=["GET"])
|
||||||
|
def video_enrichment_services():
|
||||||
|
try:
|
||||||
|
return jsonify({"services": engine().services()})
|
||||||
|
except Exception:
|
||||||
|
logger.exception("video enrichment services failed")
|
||||||
|
return jsonify({"services": []})
|
||||||
|
|
||||||
|
@bp.route("/enrichment/<service>/status", methods=["GET"])
|
||||||
|
def video_enrichment_status(service):
|
||||||
|
w = engine().worker(service)
|
||||||
|
if not w:
|
||||||
|
return jsonify({"error": "unknown service"}), 404
|
||||||
|
return jsonify(w.get_stats())
|
||||||
|
|
||||||
|
@bp.route("/enrichment/<service>/pause", methods=["POST"])
|
||||||
|
def video_enrichment_pause(service):
|
||||||
|
w = engine().worker(service)
|
||||||
|
if not w:
|
||||||
|
return jsonify({"error": "unknown service"}), 404
|
||||||
|
w.pause()
|
||||||
|
return jsonify({"status": "paused"})
|
||||||
|
|
||||||
|
@bp.route("/enrichment/<service>/resume", methods=["POST"])
|
||||||
|
def video_enrichment_resume(service):
|
||||||
|
w = engine().worker(service)
|
||||||
|
if not w:
|
||||||
|
return jsonify({"error": "unknown service"}), 404
|
||||||
|
w.resume()
|
||||||
|
return jsonify({"status": "running"})
|
||||||
|
|
||||||
|
@bp.route("/enrichment/<service>/breakdown", methods=["GET"])
|
||||||
|
def video_enrichment_breakdown(service):
|
||||||
|
from . import get_video_db
|
||||||
|
return jsonify({"service": service, "breakdown": get_video_db().enrichment_breakdown(service)})
|
||||||
|
|
||||||
|
@bp.route("/enrichment/<service>/unmatched", methods=["GET"])
|
||||||
|
def video_enrichment_unmatched(service):
|
||||||
|
from . import get_video_db
|
||||||
|
kind = request.args.get("kind", "movie")
|
||||||
|
res = get_video_db().enrichment_unmatched(
|
||||||
|
service, kind,
|
||||||
|
status=request.args.get("status", "not_found"),
|
||||||
|
search=request.args.get("q") or None,
|
||||||
|
limit=_int(request.args.get("limit"), 50),
|
||||||
|
offset=_int(request.args.get("offset"), 0))
|
||||||
|
res.update({"service": service, "kind": kind})
|
||||||
|
return jsonify(res)
|
||||||
|
|
||||||
|
@bp.route("/enrichment/<service>/retry", methods=["POST"])
|
||||||
|
def video_enrichment_retry(service):
|
||||||
|
from . import get_video_db
|
||||||
|
body = request.get_json(silent=True) or {}
|
||||||
|
n = get_video_db().enrichment_retry(
|
||||||
|
service, body.get("kind", "movie"),
|
||||||
|
scope=body.get("scope", "failed"), item_id=body.get("item_id"))
|
||||||
|
return jsonify({"success": True, "reset": n})
|
||||||
|
|
@ -35,6 +35,9 @@ def test_blueprint_exposes_dashboard_route():
|
||||||
assert "/api/video/library" in rules
|
assert "/api/video/library" in rules
|
||||||
assert "/api/video/libraries" in rules
|
assert "/api/video/libraries" in rules
|
||||||
assert any(r.startswith("/api/video/poster/") for r in rules)
|
assert any(r.startswith("/api/video/poster/") for r in rules)
|
||||||
|
assert "/api/video/enrichment/services" in rules
|
||||||
|
assert "/api/video/enrichment/<service>/status" in rules
|
||||||
|
assert "/api/video/enrichment/<service>/unmatched" in rules
|
||||||
|
|
||||||
|
|
||||||
def test_dashboard_endpoint_returns_zeroed_json(tmp_path):
|
def test_dashboard_endpoint_returns_zeroed_json(tmp_path):
|
||||||
|
|
@ -87,6 +90,46 @@ def test_libraries_endpoint_lists_and_saves(tmp_path, monkeypatch):
|
||||||
videoapi._video_db = None
|
videoapi._video_db = None
|
||||||
|
|
||||||
|
|
||||||
|
def test_enrichment_endpoints(tmp_path):
|
||||||
|
import api.video as videoapi
|
||||||
|
from database.video_database import VideoDatabase
|
||||||
|
import core.video.enrichment.engine as eng_mod
|
||||||
|
from core.video.enrichment.engine import VideoEnrichmentEngine
|
||||||
|
|
||||||
|
class FakeClient:
|
||||||
|
enabled = True
|
||||||
|
def match(self, *a): return None
|
||||||
|
|
||||||
|
db = VideoDatabase(database_path=str(tmp_path / "video_library.db"))
|
||||||
|
videoapi._video_db = db
|
||||||
|
eng_mod._engine = VideoEnrichmentEngine(db, {"tmdb": FakeClient(), "tvdb": FakeClient()})
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.register_blueprint(videoapi.create_video_blueprint(), url_prefix="/api/video")
|
||||||
|
client = app.test_client()
|
||||||
|
try:
|
||||||
|
svc = client.get("/api/video/enrichment/services").get_json()
|
||||||
|
assert {s["id"] for s in svc["services"]} == {"tmdb", "tvdb"}
|
||||||
|
|
||||||
|
mid = db.upsert_movie("plex", {"server_id": "m1", "title": "X"})
|
||||||
|
st = client.get("/api/video/enrichment/tmdb/status").get_json()
|
||||||
|
assert st["enabled"] is True and st["stats"]["pending"] == 1
|
||||||
|
|
||||||
|
db.enrichment_apply("tmdb", "movie", mid, matched=False)
|
||||||
|
bd = client.get("/api/video/enrichment/tmdb/breakdown").get_json()
|
||||||
|
assert bd["breakdown"]["movie"]["not_found"] == 1
|
||||||
|
un = client.get("/api/video/enrichment/tmdb/unmatched?kind=movie&status=not_found").get_json()
|
||||||
|
assert un["total"] == 1 and un["kind"] == "movie"
|
||||||
|
|
||||||
|
assert client.post("/api/video/enrichment/tmdb/pause").get_json()["status"] == "paused"
|
||||||
|
assert client.post("/api/video/enrichment/tmdb/resume").get_json()["status"] == "running"
|
||||||
|
assert client.post("/api/video/enrichment/tmdb/retry",
|
||||||
|
json={"kind": "movie", "scope": "failed"}).get_json()["reset"] == 1
|
||||||
|
assert client.get("/api/video/enrichment/nope/status").status_code == 404
|
||||||
|
finally:
|
||||||
|
videoapi._video_db = None
|
||||||
|
eng_mod._engine = None
|
||||||
|
|
||||||
|
|
||||||
def test_video_api_imports_nothing_from_music():
|
def test_video_api_imports_nothing_from_music():
|
||||||
base = Path(__file__).resolve().parent.parent / "api" / "video"
|
base = Path(__file__).resolve().parent.parent / "api" / "video"
|
||||||
for py in base.glob("*.py"):
|
for py in base.glob("*.py"):
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue