2025-11-27 10:59:01 -08:00
|
|
|
local mp = require 'mp'
|
|
|
|
|
local utils = require 'mp.utils'
|
|
|
|
|
local msg = require 'mp.msg'
|
|
|
|
|
|
|
|
|
|
local M = {}
|
|
|
|
|
|
2025-12-18 22:50:21 -08:00
|
|
|
local MEDEIA_LUA_VERSION = '2025-12-18'
|
|
|
|
|
|
|
|
|
|
-- Track whether uosc is available so menu calls don't fail with
|
|
|
|
|
-- "Can't find script 'uosc' to send message to."
|
|
|
|
|
local _uosc_loaded = false
|
|
|
|
|
|
|
|
|
|
mp.register_script_message('uosc-version', function(_ver)
|
|
|
|
|
_uosc_loaded = true
|
|
|
|
|
end)
|
|
|
|
|
|
|
|
|
|
local function _is_script_loaded(name)
|
|
|
|
|
local ok, list = pcall(mp.get_property_native, 'script-list')
|
|
|
|
|
if not ok or type(list) ~= 'table' then
|
|
|
|
|
return false
|
|
|
|
|
end
|
|
|
|
|
for _, s in ipairs(list) do
|
|
|
|
|
if type(s) == 'table' then
|
|
|
|
|
local n = s.name or ''
|
|
|
|
|
if n == name or tostring(n):match('^' .. name .. '%d*$') then
|
|
|
|
|
return true
|
|
|
|
|
end
|
|
|
|
|
elseif type(s) == 'string' then
|
|
|
|
|
local n = s
|
|
|
|
|
if n == name or tostring(n):match('^' .. name .. '%d*$') then
|
|
|
|
|
return true
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
return false
|
|
|
|
|
end
|
|
|
|
|
|
2025-12-17 17:42:46 -08:00
|
|
|
local LOAD_URL_MENU_TYPE = 'medios_load_url'
|
|
|
|
|
|
2025-12-18 22:50:21 -08:00
|
|
|
local DOWNLOAD_FORMAT_MENU_TYPE = 'medios_download_pick_format'
|
|
|
|
|
local DOWNLOAD_STORE_MENU_TYPE = 'medios_download_pick_store'
|
|
|
|
|
|
2025-12-17 17:42:46 -08:00
|
|
|
local PIPELINE_REQ_PROP = 'user-data/medeia-pipeline-request'
|
|
|
|
|
local PIPELINE_RESP_PROP = 'user-data/medeia-pipeline-response'
|
|
|
|
|
local PIPELINE_READY_PROP = 'user-data/medeia-pipeline-ready'
|
|
|
|
|
|
2025-12-18 22:50:21 -08:00
|
|
|
-- Dedicated Lua log (next to mpv log-file) because mp.msg output is not always
|
|
|
|
|
-- included in --log-file depending on msg-level and build.
|
|
|
|
|
local function _lua_log(text)
|
|
|
|
|
local payload = (text and tostring(text) or '')
|
|
|
|
|
if payload == '' then
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
local dir = ''
|
|
|
|
|
|
|
|
|
|
-- Prefer repo-root Log/ for consistency with Python helper logs.
|
|
|
|
|
do
|
|
|
|
|
local function find_up(start_dir, relative_path, max_levels)
|
|
|
|
|
local d = start_dir
|
|
|
|
|
local levels = max_levels or 6
|
|
|
|
|
for _ = 0, levels do
|
|
|
|
|
if d and d ~= '' then
|
|
|
|
|
local candidate = d .. '/' .. relative_path
|
|
|
|
|
if utils.file_info(candidate) then
|
|
|
|
|
return candidate
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
local parent = d and d:match('(.*)[/\\]') or nil
|
|
|
|
|
if not parent or parent == d or parent == '' then
|
|
|
|
|
break
|
|
|
|
|
end
|
|
|
|
|
d = parent
|
|
|
|
|
end
|
|
|
|
|
return nil
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local base = mp.get_script_directory() or utils.getcwd() or ''
|
|
|
|
|
if base ~= '' then
|
|
|
|
|
local cli = find_up(base, 'CLI.py', 8)
|
|
|
|
|
if cli and cli ~= '' then
|
|
|
|
|
local root = cli:match('(.*)[/\\]') or ''
|
|
|
|
|
if root ~= '' then
|
|
|
|
|
dir = utils.join_path(root, 'Log')
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- Fallback: next to mpv --log-file.
|
|
|
|
|
if dir == '' then
|
|
|
|
|
local log_file = mp.get_property('options/log-file') or ''
|
|
|
|
|
dir = log_file:match('(.*)[/\\]') or ''
|
|
|
|
|
end
|
|
|
|
|
if dir == '' then
|
|
|
|
|
dir = mp.get_script_directory() or utils.getcwd() or ''
|
|
|
|
|
end
|
|
|
|
|
if dir == '' then
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
local path = utils.join_path(dir, 'medeia-mpv-lua.log')
|
|
|
|
|
local fh = io.open(path, 'a')
|
|
|
|
|
if not fh then
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
local line = '[' .. os.date('%Y-%m-%d %H:%M:%S') .. '] ' .. payload
|
|
|
|
|
fh:write(line .. '\n')
|
|
|
|
|
fh:close()
|
|
|
|
|
|
|
|
|
|
-- Also mirror Lua-side debug into the Python helper log file so there's one
|
|
|
|
|
-- place to look when diagnosing mpv↔python IPC issues.
|
|
|
|
|
do
|
|
|
|
|
local helper_path = utils.join_path(dir, 'medeia-mpv-helper.log')
|
|
|
|
|
local fh2 = io.open(helper_path, 'a')
|
|
|
|
|
if fh2 then
|
|
|
|
|
fh2:write('[lua] ' .. line .. '\n')
|
|
|
|
|
fh2:close()
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local function ensure_uosc_loaded()
|
|
|
|
|
if _uosc_loaded or _is_script_loaded('uosc') then
|
|
|
|
|
_uosc_loaded = true
|
|
|
|
|
return true
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local entry = nil
|
|
|
|
|
pcall(function()
|
|
|
|
|
entry = mp.find_config_file('scripts/uosc.lua')
|
|
|
|
|
end)
|
|
|
|
|
if not entry or entry == '' then
|
|
|
|
|
_lua_log('uosc entry not found at scripts/uosc.lua under config-dir')
|
|
|
|
|
return false
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local ok = pcall(mp.commandv, 'load-script', entry)
|
|
|
|
|
if ok then
|
|
|
|
|
_lua_log('Loaded uosc from: ' .. tostring(entry))
|
|
|
|
|
else
|
|
|
|
|
_lua_log('Failed to load uosc from: ' .. tostring(entry))
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- uosc will broadcast uosc-version on load; also re-check script-list if available.
|
|
|
|
|
if _is_script_loaded('uosc') then
|
|
|
|
|
_uosc_loaded = true
|
|
|
|
|
return true
|
|
|
|
|
end
|
|
|
|
|
return _uosc_loaded
|
|
|
|
|
end
|
|
|
|
|
|
2025-12-17 17:42:46 -08:00
|
|
|
local function write_temp_log(prefix, text)
|
|
|
|
|
if not text or text == '' then
|
|
|
|
|
return nil
|
|
|
|
|
end
|
2025-12-18 22:50:21 -08:00
|
|
|
|
|
|
|
|
local dir = ''
|
|
|
|
|
-- Prefer repo-root Log/ for easier discovery.
|
|
|
|
|
do
|
|
|
|
|
local function find_up(start_dir, relative_path, max_levels)
|
|
|
|
|
local d = start_dir
|
|
|
|
|
local levels = max_levels or 6
|
|
|
|
|
for _ = 0, levels do
|
|
|
|
|
if d and d ~= '' then
|
|
|
|
|
local candidate = d .. '/' .. relative_path
|
|
|
|
|
if utils.file_info(candidate) then
|
|
|
|
|
return candidate
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
local parent = d and d:match('(.*)[/\\]') or nil
|
|
|
|
|
if not parent or parent == d or parent == '' then
|
|
|
|
|
break
|
|
|
|
|
end
|
|
|
|
|
d = parent
|
|
|
|
|
end
|
|
|
|
|
return nil
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local base = mp.get_script_directory() or utils.getcwd() or ''
|
|
|
|
|
if base ~= '' then
|
|
|
|
|
local cli = find_up(base, 'CLI.py', 6)
|
|
|
|
|
if cli and cli ~= '' then
|
|
|
|
|
local parent = cli:match('(.*)[/\\]') or ''
|
|
|
|
|
if parent ~= '' then
|
|
|
|
|
dir = utils.join_path(parent, 'Log')
|
|
|
|
|
-- Best-effort create dir.
|
|
|
|
|
local sep = package and package.config and package.config:sub(1, 1) or '/'
|
|
|
|
|
if sep == '\\' then
|
|
|
|
|
pcall(utils.subprocess, { args = { 'cmd.exe', '/c', 'mkdir "' .. dir .. '" 1>nul 2>nul' } })
|
|
|
|
|
else
|
|
|
|
|
pcall(utils.subprocess, { args = { 'sh', '-lc', 'mkdir -p ' .. string.format('%q', dir) .. ' >/dev/null 2>&1' } })
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
if dir == '' then
|
|
|
|
|
dir = os.getenv('TEMP') or os.getenv('TMP') or utils.getcwd() or ''
|
|
|
|
|
end
|
2025-12-17 17:42:46 -08:00
|
|
|
if dir == '' then
|
|
|
|
|
return nil
|
|
|
|
|
end
|
|
|
|
|
local name = (prefix or 'medeia-mpv') .. '-' .. tostring(math.floor(mp.get_time() * 1000)) .. '.log'
|
|
|
|
|
local path = utils.join_path(dir, name)
|
|
|
|
|
local fh = io.open(path, 'w')
|
|
|
|
|
if not fh then
|
|
|
|
|
return nil
|
|
|
|
|
end
|
|
|
|
|
fh:write(text)
|
|
|
|
|
fh:close()
|
|
|
|
|
return path
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local function trim(s)
|
|
|
|
|
return (s:gsub('^%s+', ''):gsub('%s+$', ''))
|
|
|
|
|
end
|
|
|
|
|
|
2025-12-12 21:55:38 -08:00
|
|
|
-- Lyrics overlay toggle
|
|
|
|
|
-- The Python helper (python -m MPV.lyric) will read this property via IPC.
|
|
|
|
|
local LYRIC_VISIBLE_PROP = "user-data/medeia-lyric-visible"
|
|
|
|
|
|
|
|
|
|
local function lyric_get_visible()
|
|
|
|
|
local ok, v = pcall(mp.get_property_native, LYRIC_VISIBLE_PROP)
|
|
|
|
|
if not ok or v == nil then
|
|
|
|
|
return true
|
|
|
|
|
end
|
|
|
|
|
return v and true or false
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local function lyric_set_visible(v)
|
|
|
|
|
pcall(mp.set_property_native, LYRIC_VISIBLE_PROP, v and true or false)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local function lyric_toggle()
|
|
|
|
|
local now = not lyric_get_visible()
|
|
|
|
|
lyric_set_visible(now)
|
|
|
|
|
mp.osd_message("Lyrics: " .. (now and "on" or "off"), 1)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- Default to visible unless user overrides.
|
|
|
|
|
lyric_set_visible(true)
|
|
|
|
|
|
2025-11-27 10:59:01 -08:00
|
|
|
-- Configuration
|
|
|
|
|
local opts = {
|
|
|
|
|
python_path = "python",
|
|
|
|
|
cli_path = nil -- Will be auto-detected if nil
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-17 17:42:46 -08:00
|
|
|
local function find_file_upwards(start_dir, relative_path, max_levels)
|
|
|
|
|
local dir = start_dir
|
|
|
|
|
local levels = max_levels or 6
|
|
|
|
|
for _ = 0, levels do
|
|
|
|
|
if dir and dir ~= "" then
|
|
|
|
|
local candidate = dir .. "/" .. relative_path
|
|
|
|
|
if utils.file_info(candidate) then
|
|
|
|
|
return candidate
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
local parent = dir and dir:match("(.*)[/\\]") or nil
|
|
|
|
|
if not parent or parent == dir or parent == "" then
|
|
|
|
|
break
|
|
|
|
|
end
|
|
|
|
|
dir = parent
|
|
|
|
|
end
|
|
|
|
|
return nil
|
|
|
|
|
end
|
|
|
|
|
|
2025-12-18 22:50:21 -08:00
|
|
|
local _cached_store_names = {}
|
|
|
|
|
local _store_cache_loaded = false
|
|
|
|
|
|
2025-12-17 17:42:46 -08:00
|
|
|
local _pipeline_helper_started = false
|
|
|
|
|
|
2025-12-18 22:50:21 -08:00
|
|
|
local function _is_pipeline_helper_ready()
|
|
|
|
|
local ready = mp.get_property_native(PIPELINE_READY_PROP)
|
|
|
|
|
if not ready then
|
|
|
|
|
return false
|
|
|
|
|
end
|
|
|
|
|
local s = tostring(ready)
|
|
|
|
|
if s == '' or s == '0' then
|
|
|
|
|
return false
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- Back-compat: older helpers may set "1". New helpers set unix timestamps.
|
|
|
|
|
local n = tonumber(s)
|
|
|
|
|
if n and n > 1000000000 then
|
|
|
|
|
local now = (os and os.time) and os.time() or nil
|
|
|
|
|
if not now then
|
|
|
|
|
return true
|
|
|
|
|
end
|
|
|
|
|
local age = now - n
|
|
|
|
|
if age < 0 then
|
|
|
|
|
age = 0
|
|
|
|
|
end
|
|
|
|
|
return age <= 10
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
end
|
|
|
|
|
|
2025-12-17 17:42:46 -08:00
|
|
|
local function get_mpv_ipc_path()
|
|
|
|
|
local ipc = mp.get_property('input-ipc-server')
|
|
|
|
|
if ipc and ipc ~= '' then
|
|
|
|
|
return ipc
|
|
|
|
|
end
|
|
|
|
|
-- Fallback: fixed pipe/socket name used by MPV/mpv_ipc.py
|
|
|
|
|
local sep = package and package.config and package.config:sub(1, 1) or '/'
|
|
|
|
|
if sep == '\\' then
|
|
|
|
|
return '\\\\.\\pipe\\mpv-medeia-macina'
|
|
|
|
|
end
|
|
|
|
|
return '/tmp/mpv-medeia-macina.sock'
|
|
|
|
|
end
|
|
|
|
|
|
2025-12-18 22:50:21 -08:00
|
|
|
local function ensure_mpv_ipc_server()
|
|
|
|
|
-- `.pipe -play` (Python) controls MPV via JSON IPC. If mpv was started
|
|
|
|
|
-- without --input-ipc-server, make sure we set one so the running instance
|
|
|
|
|
-- can be controlled (instead of Python spawning a separate mpv).
|
|
|
|
|
local ipc = mp.get_property('input-ipc-server')
|
|
|
|
|
if ipc and ipc ~= '' then
|
2025-12-17 17:42:46 -08:00
|
|
|
return true
|
|
|
|
|
end
|
2025-12-18 22:50:21 -08:00
|
|
|
|
|
|
|
|
local desired = get_mpv_ipc_path()
|
|
|
|
|
if not desired or desired == '' then
|
|
|
|
|
return false
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local ok = pcall(mp.set_property, 'input-ipc-server', desired)
|
|
|
|
|
if not ok then
|
|
|
|
|
return false
|
|
|
|
|
end
|
|
|
|
|
local now = mp.get_property('input-ipc-server')
|
|
|
|
|
return (now and now ~= '') and true or false
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local function quote_pipeline_arg(s)
|
|
|
|
|
-- Ensure URLs with special characters (e.g. &, #) survive pipeline parsing.
|
|
|
|
|
s = tostring(s or '')
|
|
|
|
|
s = s:gsub('\\', '\\\\'):gsub('"', '\\"')
|
|
|
|
|
return '"' .. s .. '"'
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local function _is_windows()
|
|
|
|
|
local sep = package and package.config and package.config:sub(1, 1) or '/'
|
|
|
|
|
return sep == '\\'
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local function _extract_target_from_memory_uri(text)
|
|
|
|
|
if type(text) ~= 'string' then
|
|
|
|
|
return nil
|
|
|
|
|
end
|
|
|
|
|
if not text:match('^memory://') then
|
|
|
|
|
return nil
|
|
|
|
|
end
|
|
|
|
|
for line in text:gmatch('[^\r\n]+') do
|
|
|
|
|
line = trim(line)
|
|
|
|
|
if line ~= '' and not line:match('^#') and not line:match('^memory://') then
|
|
|
|
|
return line
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
return nil
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local function _percent_decode(s)
|
|
|
|
|
if type(s) ~= 'string' then
|
|
|
|
|
return s
|
|
|
|
|
end
|
|
|
|
|
return (s:gsub('%%(%x%x)', function(hex)
|
|
|
|
|
return string.char(tonumber(hex, 16))
|
|
|
|
|
end))
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local function _extract_query_param(url, key)
|
|
|
|
|
if type(url) ~= 'string' then
|
|
|
|
|
return nil
|
|
|
|
|
end
|
|
|
|
|
key = tostring(key or '')
|
|
|
|
|
if key == '' then
|
|
|
|
|
return nil
|
|
|
|
|
end
|
|
|
|
|
local pattern = '[?&]' .. key:gsub('([^%w])', '%%%1') .. '=([^&#]+)'
|
|
|
|
|
local v = url:match(pattern)
|
|
|
|
|
if v then
|
|
|
|
|
return _percent_decode(v)
|
|
|
|
|
end
|
|
|
|
|
return nil
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local function _current_target()
|
|
|
|
|
local path = mp.get_property('path')
|
|
|
|
|
if not path or path == '' then
|
|
|
|
|
return nil
|
|
|
|
|
end
|
|
|
|
|
local mem = _extract_target_from_memory_uri(path)
|
|
|
|
|
if mem and mem ~= '' then
|
|
|
|
|
return mem
|
|
|
|
|
end
|
|
|
|
|
return path
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local function _extract_store_hash(target)
|
|
|
|
|
if type(target) ~= 'string' or target == '' then
|
|
|
|
|
return nil
|
|
|
|
|
end
|
|
|
|
|
local hash = _extract_query_param(target, 'hash')
|
|
|
|
|
local store = _extract_query_param(target, 'store')
|
|
|
|
|
if hash and store then
|
|
|
|
|
local h = tostring(hash):lower()
|
|
|
|
|
if h:match('^[0-9a-f]+$') and #h == 64 then
|
|
|
|
|
return { store = tostring(store), hash = h }
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
return nil
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local function _pick_folder_windows()
|
|
|
|
|
-- Native folder picker via PowerShell + WinForms.
|
|
|
|
|
local ps = [[Add-Type -AssemblyName System.Windows.Forms; $d = New-Object System.Windows.Forms.FolderBrowserDialog; $d.Description = 'Select download folder'; $d.ShowNewFolderButton = $true; if ($d.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { $d.SelectedPath }]]
|
|
|
|
|
local res = utils.subprocess({
|
|
|
|
|
args = { 'powershell', '-NoProfile', '-STA', '-ExecutionPolicy', 'Bypass', '-Command', ps },
|
|
|
|
|
cancellable = false,
|
|
|
|
|
})
|
|
|
|
|
if res and res.status == 0 and res.stdout then
|
|
|
|
|
local out = trim(tostring(res.stdout))
|
|
|
|
|
if out ~= '' then
|
|
|
|
|
return out
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
return nil
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- Forward declaration: used by run_pipeline_via_ipc_response before definition.
|
|
|
|
|
local ensure_pipeline_helper_running
|
|
|
|
|
|
|
|
|
|
local function _run_helper_request_response(req, timeout_seconds)
|
|
|
|
|
if not ensure_pipeline_helper_running() then
|
|
|
|
|
_lua_log('ipc: helper not running; cannot execute request')
|
|
|
|
|
return nil
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
do
|
|
|
|
|
local deadline = mp.get_time() + 3.0
|
|
|
|
|
while mp.get_time() < deadline do
|
|
|
|
|
if _is_pipeline_helper_ready() then
|
|
|
|
|
break
|
|
|
|
|
end
|
|
|
|
|
mp.wait_event(0.05)
|
|
|
|
|
end
|
|
|
|
|
if not _is_pipeline_helper_ready() then
|
|
|
|
|
_lua_log('ipc: helper not ready; ready=' .. tostring(mp.get_property_native(PIPELINE_READY_PROP)))
|
|
|
|
|
_pipeline_helper_started = false
|
|
|
|
|
return nil
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if type(req) ~= 'table' then
|
|
|
|
|
return nil
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local id = tostring(req.id or '')
|
|
|
|
|
if id == '' then
|
|
|
|
|
id = tostring(math.floor(mp.get_time() * 1000)) .. '-' .. tostring(math.random(100000, 999999))
|
|
|
|
|
req.id = id
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local label = ''
|
|
|
|
|
if req.op then
|
|
|
|
|
label = 'op=' .. tostring(req.op)
|
|
|
|
|
elseif req.pipeline then
|
|
|
|
|
label = 'cmd=' .. tostring(req.pipeline)
|
|
|
|
|
else
|
|
|
|
|
label = '(unknown)'
|
|
|
|
|
end
|
|
|
|
|
_lua_log('ipc: send request id=' .. tostring(id) .. ' ' .. label)
|
|
|
|
|
|
|
|
|
|
mp.set_property(PIPELINE_RESP_PROP, '')
|
|
|
|
|
mp.set_property(PIPELINE_REQ_PROP, utils.format_json(req))
|
|
|
|
|
|
|
|
|
|
local deadline = mp.get_time() + (timeout_seconds or 5)
|
|
|
|
|
while mp.get_time() < deadline do
|
|
|
|
|
local resp_json = mp.get_property(PIPELINE_RESP_PROP)
|
|
|
|
|
if resp_json and resp_json ~= '' then
|
|
|
|
|
local ok, resp = pcall(utils.parse_json, resp_json)
|
|
|
|
|
if ok and resp and resp.id == id then
|
|
|
|
|
_lua_log('ipc: got response id=' .. tostring(id) .. ' success=' .. tostring(resp.success))
|
|
|
|
|
return resp
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
mp.wait_event(0.05)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
_lua_log('ipc: timeout waiting response; ' .. label)
|
|
|
|
|
_pipeline_helper_started = false
|
|
|
|
|
return nil
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- IPC helper: return the whole response object (stdout/stderr/error/table)
|
|
|
|
|
local function run_pipeline_via_ipc_response(pipeline_cmd, seeds, timeout_seconds)
|
|
|
|
|
local req = { pipeline = pipeline_cmd }
|
|
|
|
|
if seeds then
|
|
|
|
|
req.seeds = seeds
|
|
|
|
|
end
|
|
|
|
|
return _run_helper_request_response(req, timeout_seconds)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local function _refresh_store_cache(timeout_seconds)
|
|
|
|
|
ensure_mpv_ipc_server()
|
|
|
|
|
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; stderr=' .. tostring(resp and resp.stderr or '') .. ' error=' .. tostring(resp and resp.error or ''))
|
|
|
|
|
|
|
|
|
|
-- Fallback: directly call Python to import CLI.get_store_choices().
|
|
|
|
|
-- This avoids helper IPC issues and still stays in sync with the REPL.
|
|
|
|
|
local python = (opts and opts.python_path) and tostring(opts.python_path) or 'python'
|
|
|
|
|
local cli_path = (opts and opts.cli_path) and tostring(opts.cli_path) or nil
|
|
|
|
|
if not cli_path or cli_path == '' or not utils.file_info(cli_path) then
|
|
|
|
|
local base_dir = mp.get_script_directory() or utils.getcwd() or ''
|
|
|
|
|
if base_dir ~= '' then
|
|
|
|
|
cli_path = find_file_upwards(base_dir, 'CLI.py', 8)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if cli_path and cli_path ~= '' then
|
|
|
|
|
local root = tostring(cli_path):match('(.*)[/\\]') or ''
|
|
|
|
|
if root ~= '' then
|
|
|
|
|
local code = "import json, sys; sys.path.insert(0, r'" .. root .. "'); from CLI import get_store_choices; print(json.dumps(get_store_choices()))"
|
|
|
|
|
local res = utils.subprocess({
|
|
|
|
|
args = { python, '-c', code },
|
|
|
|
|
cancellable = false,
|
|
|
|
|
})
|
|
|
|
|
if res and res.status == 0 and res.stdout then
|
|
|
|
|
local out_text = tostring(res.stdout)
|
|
|
|
|
local last_line = ''
|
|
|
|
|
for line in out_text:gmatch('[^\r\n]+') do
|
|
|
|
|
if trim(line) ~= '' then
|
|
|
|
|
last_line = line
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
local ok, parsed = pcall(utils.parse_json, last_line ~= '' and last_line or out_text)
|
|
|
|
|
if ok and type(parsed) == 'table' then
|
|
|
|
|
local out = {}
|
|
|
|
|
for _, v in ipairs(parsed) do
|
|
|
|
|
local name = trim(tostring(v or ''))
|
|
|
|
|
if name ~= '' then
|
|
|
|
|
out[#out + 1] = name
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
if #out > 0 then
|
|
|
|
|
_cached_store_names = out
|
|
|
|
|
_store_cache_loaded = true
|
|
|
|
|
_lua_log('stores: loaded ' .. tostring(#_cached_store_names) .. ' stores via python fallback')
|
|
|
|
|
return true
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
else
|
|
|
|
|
_lua_log('stores: python fallback failed; status=' .. tostring(res and res.status or 'nil') .. ' stderr=' .. tostring(res and res.stderr or ''))
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
_lua_log('stores: loaded ' .. tostring(#_cached_store_names) .. ' stores via helper')
|
|
|
|
|
return true
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local function _uosc_open_list_picker(menu_type, title, items)
|
|
|
|
|
local menu_data = {
|
|
|
|
|
type = menu_type,
|
|
|
|
|
title = title,
|
|
|
|
|
items = items or {},
|
|
|
|
|
}
|
|
|
|
|
if ensure_uosc_loaded() then
|
|
|
|
|
mp.commandv('script-message-to', 'uosc', 'open-menu', utils.format_json(menu_data))
|
|
|
|
|
else
|
|
|
|
|
_lua_log('menu: uosc not available; cannot open-menu')
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- No-op handler for placeholder menu items.
|
|
|
|
|
mp.register_script_message('medios-nop', function()
|
|
|
|
|
return
|
|
|
|
|
end)
|
|
|
|
|
|
|
|
|
|
local _pending_download = nil
|
|
|
|
|
local _pending_format_change = nil
|
|
|
|
|
|
|
|
|
|
-- Cache yt-dlp format lists per URL so Change Format is instant.
|
|
|
|
|
M.file = M.file or {}
|
|
|
|
|
M.file.formats_table = nil
|
|
|
|
|
M.file.url = nil
|
|
|
|
|
local _formats_cache = {}
|
|
|
|
|
local _formats_inflight = {}
|
|
|
|
|
|
|
|
|
|
local function _is_http_url(u)
|
|
|
|
|
if type(u) ~= 'string' then
|
|
|
|
|
return false
|
|
|
|
|
end
|
|
|
|
|
return u:match('^https?://') ~= nil
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local function _cache_formats_for_url(url, tbl)
|
|
|
|
|
if type(url) ~= 'string' or url == '' then
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
if type(tbl) ~= 'table' then
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
_formats_cache[url] = { table = tbl, ts = mp.get_time() }
|
|
|
|
|
M.file.url = url
|
|
|
|
|
M.file.formats_table = tbl
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local function _get_cached_formats_table(url)
|
|
|
|
|
if type(url) ~= 'string' or url == '' then
|
|
|
|
|
return nil
|
|
|
|
|
end
|
|
|
|
|
local hit = _formats_cache[url]
|
|
|
|
|
if type(hit) == 'table' and type(hit.table) == 'table' then
|
|
|
|
|
return hit.table
|
|
|
|
|
end
|
|
|
|
|
return nil
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local function _prefetch_formats_for_url(url)
|
|
|
|
|
url = tostring(url or '')
|
|
|
|
|
if url == '' or not _is_http_url(url) then
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- Only applies to plain URLs (not store hash URLs).
|
|
|
|
|
if _extract_store_hash(url) then
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if _get_cached_formats_table(url) then
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
if _formats_inflight[url] then
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
_formats_inflight[url] = true
|
|
|
|
|
|
|
|
|
|
mp.add_timeout(0.01, function()
|
|
|
|
|
if _get_cached_formats_table(url) then
|
|
|
|
|
_formats_inflight[url] = nil
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
ensure_mpv_ipc_server()
|
|
|
|
|
local resp = _run_helper_request_response({ op = 'ytdlp-formats', data = { url = url } }, 20)
|
|
|
|
|
_formats_inflight[url] = nil
|
|
|
|
|
|
|
|
|
|
if resp and resp.success and type(resp.table) == 'table' then
|
|
|
|
|
_cache_formats_for_url(url, resp.table)
|
|
|
|
|
_lua_log('formats: cached ' .. tostring((resp.table.rows and #resp.table.rows) or 0) .. ' rows for url')
|
|
|
|
|
end
|
|
|
|
|
end)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local function _open_loading_formats_menu(title)
|
|
|
|
|
_uosc_open_list_picker(DOWNLOAD_FORMAT_MENU_TYPE, title or 'Pick format', {
|
|
|
|
|
{
|
|
|
|
|
title = 'Loading formats…',
|
|
|
|
|
hint = 'Fetching format list',
|
|
|
|
|
value = { 'script-message-to', mp.get_script_name(), 'medios-nop', '{}' },
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local function _current_ytdl_format_string()
|
|
|
|
|
-- Preferred: mpv exposes the active ytdl format string.
|
|
|
|
|
local fmt = trim(tostring(mp.get_property_native('ytdl-format') or ''))
|
|
|
|
|
if fmt ~= '' then
|
|
|
|
|
return fmt
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- Fallbacks: option value, or raw info if available.
|
|
|
|
|
local opt = trim(tostring(mp.get_property('options/ytdl-format') or ''))
|
|
|
|
|
if opt ~= '' then
|
|
|
|
|
return opt
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local raw = mp.get_property_native('ytdl-raw-info')
|
|
|
|
|
if type(raw) == 'table' then
|
|
|
|
|
if raw.format_id and tostring(raw.format_id) ~= '' then
|
|
|
|
|
return tostring(raw.format_id)
|
|
|
|
|
end
|
|
|
|
|
local rf = raw.requested_formats
|
|
|
|
|
if type(rf) == 'table' then
|
|
|
|
|
local parts = {}
|
|
|
|
|
for _, item in ipairs(rf) do
|
|
|
|
|
if type(item) == 'table' and item.format_id and tostring(item.format_id) ~= '' then
|
|
|
|
|
parts[#parts + 1] = tostring(item.format_id)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
if #parts >= 1 then
|
|
|
|
|
return table.concat(parts, '+')
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local function _run_pipeline_detached(pipeline_cmd)
|
|
|
|
|
if not pipeline_cmd or pipeline_cmd == '' then
|
|
|
|
|
return false
|
|
|
|
|
end
|
|
|
|
|
local python = (opts and opts.python_path) and tostring(opts.python_path) or 'python'
|
|
|
|
|
local cli = (opts and opts.cli_path) and tostring(opts.cli_path) or 'CLI.py'
|
|
|
|
|
local args = { python, cli, 'pipeline', '--pipeline', pipeline_cmd }
|
|
|
|
|
local ok = utils.subprocess_detached({ args = args })
|
|
|
|
|
return ok ~= nil
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local function _open_save_location_picker_for_pending_download()
|
|
|
|
|
if type(_pending_download) ~= 'table' or not _pending_download.url or not _pending_download.format then
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local function build_items()
|
|
|
|
|
local items = {
|
|
|
|
|
{
|
|
|
|
|
title = 'Pick folder…',
|
|
|
|
|
hint = 'Save to a local folder',
|
|
|
|
|
value = { 'script-message-to', mp.get_script_name(), 'medios-download-pick-path', '{}' },
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
value = { 'script-message-to', mp.get_script_name(), 'medios-download-pick-store', utils.format_json(payload) },
|
|
|
|
|
}
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
return items
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- Always open immediately with whatever store cache we have.
|
|
|
|
|
_uosc_open_list_picker(DOWNLOAD_STORE_MENU_TYPE, 'Save location', build_items())
|
|
|
|
|
|
|
|
|
|
-- Best-effort refresh; if it succeeds, reopen menu with stores.
|
|
|
|
|
mp.add_timeout(0.05, function()
|
|
|
|
|
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
|
|
|
|
|
_uosc_open_list_picker(DOWNLOAD_STORE_MENU_TYPE, 'Save location', build_items())
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- Prime store cache shortly after load (best-effort; picker also refreshes on-demand).
|
|
|
|
|
mp.add_timeout(0.10, function()
|
|
|
|
|
if not _store_cache_loaded then
|
|
|
|
|
pcall(_refresh_store_cache, 1.5)
|
|
|
|
|
end
|
|
|
|
|
end)
|
|
|
|
|
|
|
|
|
|
local function _apply_ytdl_format_and_reload(url, fmt)
|
|
|
|
|
if not url or url == '' or not fmt or fmt == '' then
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local pos = mp.get_property_number('time-pos')
|
|
|
|
|
local paused = mp.get_property_native('pause') and true or false
|
|
|
|
|
|
|
|
|
|
_lua_log('change-format: setting options/ytdl-format=' .. tostring(fmt))
|
|
|
|
|
pcall(mp.set_property, 'options/ytdl-format', tostring(fmt))
|
|
|
|
|
|
|
|
|
|
if pos and pos > 0 then
|
|
|
|
|
mp.commandv('loadfile', url, 'replace', 'start=' .. tostring(pos))
|
|
|
|
|
else
|
|
|
|
|
mp.commandv('loadfile', url, 'replace')
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if paused then
|
|
|
|
|
mp.set_property_native('pause', true)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local function _start_download_flow_for_current()
|
|
|
|
|
local target = _current_target()
|
|
|
|
|
if not target or target == '' then
|
|
|
|
|
mp.osd_message('No current item', 2)
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
_lua_log('download: current target=' .. tostring(target))
|
|
|
|
|
|
|
|
|
|
local store_hash = _extract_store_hash(target)
|
|
|
|
|
if store_hash then
|
|
|
|
|
if not _is_windows() then
|
|
|
|
|
mp.osd_message('Download folder picker is Windows-only', 4)
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
local folder = _pick_folder_windows()
|
|
|
|
|
if not folder or folder == '' then
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
ensure_mpv_ipc_server()
|
|
|
|
|
M.run_pipeline('get-file -store ' .. quote_pipeline_arg(store_hash.store) .. ' -hash ' .. store_hash.hash .. ' -path ' .. quote_pipeline_arg(folder))
|
|
|
|
|
mp.osd_message('Download started', 2)
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- Non-store URL flow: use the current yt-dlp-selected format and ask for save location.
|
|
|
|
|
local url = tostring(target)
|
|
|
|
|
local fmt = _current_ytdl_format_string()
|
|
|
|
|
|
|
|
|
|
if not fmt or fmt == '' then
|
|
|
|
|
_lua_log('download: could not determine current ytdl format string')
|
|
|
|
|
mp.osd_message('Cannot determine current format; use Change Format first', 5)
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
_lua_log('download: using current format=' .. tostring(fmt))
|
|
|
|
|
_pending_download = { url = url, format = fmt }
|
|
|
|
|
_open_save_location_picker_for_pending_download()
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
mp.register_script_message('medios-download-current', function()
|
|
|
|
|
_start_download_flow_for_current()
|
|
|
|
|
end)
|
|
|
|
|
|
|
|
|
|
mp.register_script_message('medios-change-format-current', function()
|
|
|
|
|
local target = _current_target()
|
|
|
|
|
if not target or target == '' then
|
|
|
|
|
mp.osd_message('No current item', 2)
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local store_hash = _extract_store_hash(target)
|
|
|
|
|
if store_hash then
|
|
|
|
|
mp.osd_message('Change Format is only for URL playback', 4)
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local url = tostring(target)
|
|
|
|
|
|
|
|
|
|
-- If formats were already prefetched for this URL, open instantly.
|
|
|
|
|
local cached_tbl = _get_cached_formats_table(url)
|
|
|
|
|
if type(cached_tbl) == 'table' and type(cached_tbl.rows) == 'table' and #cached_tbl.rows > 0 then
|
|
|
|
|
_pending_format_change = { url = url, token = 'cached', formats_table = cached_tbl }
|
|
|
|
|
|
|
|
|
|
local items = {}
|
|
|
|
|
for idx, row in ipairs(cached_tbl.rows) do
|
|
|
|
|
local cols = row.columns or {}
|
|
|
|
|
local id_val = ''
|
|
|
|
|
local res_val = ''
|
|
|
|
|
local ext_val = ''
|
|
|
|
|
local size_val = ''
|
|
|
|
|
for _, c in ipairs(cols) do
|
|
|
|
|
if c.name == 'ID' then id_val = tostring(c.value or '') end
|
|
|
|
|
if c.name == 'Resolution' then res_val = tostring(c.value or '') end
|
|
|
|
|
if c.name == 'Ext' then ext_val = tostring(c.value or '') end
|
|
|
|
|
if c.name == 'Size' then size_val = tostring(c.value or '') end
|
|
|
|
|
end
|
|
|
|
|
local label = id_val ~= '' and id_val or ('Format ' .. tostring(idx))
|
|
|
|
|
local hint_parts = {}
|
|
|
|
|
if res_val ~= '' and res_val ~= 'N/A' then table.insert(hint_parts, res_val) end
|
|
|
|
|
if ext_val ~= '' then table.insert(hint_parts, ext_val) end
|
|
|
|
|
if size_val ~= '' and size_val ~= 'N/A' then table.insert(hint_parts, size_val) end
|
|
|
|
|
local hint = table.concat(hint_parts, ' | ')
|
|
|
|
|
|
|
|
|
|
local payload = { index = idx }
|
|
|
|
|
items[#items + 1] = {
|
|
|
|
|
title = label,
|
|
|
|
|
hint = hint,
|
|
|
|
|
value = { 'script-message-to', mp.get_script_name(), 'medios-change-format-pick', utils.format_json(payload) },
|
|
|
|
|
}
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
_uosc_open_list_picker(DOWNLOAD_FORMAT_MENU_TYPE, 'Change format', items)
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local token = tostring(math.floor(mp.get_time() * 1000)) .. '-' .. tostring(math.random(100000, 999999))
|
|
|
|
|
_pending_format_change = { url = url, token = token }
|
|
|
|
|
_open_loading_formats_menu('Change format')
|
|
|
|
|
|
|
|
|
|
mp.add_timeout(0.05, function()
|
|
|
|
|
if type(_pending_format_change) ~= 'table' or _pending_format_change.token ~= token then
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
ensure_mpv_ipc_server()
|
|
|
|
|
_lua_log('change-format: requesting formats via helper op for url')
|
|
|
|
|
|
|
|
|
|
local resp = _run_helper_request_response({ op = 'ytdlp-formats', data = { url = url } }, 30)
|
|
|
|
|
if type(_pending_format_change) ~= 'table' or _pending_format_change.token ~= token then
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if not resp or not resp.success or type(resp.table) ~= 'table' then
|
|
|
|
|
local err = ''
|
|
|
|
|
if type(resp) == 'table' then
|
|
|
|
|
if resp.error and tostring(resp.error) ~= '' then err = tostring(resp.error) end
|
|
|
|
|
if resp.stderr and tostring(resp.stderr) ~= '' then
|
|
|
|
|
err = (err ~= '' and (err .. ' | ') or '') .. tostring(resp.stderr)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
_lua_log('change-format: formats failed: ' .. (err ~= '' and err or '(no details)'))
|
|
|
|
|
mp.osd_message('Failed to load format list', 5)
|
|
|
|
|
_uosc_open_list_picker(DOWNLOAD_FORMAT_MENU_TYPE, 'Change format', {
|
|
|
|
|
{
|
|
|
|
|
title = 'Failed to load format list',
|
|
|
|
|
hint = 'Check logs (medeia-mpv-lua.log / medeia-mpv-helper.log)',
|
|
|
|
|
value = { 'script-message-to', mp.get_script_name(), 'medios-nop', '{}' },
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local tbl = resp.table
|
|
|
|
|
if type(tbl.rows) ~= 'table' or #tbl.rows == 0 then
|
|
|
|
|
mp.osd_message('No formats available', 4)
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local items = {}
|
|
|
|
|
for idx, row in ipairs(tbl.rows) do
|
|
|
|
|
local cols = row.columns or {}
|
|
|
|
|
local id_val = ''
|
|
|
|
|
local res_val = ''
|
|
|
|
|
local ext_val = ''
|
|
|
|
|
local size_val = ''
|
|
|
|
|
for _, c in ipairs(cols) do
|
|
|
|
|
if c.name == 'ID' then id_val = tostring(c.value or '') end
|
|
|
|
|
if c.name == 'Resolution' then res_val = tostring(c.value or '') end
|
|
|
|
|
if c.name == 'Ext' then ext_val = tostring(c.value or '') end
|
|
|
|
|
if c.name == 'Size' then size_val = tostring(c.value or '') end
|
|
|
|
|
end
|
|
|
|
|
local label = id_val ~= '' and id_val or ('Format ' .. tostring(idx))
|
|
|
|
|
local hint_parts = {}
|
|
|
|
|
if res_val ~= '' and res_val ~= 'N/A' then table.insert(hint_parts, res_val) end
|
|
|
|
|
if ext_val ~= '' then table.insert(hint_parts, ext_val) end
|
|
|
|
|
if size_val ~= '' and size_val ~= 'N/A' then table.insert(hint_parts, size_val) end
|
|
|
|
|
local hint = table.concat(hint_parts, ' | ')
|
|
|
|
|
|
|
|
|
|
local payload = { index = idx }
|
|
|
|
|
items[#items + 1] = {
|
|
|
|
|
title = label,
|
|
|
|
|
hint = hint,
|
|
|
|
|
value = { 'script-message-to', mp.get_script_name(), 'medios-change-format-pick', utils.format_json(payload) },
|
|
|
|
|
}
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
_pending_format_change.formats_table = tbl
|
|
|
|
|
_cache_formats_for_url(url, tbl)
|
|
|
|
|
_uosc_open_list_picker(DOWNLOAD_FORMAT_MENU_TYPE, 'Change format', items)
|
|
|
|
|
end)
|
|
|
|
|
end)
|
|
|
|
|
|
|
|
|
|
-- Prefetch formats for yt-dlp-supported URLs on load so Change Format is instant.
|
|
|
|
|
mp.register_event('file-loaded', function()
|
|
|
|
|
local target = _current_target()
|
|
|
|
|
if not target or target == '' then
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
local url = tostring(target)
|
|
|
|
|
if not _is_http_url(url) then
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
_prefetch_formats_for_url(url)
|
|
|
|
|
end)
|
|
|
|
|
|
|
|
|
|
mp.register_script_message('medios-change-format-pick', function(json)
|
|
|
|
|
if type(_pending_format_change) ~= 'table' or not _pending_format_change.url then
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
local ok, ev = pcall(utils.parse_json, json)
|
|
|
|
|
if not ok or type(ev) ~= 'table' then
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
local idx = tonumber(ev.index or 0) or 0
|
|
|
|
|
if idx <= 0 then
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
local tbl = _pending_format_change.formats_table
|
|
|
|
|
if type(tbl) ~= 'table' or type(tbl.rows) ~= 'table' or not tbl.rows[idx] then
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
local row = tbl.rows[idx]
|
|
|
|
|
local sel = row.selection_args
|
|
|
|
|
local fmt = nil
|
|
|
|
|
if type(sel) == 'table' then
|
|
|
|
|
for i = 1, #sel do
|
|
|
|
|
if tostring(sel[i]) == '-format' and sel[i + 1] then
|
|
|
|
|
fmt = tostring(sel[i + 1])
|
|
|
|
|
break
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
if not fmt or fmt == '' then
|
|
|
|
|
mp.osd_message('Invalid format selection', 3)
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local url = tostring(_pending_format_change.url)
|
|
|
|
|
_pending_format_change = nil
|
|
|
|
|
_apply_ytdl_format_and_reload(url, fmt)
|
|
|
|
|
end)
|
|
|
|
|
|
|
|
|
|
mp.register_script_message('medios-download-pick-store', function(json)
|
|
|
|
|
if type(_pending_download) ~= 'table' or not _pending_download.url or not _pending_download.format then
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
local url = tostring(_pending_download.url)
|
|
|
|
|
local fmt = tostring(_pending_download.format)
|
|
|
|
|
|
|
|
|
|
local pipeline_cmd = 'download-media -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)
|
|
|
|
|
end
|
|
|
|
|
mp.osd_message('Download started', 3)
|
|
|
|
|
_pending_download = nil
|
|
|
|
|
end)
|
|
|
|
|
|
|
|
|
|
mp.register_script_message('medios-download-pick-path', function()
|
|
|
|
|
if type(_pending_download) ~= 'table' or not _pending_download.url or not _pending_download.format then
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
if not _is_windows() then
|
|
|
|
|
mp.osd_message('Folder picker is Windows-only', 4)
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local folder = _pick_folder_windows()
|
|
|
|
|
if not folder or folder == '' then
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local url = tostring(_pending_download.url)
|
|
|
|
|
local fmt = tostring(_pending_download.format)
|
|
|
|
|
|
|
|
|
|
local pipeline_cmd = 'download-media -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)
|
|
|
|
|
end
|
|
|
|
|
mp.osd_message('Download started', 3)
|
|
|
|
|
_pending_download = nil
|
|
|
|
|
end)
|
|
|
|
|
|
|
|
|
|
ensure_pipeline_helper_running = function()
|
|
|
|
|
-- If a helper is already running (e.g. started by the launcher), just use it.
|
|
|
|
|
if _is_pipeline_helper_ready() then
|
|
|
|
|
_pipeline_helper_started = true
|
2025-12-17 17:42:46 -08:00
|
|
|
return true
|
|
|
|
|
end
|
|
|
|
|
|
2025-12-18 22:50:21 -08:00
|
|
|
-- We tried to start a helper before but it isn't ready anymore; restart.
|
|
|
|
|
if _pipeline_helper_started then
|
|
|
|
|
_pipeline_helper_started = false
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local helper_path = nil
|
|
|
|
|
|
|
|
|
|
-- Prefer deriving repo root from located CLI.py if available.
|
|
|
|
|
if opts and opts.cli_path and utils.file_info(opts.cli_path) then
|
|
|
|
|
local root = tostring(opts.cli_path):match('(.*)[/\\]') or ''
|
|
|
|
|
if root ~= '' then
|
|
|
|
|
local candidate = utils.join_path(root, 'MPV/pipeline_helper.py')
|
|
|
|
|
if utils.file_info(candidate) then
|
|
|
|
|
helper_path = candidate
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if not helper_path then
|
|
|
|
|
local base_dir = mp.get_script_directory() or ""
|
|
|
|
|
if base_dir == "" then
|
|
|
|
|
base_dir = utils.getcwd() or ""
|
|
|
|
|
end
|
|
|
|
|
helper_path = find_file_upwards(base_dir, 'MPV/pipeline_helper.py', 8)
|
2025-12-17 17:42:46 -08:00
|
|
|
end
|
|
|
|
|
if not helper_path then
|
2025-12-18 22:50:21 -08:00
|
|
|
_lua_log('ipc: cannot find helper script MPV/pipeline_helper.py (script_dir=' .. tostring(mp.get_script_directory() or '') .. ')')
|
2025-12-17 17:42:46 -08:00
|
|
|
return false
|
|
|
|
|
end
|
|
|
|
|
|
2025-12-18 22:50:21 -08:00
|
|
|
-- Ensure mpv actually has a JSON IPC server for the helper to connect to.
|
|
|
|
|
if not ensure_mpv_ipc_server() then
|
|
|
|
|
_lua_log('ipc: mpv input-ipc-server is not set; start mpv with --input-ipc-server=\\\\.\\pipe\\mpv-medeia-macina')
|
|
|
|
|
return false
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local python = (opts and opts.python_path) and tostring(opts.python_path) or 'python'
|
|
|
|
|
local ipc = get_mpv_ipc_path()
|
|
|
|
|
-- Give the helper enough time to connect (Windows pipe can take a moment).
|
|
|
|
|
local args = {python, helper_path, '--ipc', ipc, '--timeout', '30'}
|
|
|
|
|
_lua_log('ipc: starting helper: ' .. table.concat(args, ' '))
|
2025-12-17 17:42:46 -08:00
|
|
|
|
|
|
|
|
local ok = utils.subprocess_detached({ args = args })
|
2025-12-18 22:50:21 -08:00
|
|
|
if ok == nil then
|
|
|
|
|
_lua_log('ipc: failed to start helper (subprocess_detached returned nil)')
|
|
|
|
|
return false
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
_pipeline_helper_started = true
|
|
|
|
|
return true
|
2025-12-17 17:42:46 -08:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local function run_pipeline_via_ipc(pipeline_cmd, seeds, timeout_seconds)
|
|
|
|
|
if not ensure_pipeline_helper_running() then
|
|
|
|
|
return nil
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- Avoid a race where we send the request before the helper has connected
|
|
|
|
|
-- and installed its property observer, which would cause a timeout and
|
|
|
|
|
-- force a noisy CLI fallback.
|
|
|
|
|
do
|
|
|
|
|
local deadline = mp.get_time() + 1.0
|
|
|
|
|
while mp.get_time() < deadline do
|
2025-12-18 22:50:21 -08:00
|
|
|
if _is_pipeline_helper_ready() then
|
2025-12-17 17:42:46 -08:00
|
|
|
break
|
|
|
|
|
end
|
|
|
|
|
mp.wait_event(0.05)
|
|
|
|
|
end
|
2025-12-18 22:50:21 -08:00
|
|
|
if not _is_pipeline_helper_ready() then
|
2025-12-17 17:42:46 -08:00
|
|
|
_pipeline_helper_started = false
|
|
|
|
|
return nil
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local id = tostring(math.floor(mp.get_time() * 1000)) .. '-' .. tostring(math.random(100000, 999999))
|
|
|
|
|
local req = { id = id, pipeline = pipeline_cmd }
|
|
|
|
|
if seeds then
|
|
|
|
|
req.seeds = seeds
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- Clear any previous response to reduce chances of reading stale data.
|
|
|
|
|
mp.set_property(PIPELINE_RESP_PROP, '')
|
|
|
|
|
mp.set_property(PIPELINE_REQ_PROP, utils.format_json(req))
|
|
|
|
|
|
|
|
|
|
local deadline = mp.get_time() + (timeout_seconds or 5)
|
|
|
|
|
while mp.get_time() < deadline do
|
|
|
|
|
local resp_json = mp.get_property(PIPELINE_RESP_PROP)
|
|
|
|
|
if resp_json and resp_json ~= '' then
|
|
|
|
|
local ok, resp = pcall(utils.parse_json, resp_json)
|
|
|
|
|
if ok and resp and resp.id == id then
|
|
|
|
|
if resp.success then
|
|
|
|
|
return resp.stdout or ''
|
|
|
|
|
end
|
|
|
|
|
local details = ''
|
|
|
|
|
if resp.error and tostring(resp.error) ~= '' then
|
|
|
|
|
details = tostring(resp.error)
|
|
|
|
|
end
|
|
|
|
|
if resp.stderr and tostring(resp.stderr) ~= '' then
|
|
|
|
|
if details ~= '' then
|
|
|
|
|
details = details .. "\n"
|
|
|
|
|
end
|
|
|
|
|
details = details .. tostring(resp.stderr)
|
|
|
|
|
end
|
|
|
|
|
local log_path = resp.log_path
|
|
|
|
|
if log_path and tostring(log_path) ~= '' then
|
|
|
|
|
details = (details ~= '' and (details .. "\n") or '') .. 'Log: ' .. tostring(log_path)
|
|
|
|
|
end
|
|
|
|
|
return nil, (details ~= '' and details or 'unknown')
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
mp.wait_event(0.05)
|
|
|
|
|
end
|
|
|
|
|
-- Helper may have crashed or never started; allow retry on next call.
|
|
|
|
|
_pipeline_helper_started = false
|
|
|
|
|
return nil
|
|
|
|
|
end
|
|
|
|
|
|
2025-11-27 10:59:01 -08:00
|
|
|
-- Detect CLI path
|
2025-12-07 00:21:30 -08:00
|
|
|
local function detect_script_dir()
|
|
|
|
|
local dir = mp.get_script_directory()
|
|
|
|
|
if dir and dir ~= "" then return dir end
|
|
|
|
|
|
|
|
|
|
-- Fallback to debug info path
|
|
|
|
|
local src = debug.getinfo(1, "S").source
|
|
|
|
|
if src and src:sub(1, 1) == "@" then
|
|
|
|
|
local path = src:sub(2)
|
|
|
|
|
local parent = path:match("(.*)[/\\]")
|
|
|
|
|
if parent and parent ~= "" then
|
|
|
|
|
return parent
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- Fallback to working directory
|
|
|
|
|
local cwd = utils.getcwd()
|
|
|
|
|
if cwd and cwd ~= "" then return cwd end
|
|
|
|
|
return nil
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local script_dir = detect_script_dir() or ""
|
2025-11-27 10:59:01 -08:00
|
|
|
if not opts.cli_path then
|
2025-12-17 17:42:46 -08:00
|
|
|
-- Try to locate CLI.py by walking up from this script directory.
|
|
|
|
|
-- Typical layout here is: <repo>/MPV/LUA/main.lua, and <repo>/CLI.py
|
|
|
|
|
opts.cli_path = find_file_upwards(script_dir, "CLI.py", 6) or "CLI.py"
|
2025-11-27 10:59:01 -08:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- Helper to run pipeline
|
|
|
|
|
function M.run_pipeline(pipeline_cmd, seeds)
|
2025-12-17 17:42:46 -08:00
|
|
|
local out, err = run_pipeline_via_ipc(pipeline_cmd, seeds, 5)
|
|
|
|
|
if out ~= nil then
|
|
|
|
|
return out
|
|
|
|
|
end
|
|
|
|
|
if err ~= nil then
|
|
|
|
|
local log_path = write_temp_log('medeia-pipeline-error', tostring(err))
|
|
|
|
|
local suffix = log_path and (' (log: ' .. log_path .. ')') or ''
|
2025-12-18 22:50:21 -08:00
|
|
|
_lua_log('Pipeline error: ' .. tostring(err) .. suffix)
|
2025-12-17 17:42:46 -08:00
|
|
|
mp.osd_message('Error: pipeline failed' .. suffix, 6)
|
|
|
|
|
return nil
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local args = {opts.python_path, opts.cli_path, "pipeline", "--pipeline", pipeline_cmd}
|
2025-11-27 10:59:01 -08:00
|
|
|
|
|
|
|
|
if seeds then
|
|
|
|
|
local seeds_json = utils.format_json(seeds)
|
2025-12-17 17:42:46 -08:00
|
|
|
table.insert(args, "--seeds-json")
|
2025-11-27 10:59:01 -08:00
|
|
|
table.insert(args, seeds_json)
|
|
|
|
|
end
|
|
|
|
|
|
2025-12-18 22:50:21 -08:00
|
|
|
_lua_log("Running pipeline: " .. pipeline_cmd)
|
|
|
|
|
-- If the persistent IPC helper isn't available, fall back to a subprocess.
|
|
|
|
|
-- Note: mpv's subprocess helper does not support an `env` parameter.
|
2025-11-27 10:59:01 -08:00
|
|
|
local res = utils.subprocess({
|
|
|
|
|
args = args,
|
|
|
|
|
cancellable = false,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if res.status ~= 0 then
|
2025-12-17 17:42:46 -08:00
|
|
|
local err = (res.stderr and res.stderr ~= "") and res.stderr
|
|
|
|
|
or (res.error_string and res.error_string ~= "") and res.error_string
|
|
|
|
|
or "unknown"
|
|
|
|
|
local log_path = write_temp_log('medeia-cli-pipeline-stderr', tostring(res.stderr or err))
|
|
|
|
|
local suffix = log_path and (' (log: ' .. log_path .. ')') or ''
|
2025-12-18 22:50:21 -08:00
|
|
|
_lua_log("Pipeline error: " .. err .. suffix)
|
2025-12-17 17:42:46 -08:00
|
|
|
mp.osd_message("Error: pipeline failed" .. suffix, 6)
|
2025-11-27 10:59:01 -08:00
|
|
|
return nil
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
return res.stdout
|
|
|
|
|
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"
|
|
|
|
|
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
|
2025-12-18 22:50:21 -08:00
|
|
|
_lua_log("Failed to parse JSON: " .. output)
|
2025-11-27 10:59:01 -08:00
|
|
|
return nil
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
return nil
|
|
|
|
|
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
|
2025-12-18 22:50:21 -08:00
|
|
|
_lua_log("Metadata: " .. utils.format_json(data))
|
2025-11-27 10:59:01 -08:00
|
|
|
mp.osd_message("Metadata loaded (check console)", 3)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- Command: Delete current file
|
|
|
|
|
function M.delete_current_file()
|
|
|
|
|
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")
|
|
|
|
|
end
|
|
|
|
|
|
2025-12-17 17:42:46 -08:00
|
|
|
-- Command: Load a URL via pipeline (Ctrl+Enter in prompt)
|
|
|
|
|
function M.open_load_url_prompt()
|
|
|
|
|
local menu_data = {
|
|
|
|
|
type = LOAD_URL_MENU_TYPE,
|
|
|
|
|
title = 'Load URL',
|
|
|
|
|
search_style = 'palette',
|
|
|
|
|
search_debounce = 'submit',
|
|
|
|
|
on_search = 'callback',
|
|
|
|
|
footnote = 'Paste/type URL, then Ctrl+Enter to load.',
|
|
|
|
|
callback = {mp.get_script_name(), 'medios-load-url-event'},
|
|
|
|
|
items = {},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
local json = utils.format_json(menu_data)
|
2025-12-18 22:50:21 -08:00
|
|
|
if ensure_uosc_loaded() then
|
|
|
|
|
mp.commandv('script-message-to', 'uosc', 'open-menu', json)
|
|
|
|
|
else
|
|
|
|
|
_lua_log('menu: uosc not available; cannot open-menu')
|
|
|
|
|
end
|
2025-12-17 17:42:46 -08:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
mp.register_script_message('medios-load-url', function()
|
|
|
|
|
M.open_load_url_prompt()
|
|
|
|
|
end)
|
|
|
|
|
|
|
|
|
|
mp.register_script_message('medios-load-url-event', function(json)
|
|
|
|
|
local ok, event = pcall(utils.parse_json, json)
|
|
|
|
|
if not ok or type(event) ~= 'table' then
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
if event.type ~= 'search' then
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local url = trim(tostring(event.query or ''))
|
|
|
|
|
if url == '' then
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
|
2025-12-18 22:50:21 -08:00
|
|
|
ensure_mpv_ipc_server()
|
|
|
|
|
local out = M.run_pipeline('.pipe -url ' .. quote_pipeline_arg(url) .. ' -play')
|
2025-12-17 17:42:46 -08:00
|
|
|
if out ~= nil then
|
2025-12-18 22:50:21 -08:00
|
|
|
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
|
2025-12-17 17:42:46 -08:00
|
|
|
end
|
|
|
|
|
end)
|
|
|
|
|
|
2025-11-27 10:59:01 -08:00
|
|
|
-- Menu integration with UOSC
|
|
|
|
|
function M.show_menu()
|
|
|
|
|
local menu_data = {
|
|
|
|
|
title = "Medios Macina",
|
|
|
|
|
items = {
|
|
|
|
|
{ title = "Get Metadata", value = "script-binding medios-info", hint = "Ctrl+i" },
|
|
|
|
|
{ title = "Delete File", value = "script-binding medios-delete", hint = "Ctrl+Del" },
|
2025-12-17 17:42:46 -08:00
|
|
|
{ title = "Load URL", value = {"script-message-to", mp.get_script_name(), "medios-load-url"} },
|
2025-12-18 22:50:21 -08:00
|
|
|
{ title = "Download", value = {"script-message-to", mp.get_script_name(), "medios-download-current"} },
|
|
|
|
|
{ title = "Change Format", value = {"script-message-to", mp.get_script_name(), "medios-change-format-current"} },
|
2025-11-27 10:59:01 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
local json = utils.format_json(menu_data)
|
2025-12-18 22:50:21 -08:00
|
|
|
if ensure_uosc_loaded() then
|
|
|
|
|
mp.commandv('script-message-to', 'uosc', 'open-menu', json)
|
|
|
|
|
else
|
|
|
|
|
_lua_log('menu: uosc not available; cannot open-menu')
|
|
|
|
|
end
|
2025-11-27 10:59:01 -08:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- Keybindings
|
|
|
|
|
mp.add_key_binding("m", "medios-menu", M.show_menu)
|
|
|
|
|
mp.add_key_binding("mbtn_right", "medios-menu-right-click", M.show_menu)
|
|
|
|
|
mp.add_key_binding("ctrl+i", "medios-info", M.get_file_info)
|
|
|
|
|
mp.add_key_binding("ctrl+del", "medios-delete", M.delete_current_file)
|
|
|
|
|
|
2025-12-12 21:55:38 -08:00
|
|
|
-- Lyrics toggle (requested: 'L')
|
|
|
|
|
mp.add_key_binding("l", "medeia-lyric-toggle", lyric_toggle)
|
|
|
|
|
mp.add_key_binding("L", "medeia-lyric-toggle-shift", lyric_toggle)
|
|
|
|
|
|
2025-12-17 17:42:46 -08:00
|
|
|
-- Start the persistent pipeline helper eagerly at launch.
|
|
|
|
|
-- This avoids spawning Python per command and works cross-platform via MPV IPC.
|
|
|
|
|
mp.add_timeout(0, function()
|
2025-12-18 22:50:21 -08:00
|
|
|
pcall(ensure_mpv_ipc_server)
|
2025-12-17 17:42:46 -08:00
|
|
|
pcall(ensure_pipeline_helper_running)
|
2025-12-18 22:50:21 -08:00
|
|
|
pcall(_lua_log, 'medeia-lua loaded version=' .. MEDEIA_LUA_VERSION)
|
2025-12-17 17:42:46 -08:00
|
|
|
end)
|
|
|
|
|
|
2025-11-27 10:59:01 -08:00
|
|
|
return M
|