This commit is contained in:
2026-01-07 05:09:59 -08:00
parent edc33f4528
commit f0799191ff
10 changed files with 956 additions and 353 deletions

View File

@@ -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

View File

@@ -1,2 +1,2 @@
# Medeia MPV script options
store=tutorial
store=rpi