local mp = require 'mp' local utils = require 'mp.utils' local msg = require 'mp.msg' local M = {} 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 -- 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) -- Configuration local opts = { python_path = "python", cli_path = nil -- Will be auto-detected if nil } 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 -- Detect CLI path 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 "" if not opts.cli_path then -- Try to locate CLI.py by walking up from this script directory. -- Typical layout here is: /MPV/LUA/main.lua, and /CLI.py opts.cli_path = find_file_upwards(script_dir, "CLI.py", 6) or "CLI.py" end -- Helper to run pipeline function M.run_pipeline(pipeline_cmd, seeds) 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} if seeds then local seeds_json = utils.format_json(seeds) table.insert(args, "--seeds-json") 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 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) 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 -- 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) -- 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" }, { title = "Load URL", value = {"script-message-to", mp.get_script_name(), "medios-load-url"} }, } } 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) -- 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) -- 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) return M