Kodi was overkill for a phone-driven "play this video" box: a full media center (web server, CEC, library DB) whose surface area is exactly what wedged the Pi -- a video-decode session left Kodi stuck in an uninterruptible firmware-mailbox call after a kernel keyring Oops, dead until a power cycle. mpv is just a video player: hardware-decoded straight on KMS/DRM, no media-center baggage. It runs as its own --idle systemd service holding the playlist, so playback keeps going even if the app/phone/WiFi drop -- the same autonomy Kodi's native playlist gave us. The app talks to it over its JSON IPC Unix socket. PlayerStateManager keeps the exact same public API, so the Flask routes, templates, and UI are unchanged (only the engine underneath swaps out). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
132 lines
3.8 KiB
Python
132 lines
3.8 KiB
Python
from flask import Blueprint, current_app, jsonify, request
|
|
|
|
from ..mpv import MpvError
|
|
from ..media import PathError, list_directory, resolve_path
|
|
|
|
bp = Blueprint("api", __name__, url_prefix="/api")
|
|
|
|
|
|
@bp.route("/browse")
|
|
def browse():
|
|
cfg = current_app.config
|
|
requested = request.args.get("path") or None
|
|
try:
|
|
resolved = resolve_path(requested, cfg["MEDIA_ROOTS"])
|
|
except PathError as exc:
|
|
return jsonify({"error": str(exc)}), 400
|
|
|
|
listing = list_directory(resolved, cfg["MEDIA_ROOTS"], cfg["VIDEO_EXTENSIONS"])
|
|
return jsonify({"path": resolved, **listing})
|
|
|
|
|
|
@bp.route("/play", methods=["POST"])
|
|
def play():
|
|
cfg = current_app.config
|
|
body = request.get_json(force=True, silent=True) or {}
|
|
requested = body.get("path")
|
|
mode = body.get("mode")
|
|
|
|
if not requested or mode not in ("file", "folder"):
|
|
return jsonify({"error": "expected {path, mode: 'file'|'folder'}"}), 400
|
|
|
|
try:
|
|
resolved = resolve_path(requested, cfg["MEDIA_ROOTS"])
|
|
except PathError as exc:
|
|
return jsonify({"error": str(exc)}), 400
|
|
|
|
player = current_app.player
|
|
try:
|
|
if mode == "file":
|
|
player.play_file(resolved)
|
|
else:
|
|
player.play_folder(resolved)
|
|
except ValueError as exc:
|
|
return jsonify({"error": str(exc)}), 400
|
|
except MpvError as exc:
|
|
return jsonify({"error": f"player unavailable: {exc}"}), 503
|
|
|
|
return jsonify({"ok": True})
|
|
|
|
|
|
@bp.route("/control/playpause", methods=["POST"])
|
|
def playpause():
|
|
try:
|
|
current_app.player.playpause()
|
|
except MpvError as exc:
|
|
return jsonify({"error": f"player unavailable: {exc}"}), 503
|
|
return jsonify({"ok": True})
|
|
|
|
|
|
@bp.route("/control/next", methods=["POST"])
|
|
def next_clip():
|
|
try:
|
|
current_app.player.next()
|
|
except MpvError as exc:
|
|
return jsonify({"error": f"player unavailable: {exc}"}), 503
|
|
return jsonify({"ok": True})
|
|
|
|
|
|
@bp.route("/control/previous", methods=["POST"])
|
|
def previous_clip():
|
|
try:
|
|
current_app.player.previous()
|
|
except MpvError as exc:
|
|
return jsonify({"error": f"player unavailable: {exc}"}), 503
|
|
return jsonify({"ok": True})
|
|
|
|
|
|
@bp.route("/control/seek", methods=["POST"])
|
|
def seek():
|
|
body = request.get_json(force=True, silent=True) or {}
|
|
try:
|
|
offset = float(body.get("offset"))
|
|
except (TypeError, ValueError):
|
|
return jsonify({"error": "expected {offset: number}"}), 400
|
|
|
|
try:
|
|
current_app.player.seek(offset)
|
|
except MpvError as exc:
|
|
return jsonify({"error": f"player unavailable: {exc}"}), 503
|
|
return jsonify({"ok": True})
|
|
|
|
|
|
@bp.route("/control/seekto", methods=["POST"])
|
|
def seekto():
|
|
body = request.get_json(force=True, silent=True) or {}
|
|
try:
|
|
position = float(body.get("position"))
|
|
except (TypeError, ValueError):
|
|
return jsonify({"error": "expected {position: seconds}"}), 400
|
|
|
|
try:
|
|
current_app.player.seek_to(position)
|
|
except MpvError as exc:
|
|
return jsonify({"error": f"player unavailable: {exc}"}), 503
|
|
return jsonify({"ok": True})
|
|
|
|
|
|
@bp.route("/control/volume", methods=["POST"])
|
|
def volume():
|
|
body = request.get_json(force=True, silent=True) or {}
|
|
try:
|
|
value = int(body.get("value"))
|
|
except (TypeError, ValueError):
|
|
return jsonify({"error": "expected {value: 0-100}"}), 400
|
|
|
|
try:
|
|
current_app.player.set_volume(value)
|
|
except MpvError as exc:
|
|
return jsonify({"error": f"player unavailable: {exc}"}), 503
|
|
return jsonify({"ok": True})
|
|
|
|
|
|
@bp.route("/control/keep-playing", methods=["POST"])
|
|
def keep_playing():
|
|
body = request.get_json(force=True, silent=True) or {}
|
|
current_app.player.set_keep_playing(bool(body.get("enabled")))
|
|
return jsonify({"ok": True})
|
|
|
|
|
|
@bp.route("/status")
|
|
def status():
|
|
return jsonify(current_app.player.get_status())
|