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 .libraries import register_routes as reg_libraries
|
||||
from .poster import register_routes as reg_poster
|
||||
from .enrichment import register_routes as reg_enrichment
|
||||
reg_dashboard(bp)
|
||||
reg_scan(bp)
|
||||
reg_library(bp)
|
||||
reg_libraries(bp)
|
||||
reg_poster(bp)
|
||||
reg_enrichment(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/libraries" 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):
|
||||
|
|
@ -87,6 +90,46 @@ def test_libraries_endpoint_lists_and_saves(tmp_path, monkeypatch):
|
|||
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():
|
||||
base = Path(__file__).resolve().parent.parent / "api" / "video"
|
||||
for py in base.glob("*.py"):
|
||||
|
|
|
|||
Loading…
Reference in a new issue