k
This commit is contained in:
392
MPV/LUA/main.lua
392
MPV/LUA/main.lua
@@ -1139,47 +1139,80 @@ local function run_pipeline_via_ipc_response(pipeline_cmd, seeds, timeout_second
|
||||
return _run_helper_request_response(req, timeout_seconds)
|
||||
end
|
||||
|
||||
local function _refresh_store_cache(timeout_seconds)
|
||||
local function _store_names_key(names)
|
||||
if type(names) ~= 'table' or #names == 0 then
|
||||
return ''
|
||||
end
|
||||
local normalized = {}
|
||||
for _, name in ipairs(names) do
|
||||
normalized[#normalized + 1] = trim(tostring(name or ''))
|
||||
end
|
||||
return table.concat(normalized, '\0')
|
||||
end
|
||||
|
||||
local function _run_pipeline_request_async(pipeline_cmd, seeds, timeout_seconds, cb)
|
||||
cb = cb or function() end
|
||||
pipeline_cmd = trim(tostring(pipeline_cmd or ''))
|
||||
if pipeline_cmd == '' then
|
||||
cb(nil, 'empty pipeline command')
|
||||
return
|
||||
end
|
||||
ensure_mpv_ipc_server()
|
||||
local req = { pipeline = pipeline_cmd }
|
||||
if seeds then
|
||||
req.seeds = seeds
|
||||
end
|
||||
_run_helper_request_async(req, timeout_seconds or 30, cb)
|
||||
end
|
||||
|
||||
local function _refresh_store_cache(timeout_seconds, on_complete)
|
||||
ensure_mpv_ipc_server()
|
||||
|
||||
-- First, try reading the pre-computed cached property (set by helper at startup).
|
||||
-- This avoids a request/response timeout if observe_property isn't working.
|
||||
local prev_count = (type(_cached_store_names) == 'table') and #_cached_store_names or 0
|
||||
local prev_key = _store_names_key(_cached_store_names)
|
||||
|
||||
local cached_json = mp.get_property('user-data/medeia-store-choices-cached')
|
||||
_lua_log('stores: cache_read cached_json=' .. tostring(cached_json) .. ' len=' .. tostring(cached_json and #cached_json or 0))
|
||||
|
||||
|
||||
if cached_json and cached_json ~= '' then
|
||||
-- Try to parse as JSON (may fail if not valid JSON)
|
||||
local function handle_cached(resp)
|
||||
if not resp or type(resp) ~= 'table' or type(resp.choices) ~= 'table' then
|
||||
_lua_log('stores: cache_parse result missing choices table; resp_type=' .. tostring(type(resp)))
|
||||
return false
|
||||
end
|
||||
|
||||
local out = {}
|
||||
for _, v in ipairs(resp.choices) do
|
||||
local name = trim(tostring(v or ''))
|
||||
if name ~= '' then
|
||||
out[#out + 1] = name
|
||||
end
|
||||
end
|
||||
_cached_store_names = out
|
||||
_store_cache_loaded = true
|
||||
local preview = ''
|
||||
if #out > 0 then
|
||||
preview = table.concat(out, ', ')
|
||||
end
|
||||
_lua_log('stores: loaded ' .. tostring(#out) .. ' stores from cache: ' .. tostring(preview))
|
||||
if type(on_complete) == 'function' then
|
||||
on_complete(true, _store_names_key(out) ~= prev_key)
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local ok, cached_resp = pcall(utils.parse_json, cached_json)
|
||||
_lua_log('stores: cache_parse ok=' .. tostring(ok) .. ' resp_type=' .. tostring(type(cached_resp)))
|
||||
|
||||
-- Handle both cases: parsed object OR string (if JSON lib returns string)
|
||||
if ok then
|
||||
-- If parse returned a string, it might still be valid JSON; try parsing again
|
||||
if type(cached_resp) == 'string' then
|
||||
_lua_log('stores: cache_parse returned string, trying again...')
|
||||
ok, cached_resp = pcall(utils.parse_json, cached_resp)
|
||||
_lua_log('stores: cache_parse retry ok=' .. tostring(ok) .. ' resp_type=' .. tostring(type(cached_resp)))
|
||||
end
|
||||
|
||||
-- Now check if we have a table with choices
|
||||
if type(cached_resp) == 'table' and type(cached_resp.choices) == 'table' then
|
||||
local out = {}
|
||||
for _, v in ipairs(cached_resp.choices) do
|
||||
local name = trim(tostring(v or ''))
|
||||
if name ~= '' then
|
||||
out[#out + 1] = name
|
||||
end
|
||||
if ok then
|
||||
if handle_cached(cached_resp) then
|
||||
return true
|
||||
end
|
||||
_cached_store_names = out
|
||||
_store_cache_loaded = true
|
||||
local preview = ''
|
||||
if #_cached_store_names > 0 then
|
||||
preview = table.concat(_cached_store_names, ', ')
|
||||
end
|
||||
_lua_log('stores: loaded ' .. tostring(#_cached_store_names) .. ' stores from cache: ' .. tostring(preview))
|
||||
return true
|
||||
else
|
||||
_lua_log('stores: cache_parse final type mismatch resp_type=' .. tostring(type(cached_resp)) .. ' choices_type=' .. tostring(cached_resp and type(cached_resp.choices) or 'n/a'))
|
||||
end
|
||||
else
|
||||
_lua_log('stores: cache_parse failed ok=' .. tostring(ok) .. ' resp=' .. tostring(cached_resp))
|
||||
@@ -1188,38 +1221,44 @@ local function _refresh_store_cache(timeout_seconds)
|
||||
_lua_log('stores: cache_empty cached_json=' .. tostring(cached_json))
|
||||
end
|
||||
|
||||
-- Fallback: request fresh store-choices from helper (with timeout).
|
||||
_lua_log('stores: requesting store-choices via helper (fallback)')
|
||||
local resp = _run_helper_request_response({ op = 'store-choices' }, timeout_seconds or 1)
|
||||
if not resp or not resp.success or type(resp.choices) ~= 'table' then
|
||||
_lua_log(
|
||||
'stores: failed to load store choices via helper; success='
|
||||
.. tostring(resp and resp.success or false)
|
||||
.. ' choices_type='
|
||||
.. tostring(resp and type(resp.choices) or 'nil')
|
||||
.. ' stderr='
|
||||
.. tostring(resp and resp.stderr or '')
|
||||
.. ' error='
|
||||
.. tostring(resp and resp.error or '')
|
||||
)
|
||||
return false
|
||||
end
|
||||
|
||||
local out = {}
|
||||
for _, v in ipairs(resp.choices) do
|
||||
local name = trim(tostring(v or ''))
|
||||
if name ~= '' then
|
||||
out[#out + 1] = name
|
||||
_run_helper_request_async({ op = 'store-choices' }, timeout_seconds or 1, function(resp, err)
|
||||
local success = false
|
||||
local changed = false
|
||||
if resp and resp.success and type(resp.choices) == 'table' then
|
||||
local out = {}
|
||||
for _, v in ipairs(resp.choices) do
|
||||
local name = trim(tostring(v or ''))
|
||||
if name ~= '' then
|
||||
out[#out + 1] = name
|
||||
end
|
||||
end
|
||||
_cached_store_names = out
|
||||
_store_cache_loaded = true
|
||||
local preview = ''
|
||||
if #out > 0 then
|
||||
preview = table.concat(out, ', ')
|
||||
end
|
||||
_lua_log('stores: loaded ' .. tostring(#out) .. ' stores via helper request: ' .. tostring(preview))
|
||||
success = true
|
||||
changed = (#out ~= prev_count) or (_store_names_key(out) ~= prev_key)
|
||||
else
|
||||
_lua_log(
|
||||
'stores: failed to load store choices via helper; success='
|
||||
.. tostring(resp and resp.success or false)
|
||||
.. ' choices_type='
|
||||
.. tostring(resp and type(resp.choices) or 'nil')
|
||||
.. ' stderr='
|
||||
.. tostring(resp and resp.stderr or '')
|
||||
.. ' error='
|
||||
.. tostring(resp and resp.error or err or '')
|
||||
)
|
||||
end
|
||||
end
|
||||
_cached_store_names = out
|
||||
_store_cache_loaded = true
|
||||
local preview = ''
|
||||
if #_cached_store_names > 0 then
|
||||
preview = table.concat(_cached_store_names, ', ')
|
||||
end
|
||||
_lua_log('stores: loaded ' .. tostring(#_cached_store_names) .. ' stores via helper request: ' .. tostring(preview))
|
||||
return true
|
||||
if type(on_complete) == 'function' then
|
||||
on_complete(success, changed)
|
||||
end
|
||||
end)
|
||||
return false
|
||||
end
|
||||
|
||||
local function _uosc_open_list_picker(menu_type, title, items)
|
||||
@@ -1286,35 +1325,12 @@ local function _open_store_picker()
|
||||
-- Best-effort refresh; retry briefly to avoid races where the helper isn't
|
||||
-- ready/observing yet at the exact moment the menu opens.
|
||||
local function attempt_refresh(tries_left)
|
||||
local before_count = (type(_cached_store_names) == 'table') and #_cached_store_names or 0
|
||||
local before_preview = ''
|
||||
if type(_cached_store_names) == 'table' and #_cached_store_names > 0 then
|
||||
before_preview = table.concat(_cached_store_names, ', ')
|
||||
end
|
||||
|
||||
local ok = _refresh_store_cache(1.2)
|
||||
local after_count = (type(_cached_store_names) == 'table') and #_cached_store_names or 0
|
||||
local after_preview = ''
|
||||
if type(_cached_store_names) == 'table' and #_cached_store_names > 0 then
|
||||
after_preview = table.concat(_cached_store_names, ', ')
|
||||
end
|
||||
|
||||
_lua_log(
|
||||
'stores: refresh attempt ok='
|
||||
.. tostring(ok)
|
||||
.. ' before='
|
||||
.. tostring(before_count)
|
||||
.. ' after='
|
||||
.. tostring(after_count)
|
||||
.. ' after='
|
||||
.. tostring(after_preview)
|
||||
)
|
||||
|
||||
if after_count > 0 and (after_count ~= before_count or after_preview ~= before_preview) then
|
||||
_lua_log('stores: reopening menu (store list changed)')
|
||||
_uosc_open_list_picker(STORE_PICKER_MENU_TYPE, 'Store', build_items())
|
||||
return
|
||||
end
|
||||
_refresh_store_cache(1.2, function(success, changed)
|
||||
if success and changed then
|
||||
_lua_log('stores: reopening menu (store list changed)')
|
||||
_uosc_open_list_picker(STORE_PICKER_MENU_TYPE, 'Store', build_items())
|
||||
end
|
||||
end)
|
||||
|
||||
if tries_left > 0 then
|
||||
mp.add_timeout(0.25, function()
|
||||
@@ -1524,13 +1540,11 @@ function FileState:fetch_formats(cb)
|
||||
return
|
||||
end
|
||||
|
||||
-- Only applies to plain URLs (not store hash URLs).
|
||||
if _extract_store_hash(url) then
|
||||
if cb then cb(false, 'store-hash url') end
|
||||
return
|
||||
end
|
||||
|
||||
-- Cache hit.
|
||||
local cached = _get_cached_formats_table(url)
|
||||
if type(cached) == 'table' then
|
||||
self:set_formats(url, cached)
|
||||
@@ -1538,7 +1552,6 @@ function FileState:fetch_formats(cb)
|
||||
return
|
||||
end
|
||||
|
||||
-- In-flight: register waiter.
|
||||
if _formats_inflight[url] then
|
||||
_formats_waiters[url] = _formats_waiters[url] or {}
|
||||
if cb then table.insert(_formats_waiters[url], cb) end
|
||||
@@ -1548,7 +1561,6 @@ function FileState:fetch_formats(cb)
|
||||
_formats_waiters[url] = _formats_waiters[url] or {}
|
||||
if cb then table.insert(_formats_waiters[url], cb) end
|
||||
|
||||
-- Async request so the UI never blocks.
|
||||
_run_helper_request_async({ op = 'ytdlp-formats', data = { url = url } }, 90, function(resp, err)
|
||||
_formats_inflight[url] = nil
|
||||
|
||||
@@ -1664,12 +1676,26 @@ local function _current_ytdl_format_string()
|
||||
return nil
|
||||
end
|
||||
|
||||
local function _run_pipeline_detached(pipeline_cmd)
|
||||
local function _run_pipeline_detached(pipeline_cmd, on_failure)
|
||||
if not pipeline_cmd or pipeline_cmd == '' then
|
||||
return false
|
||||
end
|
||||
local resp = _run_helper_request_response({ op = 'run-detached', data = { pipeline = pipeline_cmd } }, 1.0)
|
||||
return (resp and resp.success) and true or false
|
||||
ensure_mpv_ipc_server()
|
||||
if not ensure_pipeline_helper_running() then
|
||||
if type(on_failure) == 'function' then
|
||||
on_failure(nil, 'helper not running')
|
||||
end
|
||||
return false
|
||||
end
|
||||
_run_helper_request_async({ op = 'run-detached', data = { pipeline = pipeline_cmd } }, 1.0, function(resp, err)
|
||||
if resp and resp.success then
|
||||
return
|
||||
end
|
||||
if type(on_failure) == 'function' then
|
||||
on_failure(resp, err)
|
||||
end
|
||||
end)
|
||||
return true
|
||||
end
|
||||
|
||||
local function _open_save_location_picker_for_pending_download()
|
||||
@@ -1709,13 +1735,11 @@ 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
|
||||
end
|
||||
local before = (type(_cached_store_names) == 'table') and #_cached_store_names or 0
|
||||
if _refresh_store_cache(1.5) then
|
||||
local after = (type(_cached_store_names) == 'table') and #_cached_store_names or 0
|
||||
if after > 0 and after ~= before then
|
||||
_refresh_store_cache(1.5, function(success, changed)
|
||||
if success and changed then
|
||||
_uosc_open_list_picker(DOWNLOAD_STORE_MENU_TYPE, 'Save location', build_items())
|
||||
end
|
||||
end
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
@@ -1769,7 +1793,12 @@ local function _start_download_flow_for_current()
|
||||
return
|
||||
end
|
||||
ensure_mpv_ipc_server()
|
||||
M.run_pipeline('get-file -store ' .. quote_pipeline_arg(store_hash.store) .. ' -query ' .. quote_pipeline_arg('hash:' .. store_hash.hash) .. ' -path ' .. quote_pipeline_arg(folder))
|
||||
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)
|
||||
M.run_pipeline(pipeline_cmd, nil, function(_, err)
|
||||
if err then
|
||||
mp.osd_message('Download failed: ' .. tostring(err), 5)
|
||||
end
|
||||
end)
|
||||
mp.osd_message('Download started', 2)
|
||||
return
|
||||
end
|
||||
@@ -1994,9 +2023,18 @@ mp.register_script_message('medios-download-pick-store', function(json)
|
||||
local pipeline_cmd = 'download-file -url ' .. quote_pipeline_arg(url) .. ' -format ' .. quote_pipeline_arg(fmt)
|
||||
.. ' | add-file -store ' .. quote_pipeline_arg(store)
|
||||
|
||||
if not _run_pipeline_detached(pipeline_cmd) then
|
||||
-- Fall back to synchronous execution if detached failed.
|
||||
M.run_pipeline(pipeline_cmd)
|
||||
local function run_pipeline_direct()
|
||||
M.run_pipeline(pipeline_cmd, nil, function(_, err)
|
||||
if err then
|
||||
mp.osd_message('Download failed: ' .. tostring(err), 5)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
if not _run_pipeline_detached(pipeline_cmd, function()
|
||||
run_pipeline_direct()
|
||||
end) then
|
||||
run_pipeline_direct()
|
||||
end
|
||||
mp.osd_message('Download started', 3)
|
||||
_pending_download = nil
|
||||
@@ -2022,8 +2060,18 @@ mp.register_script_message('medios-download-pick-path', function()
|
||||
local pipeline_cmd = 'download-file -url ' .. quote_pipeline_arg(url) .. ' -format ' .. quote_pipeline_arg(fmt)
|
||||
.. ' | add-file -path ' .. quote_pipeline_arg(folder)
|
||||
|
||||
if not _run_pipeline_detached(pipeline_cmd) then
|
||||
M.run_pipeline(pipeline_cmd)
|
||||
local function run_pipeline_direct()
|
||||
M.run_pipeline(pipeline_cmd, nil, function(_, err)
|
||||
if err then
|
||||
mp.osd_message('Download failed: ' .. tostring(err), 5)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
if not _run_pipeline_detached(pipeline_cmd, function()
|
||||
run_pipeline_direct()
|
||||
end) then
|
||||
run_pipeline_direct()
|
||||
end
|
||||
mp.osd_message('Download started', 3)
|
||||
_pending_download = nil
|
||||
@@ -2197,84 +2245,96 @@ local function _call_mpv_api(request)
|
||||
end
|
||||
|
||||
-- Run a Medeia pipeline command via the Python pipeline helper (IPC request/response).
|
||||
-- Returns stdout string on success, or nil on failure.
|
||||
function M.run_pipeline(pipeline_cmd, seeds)
|
||||
-- Calls the callback with stdout on success or error message on failure.
|
||||
function M.run_pipeline(pipeline_cmd, seeds, cb)
|
||||
cb = cb or function() end
|
||||
pipeline_cmd = trim(tostring(pipeline_cmd or ''))
|
||||
if pipeline_cmd == '' then
|
||||
return nil
|
||||
cb(nil, 'empty pipeline command')
|
||||
return
|
||||
end
|
||||
|
||||
ensure_mpv_ipc_server()
|
||||
|
||||
local resp = run_pipeline_via_ipc_response(pipeline_cmd, seeds, 30)
|
||||
if type(resp) == 'table' and resp.success then
|
||||
return resp.stdout or ''
|
||||
end
|
||||
|
||||
local err = ''
|
||||
if type(resp) == 'table' then
|
||||
if resp.error and tostring(resp.error) ~= '' then
|
||||
err = tostring(resp.error)
|
||||
elseif resp.stderr and tostring(resp.stderr) ~= '' then
|
||||
err = tostring(resp.stderr)
|
||||
_run_pipeline_request_async(pipeline_cmd, seeds, 30, function(resp, err)
|
||||
if resp and resp.success then
|
||||
cb(resp.stdout or '', nil)
|
||||
return
|
||||
end
|
||||
end
|
||||
if err ~= '' then
|
||||
_lua_log('pipeline failed cmd=' .. tostring(pipeline_cmd) .. ' err=' .. err)
|
||||
else
|
||||
_lua_log('pipeline failed cmd=' .. tostring(pipeline_cmd) .. ' err=<unknown>')
|
||||
end
|
||||
return nil
|
||||
local details = err or ''
|
||||
if details == '' and type(resp) == 'table' then
|
||||
if resp.error and tostring(resp.error) ~= '' then
|
||||
details = tostring(resp.error)
|
||||
elseif resp.stderr and tostring(resp.stderr) ~= '' then
|
||||
details = tostring(resp.stderr)
|
||||
end
|
||||
end
|
||||
if details == '' then
|
||||
details = 'unknown'
|
||||
end
|
||||
_lua_log('pipeline failed cmd=' .. tostring(pipeline_cmd) .. ' err=' .. details)
|
||||
cb(nil, details)
|
||||
end)
|
||||
end
|
||||
|
||||
-- Helper to run pipeline and parse JSON output
|
||||
function M.run_pipeline_json(pipeline_cmd, seeds)
|
||||
-- Append | output-json if not present
|
||||
if not pipeline_cmd:match("output%-json$") then
|
||||
pipeline_cmd = pipeline_cmd .. " | output-json"
|
||||
function M.run_pipeline_json(pipeline_cmd, seeds, cb)
|
||||
cb = cb or function() end
|
||||
if not pipeline_cmd:match('output%-json$') then
|
||||
pipeline_cmd = pipeline_cmd .. ' | output-json'
|
||||
end
|
||||
|
||||
local output = M.run_pipeline(pipeline_cmd, seeds)
|
||||
if output then
|
||||
local ok, data = pcall(utils.parse_json, output)
|
||||
if ok then
|
||||
return data
|
||||
else
|
||||
_lua_log("Failed to parse JSON: " .. output)
|
||||
return nil
|
||||
M.run_pipeline(pipeline_cmd, seeds, function(output, err)
|
||||
if output then
|
||||
local ok, data = pcall(utils.parse_json, output)
|
||||
if ok then
|
||||
cb(data, nil)
|
||||
return
|
||||
end
|
||||
_lua_log('Failed to parse JSON: ' .. output)
|
||||
cb(nil, 'malformed JSON response')
|
||||
return
|
||||
end
|
||||
end
|
||||
return nil
|
||||
cb(nil, err)
|
||||
end)
|
||||
end
|
||||
|
||||
-- Command: Get info for current file
|
||||
function M.get_file_info()
|
||||
local path = mp.get_property("path")
|
||||
if not path then return end
|
||||
|
||||
-- We can pass the path as a seed item
|
||||
local seed = {{path = path}}
|
||||
|
||||
-- Run pipeline: get-metadata
|
||||
local data = M.run_pipeline_json("get-metadata", seed)
|
||||
|
||||
if data then
|
||||
-- Display metadata
|
||||
_lua_log("Metadata: " .. utils.format_json(data))
|
||||
mp.osd_message("Metadata loaded (check console)", 3)
|
||||
local path = mp.get_property('path')
|
||||
if not path then
|
||||
return
|
||||
end
|
||||
|
||||
local seed = {{path = path}}
|
||||
|
||||
M.run_pipeline_json('get-metadata', seed, function(data, err)
|
||||
if data then
|
||||
_lua_log('Metadata: ' .. utils.format_json(data))
|
||||
mp.osd_message('Metadata loaded (check console)', 3)
|
||||
return
|
||||
end
|
||||
if err then
|
||||
mp.osd_message('Failed to load metadata: ' .. tostring(err), 3)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
-- Command: Delete current file
|
||||
function M.delete_current_file()
|
||||
local path = mp.get_property("path")
|
||||
if not path then return end
|
||||
|
||||
local path = mp.get_property('path')
|
||||
if not path then
|
||||
return
|
||||
end
|
||||
|
||||
local seed = {{path = path}}
|
||||
|
||||
M.run_pipeline("delete-file", seed)
|
||||
mp.osd_message("File deleted", 3)
|
||||
mp.command("playlist-next")
|
||||
|
||||
M.run_pipeline('delete-file', seed, function(_, err)
|
||||
if err then
|
||||
mp.osd_message('Delete failed: ' .. tostring(err), 3)
|
||||
return
|
||||
end
|
||||
mp.osd_message('File deleted', 3)
|
||||
mp.command('playlist-next')
|
||||
end)
|
||||
end
|
||||
|
||||
-- Command: Load a URL via pipeline (Ctrl+Enter in prompt)
|
||||
@@ -2619,14 +2679,18 @@ mp.register_script_message('medios-load-url-event', function(json)
|
||||
end
|
||||
|
||||
ensure_mpv_ipc_server()
|
||||
local out = M.run_pipeline('.mpv -url ' .. quote_pipeline_arg(url) .. ' -play')
|
||||
if out ~= nil then
|
||||
local pipeline_cmd = '.mpv -url ' .. quote_pipeline_arg(url) .. ' -play'
|
||||
M.run_pipeline(pipeline_cmd, nil, function(_, err)
|
||||
if err then
|
||||
mp.osd_message('Load URL failed: ' .. tostring(err), 3)
|
||||
return
|
||||
end
|
||||
if ensure_uosc_loaded() then
|
||||
mp.commandv('script-message-to', 'uosc', 'close-menu', LOAD_URL_MENU_TYPE)
|
||||
else
|
||||
_lua_log('menu: uosc not available; cannot close-menu')
|
||||
end
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
-- Menu integration with UOSC
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
# Medeia MPV script options
|
||||
store=tutorial
|
||||
store=rpi
|
||||
|
||||
Reference in New Issue
Block a user