fd
Some checks failed
smoke-mm / Install & smoke test mm --help (push) Has been cancelled
Some checks failed
smoke-mm / Install & smoke test mm --help (push) Has been cancelled
This commit is contained in:
577
MPV/LUA/main.lua
577
MPV/LUA/main.lua
@@ -6,6 +6,9 @@ local M = {}
|
|||||||
|
|
||||||
local MEDEIA_LUA_VERSION = '2025-12-24'
|
local MEDEIA_LUA_VERSION = '2025-12-24'
|
||||||
|
|
||||||
|
-- Expose a tiny breadcrumb for debugging which script version is loaded.
|
||||||
|
pcall(mp.set_property, 'user-data/medeia-lua-version', MEDEIA_LUA_VERSION)
|
||||||
|
|
||||||
-- Track whether uosc is available so menu calls don't fail with
|
-- Track whether uosc is available so menu calls don't fail with
|
||||||
-- "Can't find script 'uosc' to send message to."
|
-- "Can't find script 'uosc' to send message to."
|
||||||
local _uosc_loaded = false
|
local _uosc_loaded = false
|
||||||
@@ -57,6 +60,24 @@ local function _lua_log(text)
|
|||||||
end
|
end
|
||||||
local dir = ''
|
local dir = ''
|
||||||
|
|
||||||
|
-- Prefer a stable repo-root Log/ folder based on the script directory.
|
||||||
|
do
|
||||||
|
local function _dirname(p)
|
||||||
|
p = tostring(p or '')
|
||||||
|
p = p:gsub('[/\\]+$', '')
|
||||||
|
return p:match('(.*)[/\\]') or ''
|
||||||
|
end
|
||||||
|
|
||||||
|
local base = mp.get_script_directory() or ''
|
||||||
|
if base ~= '' then
|
||||||
|
-- base is expected to be <repo>/MPV/LUA
|
||||||
|
local root = _dirname(_dirname(base))
|
||||||
|
if root ~= '' then
|
||||||
|
dir = utils.join_path(root, 'Log')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- Prefer repo-root Log/ for consistency with Python helper logs.
|
-- Prefer repo-root Log/ for consistency with Python helper logs.
|
||||||
do
|
do
|
||||||
local function find_up(start_dir, relative_path, max_levels)
|
local function find_up(start_dir, relative_path, max_levels)
|
||||||
@@ -122,6 +143,8 @@ local function _lua_log(text)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
_lua_log('medeia lua loaded version=' .. tostring(MEDEIA_LUA_VERSION) .. ' script=' .. tostring(mp.get_script_name()))
|
||||||
|
|
||||||
local function ensure_uosc_loaded()
|
local function ensure_uosc_loaded()
|
||||||
if _uosc_loaded or _is_script_loaded('uosc') then
|
if _uosc_loaded or _is_script_loaded('uosc') then
|
||||||
_uosc_loaded = true
|
_uosc_loaded = true
|
||||||
@@ -274,13 +297,107 @@ end
|
|||||||
local _cached_store_names = {}
|
local _cached_store_names = {}
|
||||||
local _store_cache_loaded = false
|
local _store_cache_loaded = false
|
||||||
|
|
||||||
|
local SELECTED_STORE_PROP = 'user-data/medeia-selected-store'
|
||||||
|
local STORE_PICKER_MENU_TYPE = 'medeia_store_picker'
|
||||||
|
local _selected_store_loaded = false
|
||||||
|
|
||||||
|
local function _get_script_opts_dir()
|
||||||
|
local dir = nil
|
||||||
|
pcall(function()
|
||||||
|
dir = mp.command_native({ 'expand-path', '~~/script-opts' })
|
||||||
|
end)
|
||||||
|
if type(dir) ~= 'string' or dir == '' then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
return dir
|
||||||
|
end
|
||||||
|
|
||||||
|
local function _get_selected_store_conf_path()
|
||||||
|
local dir = _get_script_opts_dir()
|
||||||
|
if not dir then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
return utils.join_path(dir, 'medeia.conf')
|
||||||
|
end
|
||||||
|
|
||||||
|
local function _load_selected_store_from_disk()
|
||||||
|
local path = _get_selected_store_conf_path()
|
||||||
|
if not path then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
local fh = io.open(path, 'r')
|
||||||
|
if not fh then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
for line in fh:lines() do
|
||||||
|
local s = trim(tostring(line or ''))
|
||||||
|
if s ~= '' and s:sub(1, 1) ~= '#' and s:sub(1, 1) ~= ';' then
|
||||||
|
local k, v = s:match('^([%w_%-]+)%s*=%s*(.*)$')
|
||||||
|
if k and v and k:lower() == 'store' then
|
||||||
|
fh:close()
|
||||||
|
v = trim(tostring(v or ''))
|
||||||
|
return v ~= '' and v or nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
fh:close()
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function _save_selected_store_to_disk(store)
|
||||||
|
local path = _get_selected_store_conf_path()
|
||||||
|
if not path then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
local fh = io.open(path, 'w')
|
||||||
|
if not fh then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
fh:write('# Medeia MPV script options\n')
|
||||||
|
fh:write('store=' .. tostring(store or '') .. '\n')
|
||||||
|
fh:close()
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function _get_selected_store()
|
||||||
|
local v = ''
|
||||||
|
pcall(function()
|
||||||
|
v = tostring(mp.get_property(SELECTED_STORE_PROP) or '')
|
||||||
|
end)
|
||||||
|
return trim(tostring(v or ''))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function _set_selected_store(store)
|
||||||
|
store = trim(tostring(store or ''))
|
||||||
|
pcall(mp.set_property, SELECTED_STORE_PROP, store)
|
||||||
|
pcall(_save_selected_store_to_disk, store)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function _ensure_selected_store_loaded()
|
||||||
|
if _selected_store_loaded then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
_selected_store_loaded = true
|
||||||
|
local disk = nil
|
||||||
|
pcall(function()
|
||||||
|
disk = _load_selected_store_from_disk()
|
||||||
|
end)
|
||||||
|
disk = trim(tostring(disk or ''))
|
||||||
|
if disk ~= '' then
|
||||||
|
pcall(mp.set_property, SELECTED_STORE_PROP, disk)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
local _pipeline_helper_started = false
|
local _pipeline_helper_started = false
|
||||||
local _last_ipc_error = ''
|
local _last_ipc_error = ''
|
||||||
local _last_ipc_last_req_json = ''
|
local _last_ipc_last_req_json = ''
|
||||||
local _last_ipc_last_resp_json = ''
|
local _last_ipc_last_resp_json = ''
|
||||||
|
|
||||||
local function _is_pipeline_helper_ready()
|
local function _is_pipeline_helper_ready()
|
||||||
local ready = mp.get_property_native(PIPELINE_READY_PROP)
|
local ready = mp.get_property(PIPELINE_READY_PROP)
|
||||||
|
if ready == nil or ready == '' then
|
||||||
|
ready = mp.get_property_native(PIPELINE_READY_PROP)
|
||||||
|
end
|
||||||
if not ready then
|
if not ready then
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
@@ -303,6 +420,7 @@ local function _is_pipeline_helper_ready()
|
|||||||
return age <= 10
|
return age <= 10
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- If it's some other non-empty value, treat as ready.
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -833,13 +951,14 @@ local ensure_pipeline_helper_running
|
|||||||
local function _run_helper_request_response(req, timeout_seconds)
|
local function _run_helper_request_response(req, timeout_seconds)
|
||||||
_last_ipc_error = ''
|
_last_ipc_error = ''
|
||||||
if not ensure_pipeline_helper_running() then
|
if not ensure_pipeline_helper_running() then
|
||||||
_lua_log('ipc: helper not ready; cannot execute request')
|
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'
|
_last_ipc_error = 'helper not ready'
|
||||||
return nil
|
|
||||||
end
|
end
|
||||||
|
|
||||||
do
|
do
|
||||||
local deadline = mp.get_time() + 3.0
|
-- 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
|
while mp.get_time() < deadline do
|
||||||
if _is_pipeline_helper_ready() then
|
if _is_pipeline_helper_ready() then
|
||||||
break
|
break
|
||||||
@@ -847,10 +966,9 @@ local function _run_helper_request_response(req, timeout_seconds)
|
|||||||
mp.wait_event(0.05)
|
mp.wait_event(0.05)
|
||||||
end
|
end
|
||||||
if not _is_pipeline_helper_ready() then
|
if not _is_pipeline_helper_ready() then
|
||||||
local rv = tostring(mp.get_property_native(PIPELINE_READY_PROP))
|
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)
|
_lua_log('ipc: proceeding without helper heartbeat; ready=' .. rv)
|
||||||
_last_ipc_error = 'helper not ready (ready=' .. rv .. ')'
|
_last_ipc_error = 'helper heartbeat missing (ready=' .. rv .. ')'
|
||||||
return nil
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -914,9 +1032,67 @@ end
|
|||||||
|
|
||||||
local function _refresh_store_cache(timeout_seconds)
|
local function _refresh_store_cache(timeout_seconds)
|
||||||
ensure_mpv_ipc_server()
|
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 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 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
|
||||||
|
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))
|
||||||
|
end
|
||||||
|
else
|
||||||
|
_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)
|
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
|
if not resp or not resp.success or type(resp.choices) ~= 'table' then
|
||||||
_lua_log('stores: failed to load store choices via helper; stderr=' .. tostring(resp and resp.stderr or '') .. ' error=' .. tostring(resp and resp.error or ''))
|
_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
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -929,7 +1105,11 @@ local function _refresh_store_cache(timeout_seconds)
|
|||||||
end
|
end
|
||||||
_cached_store_names = out
|
_cached_store_names = out
|
||||||
_store_cache_loaded = true
|
_store_cache_loaded = true
|
||||||
_lua_log('stores: loaded ' .. tostring(#_cached_store_names) .. ' stores via helper')
|
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
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -946,6 +1126,116 @@ local function _uosc_open_list_picker(menu_type, title, items)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function _open_store_picker()
|
||||||
|
_ensure_selected_store_loaded()
|
||||||
|
|
||||||
|
local selected = _get_selected_store()
|
||||||
|
local cached_count = (type(_cached_store_names) == 'table') and #_cached_store_names or 0
|
||||||
|
local cached_preview = ''
|
||||||
|
if type(_cached_store_names) == 'table' and #_cached_store_names > 0 then
|
||||||
|
cached_preview = table.concat(_cached_store_names, ', ')
|
||||||
|
end
|
||||||
|
_lua_log(
|
||||||
|
'stores: open picker selected='
|
||||||
|
.. tostring(selected)
|
||||||
|
.. ' cached_count='
|
||||||
|
.. tostring(cached_count)
|
||||||
|
.. ' cached='
|
||||||
|
.. tostring(cached_preview)
|
||||||
|
)
|
||||||
|
|
||||||
|
local function build_items()
|
||||||
|
local selected = _get_selected_store()
|
||||||
|
local items = {}
|
||||||
|
|
||||||
|
if type(_cached_store_names) == 'table' and #_cached_store_names > 0 then
|
||||||
|
for _, name in ipairs(_cached_store_names) do
|
||||||
|
name = trim(tostring(name or ''))
|
||||||
|
if name ~= '' then
|
||||||
|
local payload = { store = name }
|
||||||
|
items[#items + 1] = {
|
||||||
|
title = name,
|
||||||
|
active = (selected ~= '' and name == selected) and true or false,
|
||||||
|
value = { 'script-message-to', mp.get_script_name(), 'medeia-store-select', utils.format_json(payload) },
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
items[#items + 1] = {
|
||||||
|
title = 'No stores found',
|
||||||
|
hint = 'Configure stores in config.conf',
|
||||||
|
selectable = false,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
return items
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Open immediately with whatever cache we have.
|
||||||
|
_uosc_open_list_picker(STORE_PICKER_MENU_TYPE, 'Store', build_items())
|
||||||
|
|
||||||
|
-- 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
|
||||||
|
|
||||||
|
if tries_left > 0 then
|
||||||
|
mp.add_timeout(0.25, function()
|
||||||
|
attempt_refresh(tries_left - 1)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
mp.add_timeout(0.05, function()
|
||||||
|
attempt_refresh(6)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
mp.register_script_message('medeia-store-picker', function()
|
||||||
|
_open_store_picker()
|
||||||
|
end)
|
||||||
|
|
||||||
|
mp.register_script_message('medeia-store-select', function(json)
|
||||||
|
local ok, ev = pcall(utils.parse_json, json)
|
||||||
|
if not ok or type(ev) ~= 'table' then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local store = trim(tostring(ev.store or ''))
|
||||||
|
if store == '' then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
_set_selected_store(store)
|
||||||
|
mp.osd_message('Store: ' .. store, 2)
|
||||||
|
end)
|
||||||
|
|
||||||
-- No-op handler for placeholder menu items.
|
-- No-op handler for placeholder menu items.
|
||||||
mp.register_script_message('medios-nop', function()
|
mp.register_script_message('medios-nop', function()
|
||||||
return
|
return
|
||||||
@@ -1322,6 +1612,7 @@ end
|
|||||||
|
|
||||||
-- Prime store cache shortly after load (best-effort; picker also refreshes on-demand).
|
-- Prime store cache shortly after load (best-effort; picker also refreshes on-demand).
|
||||||
mp.add_timeout(0.10, function()
|
mp.add_timeout(0.10, function()
|
||||||
|
pcall(_ensure_selected_store_loaded)
|
||||||
if not _store_cache_loaded then
|
if not _store_cache_loaded then
|
||||||
pcall(_refresh_store_cache, 1.5)
|
pcall(_refresh_store_cache, 1.5)
|
||||||
end
|
end
|
||||||
@@ -1729,23 +2020,71 @@ if not opts.cli_path then
|
|||||||
opts.cli_path = find_file_upwards(script_dir, "CLI.py", 6) or "CLI.py"
|
opts.cli_path = find_file_upwards(script_dir, "CLI.py", 6) or "CLI.py"
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Helper to run pipeline
|
-- Clean API wrapper for executing Python functions from Lua
|
||||||
function M.run_pipeline(pipeline_cmd, seeds)
|
local function _call_mpv_api(request)
|
||||||
local out, err = run_pipeline_via_ipc(pipeline_cmd, seeds, 5)
|
-- Call the MPV Lua API (mpv_lua_api.py) with a JSON request.
|
||||||
if out ~= nil then
|
-- Returns: JSON-decoded response object with {success, stdout, stderr, error, ...}
|
||||||
return out
|
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
|
end
|
||||||
if err ~= nil then
|
|
||||||
local log_path = write_temp_log('medeia-pipeline-error', tostring(err))
|
_lua_log('api: calling mpv_lua_api cmd=' .. tostring(request.cmd))
|
||||||
local suffix = log_path and (' (log: ' .. log_path .. ')') or ''
|
|
||||||
_lua_log('Pipeline error: ' .. tostring(err) .. suffix)
|
local python_exe = _resolve_python_exe(true)
|
||||||
mp.osd_message('Error: pipeline failed' .. suffix, 6)
|
if not python_exe or python_exe == '' then
|
||||||
return nil
|
_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
|
||||||
|
|
||||||
mp.osd_message('Error: pipeline helper not available', 6)
|
|
||||||
_lua_log('ipc: helper not available; refusing to spawn python subprocess')
|
|
||||||
return nil
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Helper to run pipeline and parse JSON output
|
-- Helper to run pipeline and parse JSON output
|
||||||
@@ -1858,62 +2197,218 @@ end
|
|||||||
|
|
||||||
-- Prompt for trim range via an input box and callback
|
-- Prompt for trim range via an input box and callback
|
||||||
local function _start_trim_with_range(range)
|
local function _start_trim_with_range(range)
|
||||||
|
_lua_log('=== TRIM START: range=' .. tostring(range))
|
||||||
|
mp.osd_message('Trimming...', 10)
|
||||||
|
|
||||||
|
-- Load the trim module for direct FFmpeg trimming
|
||||||
|
local script_dir = mp.get_script_directory()
|
||||||
|
_lua_log('trim: script_dir=' .. tostring(script_dir))
|
||||||
|
|
||||||
|
-- Try multiple locations for trim.lua
|
||||||
|
local trim_paths = {}
|
||||||
|
if script_dir and script_dir ~= '' then
|
||||||
|
table.insert(trim_paths, script_dir .. '/trim.lua')
|
||||||
|
table.insert(trim_paths, script_dir .. '/LUA/trim.lua') -- if called from parent
|
||||||
|
table.insert(trim_paths, script_dir .. '/../trim.lua')
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Also try absolute path
|
||||||
|
table.insert(trim_paths, '/medios/Medios-Macina/MPV/LUA/trim.lua')
|
||||||
|
table.insert(trim_paths, 'C:/medios/Medios-Macina/MPV/LUA/trim.lua')
|
||||||
|
|
||||||
|
local trim_module = nil
|
||||||
|
local load_err = nil
|
||||||
|
|
||||||
|
for _, trim_path in ipairs(trim_paths) do
|
||||||
|
_lua_log('trim: trying path=' .. trim_path)
|
||||||
|
local ok, result = pcall(loadfile, trim_path)
|
||||||
|
if ok and result then
|
||||||
|
trim_module = result()
|
||||||
|
_lua_log('trim: loaded successfully from ' .. trim_path)
|
||||||
|
break
|
||||||
|
else
|
||||||
|
load_err = tostring(result or 'unknown error')
|
||||||
|
_lua_log('trim: failed to load from ' .. trim_path .. ' (' .. load_err .. ')')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if not trim_module or not trim_module.trim_file then
|
||||||
|
mp.osd_message('ERROR: Could not load trim module from any path', 3)
|
||||||
|
_lua_log('trim: FAILED - all paths exhausted, last error=' .. tostring(load_err))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
range = trim(tostring(range or ''))
|
range = trim(tostring(range or ''))
|
||||||
|
_lua_log('trim: after_trim range=' .. tostring(range))
|
||||||
|
|
||||||
if range == '' then
|
if range == '' then
|
||||||
mp.osd_message('Trim cancelled (no range provided)', 3)
|
mp.osd_message('Trim cancelled (no range provided)', 3)
|
||||||
|
_lua_log('trim: CANCELLED - empty range')
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local target = _current_target()
|
local target = _current_target()
|
||||||
if not target or target == '' then
|
if not target or target == '' then
|
||||||
mp.osd_message('No file to trim', 3)
|
mp.osd_message('No file to trim', 3)
|
||||||
|
_lua_log('trim: FAILED - no target')
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
_lua_log('trim: target=' .. tostring(target))
|
||||||
|
|
||||||
local store_hash = _extract_store_hash(target)
|
local store_hash = _extract_store_hash(target)
|
||||||
|
if store_hash then
|
||||||
|
_lua_log('trim: store_hash detected store=' .. tostring(store_hash.store) .. ' hash=' .. tostring(store_hash.hash))
|
||||||
|
else
|
||||||
|
_lua_log('trim: store_hash=nil (local file)')
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Get the selected store (this reads from saved config or mpv property)
|
||||||
|
_ensure_selected_store_loaded()
|
||||||
|
local selected_store = _get_selected_store()
|
||||||
|
-- Strip any existing quotes from the store name
|
||||||
|
selected_store = selected_store:gsub('^"', ''):gsub('"$', '')
|
||||||
|
_lua_log('trim: selected_store=' .. tostring(selected_store or 'NONE'))
|
||||||
|
_lua_log('trim: _cached_store_names=' .. tostring(_cached_store_names and #_cached_store_names or 0))
|
||||||
|
_lua_log('trim: _selected_store_index=' .. tostring(_selected_store_index or 'nil'))
|
||||||
|
|
||||||
-- Prefer the resolved stream URL/filename so trimming can avoid full downloads where possible.
|
|
||||||
local stream = trim(tostring(mp.get_property('stream-open-filename') or ''))
|
local stream = trim(tostring(mp.get_property('stream-open-filename') or ''))
|
||||||
if stream == '' then
|
if stream == '' then
|
||||||
stream = tostring(target)
|
stream = tostring(target)
|
||||||
end
|
end
|
||||||
|
_lua_log('trim: stream=' .. tostring(stream))
|
||||||
|
|
||||||
local pipeline_cmd
|
local title = trim(tostring(mp.get_property('media-title') or ''))
|
||||||
|
if title == '' then
|
||||||
|
title = 'clip'
|
||||||
|
end
|
||||||
|
_lua_log('trim: title=' .. tostring(title))
|
||||||
|
|
||||||
|
-- ===== TRIM IN LUA USING FFMPEG =====
|
||||||
|
mp.osd_message('Starting FFmpeg trim...', 1)
|
||||||
|
_lua_log('trim: calling trim_module.trim_file with range=' .. range)
|
||||||
|
|
||||||
|
-- Get temp directory from config or use default
|
||||||
|
local temp_dir = mp.get_property('user-data/medeia-config-temp') or os.getenv('TEMP') or os.getenv('TMP') or '/tmp'
|
||||||
|
_lua_log('trim: using temp_dir=' .. temp_dir)
|
||||||
|
|
||||||
|
local success, output_path, error_msg = trim_module.trim_file(stream, range, temp_dir)
|
||||||
|
|
||||||
|
if not success then
|
||||||
|
mp.osd_message('Trim failed: ' .. error_msg, 3)
|
||||||
|
_lua_log('trim: FAILED - ' .. error_msg)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
_lua_log('trim: FFmpeg SUCCESS - output_path=' .. output_path)
|
||||||
|
mp.osd_message('Trim complete, uploading...', 2)
|
||||||
|
|
||||||
|
-- ===== UPLOAD TO PYTHON FOR STORAGE AND METADATA =====
|
||||||
|
local pipeline_cmd = nil
|
||||||
|
_lua_log('trim: === BUILDING UPLOAD PIPELINE ===')
|
||||||
|
_lua_log('trim: store_hash=' .. tostring(store_hash and (store_hash.store .. '/' .. store_hash.hash) or 'nil'))
|
||||||
|
_lua_log('trim: selected_store=' .. tostring(selected_store or 'nil'))
|
||||||
|
|
||||||
if store_hash then
|
if store_hash then
|
||||||
pipeline_cmd =
|
-- Original file is from a store - set relationship to it
|
||||||
'get-tag -emit -store ' .. quote_pipeline_arg(store_hash.store) ..
|
_lua_log('trim: building store file pipeline (original from store)')
|
||||||
' -query ' .. quote_pipeline_arg('hash:' .. store_hash.hash) ..
|
if selected_store then
|
||||||
' | trim-file -input ' .. quote_pipeline_arg(stream) ..
|
pipeline_cmd =
|
||||||
' -range ' .. quote_pipeline_arg(range) ..
|
'get-tag -emit -store ' .. quote_pipeline_arg(store_hash.store) ..
|
||||||
' | add-file -store ' .. quote_pipeline_arg(store_hash.store)
|
' -query ' .. quote_pipeline_arg('hash:' .. store_hash.hash) ..
|
||||||
else
|
' | add-file -path ' .. quote_pipeline_arg(output_path) ..
|
||||||
if utils.file_info(tostring(target)) then
|
' -store "' .. selected_store .. '"' ..
|
||||||
pipeline_cmd = 'trim-file -path ' .. quote_pipeline_arg(target) .. ' -range ' .. quote_pipeline_arg(range)
|
' | add-relationship -store "' .. selected_store .. '"' ..
|
||||||
|
' -to-hash ' .. quote_pipeline_arg(store_hash.hash)
|
||||||
else
|
else
|
||||||
pipeline_cmd = 'trim-file -input ' .. quote_pipeline_arg(stream) .. ' -range ' .. quote_pipeline_arg(range)
|
pipeline_cmd =
|
||||||
|
'get-tag -emit -store ' .. quote_pipeline_arg(store_hash.store) ..
|
||||||
|
' -query ' .. quote_pipeline_arg('hash:' .. store_hash.hash) ..
|
||||||
|
' | add-file -path ' .. quote_pipeline_arg(output_path) ..
|
||||||
|
' -store "' .. store_hash.store .. '"' ..
|
||||||
|
' | add-relationship -store "' .. store_hash.store .. '"' ..
|
||||||
|
' -to-hash ' .. quote_pipeline_arg(store_hash.hash)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
-- Local file: save to selected store if available
|
||||||
|
_lua_log('trim: local file pipeline (not from store)')
|
||||||
|
if selected_store then
|
||||||
|
_lua_log('trim: building add-file command to selected_store=' .. selected_store)
|
||||||
|
-- Don't add title if empty - the file path will be used as title by default
|
||||||
|
pipeline_cmd = 'add-file -path ' .. quote_pipeline_arg(output_path) ..
|
||||||
|
' -store "' .. selected_store .. '"'
|
||||||
|
_lua_log('trim: pipeline_cmd=' .. pipeline_cmd)
|
||||||
|
else
|
||||||
|
mp.osd_message('Trim complete: ' .. output_path, 5)
|
||||||
|
_lua_log('trim: no store selected, trim complete at ' .. output_path)
|
||||||
|
return
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if not _run_pipeline_detached(pipeline_cmd) then
|
if not pipeline_cmd or pipeline_cmd == '' then
|
||||||
M.run_pipeline(pipeline_cmd)
|
mp.osd_message('Trim error: could not build upload command', 3)
|
||||||
|
_lua_log('trim: FAILED - empty pipeline_cmd')
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
_lua_log('trim: final upload_cmd=' .. pipeline_cmd)
|
||||||
|
_lua_log('trim: === CALLING API FOR UPLOAD ===')
|
||||||
|
|
||||||
|
-- Call the API to handle metadata/storage
|
||||||
|
local response = _call_mpv_api({
|
||||||
|
cmd = 'execute_pipeline',
|
||||||
|
pipeline = pipeline_cmd,
|
||||||
|
})
|
||||||
|
|
||||||
|
_lua_log('trim: api response success=' .. tostring(response.success))
|
||||||
|
_lua_log('trim: api response error=' .. tostring(response.error or 'nil'))
|
||||||
|
_lua_log('trim: api response stderr=' .. tostring(response.stderr or 'nil'))
|
||||||
|
_lua_log('trim: api response returncode=' .. tostring(response.returncode or 'nil'))
|
||||||
|
|
||||||
|
if response.stderr and response.stderr ~= '' then
|
||||||
|
_lua_log('trim: STDERR OUTPUT: ' .. response.stderr)
|
||||||
|
end
|
||||||
|
|
||||||
|
if response.success then
|
||||||
|
local msg = 'Trim and upload completed'
|
||||||
|
if selected_store then
|
||||||
|
msg = msg .. ' (store: ' .. selected_store .. ')'
|
||||||
|
end
|
||||||
|
mp.osd_message(msg, 5)
|
||||||
|
_lua_log('trim: SUCCESS - ' .. msg)
|
||||||
|
else
|
||||||
|
local err_msg = response.error or response.stderr or 'unknown error'
|
||||||
|
mp.osd_message('Upload failed: ' .. err_msg, 5)
|
||||||
|
_lua_log('trim: upload FAILED - ' .. err_msg)
|
||||||
end
|
end
|
||||||
mp.osd_message('Trim started', 3)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.open_trim_prompt()
|
function M.open_trim_prompt()
|
||||||
|
_lua_log('=== OPEN_TRIM_PROMPT called')
|
||||||
|
|
||||||
local marker_range = _get_trim_range_from_clip_markers()
|
local marker_range = _get_trim_range_from_clip_markers()
|
||||||
|
_lua_log('trim_prompt: marker_range=' .. tostring(marker_range or 'NONE'))
|
||||||
|
|
||||||
if marker_range then
|
if marker_range then
|
||||||
|
_lua_log('trim_prompt: using auto-detected markers, starting trim')
|
||||||
|
mp.osd_message('Using clip markers: ' .. marker_range, 2)
|
||||||
_start_trim_with_range(marker_range)
|
_start_trim_with_range(marker_range)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
_lua_log('trim_prompt: no clip markers detected, showing prompt')
|
||||||
|
mp.osd_message('Set 2 clip markers with the marker button, or enter range manually', 3)
|
||||||
|
|
||||||
|
local selected_store = _cached_store_names and #_cached_store_names > 0 and _selected_store_index
|
||||||
|
and _cached_store_names[_selected_store_index] or nil
|
||||||
|
local store_hint = selected_store and ' (saving to: ' .. selected_store .. ')' or ' (no store selected; will save locally)'
|
||||||
|
|
||||||
local menu_data = {
|
local menu_data = {
|
||||||
type = TRIM_PROMPT_MENU_TYPE,
|
type = TRIM_PROMPT_MENU_TYPE,
|
||||||
title = 'Trim file',
|
title = 'Trim file',
|
||||||
search_style = 'palette',
|
search_style = 'palette',
|
||||||
search_debounce = 'submit',
|
search_debounce = 'submit',
|
||||||
on_search = 'callback',
|
on_search = 'callback',
|
||||||
footnote = "Enter time range (e.g. '00:03:45-00:03:55' or '1h3m-1h10m30s') and press Enter",
|
footnote = "Enter time range (e.g. '00:03:45-00:03:55' or '1h3m-1h10m30s') and press Enter" .. store_hint,
|
||||||
callback = { mp.get_script_name(), 'medios-trim-run' },
|
callback = { mp.get_script_name(), 'medios-trim-run' },
|
||||||
items = {
|
items = {
|
||||||
{
|
{
|
||||||
|
|||||||
204
MPV/LUA/trim.lua
Normal file
204
MPV/LUA/trim.lua
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
-- Trim file directly in MPV using FFmpeg
|
||||||
|
-- This script handles the actual video trimming with FFmpeg subprocess
|
||||||
|
-- Then passes the trimmed file to Python for upload/metadata handling
|
||||||
|
|
||||||
|
local mp = require "mp"
|
||||||
|
local utils = require "mp.utils"
|
||||||
|
|
||||||
|
local trim = {}
|
||||||
|
|
||||||
|
-- Configuration for trim presets
|
||||||
|
trim.config = {
|
||||||
|
output_dir = os.getenv('TEMP') or os.getenv('TMP') or '/tmp', -- use temp dir by default
|
||||||
|
video_codec = "copy", -- lossless by default
|
||||||
|
audio_codec = "copy",
|
||||||
|
container = "auto",
|
||||||
|
audio_bitrate = "",
|
||||||
|
osd_duration = 2000,
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Quality presets for video trimming
|
||||||
|
trim.presets = {
|
||||||
|
copy = { video_codec="copy", audio_codec="copy" },
|
||||||
|
high = { video_codec="libx264", crf="18", preset="slower", audio_codec="aac", audio_bitrate="192k" },
|
||||||
|
medium = { video_codec="libx264", crf="20", preset="medium", audio_codec="aac", audio_bitrate="128k" },
|
||||||
|
fast = { video_codec="libx264", crf="23", preset="fast", audio_codec="aac", audio_bitrate="96k" },
|
||||||
|
tiny = { video_codec="libx264", crf="28", preset="ultrafast", audio_codec="aac", audio_bitrate="64k" },
|
||||||
|
}
|
||||||
|
|
||||||
|
trim.current_quality = "copy"
|
||||||
|
|
||||||
|
-- Get active preset with current quality
|
||||||
|
local function _get_active_preset()
|
||||||
|
local preset = trim.presets[trim.current_quality] or {}
|
||||||
|
local merged = {}
|
||||||
|
for k, v in pairs(preset) do
|
||||||
|
merged[k] = v
|
||||||
|
end
|
||||||
|
for k, v in pairs(trim.config) do
|
||||||
|
if merged[k] == nil then
|
||||||
|
merged[k] = v
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return merged
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Extract title from file path, handling special URL formats
|
||||||
|
local function _parse_file_title(filepath)
|
||||||
|
-- For torrent URLs, try to extract meaningful filename
|
||||||
|
if filepath:match("torrentio%.strem%.fun") then
|
||||||
|
-- Format: https://torrentio.strem.fun/resolve/alldebrid/.../filename/0/filename
|
||||||
|
local filename = filepath:match("([^/]+)/0/[^/]+$")
|
||||||
|
if filename then
|
||||||
|
filename = filename:gsub("%.mkv$", ""):gsub("%.mp4$", ""):gsub("%.avi$", "")
|
||||||
|
return filename
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Standard file path
|
||||||
|
local dir, name = utils.split_path(filepath)
|
||||||
|
return name:gsub("%..+$", "")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Format time duration as "1h3m-1h3m15s"
|
||||||
|
local function _format_time_range(start_sec, end_sec)
|
||||||
|
local function _sec_to_str(sec)
|
||||||
|
local h = math.floor(sec / 3600)
|
||||||
|
local m = math.floor((sec % 3600) / 60)
|
||||||
|
local s = math.floor(sec % 60)
|
||||||
|
|
||||||
|
local parts = {}
|
||||||
|
if h > 0 then table.insert(parts, h .. "h") end
|
||||||
|
if m > 0 then table.insert(parts, m .. "m") end
|
||||||
|
if s > 0 or #parts == 0 then table.insert(parts, s .. "s") end
|
||||||
|
|
||||||
|
return table.concat(parts)
|
||||||
|
end
|
||||||
|
|
||||||
|
return _sec_to_str(start_sec) .. "-" .. _sec_to_str(end_sec)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Trim file using FFmpeg with range in format "1h3m-1h3m15s"
|
||||||
|
-- Returns: (success, output_path, error_msg)
|
||||||
|
function trim.trim_file(input_file, range_str, temp_dir)
|
||||||
|
if not input_file or input_file == "" then
|
||||||
|
return false, nil, "No input file specified"
|
||||||
|
end
|
||||||
|
|
||||||
|
if not range_str or range_str == "" then
|
||||||
|
return false, nil, "No range specified (format: 1h3m-1h3m15s)"
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Use provided temp_dir or fall back to config
|
||||||
|
if not temp_dir or temp_dir == "" then
|
||||||
|
temp_dir = trim.config.output_dir
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Parse range string "1h3m-1h3m15s"
|
||||||
|
local start_str, end_str = range_str:match("^([^-]+)-(.+)$")
|
||||||
|
if not start_str or not end_str then
|
||||||
|
return false, nil, "Invalid range format. Use: 1h3m-1h3m15s"
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Convert time string to seconds
|
||||||
|
local function _parse_time(time_str)
|
||||||
|
local sec = 0
|
||||||
|
local h = time_str:match("(%d+)h")
|
||||||
|
local m = time_str:match("(%d+)m")
|
||||||
|
local s = time_str:match("(%d+)s")
|
||||||
|
|
||||||
|
if h then sec = sec + tonumber(h) * 3600 end
|
||||||
|
if m then sec = sec + tonumber(m) * 60 end
|
||||||
|
if s then sec = sec + tonumber(s) end
|
||||||
|
|
||||||
|
return sec
|
||||||
|
end
|
||||||
|
|
||||||
|
local start_time = _parse_time(start_str)
|
||||||
|
local end_time = _parse_time(end_str)
|
||||||
|
local duration = end_time - start_time
|
||||||
|
|
||||||
|
if duration <= 0 then
|
||||||
|
return false, nil, "Invalid range: end time must be after start time"
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Prepare output path
|
||||||
|
local dir, name = utils.split_path(input_file)
|
||||||
|
|
||||||
|
-- If input is a URL, extract filename from URL path
|
||||||
|
if input_file:match("^https?://") then
|
||||||
|
-- For URLs, try to extract the meaningful filename
|
||||||
|
name = input_file:match("([^/]+)$") or "stream"
|
||||||
|
dir = trim.config.output_dir
|
||||||
|
end
|
||||||
|
|
||||||
|
local out_dir = trim.config.output_dir
|
||||||
|
local ext = (trim.config.container == "auto") and input_file:match("^.+(%..+)$") or ("." .. trim.config.container)
|
||||||
|
local base_name = name:gsub("%..+$", "")
|
||||||
|
local out_path = utils.join_path(out_dir, base_name .. "_" .. range_str .. ext)
|
||||||
|
|
||||||
|
-- Normalize path to use consistent backslashes on Windows
|
||||||
|
out_path = out_path:gsub("/", "\\")
|
||||||
|
|
||||||
|
-- Build FFmpeg command
|
||||||
|
local p = _get_active_preset()
|
||||||
|
local args = { "ffmpeg", "-y", "-ss", tostring(start_time), "-i", input_file, "-t", tostring(duration) }
|
||||||
|
|
||||||
|
-- Video codec
|
||||||
|
if p.video_codec == "copy" then
|
||||||
|
table.insert(args, "-c:v")
|
||||||
|
table.insert(args, "copy")
|
||||||
|
else
|
||||||
|
table.insert(args, "-c:v")
|
||||||
|
table.insert(args, p.video_codec)
|
||||||
|
if p.crf and p.crf ~= "" then
|
||||||
|
table.insert(args, "-crf")
|
||||||
|
table.insert(args, p.crf)
|
||||||
|
end
|
||||||
|
if p.preset and p.preset ~= "" then
|
||||||
|
table.insert(args, "-preset")
|
||||||
|
table.insert(args, p.preset)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Audio codec
|
||||||
|
if p.audio_codec == "copy" then
|
||||||
|
table.insert(args, "-c:a")
|
||||||
|
table.insert(args, "copy")
|
||||||
|
else
|
||||||
|
table.insert(args, "-c:a")
|
||||||
|
table.insert(args, p.audio_codec)
|
||||||
|
if p.audio_bitrate and p.audio_bitrate ~= "" then
|
||||||
|
table.insert(args, "-b:a")
|
||||||
|
table.insert(args, p.audio_bitrate)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(args, out_path)
|
||||||
|
|
||||||
|
-- Execute FFmpeg synchronously
|
||||||
|
local result = mp.command_native({ name = "subprocess", args = args, capture_stdout = true, capture_stderr = true })
|
||||||
|
|
||||||
|
if not result or result.status ~= 0 then
|
||||||
|
local error_msg = result and result.stderr or "Unknown FFmpeg error"
|
||||||
|
return false, nil, "FFmpeg failed: " .. error_msg
|
||||||
|
end
|
||||||
|
|
||||||
|
return true, out_path, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Cycle to next quality preset
|
||||||
|
function trim.cycle_quality()
|
||||||
|
local presets = { "copy", "high", "medium", "fast", "tiny" }
|
||||||
|
local idx = 1
|
||||||
|
for i, v in ipairs(presets) do
|
||||||
|
if v == trim.current_quality then
|
||||||
|
idx = i
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
trim.current_quality = presets[(idx % #presets) + 1]
|
||||||
|
return trim.current_quality
|
||||||
|
end
|
||||||
|
|
||||||
|
return trim
|
||||||
173
MPV/mpv_lua_api.py
Normal file
173
MPV/mpv_lua_api.py
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
"""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)
|
||||||
@@ -40,6 +40,21 @@ def _repo_root() -> Path:
|
|||||||
return Path(__file__).resolve().parent.parent
|
return Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
def _runtime_config_root() -> Path:
|
||||||
|
"""Best-effort config root for runtime execution.
|
||||||
|
|
||||||
|
MPV can spawn this helper from an installed location while setting `cwd` to
|
||||||
|
the repo root (see MPV.mpv_ipc). Prefer `cwd` when it contains `config.conf`.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
cwd = Path.cwd().resolve()
|
||||||
|
if (cwd / "config.conf").exists():
|
||||||
|
return cwd
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return _repo_root()
|
||||||
|
|
||||||
|
|
||||||
# Make repo-local packages importable even when mpv starts us from another cwd.
|
# Make repo-local packages importable even when mpv starts us from another cwd.
|
||||||
_ROOT = str(_repo_root())
|
_ROOT = str(_repo_root())
|
||||||
if _ROOT not in sys.path:
|
if _ROOT not in sys.path:
|
||||||
@@ -223,19 +238,57 @@ def _run_op(op: str, data: Any) -> Dict[str, Any]:
|
|||||||
|
|
||||||
# Provide store backend choices using the same source as CLI/Typer autocomplete.
|
# Provide store backend choices using the same source as CLI/Typer autocomplete.
|
||||||
if op_name in {"store-choices", "store_choices", "get-store-choices", "get_store_choices"}:
|
if op_name in {"store-choices", "store_choices", "get-store-choices", "get_store_choices"}:
|
||||||
from CLI import MedeiaCLI # noqa: WPS433
|
# IMPORTANT:
|
||||||
|
# - Prefer runtime cwd for config discovery (mpv spawns us with cwd=repo_root).
|
||||||
|
# - Avoid returning a cached empty result if config was loaded before it existed.
|
||||||
|
try:
|
||||||
|
from config import reload_config # noqa: WPS433
|
||||||
|
from Store import Store # noqa: WPS433
|
||||||
|
|
||||||
backends = MedeiaCLI.get_store_choices()
|
config_root = _runtime_config_root()
|
||||||
choices = sorted({str(n) for n in (backends or []) if str(n).strip()})
|
cfg = reload_config(config_dir=config_root)
|
||||||
|
|
||||||
return {
|
storage = Store(config=cfg, suppress_debug=True)
|
||||||
"success": True,
|
backends = storage.list_backends() or []
|
||||||
"stdout": "",
|
choices = sorted({str(n) for n in backends if str(n).strip()})
|
||||||
"stderr": "",
|
|
||||||
"error": None,
|
# Fallback: if initialization gated all backends (e.g., missing deps or offline stores),
|
||||||
"table": None,
|
# still return configured instance names so the UI can present something.
|
||||||
"choices": choices,
|
if not choices:
|
||||||
}
|
store_cfg = cfg.get("store") if isinstance(cfg, dict) else None
|
||||||
|
if isinstance(store_cfg, dict):
|
||||||
|
seen = set()
|
||||||
|
for _, instances in store_cfg.items():
|
||||||
|
if not isinstance(instances, dict):
|
||||||
|
continue
|
||||||
|
for instance_key, instance_cfg in instances.items():
|
||||||
|
name = None
|
||||||
|
if isinstance(instance_cfg, dict):
|
||||||
|
name = instance_cfg.get("NAME") or instance_cfg.get("name")
|
||||||
|
candidate = (str(name or instance_key or "").strip())
|
||||||
|
if candidate:
|
||||||
|
seen.add(candidate)
|
||||||
|
choices = sorted(seen)
|
||||||
|
|
||||||
|
debug(f"[store-choices] config_dir={config_root} choices={len(choices)}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"stdout": "",
|
||||||
|
"stderr": "",
|
||||||
|
"error": None,
|
||||||
|
"table": None,
|
||||||
|
"choices": choices,
|
||||||
|
}
|
||||||
|
except Exception as exc:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"stdout": "",
|
||||||
|
"stderr": "",
|
||||||
|
"error": f"store-choices failed: {type(exc).__name__}: {exc}",
|
||||||
|
"table": None,
|
||||||
|
"choices": [],
|
||||||
|
}
|
||||||
|
|
||||||
# Provide yt-dlp format list for a URL (for MPV "Change format" menu).
|
# Provide yt-dlp format list for a URL (for MPV "Change format" menu).
|
||||||
# Returns a ResultTable-like payload so the Lua UI can render without running cmdlets.
|
# Returns a ResultTable-like payload so the Lua UI can render without running cmdlets.
|
||||||
@@ -580,6 +633,17 @@ def main(argv: Optional[list[str]] = None) -> int:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
_append_helper_log(f"[helper] version={MEDEIA_MPV_HELPER_VERSION} started ipc={args.ipc}")
|
_append_helper_log(f"[helper] version={MEDEIA_MPV_HELPER_VERSION} started ipc={args.ipc}")
|
||||||
|
try:
|
||||||
|
_append_helper_log(f"[helper] file={Path(__file__).resolve()} cwd={Path.cwd().resolve()}")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
runtime_root = _runtime_config_root()
|
||||||
|
_append_helper_log(
|
||||||
|
f"[helper] config_root={runtime_root} exists={bool((runtime_root / 'config.conf').exists())}"
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
debug(f"[mpv-helper] logging to: {_helper_log_path()}")
|
debug(f"[mpv-helper] logging to: {_helper_log_path()}")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
@@ -679,13 +743,11 @@ def main(argv: Optional[list[str]] = None) -> int:
|
|||||||
if (now - last_ready_ts) < 0.75:
|
if (now - last_ready_ts) < 0.75:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
client.send_command_no_wait(["set_property", READY_PROP, str(int(now))])
|
client.send_command_no_wait(["set_property_string", READY_PROP, str(int(now))])
|
||||||
last_ready_ts = now
|
last_ready_ts = now
|
||||||
except Exception:
|
except Exception:
|
||||||
return
|
return
|
||||||
|
|
||||||
_touch_ready()
|
|
||||||
|
|
||||||
# Mirror mpv's own log messages into our helper log file so debugging does
|
# Mirror mpv's own log messages into our helper log file so debugging does
|
||||||
# not depend on the mpv on-screen console or mpv's log-file.
|
# not depend on the mpv on-screen console or mpv's log-file.
|
||||||
try:
|
try:
|
||||||
@@ -715,6 +777,46 @@ def main(argv: Optional[list[str]] = None) -> int:
|
|||||||
except Exception:
|
except Exception:
|
||||||
return 3
|
return 3
|
||||||
|
|
||||||
|
# Mark ready only after the observer is installed to avoid races where Lua
|
||||||
|
# sends a request before we can receive property-change notifications.
|
||||||
|
try:
|
||||||
|
_touch_ready()
|
||||||
|
_append_helper_log(f"[helper] ready heartbeat armed prop={READY_PROP}")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Pre-compute store choices at startup and publish to a cached property so Lua
|
||||||
|
# can read immediately without waiting for a request/response cycle (which may timeout).
|
||||||
|
try:
|
||||||
|
startup_choices_payload = _run_op("store-choices", None)
|
||||||
|
startup_choices = startup_choices_payload.get("choices") if isinstance(startup_choices_payload, dict) else None
|
||||||
|
if isinstance(startup_choices, list):
|
||||||
|
preview = ", ".join(str(x) for x in startup_choices[:50])
|
||||||
|
_append_helper_log(f"[helper] startup store-choices count={len(startup_choices)} items={preview}")
|
||||||
|
|
||||||
|
# Publish to a cached property for Lua to read without IPC request.
|
||||||
|
try:
|
||||||
|
cached_json = json.dumps({"success": True, "choices": startup_choices}, ensure_ascii=False)
|
||||||
|
client.send_command_no_wait(["set_property_string", "user-data/medeia-store-choices-cached", cached_json])
|
||||||
|
_append_helper_log(f"[helper] published store-choices to user-data/medeia-store-choices-cached")
|
||||||
|
except Exception as exc:
|
||||||
|
_append_helper_log(f"[helper] failed to publish store-choices: {type(exc).__name__}: {exc}")
|
||||||
|
else:
|
||||||
|
_append_helper_log("[helper] startup store-choices unavailable")
|
||||||
|
except Exception as exc:
|
||||||
|
_append_helper_log(f"[helper] startup store-choices failed: {type(exc).__name__}: {exc}")
|
||||||
|
|
||||||
|
# Also publish config temp directory if available
|
||||||
|
try:
|
||||||
|
from config import load_config
|
||||||
|
cfg = load_config()
|
||||||
|
temp_dir = cfg.get("temp", "").strip() or os.getenv("TEMP") or "/tmp"
|
||||||
|
if temp_dir:
|
||||||
|
client.send_command_no_wait(["set_property_string", "user-data/medeia-config-temp", temp_dir])
|
||||||
|
_append_helper_log(f"[helper] published config temp to user-data/medeia-config-temp={temp_dir}")
|
||||||
|
except Exception as exc:
|
||||||
|
_append_helper_log(f"[helper] failed to publish config temp: {type(exc).__name__}: {exc}")
|
||||||
|
|
||||||
last_seen_id: Optional[str] = None
|
last_seen_id: Optional[str] = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -864,7 +966,7 @@ def main(argv: Optional[list[str]] = None) -> int:
|
|||||||
try:
|
try:
|
||||||
# IMPORTANT: don't wait for a response here; waiting would consume
|
# IMPORTANT: don't wait for a response here; waiting would consume
|
||||||
# async events and can drop/skip property-change notifications.
|
# async events and can drop/skip property-change notifications.
|
||||||
client.send_command_no_wait(["set_property", RESPONSE_PROP, json.dumps(resp, ensure_ascii=False)])
|
client.send_command_no_wait(["set_property_string", RESPONSE_PROP, json.dumps(resp, ensure_ascii=False)])
|
||||||
except Exception:
|
except Exception:
|
||||||
# If posting results fails, there's nothing more useful to do.
|
# If posting results fails, there's nothing more useful to do.
|
||||||
pass
|
pass
|
||||||
|
|||||||
2
MPV/portable_config/script-opts/medeia.conf
Normal file
2
MPV/portable_config/script-opts/medeia.conf
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Medeia MPV script options
|
||||||
|
store=video
|
||||||
@@ -84,7 +84,7 @@ progress_line_width=20
|
|||||||
# fullscreen = cycle:crop_free:fullscreen:no/yes=fullscreen_exit!?Fullscreen
|
# fullscreen = cycle:crop_free:fullscreen:no/yes=fullscreen_exit!?Fullscreen
|
||||||
# loop-playlist = cycle:repeat:loop-playlist:no/inf!?Loop playlist
|
# loop-playlist = cycle:repeat:loop-playlist:no/inf!?Loop playlist
|
||||||
# toggle:{icon}:{prop} = cycle:{icon}:{prop}:no/yes!
|
# toggle:{icon}:{prop} = cycle:{icon}:{prop}:no/yes!
|
||||||
controls=menu,gap,<video,audio>subtitles,<has_many_audio>audio,<has_many_video>video,<has_many_edition>editions,gap,shuffle,gap,prev,items,next,space,command:photo_camera:script-message medeia-image-screenshot?Screenshot,command:content_cut:script-message medeia-image-clip?Clip Marker,command:headset:script-message medeia-audio-only?Audio
|
controls=menu,gap,<video,audio>subtitles,<has_many_audio>audio,<has_many_video>video,<has_many_edition>editions,gap,shuffle,gap,prev,items,next,space,command:photo_camera:script-message medeia-image-screenshot?Screenshot,command:content_cut:script-message medeia-image-clip?Clip Marker,command:headset:script-message medeia-audio-only?Audio,command:store:script-message medeia-store-picker?Store
|
||||||
controls_size=32
|
controls_size=32
|
||||||
controls_margin=8
|
controls_margin=8
|
||||||
controls_spacing=2
|
controls_spacing=2
|
||||||
|
|||||||
@@ -145,6 +145,20 @@ function Controls:init_options()
|
|||||||
})
|
})
|
||||||
table_assign(control, {element = element, sizing = 'static', scale = 1, ratio = 1})
|
table_assign(control, {element = element, sizing = 'static', scale = 1, ratio = 1})
|
||||||
if badge then self:register_badge_updater(badge, element) end
|
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
|
end
|
||||||
elseif kind == 'cycle' then
|
elseif kind == 'cycle' then
|
||||||
if #params ~= 3 then
|
if #params ~= 3 then
|
||||||
|
|||||||
@@ -156,12 +156,34 @@ def match_provider_name_for_url(url: str) -> Optional[str]:
|
|||||||
try:
|
try:
|
||||||
parsed = urlparse(str(url))
|
parsed = urlparse(str(url))
|
||||||
host = (parsed.hostname or "").strip().lower()
|
host = (parsed.hostname or "").strip().lower()
|
||||||
|
path = (parsed.path or "").strip()
|
||||||
except Exception:
|
except Exception:
|
||||||
host = ""
|
host = ""
|
||||||
|
path = ""
|
||||||
|
|
||||||
if not host:
|
if not host:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# Prefer Internet Archive for archive.org links unless the URL clearly refers
|
||||||
|
# to a borrow/loan flow (handled by OpenLibrary provider).
|
||||||
|
#
|
||||||
|
# This keeps direct downloads and item pages routed to `internetarchive`, while
|
||||||
|
# preserving OpenLibrary's scripted borrow pipeline for loan/reader URLs.
|
||||||
|
if host == "openlibrary.org" or host.endswith(".openlibrary.org"):
|
||||||
|
return "openlibrary" if "openlibrary" in _PROVIDERS else None
|
||||||
|
|
||||||
|
if host == "archive.org" or host.endswith(".archive.org"):
|
||||||
|
low_path = str(path or "").lower()
|
||||||
|
is_borrowish = (
|
||||||
|
low_path.startswith("/borrow/")
|
||||||
|
or low_path.startswith("/stream/")
|
||||||
|
or low_path.startswith("/services/loans/")
|
||||||
|
or "/services/loans/" in low_path
|
||||||
|
)
|
||||||
|
if is_borrowish:
|
||||||
|
return "openlibrary" if "openlibrary" in _PROVIDERS else None
|
||||||
|
return "internetarchive" if "internetarchive" in _PROVIDERS else None
|
||||||
|
|
||||||
for name, provider_class in _PROVIDERS.items():
|
for name, provider_class in _PROVIDERS.items():
|
||||||
domains = getattr(provider_class, "URL_DOMAINS", None)
|
domains = getattr(provider_class, "URL_DOMAINS", None)
|
||||||
if not isinstance(domains, (list, tuple)):
|
if not isinstance(domains, (list, tuple)):
|
||||||
|
|||||||
Reference in New Issue
Block a user