This commit is contained in:
2026-01-12 17:55:04 -08:00
parent e45138568f
commit d0a68da1f5
8 changed files with 318 additions and 553 deletions

View File

@@ -463,6 +463,193 @@ local function ensure_mpv_ipc_server()
return (now and now ~= '') and true or false
end
local function ensure_pipeline_helper_running()
-- IMPORTANT: do NOT spawn Python from inside mpv.
-- The Python side (MPV.mpv_ipc) starts pipeline_helper.py using Windows
-- no-console flags; spawning here can flash a console window.
return _is_pipeline_helper_ready() and true or false
end
local _ipc_async_busy = false
local _ipc_async_queue = {}
local function _run_helper_request_async(req, timeout_seconds, cb)
cb = cb or function() end
if _ipc_async_busy then
_ipc_async_queue[#_ipc_async_queue + 1] = { req = req, timeout = timeout_seconds, cb = cb }
return
end
_ipc_async_busy = true
local function done(resp, err)
_ipc_async_busy = false
cb(resp, err)
if #_ipc_async_queue > 0 then
local next_job = table.remove(_ipc_async_queue, 1)
-- Schedule next job slightly later to let mpv deliver any pending events.
mp.add_timeout(0.01, function()
_run_helper_request_async(next_job.req, next_job.timeout, next_job.cb)
end)
end
end
if type(req) ~= 'table' then
done(nil, 'invalid request')
return
end
ensure_mpv_ipc_server()
if not ensure_pipeline_helper_running() then
done(nil, 'helper not running')
return
end
-- Assign id.
local id = tostring(req.id or '')
if id == '' then
id = tostring(math.floor(mp.get_time() * 1000)) .. '-' .. tostring(math.random(100000, 999999))
req.id = id
end
local label = ''
if req.op then
label = 'op=' .. tostring(req.op)
elseif req.pipeline then
label = 'cmd=' .. tostring(req.pipeline)
else
label = '(unknown)'
end
-- Wait for helper READY without blocking the UI.
local ready_deadline = mp.get_time() + 3.0
local ready_timer
ready_timer = mp.add_periodic_timer(0.05, function()
if _is_pipeline_helper_ready() then
ready_timer:kill()
_lua_log('ipc-async: send request id=' .. tostring(id) .. ' ' .. label)
local req_json = utils.format_json(req)
_last_ipc_last_req_json = req_json
mp.set_property(PIPELINE_RESP_PROP, '')
mp.set_property(PIPELINE_REQ_PROP, req_json)
local deadline = mp.get_time() + (timeout_seconds or 5)
local poll_timer
poll_timer = mp.add_periodic_timer(0.05, function()
if mp.get_time() >= deadline then
poll_timer:kill()
done(nil, 'timeout waiting response (' .. label .. ')')
return
end
local resp_json = mp.get_property(PIPELINE_RESP_PROP)
if resp_json and resp_json ~= '' then
_last_ipc_last_resp_json = resp_json
local ok, resp = pcall(utils.parse_json, resp_json)
if ok and resp and resp.id == id then
poll_timer:kill()
_lua_log('ipc-async: got response id=' .. tostring(id) .. ' success=' .. tostring(resp.success))
done(resp, nil)
end
end
end)
return
end
if mp.get_time() >= ready_deadline then
ready_timer:kill()
done(nil, 'helper not ready')
return
end
end)
end
local function _run_helper_request_response(req, timeout_seconds)
_last_ipc_error = ''
if not ensure_pipeline_helper_running() then
local rv = tostring(mp.get_property(PIPELINE_READY_PROP) or mp.get_property_native(PIPELINE_READY_PROP) or '')
_lua_log('ipc: helper not ready (ready=' .. rv .. '); attempting request anyway')
_last_ipc_error = 'helper not ready'
end
do
-- Best-effort wait for heartbeat, but do not hard-fail the request.
local deadline = mp.get_time() + 1.5
while mp.get_time() < deadline do
if _is_pipeline_helper_ready() then
break
end
mp.wait_event(0.05)
end
if not _is_pipeline_helper_ready() then
local rv = tostring(mp.get_property(PIPELINE_READY_PROP) or mp.get_property_native(PIPELINE_READY_PROP) or '')
_lua_log('ipc: proceeding without helper heartbeat; ready=' .. rv)
_last_ipc_error = 'helper heartbeat missing (ready=' .. rv .. ')'
end
end
if type(req) ~= 'table' then
return nil
end
local id = tostring(req.id or '')
if id == '' then
id = tostring(math.floor(mp.get_time() * 1000)) .. '-' .. tostring(math.random(100000, 999999))
req.id = id
end
local label = ''
if req.op then
label = 'op=' .. tostring(req.op)
elseif req.pipeline then
label = 'cmd=' .. tostring(req.pipeline)
else
label = '(unknown)'
end
_lua_log('ipc: send request id=' .. tostring(id) .. ' ' .. label)
local req_json = utils.format_json(req)
_last_ipc_last_req_json = req_json
mp.set_property(PIPELINE_RESP_PROP, '')
mp.set_property(PIPELINE_REQ_PROP, req_json)
-- Read-back for debugging: confirms MPV accepted the property write.
local echoed = mp.get_property(PIPELINE_REQ_PROP) or ''
if echoed == '' then
_lua_log('ipc: WARNING request property echoed empty after set')
end
local deadline = mp.get_time() + (timeout_seconds or 5)
while mp.get_time() < deadline do
local resp_json = mp.get_property(PIPELINE_RESP_PROP)
if resp_json and resp_json ~= '' then
_last_ipc_last_resp_json = resp_json
local ok, resp = pcall(utils.parse_json, resp_json)
if ok and resp and resp.id == id then
_lua_log('ipc: got response id=' .. tostring(id) .. ' success=' .. tostring(resp.success))
return resp
end
end
mp.wait_event(0.05)
end
_lua_log('ipc: timeout waiting response; ' .. label)
_last_ipc_error = 'timeout waiting response (' .. label .. ')'
return nil
end
-- IPC helper: return the whole response object (stdout/stderr/error/table)
local function run_pipeline_via_ipc_response(pipeline_cmd, seeds, timeout_seconds)
local req = { pipeline = pipeline_cmd }
if seeds then
req.seeds = seeds
end
return _run_helper_request_response(req, timeout_seconds)
end
local function quote_pipeline_arg(s)
-- Ensure URLs with special characters (e.g. &, #) survive pipeline parsing.
s = tostring(s or '')
@@ -758,11 +945,25 @@ local function _capture_screenshot()
local temp_dir = mp.get_property('user-data/medeia-config-temp') or os.getenv('TEMP') or os.getenv('TMP') or '/tmp'
local out_path = utils.join_path(temp_dir, filename)
local ok = pcall(function()
mp.commandv('screenshot-to-file', out_path, 'video')
end)
local function do_screenshot(mode)
mode = mode or 'video'
local ok, err = pcall(function()
return mp.commandv('screenshot-to-file', out_path, mode)
end)
return ok, err
end
-- Try 'video' first (no OSD). If that fails (e.g. audio mode without video/art),
-- try 'window' as fallback.
local ok = do_screenshot('video')
if not ok then
mp.osd_message('Screenshot failed', 2)
_lua_log('screenshot: video-mode failed; trying window-mode')
ok = do_screenshot('window')
end
if not ok then
_lua_log('screenshot: BOTH video and window modes FAILED')
mp.osd_message('Screenshot failed (no frames)', 2)
return
end
@@ -775,30 +976,20 @@ local function _capture_screenshot()
mp.osd_message('Select a store first (Store button)', 2)
return
end
local python_exe = _resolve_python_exe(true)
if not python_exe or python_exe == '' then
mp.osd_message('Screenshot saved; Python not found', 3)
return
end
local start_dir = mp.get_script_directory() or ''
local cli_py = find_file_upwards(start_dir, 'CLI.py', 8)
if not cli_py or cli_py == '' or not utils.file_info(cli_py) then
mp.osd_message('Screenshot saved; CLI.py not found', 3)
return
end
local res = utils.subprocess({
args = { python_exe, cli_py, 'add-file', '-store', selected_store, '-path', out_path },
cancellable = false,
})
if res and res.status == 0 then
mp.osd_message('Saving screenshot...', 1)
-- optimization: use persistent pipeline helper instead of spawning new python process
-- escape paths for command line
local cmd = 'add-file -store "' .. selected_store .. '" -path "' .. out_path .. '"'
local resp = run_pipeline_via_ipc_response(cmd, nil, 10)
if resp and resp.success then
mp.osd_message('Screenshot saved to store: ' .. selected_store, 3)
else
local stderr = (res and res.stderr) or 'unknown error'
mp.osd_message('Screenshot upload failed: ' .. tostring(stderr), 5)
local err = (resp and resp.error) or (resp and resp.stderr) or 'IPC error'
mp.osd_message('Screenshot upload failed: ' .. tostring(err), 5)
end
end
@@ -1054,91 +1245,6 @@ local function _pick_folder_windows()
return nil
end
-- Forward declaration: used by run_pipeline_via_ipc_response before definition.
local ensure_pipeline_helper_running
local function _run_helper_request_response(req, timeout_seconds)
_last_ipc_error = ''
if not ensure_pipeline_helper_running() then
local rv = tostring(mp.get_property(PIPELINE_READY_PROP) or mp.get_property_native(PIPELINE_READY_PROP) or '')
_lua_log('ipc: helper not ready (ready=' .. rv .. '); attempting request anyway')
_last_ipc_error = 'helper not ready'
end
do
-- Best-effort wait for heartbeat, but do not hard-fail the request.
local deadline = mp.get_time() + 1.5
while mp.get_time() < deadline do
if _is_pipeline_helper_ready() then
break
end
mp.wait_event(0.05)
end
if not _is_pipeline_helper_ready() then
local rv = tostring(mp.get_property(PIPELINE_READY_PROP) or mp.get_property_native(PIPELINE_READY_PROP) or '')
_lua_log('ipc: proceeding without helper heartbeat; ready=' .. rv)
_last_ipc_error = 'helper heartbeat missing (ready=' .. rv .. ')'
end
end
if type(req) ~= 'table' then
return nil
end
local id = tostring(req.id or '')
if id == '' then
id = tostring(math.floor(mp.get_time() * 1000)) .. '-' .. tostring(math.random(100000, 999999))
req.id = id
end
local label = ''
if req.op then
label = 'op=' .. tostring(req.op)
elseif req.pipeline then
label = 'cmd=' .. tostring(req.pipeline)
else
label = '(unknown)'
end
_lua_log('ipc: send request id=' .. tostring(id) .. ' ' .. label)
local req_json = utils.format_json(req)
_last_ipc_last_req_json = req_json
mp.set_property(PIPELINE_RESP_PROP, '')
mp.set_property(PIPELINE_REQ_PROP, req_json)
-- Read-back for debugging: confirms MPV accepted the property write.
local echoed = mp.get_property(PIPELINE_REQ_PROP) or ''
if echoed == '' then
_lua_log('ipc: WARNING request property echoed empty after set')
end
local deadline = mp.get_time() + (timeout_seconds or 5)
while mp.get_time() < deadline do
local resp_json = mp.get_property(PIPELINE_RESP_PROP)
if resp_json and resp_json ~= '' then
_last_ipc_last_resp_json = resp_json
local ok, resp = pcall(utils.parse_json, resp_json)
if ok and resp and resp.id == id then
_lua_log('ipc: got response id=' .. tostring(id) .. ' success=' .. tostring(resp.success))
return resp
end
end
mp.wait_event(0.05)
end
_lua_log('ipc: timeout waiting response; ' .. label)
_last_ipc_error = 'timeout waiting response (' .. label .. ')'
return nil
end
-- IPC helper: return the whole response object (stdout/stderr/error/table)
local function run_pipeline_via_ipc_response(pipeline_cmd, seeds, timeout_seconds)
local req = { pipeline = pipeline_cmd }
if seeds then
req.seeds = seeds
end
return _run_helper_request_response(req, timeout_seconds)
end
local function _store_names_key(names)
if type(names) ~= 'table' or #names == 0 then
return ''
@@ -1438,101 +1544,6 @@ local function _get_cached_formats_table(url)
return nil
end
local function _run_helper_request_async(req, timeout_seconds, cb)
cb = cb or function() end
if _ipc_async_busy then
_ipc_async_queue[#_ipc_async_queue + 1] = { req = req, timeout = timeout_seconds, cb = cb }
return
end
_ipc_async_busy = true
local function done(resp, err)
_ipc_async_busy = false
cb(resp, err)
if #_ipc_async_queue > 0 then
local next_job = table.remove(_ipc_async_queue, 1)
-- Schedule next job slightly later to let mpv deliver any pending events.
mp.add_timeout(0.01, function()
_run_helper_request_async(next_job.req, next_job.timeout, next_job.cb)
end)
end
end
if type(req) ~= 'table' then
done(nil, 'invalid request')
return
end
ensure_mpv_ipc_server()
if not ensure_pipeline_helper_running() then
done(nil, 'helper not running')
return
end
-- Assign id.
local id = tostring(req.id or '')
if id == '' then
id = tostring(math.floor(mp.get_time() * 1000)) .. '-' .. tostring(math.random(100000, 999999))
req.id = id
end
local label = ''
if req.op then
label = 'op=' .. tostring(req.op)
elseif req.pipeline then
label = 'cmd=' .. tostring(req.pipeline)
else
label = '(unknown)'
end
-- Wait for helper READY without blocking the UI.
local ready_deadline = mp.get_time() + 3.0
local ready_timer
ready_timer = mp.add_periodic_timer(0.05, function()
if _is_pipeline_helper_ready() then
ready_timer:kill()
_lua_log('ipc-async: send request id=' .. tostring(id) .. ' ' .. label)
local req_json = utils.format_json(req)
_last_ipc_last_req_json = req_json
mp.set_property(PIPELINE_RESP_PROP, '')
mp.set_property(PIPELINE_REQ_PROP, req_json)
local deadline = mp.get_time() + (timeout_seconds or 5)
local poll_timer
poll_timer = mp.add_periodic_timer(0.05, function()
if mp.get_time() >= deadline then
poll_timer:kill()
done(nil, 'timeout waiting response (' .. label .. ')')
return
end
local resp_json = mp.get_property(PIPELINE_RESP_PROP)
if resp_json and resp_json ~= '' then
_last_ipc_last_resp_json = resp_json
local ok, resp = pcall(utils.parse_json, resp_json)
if ok and resp and resp.id == id then
poll_timer:kill()
_lua_log('ipc-async: got response id=' .. tostring(id) .. ' success=' .. tostring(resp.success))
done(resp, nil)
end
end
end)
return
end
if mp.get_time() >= ready_deadline then
ready_timer:kill()
done(nil, 'helper not ready')
return
end
end)
end
function FileState:fetch_formats(cb)
local url = tostring(self.url or '')
if url == '' or not _is_http_url(url) then
@@ -2077,13 +2088,6 @@ mp.register_script_message('medios-download-pick-path', function()
_pending_download = nil
end)
ensure_pipeline_helper_running = function()
-- IMPORTANT: do NOT spawn Python from inside mpv.
-- The Python side (MPV.mpv_ipc) starts pipeline_helper.py using Windows
-- no-console flags; spawning here can flash a console window.
return _is_pipeline_helper_ready() and true or false
end
local function run_pipeline_via_ipc(pipeline_cmd, seeds, timeout_seconds)
if not ensure_pipeline_helper_running() then
return nil
@@ -2177,72 +2181,8 @@ if not opts.cli_path then
opts.cli_path = find_file_upwards(script_dir, "CLI.py", 6) or "CLI.py"
end
-- Clean API wrapper for executing Python functions from Lua
local function _call_mpv_api(request)
-- Call the MPV Lua API (mpv_lua_api.py) with a JSON request.
-- Returns: JSON-decoded response object with {success, stdout, stderr, error, ...}
local request_json = utils.format_json(request)
-- Try to get log file path; skip if not available
local log_file = ''
local home = os.getenv('USERPROFILE') or os.getenv('HOME') or ''
if home ~= '' then
log_file = home .. '/../../medios/Medios-Macina/Log/medeia-mpv-helper.log'
end
_lua_log('api: calling mpv_lua_api cmd=' .. tostring(request.cmd))
local python_exe = _resolve_python_exe(true)
if not python_exe or python_exe == '' then
_lua_log('api: FAILED - no python exe')
return { success = false, error = 'could not find Python' }
end
-- Try to locate API script
local api_script = nil
local script_dir = mp.get_script_directory()
if script_dir and script_dir ~= '' then
api_script = script_dir .. '/mpv_lua_api.py'
if not utils.file_info(api_script) then
api_script = script_dir .. '/../mpv_lua_api.py'
end
end
if not api_script or api_script == '' or not utils.file_info(api_script) then
-- Fallback: try absolute path
local repo_root = os.getenv('USERPROFILE')
if repo_root then
api_script = repo_root .. '/../../../medios/Medios-Macina/MPV/mpv_lua_api.py'
end
end
if not api_script or api_script == '' then
_lua_log('api: FAILED - could not locate mpv_lua_api.py')
return { success = false, error = 'could not locate mpv_lua_api.py' }
end
_lua_log('api: python=' .. tostring(python_exe) .. ' script=' .. tostring(api_script))
local res = utils.subprocess({
args = { python_exe, api_script, request_json, log_file },
cancellable = false,
})
if res and res.status == 0 and res.stdout then
local ok, response = pcall(utils.parse_json, res.stdout)
if ok and response then
_lua_log('api: response success=' .. tostring(response.success))
return response
else
_lua_log('api: failed to parse response: ' .. tostring(res.stdout))
return { success = false, error = 'malformed response', stdout = res.stdout }
end
else
local stderr = res and res.stderr or 'unknown error'
_lua_log('api: subprocess failed status=' .. tostring(res and res.status or 'nil') .. ' stderr=' .. stderr)
return { success = false, error = stderr }
end
end
-- Ref: mpv_lua_api.py was removed in favor of pipeline_helper (run_pipeline_via_ipc_response).
-- This placeholder comment ensures we don't have code shifting issues.
-- Run a Medeia pipeline command via the Python pipeline helper (IPC request/response).
-- Calls the callback with stdout on success or error message on failure.
@@ -2551,13 +2491,14 @@ local function _start_trim_with_range(range)
end
_lua_log('trim: final upload_cmd=' .. pipeline_cmd)
_lua_log('trim: === CALLING API FOR UPLOAD ===')
_lua_log('trim: === CALLING PIPELINE HELPER FOR UPLOAD ===')
-- Call the API to handle metadata/storage
local response = _call_mpv_api({
cmd = 'execute_pipeline',
pipeline = pipeline_cmd,
})
-- Optimization: use persistent pipeline helper
local response = run_pipeline_via_ipc_response(pipeline_cmd, nil, 60)
if not response then
response = { success = false, error = "Timeout or IPC error" }
end
_lua_log('trim: api response success=' .. tostring(response.success))
_lua_log('trim: api response error=' .. tostring(response.error or 'nil'))

View File

@@ -262,13 +262,14 @@ class MPV:
def send(self,
command: Dict[str,
Any] | List[Any],
silent: bool = False) -> Optional[Dict[str,
silent: bool = False,
wait: bool = True) -> Optional[Dict[str,
Any]]:
client = self.client(silent=bool(silent))
try:
if not client.connect():
return None
return client.send_command(command)
return client.send_command(command, wait=wait)
except Exception as exc:
if not silent:
debug(f"MPV IPC error: {exc}")
@@ -627,7 +628,8 @@ class MPV:
# Ensure uosc.conf is available at the location uosc expects.
try:
src_uosc_conf = repo_root / "MPV" / "LUA" / "uosc" / "uosc.conf"
# Source uosc.conf is located within the bundled scripts.
src_uosc_conf = repo_root / "MPV" / "portable_config" / "scripts" / "uosc" / "uosc.conf"
dst_uosc_conf = portable_config_dir / "script-opts" / "uosc.conf"
if src_uosc_conf.exists():
# Only seed a default config if the user doesn't already have one.
@@ -639,16 +641,8 @@ class MPV:
cmd: List[str] = [
"mpv",
f"--config-dir={str(portable_config_dir)}",
# Allow mpv to auto-load scripts from <config-dir>/scripts/ (e.g., thumbfast).
"--load-scripts=yes",
"--osc=no",
"--load-console=no",
"--load-commands=no",
"--load-select=no",
"--load-context-menu=no",
"--load-positioning=no",
"--load-stats-overlay=no",
"--load-auto-profiles=no",
"--ytdl=yes",
f"--input-ipc-server={self.ipc_path}",
"--idle=yes",
@@ -656,14 +650,21 @@ class MPV:
]
# uosc and other scripts are expected to be auto-loaded from portable_config/scripts.
# We keep the back-compat fallback only if the user hasn't installed uosc.lua there.
# If --load-scripts=yes is set (standard), mpv will already pick up the loader shim
# at scripts/uosc.lua. We only add a manual --script fallback if that file is missing.
try:
uosc_entry = portable_config_dir / "scripts" / "uosc.lua"
if not uosc_entry.exists() and self.lua_script_path:
lua_dir = Path(self.lua_script_path).resolve().parent
uosc_main = lua_dir / "uosc" / "scripts" / "uosc" / "main.lua"
if uosc_main.exists():
cmd.append(f"--script={str(uosc_main)}")
# Check different possible source locations for uosc core.
uosc_paths = [
portable_config_dir / "scripts" / "uosc" / "scripts" / "uosc" / "main.lua",
lua_dir / "uosc" / "scripts" / "uosc" / "main.lua"
]
for p in uosc_paths:
if p.exists():
cmd.append(f"--script={str(p)}")
break
except Exception:
pass
@@ -1002,12 +1003,14 @@ class MPVIPCClient:
def send_command(self,
command_data: Dict[str,
Any] | List[Any]) -> Optional[Dict[str,
Any] | List[Any],
wait: bool = True) -> Optional[Dict[str,
Any]]:
"""Send a command to mpv and get response.
Args:
command_data: Command dict (e.g. {"command": [...]}) or list (e.g. ["loadfile", ...])
wait: If True, wait for the command response.
Returns:
Response dict with 'error' key (value 'success' on success), or None on error.
@@ -1030,6 +1033,7 @@ class MPVIPCClient:
if "request_id" not in request:
request["request_id"] = int(_time.time() * 1000) % 100000
rid = request["request_id"]
payload = json.dumps(request) + "\n"
# Debug: log the command being sent
@@ -1040,6 +1044,9 @@ class MPVIPCClient:
# Send command
self._write_payload(payload)
if not wait:
return {"error": "success", "request_id": rid, "async": True}
# Receive response
# We need to read lines until we find the one with matching request_id
# or until timeout/error. MPV might send events in between.

View File

@@ -1,184 +0,0 @@
"""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)

View File

@@ -5,33 +5,27 @@ osd-bar=no
# uosc will draw its own window controls and border if you disable window border
border=no
# Keep the window size stable when loading files (don't resize to match aspect).
# Ensure uosc texture/icon fonts are discoverable by libass.
osd-fonts-dir=~~/scripts/uosc/fonts
sub-fonts-dir=~~/scripts/uosc/
ontop=yes
autofit=100%
save-position-on-quit=yes
# Avoid showing embedded cover art for audio-only files.
audio-display=no
# Stretch the video to fill the window (ignore aspect ratio, may distort)
# Avoid showing embedded cover art for audio-only files if uosc isn't working,
# but we keep it enabled for now to ensure a window exists.
audio-display=yes
keepaspect=no
video-unscaled=no
cursor-autohide=1000
# gpu-next can be fragile on some Windows/D3D11 setups; prefer the stable VO.
vo=gpu
# modern gpu-next is preferred in recent mpv builds
vo=gpu-next,gpu,direct3d
# Show this after loading a new file. You can show text permanently instead by setting osd-msg1.
# Show this after loading a new file.
osd-playing-msg=${!playlist-count==1:[${playlist-pos-1}/${playlist-count}] }${media-title} ${?width:${width}x${height}} ${?current-tracks/video/image==no:${?percent-pos==0:${duration}}${!percent-pos==0:${time-pos} / ${duration} (${percent-pos}%)}}
osd-playing-msg-duration=7000
# On most platforms you can make the background transparent and avoid black
# bars while still having all the screen space available for zooming in:
# Restore transparency options
background=none
background-color=0/0
@@ -64,7 +58,7 @@ input-commands=no-osd del user-data/mpv/image; disable-section image # disable i
# Loop short videos like gifs.
[loop-short]
profile-cond=duration < 30 and p['current-tracks/video/image'] == false
profile-cond=get('duration', 100) < 30 and p['current-tracks/video/image'] == false
profile-restore=copy
loop-file
@@ -75,7 +69,7 @@ profile-restore=copy
stop-screensaver=no
[manga]
profile-cond=path:find('manga')
profile-cond=get('path', ''):find('manga')
video-align-y=-1 # start from the top
reset-on-next-file-remove=video-zoom # preserve the zoom when changing file
reset-on-next-file-remove=panscan

View File

@@ -29,9 +29,17 @@ end
if type(scripts_root) ~= 'string' then
scripts_root = ''
end
local msg = require('mp.msg')
msg.info('[uosc-shim] loading uosc...')
-- Your current folder layout is: scripts/uosc/scripts/uosc/main.lua
local uosc_dir = utils.join_path(scripts_root, 'uosc/scripts/uosc')
if not utils.file_info(utils.join_path(uosc_dir, 'main.lua')) then
msg.error('[uosc-shim] ERROR: main.lua not found at ' .. tostring(uosc_dir))
end
-- uosc uses mp.get_script_directory() to find its adjacent resources (bin/, lib/, etc).
-- Because this loader lives in scripts/, override it so uosc resolves paths correctly.
local _orig_get_script_directory = mp.get_script_directory
@@ -41,4 +49,11 @@ end
add_package_path(uosc_dir)
dofile(utils.join_path(uosc_dir, 'main.lua'))
local ok, err = pcall(dofile, utils.join_path(uosc_dir, 'main.lua'))
if not ok then
msg.error('[uosc-shim] ERROR during dofile: ' .. tostring(err))
else
msg.info('[uosc-shim] uosc loaded successfully')
-- Notify main.lua that we are alive
mp.commandv('script-message', 'uosc-version', 'bundled')
end

View File

@@ -145,20 +145,6 @@ function Controls:init_options()
})
table_assign(control, {element = element, sizing = 'static', scale = 1, ratio = 1})
if badge then self:register_badge_updater(badge, element) end
-- Medeia integration: show the persisted store name in the tooltip.
-- Triggered by a matching command string and backed by a mpv user-data prop.
if type(params[2]) == 'string' and params[2]:find('medeia%-store%-picker', 1, true) then
local store_prop = 'user-data/medeia-selected-store'
local function update_store_tooltip()
local v = mp.get_property(store_prop) or ''
v = trim(tostring(v))
element.tooltip = (v ~= '' and ('Store: ' .. v) or 'Store: (none)')
request_render()
end
element:observe_mp_property(store_prop, function() update_store_tooltip() end)
update_store_tooltip()
end
end
elseif kind == 'cycle' then
if #params ~= 3 then