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-17 17:42:46 -08:00
|
|
|
local LOAD_URL_MENU_TYPE = 'medios_load_url'
|
|
|
|
|
|
|
|
|
|
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'
|
|
|
|
|
|
|
|
|
|
local function write_temp_log(prefix, text)
|
|
|
|
|
if not text or text == '' then
|
|
|
|
|
return nil
|
|
|
|
|
end
|
|
|
|
|
local dir = os.getenv('TEMP') or os.getenv('TMP') or utils.getcwd() or ''
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
local _pipeline_helper_started = false
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
local function ensure_pipeline_helper_running()
|
|
|
|
|
local ready = mp.get_property_native(PIPELINE_READY_PROP)
|
|
|
|
|
if ready then
|
|
|
|
|
return true
|
|
|
|
|
end
|
|
|
|
|
if _pipeline_helper_started then
|
|
|
|
|
return true
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local base_dir = mp.get_script_directory() or ""
|
|
|
|
|
if base_dir == "" then
|
|
|
|
|
base_dir = utils.getcwd() or ""
|
|
|
|
|
end
|
|
|
|
|
local helper_path = find_file_upwards(base_dir, 'MPV/pipeline_helper.py', 6)
|
|
|
|
|
if not helper_path then
|
|
|
|
|
return false
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
_pipeline_helper_started = true
|
|
|
|
|
|
|
|
|
|
local args = {opts.python_path, helper_path, '--ipc', get_mpv_ipc_path()}
|
|
|
|
|
local ok = utils.subprocess_detached({ args = args })
|
|
|
|
|
return ok ~= nil
|
|
|
|
|
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
|
|
|
|
|
local ready = mp.get_property_native(PIPELINE_READY_PROP)
|
|
|
|
|
if ready and tostring(ready) ~= '' and tostring(ready) ~= '0' then
|
|
|
|
|
break
|
|
|
|
|
end
|
|
|
|
|
mp.wait_event(0.05)
|
|
|
|
|
end
|
|
|
|
|
local ready = mp.get_property_native(PIPELINE_READY_PROP)
|
|
|
|
|
if not (ready and tostring(ready) ~= '' and tostring(ready) ~= '0') then
|
|
|
|
|
_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 ''
|
|
|
|
|
msg.error('Pipeline error: ' .. tostring(err) .. suffix)
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
msg.info("Running pipeline: " .. pipeline_cmd)
|
|
|
|
|
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 ''
|
|
|
|
|
msg.error("Pipeline error: " .. err .. suffix)
|
|
|
|
|
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
|
|
|
|
|
msg.error("Failed to parse JSON: " .. output)
|
|
|
|
|
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
|
|
|
|
|
msg.info("Metadata: " .. utils.format_json(data))
|
|
|
|
|
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)
|
|
|
|
|
mp.commandv('script-message-to', 'uosc', 'open-menu', json)
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
local out = M.run_pipeline('.pipe ' .. url .. ' -play')
|
|
|
|
|
if out ~= nil then
|
|
|
|
|
mp.commandv('script-message-to', 'uosc', 'close-menu', LOAD_URL_MENU_TYPE)
|
|
|
|
|
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-11-27 10:59:01 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
local json = utils.format_json(menu_data)
|
|
|
|
|
mp.commandv('script-message-to', 'uosc', 'open-menu', json)
|
|
|
|
|
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()
|
|
|
|
|
pcall(ensure_pipeline_helper_running)
|
|
|
|
|
end)
|
|
|
|
|
|
2025-11-27 10:59:01 -08:00
|
|
|
return M
|