174 lines
5.3 KiB
Python
174 lines
5.3 KiB
Python
|
|
"""MPV Lua API - Clean interface for Lua scripts to call Python functions.
|
||
|
|
|
||
|
|
This module provides a streamlined way for mpv Lua scripts to execute Python
|
||
|
|
functions and commands without relying on the broken observe_property IPC pattern.
|
||
|
|
|
||
|
|
Instead, Lua calls Python CLI directly via subprocess, and Python returns JSON
|
||
|
|
responses that Lua can parse.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import json
|
||
|
|
import logging
|
||
|
|
import sys
|
||
|
|
from pathlib import Path
|
||
|
|
from typing import Any, Dict, Optional
|
||
|
|
|
||
|
|
# Add parent directory to path so we can import CLI, pipeline, cmdlet_catalog from root
|
||
|
|
_SCRIPT_DIR = Path(__file__).parent
|
||
|
|
_ROOT_DIR = _SCRIPT_DIR.parent
|
||
|
|
if str(_ROOT_DIR) not in sys.path:
|
||
|
|
sys.path.insert(0, str(_ROOT_DIR))
|
||
|
|
|
||
|
|
|
||
|
|
def setup_logging(log_file: Optional[Path] = None) -> logging.Logger:
|
||
|
|
"""Setup logging for MPV API calls."""
|
||
|
|
logger = logging.getLogger("mpv-lua-api")
|
||
|
|
logger.setLevel(logging.DEBUG)
|
||
|
|
|
||
|
|
if not logger.handlers:
|
||
|
|
if log_file:
|
||
|
|
handler = logging.FileHandler(str(log_file), encoding="utf-8")
|
||
|
|
else:
|
||
|
|
handler = logging.StreamHandler(sys.stderr)
|
||
|
|
|
||
|
|
formatter = logging.Formatter(
|
||
|
|
"[%(asctime)s][%(levelname)s] %(message)s",
|
||
|
|
datefmt="%Y-%m-%d %H:%M:%S"
|
||
|
|
)
|
||
|
|
handler.setFormatter(formatter)
|
||
|
|
logger.addHandler(handler)
|
||
|
|
|
||
|
|
return logger
|
||
|
|
|
||
|
|
|
||
|
|
def log_to_helper(msg: str, log_file: Optional[Path] = None) -> None:
|
||
|
|
"""Log a message that will appear in the helper log."""
|
||
|
|
if log_file:
|
||
|
|
with open(log_file, "a", encoding="utf-8") as f:
|
||
|
|
f.write(f"[lua] {msg}\n")
|
||
|
|
|
||
|
|
|
||
|
|
def execute_pipeline(
|
||
|
|
pipeline_cmd: str,
|
||
|
|
log_file: Optional[Path] = None,
|
||
|
|
dry_run: bool = False,
|
||
|
|
) -> Dict[str, Any]:
|
||
|
|
"""Execute a pipeline command and return result as JSON.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
pipeline_cmd: Pipeline command string (e.g. "trim-file -path ... | add-file -store ...")
|
||
|
|
log_file: Optional path to helper log file for logging
|
||
|
|
dry_run: If True, log but don't execute
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
JSON object with keys: success, stdout, stderr, error, returncode
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
if log_file:
|
||
|
|
log_to_helper(f"[api] execute_pipeline cmd={pipeline_cmd}", log_file)
|
||
|
|
|
||
|
|
if dry_run:
|
||
|
|
return {
|
||
|
|
"success": True,
|
||
|
|
"stdout": "",
|
||
|
|
"stderr": "DRY RUN - command not executed",
|
||
|
|
"error": None,
|
||
|
|
"returncode": 0,
|
||
|
|
"cmd": pipeline_cmd,
|
||
|
|
}
|
||
|
|
|
||
|
|
# Call the CLI directly as subprocess
|
||
|
|
import subprocess
|
||
|
|
import shlex
|
||
|
|
|
||
|
|
# Parse the pipeline command into separate arguments
|
||
|
|
cmd_args = shlex.split(pipeline_cmd)
|
||
|
|
|
||
|
|
result = subprocess.run(
|
||
|
|
[sys.executable, "-m", "CLI"] + cmd_args,
|
||
|
|
capture_output=True,
|
||
|
|
text=True,
|
||
|
|
cwd=str(_ROOT_DIR),
|
||
|
|
env={**dict(__import__('os').environ), "MEDEIA_MPV_CALLER": "lua"},
|
||
|
|
)
|
||
|
|
|
||
|
|
if log_file:
|
||
|
|
log_to_helper(
|
||
|
|
f"[api] result returncode={result.returncode} len_stdout={len(result.stdout or '')} len_stderr={len(result.stderr or '')}",
|
||
|
|
log_file
|
||
|
|
)
|
||
|
|
if result.stderr:
|
||
|
|
log_to_helper(f"[api] stderr: {result.stderr[:500]}", log_file)
|
||
|
|
|
||
|
|
return {
|
||
|
|
"success": result.returncode == 0,
|
||
|
|
"stdout": result.stdout or "",
|
||
|
|
"stderr": result.stderr or "",
|
||
|
|
"error": None if result.returncode == 0 else result.stderr,
|
||
|
|
"returncode": result.returncode,
|
||
|
|
"cmd": pipeline_cmd,
|
||
|
|
}
|
||
|
|
|
||
|
|
except Exception as exc:
|
||
|
|
msg = f"{type(exc).__name__}: {exc}"
|
||
|
|
if log_file:
|
||
|
|
log_to_helper(f"[api] exception {msg}", log_file)
|
||
|
|
|
||
|
|
return {
|
||
|
|
"success": False,
|
||
|
|
"stdout": "",
|
||
|
|
"stderr": str(exc),
|
||
|
|
"error": msg,
|
||
|
|
"returncode": 1,
|
||
|
|
"cmd": pipeline_cmd,
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def handle_api_request(request_json: str, log_file: Optional[Path] = None) -> str:
|
||
|
|
"""Handle an API request from Lua and return JSON response.
|
||
|
|
|
||
|
|
Request format:
|
||
|
|
{
|
||
|
|
"cmd": "execute_pipeline",
|
||
|
|
"pipeline": "trim-file -path ... | add-file -store ...",
|
||
|
|
...
|
||
|
|
}
|
||
|
|
|
||
|
|
Response format: JSON with result of the operation.
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
request = json.loads(request_json)
|
||
|
|
cmd = request.get("cmd")
|
||
|
|
|
||
|
|
if cmd == "execute_pipeline":
|
||
|
|
pipeline_cmd = request.get("pipeline", "")
|
||
|
|
result = execute_pipeline(pipeline_cmd, log_file)
|
||
|
|
return json.dumps(result)
|
||
|
|
|
||
|
|
else:
|
||
|
|
return json.dumps({
|
||
|
|
"success": False,
|
||
|
|
"error": f"Unknown command: {cmd}",
|
||
|
|
})
|
||
|
|
|
||
|
|
except Exception as exc:
|
||
|
|
return json.dumps({
|
||
|
|
"success": False,
|
||
|
|
"error": f"{type(exc).__name__}: {exc}",
|
||
|
|
})
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
# When called from Lua via subprocess:
|
||
|
|
# python mpv_lua_api.py <json-request>
|
||
|
|
|
||
|
|
if len(sys.argv) < 2:
|
||
|
|
print(json.dumps({"success": False, "error": "No request provided"}))
|
||
|
|
sys.exit(1)
|
||
|
|
|
||
|
|
request_json = sys.argv[1]
|
||
|
|
log_file = Path(sys.argv[2]) if len(sys.argv) > 2 else None
|
||
|
|
|
||
|
|
response = handle_api_request(request_json, log_file)
|
||
|
|
print(response)
|