f
This commit is contained in:
511
MPV/LUA/main.lua
511
MPV/LUA/main.lua
@@ -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'))
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user