added repl injection
This commit is contained in:
277
MPV/LUA/main.lua
277
MPV/LUA/main.lua
@@ -4,7 +4,7 @@ local msg = require 'mp.msg'
|
||||
|
||||
local M = {}
|
||||
|
||||
local MEDEIA_LUA_VERSION = '2025-12-24'
|
||||
local MEDEIA_LUA_VERSION = '2026-03-19.1'
|
||||
|
||||
-- Expose a tiny breadcrumb for debugging which script version is loaded.
|
||||
pcall(mp.set_property, 'user-data/medeia-lua-version', MEDEIA_LUA_VERSION)
|
||||
@@ -43,6 +43,8 @@ local LOAD_URL_MENU_TYPE = 'medios_load_url'
|
||||
local DOWNLOAD_FORMAT_MENU_TYPE = 'medios_download_pick_format'
|
||||
local DOWNLOAD_STORE_MENU_TYPE = 'medios_download_pick_store'
|
||||
local SCREENSHOT_TAG_MENU_TYPE = 'medeia_screenshot_tags'
|
||||
local SCREENSHOT_SAVE_JOB_TIMEOUT = 120
|
||||
local SCREENSHOT_SAVE_JOB_POLL_INTERVAL = 0.75
|
||||
|
||||
-- Menu types for the command submenu and trim prompt
|
||||
local CMD_MENU_TYPE = 'medios_cmd_menu'
|
||||
@@ -595,6 +597,8 @@ local _resolve_python_exe
|
||||
local _refresh_store_cache
|
||||
local _uosc_open_list_picker
|
||||
local _run_pipeline_detached
|
||||
local _run_pipeline_background_job
|
||||
local _run_pipeline_cli_async
|
||||
|
||||
local _cached_store_names = {}
|
||||
local _store_cache_loaded = false
|
||||
@@ -923,18 +927,19 @@ local function _run_helper_request_async(req, timeout_seconds, cb)
|
||||
local quiet = type(req) == 'table' and req.quiet and true or false
|
||||
local is_timeout = err_text:find('timeout waiting response', 1, true) ~= nil
|
||||
local retry_count = type(req) == 'table' and tonumber(req._retry or 0) or 0
|
||||
local is_retryable = is_timeout and type(req) == 'table'
|
||||
and tostring(req.op or '') == 'ytdlp-formats' and retry_count < 1
|
||||
local op_name = type(req) == 'table' and tostring(req.op or '') or ''
|
||||
local is_retryable = is_timeout and type(req) == 'table' and retry_count < 1
|
||||
and (op_name == 'ytdlp-formats' or op_name == 'run-background')
|
||||
|
||||
if is_retryable then
|
||||
req._retry = retry_count + 1
|
||||
req.id = nil
|
||||
_ipc_async_busy = false
|
||||
_lua_log('ipc-async: timeout on ytdlp-formats; restarting helper and retrying (attempt ' .. tostring(req._retry) .. ')')
|
||||
_lua_log('ipc-async: timeout on ' .. tostring(op_name) .. '; restarting helper and retrying (attempt ' .. tostring(req._retry) .. ')')
|
||||
pcall(mp.set_property, PIPELINE_READY_PROP, '')
|
||||
attempt_start_pipeline_helper_async(function(success)
|
||||
if success then
|
||||
_lua_log('ipc-async: helper restart succeeded; retrying ytdlp-formats')
|
||||
_lua_log('ipc-async: helper restart succeeded; retrying ' .. tostring(op_name))
|
||||
else
|
||||
_lua_log('ipc-async: helper restart failed; retrying anyway')
|
||||
end
|
||||
@@ -1272,7 +1277,7 @@ local function _check_store_for_existing_url(store, url, cb)
|
||||
local query = 'url:' .. needle
|
||||
|
||||
_lua_log('store-check: probing global query=' .. tostring(query))
|
||||
_run_helper_request_async({ op = 'url-exists', data = { url = url, needles = { needle } }, quiet = true }, 2.5, function(resp, err)
|
||||
_run_helper_request_async({ op = 'url-exists', data = { url = url, needles = { needle } }, quiet = true }, 5.0, function(resp, err)
|
||||
if resp and resp.success then
|
||||
local data = resp.data
|
||||
if type(data) ~= 'table' or #data == 0 then
|
||||
@@ -1517,6 +1522,45 @@ local function _normalize_tag_list(value)
|
||||
return tags
|
||||
end
|
||||
|
||||
local function _queue_pipeline_in_repl(pipeline_cmd, queued_message, failure_prefix, queue_label)
|
||||
pipeline_cmd = trim(tostring(pipeline_cmd or ''))
|
||||
if pipeline_cmd == '' then
|
||||
mp.osd_message((failure_prefix or 'REPL queue failed') .. ': empty pipeline command', 5)
|
||||
return false
|
||||
end
|
||||
|
||||
_lua_log(queue_label .. ': queueing repl cmd=' .. pipeline_cmd)
|
||||
ensure_mpv_ipc_server()
|
||||
if not ensure_pipeline_helper_running() then
|
||||
mp.osd_message((failure_prefix or 'REPL queue failed') .. ': helper not running', 5)
|
||||
return false
|
||||
end
|
||||
|
||||
_run_helper_request_async(
|
||||
{
|
||||
op = 'queue-repl-command',
|
||||
data = {
|
||||
command = pipeline_cmd,
|
||||
source = queue_label,
|
||||
metadata = { kind = 'mpv-download' },
|
||||
},
|
||||
},
|
||||
4.0,
|
||||
function(resp, err)
|
||||
if resp and resp.success then
|
||||
local queue_path = trim(tostring(resp.path or ''))
|
||||
_lua_log(queue_label .. ': queued repl command path=' .. tostring(queue_path))
|
||||
mp.osd_message(tostring(queued_message or 'Queued in REPL'), 3)
|
||||
return
|
||||
end
|
||||
local detail = tostring(err or (resp and resp.error) or 'unknown')
|
||||
_lua_log(queue_label .. ': queue failed err=' .. detail)
|
||||
mp.osd_message((failure_prefix or 'REPL queue failed') .. ': ' .. detail, 5)
|
||||
end
|
||||
)
|
||||
return true
|
||||
end
|
||||
|
||||
local function _start_screenshot_store_save(store, out_path, tags)
|
||||
store = _normalize_store_name(store)
|
||||
out_path = _normalize_fs_path(out_path)
|
||||
@@ -1527,21 +1571,32 @@ local function _start_screenshot_store_save(store, out_path, tags)
|
||||
|
||||
local tag_list = _normalize_tag_list(tags)
|
||||
local cmd = 'add-file -store ' .. quote_pipeline_arg(store)
|
||||
local seeds = { { path = out_path } }
|
||||
if #tag_list > 0 then
|
||||
seeds[1].tag = tag_list
|
||||
end
|
||||
.. ' -path ' .. quote_pipeline_arg(out_path)
|
||||
_set_selected_store(store)
|
||||
|
||||
if _run_pipeline_detached(cmd, function(_, err)
|
||||
mp.osd_message('Screenshot upload failed to start: ' .. tostring(err or 'unknown'), 5)
|
||||
end, seeds) then
|
||||
local tag_suffix = (#tag_list > 0) and (' | tags: ' .. tostring(#tag_list)) or ''
|
||||
mp.osd_message('Screenshot saved to store: ' .. store .. tag_suffix, 3)
|
||||
local tag_suffix = (#tag_list > 0) and (' | tags: ' .. tostring(#tag_list)) or ''
|
||||
if #tag_list > 0 then
|
||||
local tag_string = table.concat(tag_list, ',')
|
||||
cmd = cmd .. ' | add-tag ' .. quote_pipeline_arg(tag_string)
|
||||
end
|
||||
|
||||
_lua_log('screenshot-save: async pipeline cmd=' .. cmd)
|
||||
|
||||
local started, start_err = _run_pipeline_cli_async(cmd, nil, function(success, _result, detail)
|
||||
if success then
|
||||
mp.osd_message('Screenshot saved to store: ' .. store .. tag_suffix, 3)
|
||||
return
|
||||
end
|
||||
mp.osd_message('Screenshot upload failed: ' .. tostring(detail or 'unknown'), 5)
|
||||
end)
|
||||
|
||||
if started then
|
||||
mp.osd_message('Screenshot upload started to store: ' .. store .. tag_suffix, 3)
|
||||
return true
|
||||
end
|
||||
|
||||
mp.osd_message('Screenshot upload failed to start', 5)
|
||||
mp.osd_message('Screenshot upload failed to start: ' .. tostring(start_err or 'unknown'), 5)
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
@@ -1556,6 +1611,9 @@ local function _commit_pending_screenshot(tags)
|
||||
end
|
||||
|
||||
local function _apply_screenshot_tag_query(query)
|
||||
pcall(function()
|
||||
mp.commandv('script-message-to', 'uosc', 'close-menu', SCREENSHOT_TAG_MENU_TYPE)
|
||||
end)
|
||||
_commit_pending_screenshot(_normalize_tag_list(query))
|
||||
end
|
||||
|
||||
@@ -2258,7 +2316,7 @@ _refresh_store_cache = function(timeout_seconds, on_complete)
|
||||
end
|
||||
|
||||
_lua_log('stores: requesting store-choices via helper (fallback)')
|
||||
_run_helper_request_async({ op = 'store-choices' }, math.max(timeout_seconds or 0, 3.0), function(resp, err)
|
||||
_run_helper_request_async({ op = 'store-choices' }, math.max(timeout_seconds or 0, 6.0), function(resp, err)
|
||||
local success = false
|
||||
local changed = false
|
||||
if resp and resp.success and type(resp.choices) == 'table' then
|
||||
@@ -3375,10 +3433,10 @@ local function _resolve_cli_entrypoint()
|
||||
return configured ~= '' and configured or 'CLI.py'
|
||||
end
|
||||
|
||||
local function _run_pipeline_cli_detached(pipeline_cmd, seeds)
|
||||
local function _build_pipeline_cli_args(pipeline_cmd, seeds)
|
||||
pipeline_cmd = trim(tostring(pipeline_cmd or ''))
|
||||
if pipeline_cmd == '' then
|
||||
return false, 'empty pipeline command'
|
||||
return nil, 'empty pipeline command'
|
||||
end
|
||||
|
||||
local python = _resolve_python_exe(true)
|
||||
@@ -3386,7 +3444,7 @@ local function _run_pipeline_cli_detached(pipeline_cmd, seeds)
|
||||
python = _resolve_python_exe(false)
|
||||
end
|
||||
if not python or python == '' then
|
||||
return false, 'python not found'
|
||||
return nil, 'python not found'
|
||||
end
|
||||
|
||||
local cli_path = _resolve_cli_entrypoint()
|
||||
@@ -3399,6 +3457,58 @@ local function _run_pipeline_cli_detached(pipeline_cmd, seeds)
|
||||
end
|
||||
end
|
||||
|
||||
return args, nil
|
||||
end
|
||||
|
||||
_run_pipeline_cli_async = function(pipeline_cmd, seeds, cb)
|
||||
local args, build_err = _build_pipeline_cli_args(pipeline_cmd, seeds)
|
||||
if type(args) ~= 'table' then
|
||||
if type(cb) == 'function' then
|
||||
cb(false, nil, tostring(build_err or 'invalid pipeline args'))
|
||||
end
|
||||
return false, tostring(build_err or 'invalid pipeline args')
|
||||
end
|
||||
|
||||
mp.command_native_async(
|
||||
{
|
||||
name = 'subprocess',
|
||||
args = args,
|
||||
capture_stdout = true,
|
||||
capture_stderr = true,
|
||||
playback_only = false,
|
||||
},
|
||||
function(success, result, err)
|
||||
if not success then
|
||||
if type(cb) == 'function' then
|
||||
cb(false, result, tostring(err or 'subprocess failed'))
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
local ok = false
|
||||
local detail = 'invalid subprocess result'
|
||||
if type(result) == 'table' then
|
||||
local err_text = trim(tostring(result.error or ''))
|
||||
local status = tonumber(result.status)
|
||||
ok = (err_text == '' or err_text == 'success') and (status == nil or status == 0)
|
||||
detail = _describe_subprocess_result(result)
|
||||
end
|
||||
|
||||
if type(cb) == 'function' then
|
||||
cb(ok, result, detail)
|
||||
end
|
||||
end
|
||||
)
|
||||
|
||||
return true, nil
|
||||
end
|
||||
|
||||
local function _run_pipeline_cli_detached(pipeline_cmd, seeds)
|
||||
local args, build_err = _build_pipeline_cli_args(pipeline_cmd, seeds)
|
||||
if type(args) ~= 'table' then
|
||||
return false, tostring(build_err or 'invalid pipeline args')
|
||||
end
|
||||
|
||||
local cmd = {
|
||||
name = 'subprocess',
|
||||
args = args,
|
||||
@@ -3443,6 +3553,94 @@ _run_pipeline_detached = function(pipeline_cmd, on_failure, seeds)
|
||||
return true
|
||||
end
|
||||
|
||||
_run_pipeline_background_job = function(pipeline_cmd, seeds, on_started, on_complete, timeout_seconds, poll_interval_seconds)
|
||||
pipeline_cmd = trim(tostring(pipeline_cmd or ''))
|
||||
if pipeline_cmd == '' then
|
||||
if type(on_complete) == 'function' then
|
||||
on_complete(nil, 'empty pipeline command')
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
ensure_mpv_ipc_server()
|
||||
if not ensure_pipeline_helper_running() then
|
||||
if type(on_complete) == 'function' then
|
||||
on_complete(nil, 'helper not running')
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
_run_helper_request_async({ op = 'run-background', data = { pipeline = pipeline_cmd, seeds = seeds } }, 8.0, function(resp, err)
|
||||
if err or not resp or not resp.success then
|
||||
if type(on_complete) == 'function' then
|
||||
on_complete(nil, err or (resp and resp.error) or 'failed to start background job')
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
local job_id = trim(tostring(resp.job_id or ''))
|
||||
if job_id == '' then
|
||||
if type(on_complete) == 'function' then
|
||||
on_complete(nil, 'missing background job id')
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
if type(on_started) == 'function' then
|
||||
on_started(job_id, resp)
|
||||
end
|
||||
|
||||
local deadline = mp.get_time() + math.max(tonumber(timeout_seconds or 0) or 0, 15.0)
|
||||
local poll_interval = math.max(tonumber(poll_interval_seconds or 0) or 0, 0.25)
|
||||
local poll_inflight = false
|
||||
local timer = nil
|
||||
|
||||
local function finish(job, finish_err)
|
||||
if timer then
|
||||
timer:kill()
|
||||
timer = nil
|
||||
end
|
||||
if type(on_complete) == 'function' then
|
||||
on_complete(job, finish_err)
|
||||
end
|
||||
end
|
||||
|
||||
timer = mp.add_periodic_timer(poll_interval, function()
|
||||
if poll_inflight then
|
||||
return
|
||||
end
|
||||
if mp.get_time() >= deadline then
|
||||
finish(nil, 'timeout waiting background job')
|
||||
return
|
||||
end
|
||||
|
||||
poll_inflight = true
|
||||
_run_helper_request_async({ op = 'job-status', data = { job_id = job_id }, quiet = true }, math.max(poll_interval + 0.75, 1.25), function(status_resp, status_err)
|
||||
poll_inflight = false
|
||||
|
||||
if status_err then
|
||||
_lua_log('background-job: poll retry job=' .. tostring(job_id) .. ' err=' .. tostring(status_err))
|
||||
return
|
||||
end
|
||||
|
||||
if not status_resp or not status_resp.success then
|
||||
local status_error = status_resp and status_resp.error or 'job status unavailable'
|
||||
finish(nil, status_error)
|
||||
return
|
||||
end
|
||||
|
||||
local job = status_resp.job
|
||||
local status = type(job) == 'table' and tostring(job.status or '') or tostring(status_resp.status or '')
|
||||
if status == 'success' or status == 'failed' then
|
||||
finish(job, nil)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
local function _open_save_location_picker_for_pending_download()
|
||||
if type(_pending_download) ~= 'table' or not _pending_download.url or not _pending_download.format then
|
||||
return
|
||||
@@ -3562,13 +3760,12 @@ local function _start_download_flow_for_current()
|
||||
end
|
||||
ensure_mpv_ipc_server()
|
||||
local pipeline_cmd = 'get-file -store ' .. quote_pipeline_arg(store_hash.store) .. ' -query ' .. quote_pipeline_arg('hash:' .. store_hash.hash) .. ' -path ' .. quote_pipeline_arg(folder)
|
||||
if _run_pipeline_detached(pipeline_cmd, function(_, err)
|
||||
mp.osd_message('Download failed to start: ' .. tostring(err or 'unknown'), 5)
|
||||
end) then
|
||||
mp.osd_message('Download started', 2)
|
||||
else
|
||||
mp.osd_message('Download failed to start', 5)
|
||||
end
|
||||
_queue_pipeline_in_repl(
|
||||
pipeline_cmd,
|
||||
'Queued in REPL: store copy',
|
||||
'REPL queue failed',
|
||||
'download-store-copy'
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
@@ -3765,13 +3962,12 @@ mp.register_script_message('medios-download-pick-store', function(json)
|
||||
.. ' | add-file -store ' .. quote_pipeline_arg(store)
|
||||
|
||||
_set_selected_store(store)
|
||||
if _run_pipeline_detached(pipeline_cmd, function(_, err)
|
||||
mp.osd_message('Download failed to start: ' .. tostring(err or 'unknown'), 5)
|
||||
end) then
|
||||
mp.osd_message('Download started', 3)
|
||||
else
|
||||
mp.osd_message('Download failed to start', 5)
|
||||
end
|
||||
_queue_pipeline_in_repl(
|
||||
pipeline_cmd,
|
||||
'Queued in REPL: save to store ' .. store,
|
||||
'REPL queue failed',
|
||||
'download-store-save'
|
||||
)
|
||||
_pending_download = nil
|
||||
end)
|
||||
|
||||
@@ -3796,13 +3992,12 @@ mp.register_script_message('medios-download-pick-path', function()
|
||||
.. ' -query ' .. quote_pipeline_arg('format:' .. fmt)
|
||||
.. ' | add-file -path ' .. quote_pipeline_arg(folder)
|
||||
|
||||
if _run_pipeline_detached(pipeline_cmd, function(_, err)
|
||||
mp.osd_message('Download failed to start: ' .. tostring(err or 'unknown'), 5)
|
||||
end) then
|
||||
mp.osd_message('Download started', 3)
|
||||
else
|
||||
mp.osd_message('Download failed to start', 5)
|
||||
end
|
||||
_queue_pipeline_in_repl(
|
||||
pipeline_cmd,
|
||||
'Queued in REPL: save to folder',
|
||||
'REPL queue failed',
|
||||
'download-folder-save'
|
||||
)
|
||||
_pending_download = nil
|
||||
end)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user