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-24 02:13:21 -08:00
local MEDEIA_LUA_VERSION = ' 2025-12-24 '
2025-12-18 22:50:21 -08:00
2025-12-27 03:13:16 -08:00
-- Expose a tiny breadcrumb for debugging which script version is loaded.
pcall ( mp.set_property , ' user-data/medeia-lua-version ' , MEDEIA_LUA_VERSION )
2025-12-18 22:50:21 -08:00
-- 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-23 16:36:39 -08:00
-- Menu types for the command submenu and trim prompt
local CMD_MENU_TYPE = ' medios_cmd_menu '
local TRIM_PROMPT_MENU_TYPE = ' medios_trim_prompt '
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 = ' '
2025-12-27 03:13:16 -08:00
-- 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
2025-12-18 22:50:21 -08:00
-- 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
2025-12-27 03:13:16 -08:00
_lua_log ( ' medeia lua loaded version= ' .. tostring ( MEDEIA_LUA_VERSION ) .. ' script= ' .. tostring ( mp.get_script_name ( ) ) )
2026-02-04 16:59:04 -08:00
-- Log to database (logs.db) for centralized error/message tracking
-- This ensures all OSD messages and errors are persisted for debugging
local function _log_to_db ( level , message )
message = tostring ( message or ' ' ) : gsub ( ' " ' , ' \\ " ' )
level = tostring ( level or ' INFO ' ) : upper ( )
-- Find the repo root by looking for CLI.py upwards from script directory
local repo_root = ' '
do
local script_dir = mp.get_script_directory ( ) or utils.getcwd ( ) or ' '
if script_dir ~= ' ' then
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 cli = find_up ( script_dir , ' CLI.py ' , 8 )
if cli and cli ~= ' ' then
repo_root = cli : match ( ' (.*)[/ \\ ] ' ) or ' '
end
end
end
if repo_root == ' ' then
return -- Can't find repo root, skip logging to database
end
-- Escape paths for Python subprocess
repo_root = repo_root : gsub ( ' \\ ' , ' / ' )
-- Use Python to write to the database since Lua can't easily access sqlite3
-- We use a subprocess call with minimal Python to insert into logs.db
local python = ( opts and opts.python_path ) and tostring ( opts.python_path ) or ' python '
local db_path = repo_root .. ' /logs.db '
local script = string.format (
" import sqlite3,os;p='%s';c=sqlite3.connect(p) if os.path.exists(p) else None;c and (c.execute('INSERT INTO logs (level,module,message) VALUES (?,?,?)',('%s','mpv','%s')),c.commit(),c.close()) " ,
db_path : gsub ( ' \\ ' , ' / ' ) ,
level ,
message
)
pcall ( function ( )
mp.command_native_async ( { name = ' subprocess ' , args = { python , ' -c ' , script } , cwd = nil } , function ( ) end )
end )
end
-- Combined log: to file + database (for persistence and debugging)
local function _log_all ( level , text )
if not text or text == ' ' then
return
end
level = tostring ( level or ' INFO ' ) : upper ( )
text = tostring ( text )
-- Log to file
_lua_log ( ' [ ' .. level .. ' ] ' .. text )
-- Log to database (async, non-blocking)
_log_to_db ( level , text )
end
2025-12-18 22:50:21 -08:00
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.
2025-12-24 02:13:21 -08:00
-- NOTE: Avoid spawning cmd.exe/sh just to mkdir on Windows/Linux; console flashes are
-- highly undesirable. If the directory doesn't exist, we fall back to TEMP.
2025-12-18 22:50:21 -08:00
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 ' )
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
2025-12-24 02:13:21 -08:00
-- If Log/ wasn't created (or is not writable), fall back to TEMP.
local tmp = os.getenv ( ' TEMP ' ) or os.getenv ( ' TMP ' ) or ' '
if tmp ~= ' ' and tmp ~= dir then
path = utils.join_path ( tmp , name )
fh = io.open ( path , ' w ' )
end
if not fh then
return nil
end
2025-12-17 17:42:46 -08:00
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
}
2026-02-03 17:14:11 -08:00
-- Read script options from script-opts/medeia.conf when available
pcall ( function ( )
local mpopts = require ( ' mp.options ' )
mpopts.read_options ( opts , ' medeia ' )
end )
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
2026-01-03 03:37:48 -08:00
-- Optional index into _cached_store_names (used by some older menu code paths).
-- If unset, callers should fall back to reading SELECTED_STORE_PROP.
local _selected_store_index = nil
2025-12-27 03:13:16 -08:00
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
2025-12-17 17:42:46 -08:00
local _pipeline_helper_started = false
2025-12-19 02:29:42 -08:00
local _last_ipc_error = ' '
local _last_ipc_last_req_json = ' '
local _last_ipc_last_resp_json = ' '
2025-12-17 17:42:46 -08:00
2026-02-03 17:14:11 -08:00
-- Debounce helper start attempts (window in seconds)
local _helper_start_debounce_ts = 0
local HELPER_START_DEBOUNCE = 2.0
2025-12-18 22:50:21 -08:00
local function _is_pipeline_helper_ready ( )
2025-12-27 03:13:16 -08:00
local ready = mp.get_property ( PIPELINE_READY_PROP )
if ready == nil or ready == ' ' then
ready = mp.get_property_native ( PIPELINE_READY_PROP )
end
2025-12-18 22:50:21 -08:00
if not ready then
return false
end
local s = tostring ( ready )
if s == ' ' or s == ' 0 ' then
return false
end
2026-02-03 17:14:11 -08:00
-- Only support unix timestamp heartbeats from current helper version
2025-12-18 22:50:21 -08:00
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
2026-02-03 17:14:11 -08:00
-- Non-empty value treated as ready
2025-12-18 22:50:21 -08:00
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
2026-02-02 02:32:28 -08:00
return ' \\ \\ . \\ pipe \\ mpv-medios-macina '
2025-12-17 17:42:46 -08:00
end
2026-02-02 02:32:28 -08:00
return ' /tmp/mpv-medios-macina.sock '
2025-12-17 17:42:46 -08:00
end
2025-12-18 22:50:21 -08:00
local function ensure_mpv_ipc_server ( )
2026-01-03 03:37:48 -08:00
-- `.mpv -play` (Python) controls MPV via JSON IPC. If mpv was started
2025-12-18 22:50:21 -08:00
-- 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
2026-02-03 17:14:11 -08:00
local function attempt_start_pipeline_helper_async ( callback )
-- Async version: spawn helper without blocking UI. Calls callback(success) when done.
callback = callback or function ( ) end
if _is_pipeline_helper_ready ( ) then
callback ( true )
return
end
-- Debounce: don't spawn multiple helpers in quick succession
local now = mp.get_time ( )
if ( now - _helper_start_debounce_ts ) < HELPER_START_DEBOUNCE then
_lua_log ( ' attempt_start_pipeline_helper_async: debounced (recent attempt) ' )
callback ( false )
return
end
_helper_start_debounce_ts = now
local python = _resolve_python_exe ( true )
if not python or python == ' ' then
_lua_log ( ' attempt_start_pipeline_helper_async: no python executable available ' )
callback ( false )
return
end
local script_dir = mp.get_script_directory ( ) or utils.getcwd ( ) or ' '
local cli = nil
pcall ( function ( )
cli = find_file_upwards ( script_dir , ' CLI.py ' , 8 )
end )
local cwd = nil
if cli and cli ~= ' ' then
cwd = cli : match ( ' (.*)[/ \\ ] ' ) or nil
end
local args = { python , ' -m ' , ' MPV.pipeline_helper ' , ' --ipc ' , get_mpv_ipc_path ( ) , ' --timeout ' , ' 30 ' }
_lua_log ( ' attempt_start_pipeline_helper_async: spawning helper ' )
-- Spawn detached; don't wait for it here (async).
local ok = pcall ( mp.command_native , { name = ' subprocess ' , args = args , cwd = cwd , detach = true } )
if not ok then
_lua_log ( ' attempt_start_pipeline_helper_async: detached spawn failed, retrying blocking ' )
ok = pcall ( mp.command_native , { name = ' subprocess ' , args = args , cwd = cwd } )
end
if not ok then
_lua_log ( ' attempt_start_pipeline_helper_async: spawn failed ' )
callback ( false )
return
end
-- Wait for helper to become ready in background (non-blocking).
local deadline = mp.get_time ( ) + 3.0
local timer
timer = mp.add_periodic_timer ( 0.1 , function ( )
if _is_pipeline_helper_ready ( ) then
timer : kill ( )
_lua_log ( ' attempt_start_pipeline_helper_async: helper ready ' )
callback ( true )
return
end
if mp.get_time ( ) >= deadline then
timer : kill ( )
_lua_log ( ' attempt_start_pipeline_helper_async: timeout waiting for ready ' )
callback ( false )
end
end )
end
2026-01-12 17:55:04 -08:00
local function ensure_pipeline_helper_running ( )
2026-02-03 17:14:11 -08:00
-- Check if helper is already running (don't spawn from here).
-- Auto-start is handled via explicit menu action only.
return _is_pipeline_helper_ready ( )
2026-01-12 17:55:04 -08:00
end
local _ipc_async_busy = false
local _ipc_async_queue = { }
local function _run_helper_request_async ( req , timeout_seconds , cb )
cb = cb or function ( ) end
if _ipc_async_busy then
_ipc_async_queue [ # _ipc_async_queue + 1 ] = { req = req , timeout = timeout_seconds , cb = cb }
return
end
_ipc_async_busy = true
local function done ( resp , err )
_ipc_async_busy = false
cb ( resp , err )
if # _ipc_async_queue > 0 then
local next_job = table.remove ( _ipc_async_queue , 1 )
-- Schedule next job slightly later to let mpv deliver any pending events.
mp.add_timeout ( 0.01 , function ( )
_run_helper_request_async ( next_job.req , next_job.timeout , next_job.cb )
end )
end
end
if type ( req ) ~= ' table ' then
done ( nil , ' invalid request ' )
return
end
ensure_mpv_ipc_server ( )
if not ensure_pipeline_helper_running ( ) then
done ( nil , ' helper not running ' )
return
end
-- Assign id.
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
-- Wait for helper READY without blocking the UI.
local ready_deadline = mp.get_time ( ) + 3.0
local ready_timer
ready_timer = mp.add_periodic_timer ( 0.05 , function ( )
if _is_pipeline_helper_ready ( ) then
ready_timer : kill ( )
_lua_log ( ' ipc-async: send request id= ' .. tostring ( id ) .. ' ' .. label )
local req_json = utils.format_json ( req )
_last_ipc_last_req_json = req_json
mp.set_property ( PIPELINE_RESP_PROP , ' ' )
mp.set_property ( PIPELINE_REQ_PROP , req_json )
local deadline = mp.get_time ( ) + ( timeout_seconds or 5 )
local poll_timer
poll_timer = mp.add_periodic_timer ( 0.05 , function ( )
if mp.get_time ( ) >= deadline then
poll_timer : kill ( )
done ( nil , ' timeout waiting response ( ' .. label .. ' ) ' )
return
end
local resp_json = mp.get_property ( PIPELINE_RESP_PROP )
if resp_json and resp_json ~= ' ' then
_last_ipc_last_resp_json = resp_json
local ok , resp = pcall ( utils.parse_json , resp_json )
if ok and resp and resp.id == id then
poll_timer : kill ( )
_lua_log ( ' ipc-async: got response id= ' .. tostring ( id ) .. ' success= ' .. tostring ( resp.success ) )
done ( resp , nil )
end
end
end )
return
end
if mp.get_time ( ) >= ready_deadline then
ready_timer : kill ( )
done ( nil , ' helper not ready ' )
return
end
end )
end
local function _run_helper_request_response ( req , timeout_seconds )
2026-02-03 17:14:11 -08:00
-- Legacy synchronous wrapper for compatibility with run_pipeline_via_ipc_response.
-- TODO: Migrate all callers to async _run_helper_request_async and remove this.
2026-01-12 17:55:04 -08:00
_last_ipc_error = ' '
if not ensure_pipeline_helper_running ( ) then
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 '
end
do
-- 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
if _is_pipeline_helper_ready ( ) then
break
end
mp.wait_event ( 0.05 )
end
if not _is_pipeline_helper_ready ( ) then
local rv = tostring ( mp.get_property ( PIPELINE_READY_PROP ) or mp.get_property_native ( PIPELINE_READY_PROP ) or ' ' )
_lua_log ( ' ipc: proceeding without helper heartbeat; ready= ' .. rv )
_last_ipc_error = ' helper heartbeat missing (ready= ' .. rv .. ' ) '
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 )
local req_json = utils.format_json ( req )
_last_ipc_last_req_json = req_json
mp.set_property ( PIPELINE_RESP_PROP , ' ' )
mp.set_property ( PIPELINE_REQ_PROP , req_json )
-- Read-back for debugging: confirms MPV accepted the property write.
local echoed = mp.get_property ( PIPELINE_REQ_PROP ) or ' '
if echoed == ' ' then
_lua_log ( ' ipc: WARNING request property echoed empty after set ' )
end
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
_last_ipc_last_resp_json = resp_json
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 )
_last_ipc_error = ' timeout waiting response ( ' .. label .. ' ) '
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
2026-02-03 17:14:11 -08:00
local function _url_can_direct_load ( url )
-- Determine if a URL is safe to load directly via mpv loadfile (vs. requiring pipeline).
-- Complex streams like MPD/DASH manifests and ytdl URLs need the full pipeline.
url = tostring ( url or ' ' ) ;
local lower = url : lower ( )
-- File paths and simple URLs are OK
if lower : match ( ' ^file:// ' ) or lower : match ( ' ^file:/// ' ) then return true end
if not lower : match ( ' ^https?:// ' ) and not lower : match ( ' ^rtmp ' ) then return true end
-- Block ytdl and other complex streams
if lower : match ( ' youtube%.com ' ) or lower : match ( ' youtu%.be ' ) then return false end
if lower : match ( ' %.mpd%b() ' ) or lower : match ( ' %.mpd$ ' ) then return false end -- DASH manifest
if lower : match ( ' manifest%.json ' ) then return false end
if lower : match ( ' twitch%.tv ' ) or lower : match ( ' youtube ' ) then return false end
if lower : match ( ' soundcloud%.com ' ) or lower : match ( ' bandcamp%.com ' ) then return false end
if lower : match ( ' spotify ' ) or lower : match ( ' tidal ' ) then return false end
if lower : match ( ' reddit%.com ' ) or lower : match ( ' tiktok%.com ' ) then return false end
if lower : match ( ' vimeo%.com ' ) or lower : match ( ' dailymotion%.com ' ) then return false end
-- Default: assume direct load is OK for plain HTTP(S) URLs
return true
end
local function _try_direct_loadfile ( url )
-- Attempt to load URL directly via mpv without pipeline.
-- Returns (success: bool, loaded: bool) where:
-- - success=true, loaded=true: URL loaded successfully
-- - success=true, loaded=false: URL not suitable for direct load
-- - success=false: loadfile command failed
if not _url_can_direct_load ( url ) then
_lua_log ( ' _try_direct_loadfile: URL not suitable for direct load: ' .. url )
return true , false -- Not suitable, but not an error
end
_lua_log ( ' _try_direct_loadfile: attempting loadfile for ' .. url )
local ok_load = pcall ( mp.commandv , ' loadfile ' , url , ' replace ' )
_lua_log ( ' _try_direct_loadfile: loadfile result ok_load= ' .. tostring ( ok_load ) )
return ok_load , ok_load -- Fallback attempted
end
2025-12-18 22:50:21 -08:00
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
2025-12-24 02:13:21 -08:00
local function _resolve_python_exe ( prefer_no_console )
local python = ( opts and opts.python_path ) and tostring ( opts.python_path ) or ' python '
if ( not prefer_no_console ) or ( not _is_windows ( ) ) then
return python
end
local low = tostring ( python ) : lower ( )
if low == ' python ' then
return ' pythonw '
end
if low == ' python.exe ' then
return ' pythonw.exe '
end
if low : sub ( - 10 ) == ' python.exe ' then
local candidate = python : sub ( 1 , # python - 10 ) .. ' pythonw.exe '
if utils.file_info ( candidate ) then
return candidate
end
return ' pythonw '
end
-- Already pythonw or some other launcher.
return python
end
2025-12-18 22:50:21 -08:00
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
2025-12-23 16:36:39 -08:00
local ImageControl = {
enabled = false ,
binding_names = { } ,
pan_step = 0.05 ,
pan_step_slow = 0.02 ,
zoom_step = 0.45 ,
zoom_step_slow = 0.15 ,
}
local MAX_IMAGE_ZOOM = 4.5
local function _install_q_block ( )
pcall ( mp.commandv , ' keybind ' , ' q ' , ' script-message ' , ' medeia-image-quit-block ' )
end
local function _restore_q_default ( )
pcall ( mp.commandv , ' keybind ' , ' q ' , ' quit ' )
end
local function _enable_image_section ( )
pcall ( mp.commandv , ' enable-section ' , ' image ' , ' allow-hide-cursor ' )
end
local function _disable_image_section ( )
pcall ( mp.commandv , ' disable-section ' , ' image ' )
end
mp.register_script_message ( ' medeia-image-quit-block ' , function ( )
if ImageControl.enabled then
mp.osd_message ( ' Press ESC if you really want to quit ' , 0.7 )
return
end
mp.commandv ( ' quit ' )
end )
local ImageExtensions = {
jpg = true ,
jpeg = true ,
png = true ,
gif = true ,
webp = true ,
bmp = true ,
tif = true ,
tiff = true ,
heic = true ,
heif = true ,
avif = true ,
ico = true ,
}
local function _clean_path_for_extension ( path )
if type ( path ) ~= ' string ' then
return nil
end
local clean = path : match ( ' ([^?]+) ' ) or path
clean = clean : match ( ' ([^#]+) ' ) or clean
local last = clean : match ( ' ([^/ \\ ]+)$ ' ) or ' '
local ext = last : match ( ' %.([A-Za-z0-9]+)$ ' )
if not ext then
return nil
end
return ext : lower ( )
end
local function _is_image_path ( path )
local ext = _clean_path_for_extension ( path )
return ext and ImageExtensions [ ext ]
end
local function _get_current_item_is_image ( )
local video_info = mp.get_property_native ( ' current-tracks/video ' )
if type ( video_info ) == ' table ' then
2025-12-24 02:13:21 -08:00
if video_info.image == true then
2025-12-23 16:36:39 -08:00
return true
end
2025-12-24 02:13:21 -08:00
if video_info.image == false then
2025-12-23 16:36:39 -08:00
return false
end
end
local target = _current_target ( )
if target then
return _is_image_path ( target )
end
return false
end
local function _set_image_property ( value )
pcall ( mp.set_property_native , ' user-data/mpv/image ' , value and true or false )
end
local function _show_image_status ( message )
local zoom = mp.get_property_number ( ' video-zoom ' ) or 0
local pan_x = mp.get_property_number ( ' video-pan-x ' ) or 0
local pan_y = mp.get_property_number ( ' video-pan-y ' ) or 0
local zoom_percent = math.floor ( ( 1 + zoom ) * 100 + 0.5 )
local text = string.format ( ' Image: zoom %d%% pan %+.2f %+.2f ' , zoom_percent , pan_x , pan_y )
if message and message ~= ' ' then
text = message .. ' | ' .. text
end
mp.osd_message ( text , 0.7 )
end
local function _change_pan ( dx , dy )
local pan_x = mp.get_property_number ( ' video-pan-x ' ) or 0
local pan_y = mp.get_property_number ( ' video-pan-y ' ) or 0
mp.set_property_number ( ' video-pan-x ' , pan_x + dx )
mp.set_property_number ( ' video-pan-y ' , pan_y + dy )
_show_image_status ( )
end
local function _change_zoom ( delta )
local current = mp.get_property_number ( ' video-zoom ' ) or 0
local target = current + delta
if target > MAX_IMAGE_ZOOM then
target = MAX_IMAGE_ZOOM
end
if target < - 1.0 then
target = - 1.0
end
mp.set_property_number ( ' video-zoom ' , target )
mp.set_property ( ' video-unscaled ' , ' no ' )
if target >= MAX_IMAGE_ZOOM then
mp.osd_message ( ' Image zoom maxed at 450% ' , 0.7 )
else
_show_image_status ( )
end
end
local function _reset_pan_zoom ( )
mp.set_property_number ( ' video-pan-x ' , 0 )
mp.set_property_number ( ' video-pan-y ' , 0 )
mp.set_property_number ( ' video-zoom ' , 0 )
mp.set_property ( ' video-align-x ' , ' 0 ' )
mp.set_property ( ' video-align-y ' , ' 0 ' )
mp.set_property ( ' panscan ' , 0 )
mp.set_property ( ' video-unscaled ' , ' no ' )
_show_image_status ( ' Zoom reset ' )
end
2025-12-27 06:05:07 -08:00
local function _sanitize_filename_component ( s )
s = trim ( tostring ( s or ' ' ) )
if s == ' ' then
return ' screenshot '
end
-- Windows-unfriendly characters: <>:"/\|?* and control chars
s = s : gsub ( ' [%c] ' , ' ' )
s = s : gsub ( ' [<>:"/ \\ |%?%*] ' , ' _ ' )
s = trim ( s )
s = s : gsub ( ' [%.%s]+$ ' , ' ' )
if s == ' ' then
return ' screenshot '
end
return s
end
local function _strip_title_extension ( title , path )
title = trim ( tostring ( title or ' ' ) )
if title == ' ' then
return title
end
path = tostring ( path or ' ' )
local ext = path : match ( ' %.([%w%d]+)$ ' )
if not ext or ext == ' ' then
return title
end
ext = ext : lower ( )
local suffix = ' . ' .. ext
if title : lower ( ) : sub ( -# suffix ) == suffix then
return trim ( title : sub ( 1 , # title - # suffix ) )
end
return title
end
2025-12-23 16:36:39 -08:00
local function _capture_screenshot ( )
2025-12-27 06:05:07 -08:00
local function _format_time_label ( seconds )
local total = math.max ( 0 , math.floor ( tonumber ( seconds or 0 ) or 0 ) )
local hours = math.floor ( total / 3600 )
local minutes = math.floor ( total / 60 ) % 60
local secs = total % 60
local parts = { }
if hours > 0 then
table.insert ( parts , ( ' %dh ' ) : format ( hours ) )
end
if minutes > 0 or hours > 0 then
table.insert ( parts , ( ' %dm ' ) : format ( minutes ) )
end
table.insert ( parts , ( ' %ds ' ) : format ( secs ) )
return table.concat ( parts )
end
local time = mp.get_property_number ( ' time-pos ' ) or mp.get_property_number ( ' time ' ) or 0
local label = _format_time_label ( time )
local raw_title = trim ( tostring ( mp.get_property ( ' media-title ' ) or ' ' ) )
local raw_path = tostring ( mp.get_property ( ' path ' ) or ' ' )
if raw_title == ' ' then
raw_title = ' screenshot '
end
raw_title = _strip_title_extension ( raw_title , raw_path )
local safe_title = _sanitize_filename_component ( raw_title )
local filename = safe_title .. ' _ ' .. label .. ' .png '
local temp_dir = mp.get_property ( ' user-data/medeia-config-temp ' ) or os.getenv ( ' TEMP ' ) or os.getenv ( ' TMP ' ) or ' /tmp '
local out_path = utils.join_path ( temp_dir , filename )
2026-01-12 17:55:04 -08:00
local function do_screenshot ( mode )
mode = mode or ' video '
local ok , err = pcall ( function ( )
return mp.commandv ( ' screenshot-to-file ' , out_path , mode )
end )
return ok , err
end
-- Try 'video' first (no OSD). If that fails (e.g. audio mode without video/art),
-- try 'window' as fallback.
local ok = do_screenshot ( ' video ' )
2025-12-27 06:05:07 -08:00
if not ok then
2026-01-12 17:55:04 -08:00
_lua_log ( ' screenshot: video-mode failed; trying window-mode ' )
ok = do_screenshot ( ' window ' )
end
if not ok then
_lua_log ( ' screenshot: BOTH video and window modes FAILED ' )
mp.osd_message ( ' Screenshot failed (no frames) ' , 2 )
2025-12-27 06:05:07 -08:00
return
end
_ensure_selected_store_loaded ( )
local selected_store = _get_selected_store ( )
selected_store = trim ( tostring ( selected_store or ' ' ) )
selected_store = selected_store : gsub ( ' ^ \" ' , ' ' ) : gsub ( ' \" $ ' , ' ' )
if selected_store == ' ' then
mp.osd_message ( ' Select a store first (Store button) ' , 2 )
return
end
2026-01-12 17:55:04 -08:00
mp.osd_message ( ' Saving screenshot... ' , 1 )
-- optimization: use persistent pipeline helper instead of spawning new python process
-- escape paths for command line
local cmd = ' add-file -store " ' .. selected_store .. ' " -path " ' .. out_path .. ' " '
local resp = run_pipeline_via_ipc_response ( cmd , nil , 10 )
if resp and resp.success then
2025-12-27 06:05:07 -08:00
mp.osd_message ( ' Screenshot saved to store: ' .. selected_store , 3 )
else
2026-01-12 17:55:04 -08:00
local err = ( resp and resp.error ) or ( resp and resp.stderr ) or ' IPC error '
mp.osd_message ( ' Screenshot upload failed: ' .. tostring ( err ) , 5 )
2025-12-27 06:05:07 -08:00
end
2025-12-23 16:36:39 -08:00
end
mp.register_script_message ( ' medeia-image-screenshot ' , function ( )
_capture_screenshot ( )
end )
local CLIP_MARKER_SLOT_COUNT = 2
local clip_markers = { }
local initial_chapters = nil
local function _format_clip_marker_label ( time )
if type ( time ) ~= ' number ' then
return ' 0s '
end
local total = math.max ( 0 , math.floor ( time ) )
local hours = math.floor ( total / 3600 )
local minutes = math.floor ( total / 60 ) % 60
local seconds = total % 60
local parts = { }
if hours > 0 then
table.insert ( parts , ( ' %dh ' ) : format ( hours ) )
end
if minutes > 0 or hours > 0 then
table.insert ( parts , ( ' %dm ' ) : format ( minutes ) )
end
table.insert ( parts , ( ' %ds ' ) : format ( seconds ) )
return table.concat ( parts )
end
local function _apply_clip_chapters ( )
local chapters = { }
if initial_chapters then
for _ , chapter in ipairs ( initial_chapters ) do table.insert ( chapters , chapter ) end
end
for idx = 1 , CLIP_MARKER_SLOT_COUNT do
local time = clip_markers [ idx ]
if time and type ( time ) == ' number ' then
table.insert ( chapters , {
time = time ,
title = _format_clip_marker_label ( time ) ,
} )
end
end
table.sort ( chapters , function ( a , b ) return ( a.time or 0 ) < ( b.time or 0 ) end )
mp.set_property_native ( ' chapter-list ' , chapters )
end
local function _reset_clip_markers ( )
for idx = 1 , CLIP_MARKER_SLOT_COUNT do
clip_markers [ idx ] = nil
end
_apply_clip_chapters ( )
end
local function _capture_clip ( )
local time = mp.get_property_number ( ' time-pos ' ) or mp.get_property_number ( ' time ' )
if not time then
mp.osd_message ( ' Cannot capture clip; no time available ' , 0.7 )
return
end
local slot = nil
for idx = 1 , CLIP_MARKER_SLOT_COUNT do
if not clip_markers [ idx ] then
slot = idx
break
end
end
if not slot then
local best = math.huge
for idx = 1 , CLIP_MARKER_SLOT_COUNT do
local existing = clip_markers [ idx ]
local distance = math.abs ( ( existing or 0 ) - time )
if distance < best then
best = distance
slot = idx
end
end
slot = slot or 1
end
clip_markers [ slot ] = time
_apply_clip_chapters ( )
mp.commandv ( ' screenshot-to-file ' , ( ' clip-%s-%.0f.png ' ) : format ( os.date ( ' %Y%m%d-%H%M%S ' ) , time ) )
local label = _format_clip_marker_label ( time )
mp.osd_message ( ( ' Clip marker %d set at %s ' ) : format ( slot , label ) , 0.7 )
end
mp.register_event ( ' file-loaded ' , function ( )
initial_chapters = mp.get_property_native ( ' chapter-list ' ) or { }
_reset_clip_markers ( )
end )
mp.register_script_message ( ' medeia-image-clip ' , function ( )
_capture_clip ( )
end )
local function _get_trim_range_from_clip_markers ( )
local times = { }
for idx = 1 , CLIP_MARKER_SLOT_COUNT do
local t = clip_markers [ idx ]
if type ( t ) == ' number ' then
table.insert ( times , t )
end
end
table.sort ( times , function ( a , b ) return a < b end )
if # times < 2 then
return nil
end
local start_t = times [ 1 ]
local end_t = times [ 2 ]
if type ( start_t ) ~= ' number ' or type ( end_t ) ~= ' number ' then
return nil
end
if end_t <= start_t then
return nil
end
return _format_clip_marker_label ( start_t ) .. ' - ' .. _format_clip_marker_label ( end_t )
end
local function _audio_only ( )
mp.commandv ( ' set ' , ' vid ' , ' no ' )
mp.osd_message ( ' Audio-only playback enabled ' , 1 )
end
mp.register_script_message ( ' medeia-audio-only ' , function ( )
_audio_only ( )
end )
local function _bind_image_key ( key , name , fn , opts )
opts = opts or { }
if ImageControl.binding_names [ name ] then
pcall ( mp.remove_key_binding , name )
ImageControl.binding_names [ name ] = nil
end
local ok , err = pcall ( mp.add_forced_key_binding , key , name , fn , opts )
if ok then
ImageControl.binding_names [ name ] = true
else
mp.msg . warn ( ' Failed to add image binding ' .. tostring ( key ) .. ' : ' .. tostring ( err ) )
end
end
local function _unbind_image_keys ( )
for name in pairs ( ImageControl.binding_names ) do
pcall ( mp.remove_key_binding , name )
ImageControl.binding_names [ name ] = nil
end
end
local function _activate_image_controls ( )
if ImageControl.enabled then
return
end
ImageControl.enabled = true
_set_image_property ( true )
_enable_image_section ( )
mp.osd_message ( ' Image viewer controls enabled ' , 1.2 )
_bind_image_key ( ' LEFT ' , ' image-pan-left ' , function ( ) _change_pan ( - ImageControl.pan_step , 0 ) end , { repeatable = true } )
_bind_image_key ( ' RIGHT ' , ' image-pan-right ' , function ( ) _change_pan ( ImageControl.pan_step , 0 ) end , { repeatable = true } )
_bind_image_key ( ' s ' , ' image-pan-s ' , function ( ) _change_pan ( 0 , ImageControl.pan_step ) end , { repeatable = true } )
_bind_image_key ( ' a ' , ' image-pan-a ' , function ( ) _change_pan ( ImageControl.pan_step , 0 ) end , { repeatable = true } )
_bind_image_key ( ' d ' , ' image-pan-d ' , function ( ) _change_pan ( - ImageControl.pan_step , 0 ) end , { repeatable = true } )
_bind_image_key ( ' Shift+RIGHT ' , ' image-pan-right-fine ' , function ( ) _change_pan ( ImageControl.pan_step_slow , 0 ) end , { repeatable = true } )
_bind_image_key ( ' Shift+UP ' , ' image-pan-up-fine ' , function ( ) _change_pan ( 0 , - ImageControl.pan_step_slow ) end , { repeatable = true } )
_bind_image_key ( ' Shift+DOWN ' , ' image-pan-down-fine ' , function ( ) _change_pan ( 0 , ImageControl.pan_step_slow ) end , { repeatable = true } )
_bind_image_key ( ' h ' , ' image-pan-h ' , function ( ) _change_pan ( - ImageControl.pan_step , 0 ) end , { repeatable = true } )
_bind_image_key ( ' l ' , ' image-pan-l ' , function ( ) _change_pan ( ImageControl.pan_step , 0 ) end , { repeatable = true } )
_bind_image_key ( ' j ' , ' image-pan-j ' , function ( ) _change_pan ( 0 , ImageControl.pan_step ) end , { repeatable = true } )
_bind_image_key ( ' k ' , ' image-pan-k ' , function ( ) _change_pan ( 0 , - ImageControl.pan_step ) end , { repeatable = true } )
_bind_image_key ( ' w ' , ' image-pan-w ' , function ( ) _change_pan ( 0 , - ImageControl.pan_step ) end , { repeatable = true } )
_bind_image_key ( ' s ' , ' image-pan-s ' , function ( ) _change_pan ( 0 , ImageControl.pan_step ) end , { repeatable = true } )
_bind_image_key ( ' a ' , ' image-pan-a ' , function ( ) _change_pan ( ImageControl.pan_step , 0 ) end , { repeatable = true } )
_bind_image_key ( ' d ' , ' image-pan-d ' , function ( ) _change_pan ( - ImageControl.pan_step , 0 ) end , { repeatable = true } )
_bind_image_key ( ' = ' , ' image-zoom-in ' , function ( ) _change_zoom ( ImageControl.zoom_step ) end , { repeatable = true } )
_disable_image_section ( )
_bind_image_key ( ' - ' , ' image-zoom-out ' , function ( ) _change_zoom ( - ImageControl.zoom_step ) end , { repeatable = true } )
_bind_image_key ( ' + ' , ' image-zoom-in-fine ' , function ( ) _change_zoom ( ImageControl.zoom_step_slow ) end , { repeatable = true } )
_bind_image_key ( ' _ ' , ' image-zoom-out-fine ' , function ( ) _change_zoom ( - ImageControl.zoom_step_slow ) end , { repeatable = true } )
_bind_image_key ( ' 0 ' , ' image-zoom-reset ' , _reset_pan_zoom )
_bind_image_key ( ' Space ' , ' image-status ' , function ( ) _show_image_status ( ' Image status ' ) end )
_bind_image_key ( ' f ' , ' image-screenshot ' , _capture_screenshot )
_install_q_block ( )
end
local function _deactivate_image_controls ( )
if not ImageControl.enabled then
return
end
ImageControl.enabled = false
_set_image_property ( false )
_restore_q_default ( )
_unbind_image_keys ( )
mp.osd_message ( ' Image viewer controls disabled ' , 1.0 )
mp.set_property ( ' panscan ' , 0 )
mp.set_property ( ' video-zoom ' , 0 )
mp.set_property_number ( ' video-pan-x ' , 0 )
mp.set_property_number ( ' video-pan-y ' , 0 )
mp.set_property ( ' video-align-x ' , ' 0 ' )
mp.set_property ( ' video-align-y ' , ' 0 ' )
end
local function _update_image_mode ( )
local should_image = _get_current_item_is_image ( )
if should_image then
_activate_image_controls ( )
else
_deactivate_image_controls ( )
end
end
mp.register_event ( ' file-loaded ' , function ( )
_update_image_mode ( )
end )
mp.register_event ( ' shutdown ' , function ( )
_restore_q_default ( )
end )
_update_image_mode ( )
2025-12-18 22:50:21 -08:00
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 ( {
2025-12-24 02:13:21 -08:00
-- Hide the PowerShell console window (dialog still shows).
args = { ' powershell ' , ' -NoProfile ' , ' -WindowStyle ' , ' Hidden ' , ' -STA ' , ' -ExecutionPolicy ' , ' Bypass ' , ' -Command ' , ps } ,
2025-12-18 22:50:21 -08:00
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
2026-01-07 05:09:59 -08:00
local function _store_names_key ( names )
if type ( names ) ~= ' table ' or # names == 0 then
return ' '
end
local normalized = { }
for _ , name in ipairs ( names ) do
normalized [ # normalized + 1 ] = trim ( tostring ( name or ' ' ) )
end
return table.concat ( normalized , ' \0 ' )
end
local function _run_pipeline_request_async ( pipeline_cmd , seeds , timeout_seconds , cb )
cb = cb or function ( ) end
pipeline_cmd = trim ( tostring ( pipeline_cmd or ' ' ) )
if pipeline_cmd == ' ' then
cb ( nil , ' empty pipeline command ' )
return
end
2025-12-18 22:50:21 -08:00
ensure_mpv_ipc_server ( )
2026-01-07 05:09:59 -08:00
local req = { pipeline = pipeline_cmd }
if seeds then
req.seeds = seeds
end
_run_helper_request_async ( req , timeout_seconds or 30 , cb )
end
local function _refresh_store_cache ( timeout_seconds , on_complete )
ensure_mpv_ipc_server ( )
local prev_count = ( type ( _cached_store_names ) == ' table ' ) and # _cached_store_names or 0
local prev_key = _store_names_key ( _cached_store_names )
2025-12-27 03:13:16 -08:00
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 ) )
2026-01-07 05:09:59 -08:00
2025-12-27 03:13:16 -08:00
if cached_json and cached_json ~= ' ' then
2026-01-07 05:09:59 -08:00
local function handle_cached ( resp )
if not resp or type ( resp ) ~= ' table ' or type ( resp.choices ) ~= ' table ' then
_lua_log ( ' stores: cache_parse result missing choices table; resp_type= ' .. tostring ( type ( resp ) ) )
return false
end
local out = { }
for _ , v in ipairs ( resp.choices ) do
local name = trim ( tostring ( v or ' ' ) )
if name ~= ' ' then
out [ # out + 1 ] = name
end
end
_cached_store_names = out
_store_cache_loaded = true
local preview = ' '
if # out > 0 then
preview = table.concat ( out , ' , ' )
end
_lua_log ( ' stores: loaded ' .. tostring ( # out ) .. ' stores from cache: ' .. tostring ( preview ) )
if type ( on_complete ) == ' function ' then
on_complete ( true , _store_names_key ( out ) ~= prev_key )
end
return true
end
2025-12-27 03:13:16 -08:00
local ok , cached_resp = pcall ( utils.parse_json , cached_json )
_lua_log ( ' stores: cache_parse ok= ' .. tostring ( ok ) .. ' resp_type= ' .. tostring ( type ( cached_resp ) ) )
if ok then
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
2026-01-07 05:09:59 -08:00
if ok then
if handle_cached ( cached_resp ) then
return true
2025-12-27 03:13:16 -08:00
end
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
_lua_log ( ' stores: requesting store-choices via helper (fallback) ' )
2026-01-07 05:09:59 -08:00
_run_helper_request_async ( { op = ' store-choices ' } , timeout_seconds or 1 , function ( resp , err )
local success = false
local changed = false
if resp and resp.success and type ( resp.choices ) == ' table ' then
local out = { }
for _ , v in ipairs ( resp.choices ) do
local name = trim ( tostring ( v or ' ' ) )
if name ~= ' ' then
out [ # out + 1 ] = name
end
end
_cached_store_names = out
_store_cache_loaded = true
local preview = ' '
if # out > 0 then
preview = table.concat ( out , ' , ' )
end
_lua_log ( ' stores: loaded ' .. tostring ( # out ) .. ' stores via helper request: ' .. tostring ( preview ) )
success = true
changed = ( # out ~= prev_count ) or ( _store_names_key ( out ) ~= prev_key )
else
_lua_log (
' stores: failed to load store choices via helper; success= '
.. tostring ( resp and resp.success or false )
.. ' choices_type= '
.. tostring ( resp and type ( resp.choices ) or ' nil ' )
.. ' stderr= '
.. tostring ( resp and resp.stderr or ' ' )
.. ' error= '
.. tostring ( resp and resp.error or err or ' ' )
)
end
if type ( on_complete ) == ' function ' then
on_complete ( success , changed )
2025-12-18 22:50:21 -08:00
end
2026-01-07 05:09:59 -08:00
end )
return false
2025-12-18 22:50:21 -08:00
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
2025-12-27 03:13:16 -08:00
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 )
2026-01-07 05:09:59 -08:00
_refresh_store_cache ( 1.2 , function ( success , changed )
if success and changed then
_lua_log ( ' stores: reopening menu (store list changed) ' )
_uosc_open_list_picker ( STORE_PICKER_MENU_TYPE , ' Store ' , build_items ( ) )
end
end )
2025-12-27 03:13:16 -08:00
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 )
2025-12-18 22:50:21 -08:00
-- 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
2025-12-19 02:29:42 -08:00
-- Per-file state (class-like) for format caching.
local FileState = { }
FileState.__index = FileState
function FileState . new ( )
return setmetatable ( {
url = nil ,
formats = nil ,
} , FileState )
end
function FileState : has_formats ( )
return type ( self.formats ) == ' table '
and type ( self.formats . rows ) == ' table '
and # self.formats . rows > 0
end
function FileState : set_formats ( url , tbl )
self.url = url
self.formats = tbl
self.formats_table = tbl
end
M.file = M.file or FileState.new ( )
2025-12-18 22:50:21 -08:00
-- Cache yt-dlp format lists per URL so Change Format is instant.
local _formats_cache = { }
local _formats_inflight = { }
2025-12-19 02:29:42 -08:00
local _formats_waiters = { }
local _ipc_async_busy = false
local _ipc_async_queue = { }
2025-12-18 22:50:21 -08:00
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 ( ) }
2025-12-19 02:29:42 -08:00
if type ( M.file ) == ' table ' and M.file . set_formats then
M.file : set_formats ( url , tbl )
else
M.file . url = url
M.file . formats = tbl
M.file . formats_table = tbl
end
2025-12-18 22:50:21 -08:00
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
2025-12-19 02:29:42 -08:00
function FileState : fetch_formats ( cb )
local url = tostring ( self.url or ' ' )
2025-12-18 22:50:21 -08:00
if url == ' ' or not _is_http_url ( url ) then
2025-12-19 02:29:42 -08:00
if cb then cb ( false , ' not a url ' ) end
2025-12-18 22:50:21 -08:00
return
end
if _extract_store_hash ( url ) then
2025-12-19 02:29:42 -08:00
if cb then cb ( false , ' store-hash url ' ) end
2025-12-18 22:50:21 -08:00
return
end
2025-12-19 02:29:42 -08:00
local cached = _get_cached_formats_table ( url )
if type ( cached ) == ' table ' then
self : set_formats ( url , cached )
if cb then cb ( true , nil ) end
2025-12-18 22:50:21 -08:00
return
end
2025-12-19 02:29:42 -08:00
2025-12-18 22:50:21 -08:00
if _formats_inflight [ url ] then
2025-12-19 02:29:42 -08:00
_formats_waiters [ url ] = _formats_waiters [ url ] or { }
if cb then table.insert ( _formats_waiters [ url ] , cb ) end
2025-12-18 22:50:21 -08:00
return
end
_formats_inflight [ url ] = true
2025-12-19 02:29:42 -08:00
_formats_waiters [ url ] = _formats_waiters [ url ] or { }
if cb then table.insert ( _formats_waiters [ url ] , cb ) end
2025-12-18 22:50:21 -08:00
2025-12-19 02:29:42 -08:00
_run_helper_request_async ( { op = ' ytdlp-formats ' , data = { url = url } } , 90 , function ( resp , err )
2025-12-18 22:50:21 -08:00
_formats_inflight [ url ] = nil
2025-12-19 02:29:42 -08:00
local ok = false
local reason = err
2025-12-18 22:50:21 -08:00
if resp and resp.success and type ( resp.table ) == ' table ' then
2025-12-19 02:29:42 -08:00
ok = true
reason = nil
self : set_formats ( url , resp.table )
2025-12-18 22:50:21 -08:00
_cache_formats_for_url ( url , resp.table )
_lua_log ( ' formats: cached ' .. tostring ( ( resp.table . rows and # resp.table . rows ) or 0 ) .. ' rows for url ' )
2025-12-19 02:29:42 -08:00
else
if type ( resp ) == ' table ' then
if resp.error and tostring ( resp.error ) ~= ' ' then
reason = tostring ( resp.error )
elseif resp.stderr and tostring ( resp.stderr ) ~= ' ' then
reason = tostring ( resp.stderr )
end
end
end
local waiters = _formats_waiters [ url ] or { }
_formats_waiters [ url ] = nil
for _ , fn in ipairs ( waiters ) do
pcall ( fn , ok , reason )
2025-12-18 22:50:21 -08:00
end
end )
end
2025-12-19 02:29:42 -08:00
local function _prefetch_formats_for_url ( url )
url = tostring ( url or ' ' )
if url == ' ' or not _is_http_url ( url ) then
return
end
if type ( M.file ) == ' table ' then
M.file . url = url
if M.file . fetch_formats then
M.file : fetch_formats ( nil )
end
end
end
2025-12-18 22:50:21 -08:00
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
2025-12-19 02:29:42 -08:00
local function _debug_dump_formatted_formats ( url , tbl , items )
local row_count = 0
if type ( tbl ) == ' table ' and type ( tbl.rows ) == ' table ' then
row_count = # tbl.rows
end
local item_count = 0
if type ( items ) == ' table ' then
item_count = # items
end
_lua_log ( ' formats-dump: url= ' .. tostring ( url or ' ' ) .. ' rows= ' .. tostring ( row_count ) .. ' menu_items= ' .. tostring ( item_count ) )
-- Dump the formatted picker items (first 30) so we can confirm the
-- list is being built and looks sane.
if type ( items ) == ' table ' then
local limit = 30
for i = 1 , math.min ( # items , limit ) do
local it = items [ i ] or { }
local title = tostring ( it.title or ' ' )
local hint = tostring ( it.hint or ' ' )
_lua_log ( ' formats-item[ ' .. tostring ( i ) .. ' ]: ' .. title .. ( hint ~= ' ' and ( ' | ' .. hint ) or ' ' ) )
end
if # items > limit then
_lua_log ( ' formats-dump: (truncated; total= ' .. tostring ( # items ) .. ' ) ' )
end
end
end
2025-12-18 22:50:21 -08:00
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
2026-01-07 05:09:59 -08:00
local function _run_pipeline_detached ( pipeline_cmd , on_failure )
2025-12-18 22:50:21 -08:00
if not pipeline_cmd or pipeline_cmd == ' ' then
return false
end
2026-01-07 05:09:59 -08:00
ensure_mpv_ipc_server ( )
if not ensure_pipeline_helper_running ( ) then
if type ( on_failure ) == ' function ' then
on_failure ( nil , ' helper not running ' )
end
return false
end
_run_helper_request_async ( { op = ' run-detached ' , data = { pipeline = pipeline_cmd } } , 1.0 , function ( resp , err )
if resp and resp.success then
return
end
if type ( on_failure ) == ' function ' then
on_failure ( resp , err )
end
end )
return true
2025-12-18 22:50:21 -08:00
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
2026-01-07 05:09:59 -08:00
_refresh_store_cache ( 1.5 , function ( success , changed )
if success and changed then
2025-12-18 22:50:21 -08:00
_uosc_open_list_picker ( DOWNLOAD_STORE_MENU_TYPE , ' Save location ' , build_items ( ) )
end
2026-01-07 05:09:59 -08:00
end )
2025-12-18 22:50:21 -08:00
end )
end
-- Prime store cache shortly after load (best-effort; picker also refreshes on-demand).
mp.add_timeout ( 0.10 , function ( )
2025-12-27 03:13:16 -08:00
pcall ( _ensure_selected_store_loaded )
2025-12-18 22:50:21 -08:00
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 ( )
2026-01-07 05:09:59 -08:00
local pipeline_cmd = ' get-file -store ' .. quote_pipeline_arg ( store_hash.store ) .. ' -query ' .. quote_pipeline_arg ( ' hash: ' .. store_hash.hash ) .. ' -path ' .. quote_pipeline_arg ( folder )
M.run_pipeline ( pipeline_cmd , nil , function ( _ , err )
if err then
mp.osd_message ( ' Download failed: ' .. tostring ( err ) , 5 )
end
end )
2025-12-18 22:50:21 -08:00
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 )
2025-12-19 02:29:42 -08:00
-- Ensure file state is tracking the current URL.
if type ( M.file ) == ' table ' then
M.file . url = url
end
2025-12-18 22:50:21 -08:00
-- If formats were already prefetched for this URL, open instantly.
2025-12-19 02:29:42 -08:00
local cached_tbl = nil
if type ( M.file ) == ' table ' and type ( M.file . formats ) == ' table ' then
cached_tbl = M.file . formats
else
cached_tbl = _get_cached_formats_table ( url )
end
2025-12-18 22:50:21 -08:00
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
2025-12-19 02:29:42 -08:00
_debug_dump_formatted_formats ( url , cached_tbl , items )
2025-12-18 22:50:21 -08:00
_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 ' )
2025-12-19 02:29:42 -08:00
-- Non-blocking: ask the per-file state to fetch formats in the background.
if type ( M.file ) == ' table ' and M.file . fetch_formats then
_lua_log ( ' change-format: formats not cached yet; fetching in background ' )
M.file : fetch_formats ( function ( ok , err )
if type ( _pending_format_change ) ~= ' table ' or _pending_format_change.token ~= token then
return
end
if not ok then
local msg2 = tostring ( err or ' ' )
if msg2 == ' ' then
msg2 = ' unknown '
2025-12-18 22:50:21 -08:00
end
2025-12-19 02:29:42 -08:00
_lua_log ( ' change-format: formats failed: ' .. msg2 )
mp.osd_message ( ' Failed to load format list: ' .. msg2 , 7 )
_uosc_open_list_picker ( DOWNLOAD_FORMAT_MENU_TYPE , ' Change format ' , {
{
title = ' Failed to load format list ' ,
hint = msg2 ,
value = { ' script-message-to ' , mp.get_script_name ( ) , ' medios-nop ' , ' {} ' } ,
} ,
} )
return
2025-12-18 22:50:21 -08:00
end
2025-12-19 02:29:42 -08:00
local tbl = ( type ( M.file . formats ) == ' table ' ) and M.file . formats or _get_cached_formats_table ( url )
if type ( tbl ) ~= ' table ' or type ( tbl.rows ) ~= ' table ' or # tbl.rows == 0 then
mp.osd_message ( ' No formats available ' , 4 )
return
2025-12-18 22:50:21 -08:00
end
2025-12-19 02:29:42 -08:00
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
2025-12-18 22:50:21 -08:00
2025-12-19 02:29:42 -08:00
_pending_format_change.formats_table = tbl
_debug_dump_formatted_formats ( url , tbl , items )
_uosc_open_list_picker ( DOWNLOAD_FORMAT_MENU_TYPE , ' Change format ' , items )
end )
end
2025-12-18 22:50:21 -08:00
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 )
2026-01-01 20:37:27 -08:00
local pipeline_cmd = ' download-file -url ' .. quote_pipeline_arg ( url ) .. ' -format ' .. quote_pipeline_arg ( fmt )
2025-12-18 22:50:21 -08:00
.. ' | add-file -store ' .. quote_pipeline_arg ( store )
2026-01-07 05:09:59 -08:00
local function run_pipeline_direct ( )
M.run_pipeline ( pipeline_cmd , nil , function ( _ , err )
if err then
mp.osd_message ( ' Download failed: ' .. tostring ( err ) , 5 )
end
end )
end
if not _run_pipeline_detached ( pipeline_cmd , function ( )
run_pipeline_direct ( )
end ) then
run_pipeline_direct ( )
2025-12-18 22:50:21 -08:00
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 )
2026-01-01 20:37:27 -08:00
local pipeline_cmd = ' download-file -url ' .. quote_pipeline_arg ( url ) .. ' -format ' .. quote_pipeline_arg ( fmt )
2025-12-18 22:50:21 -08:00
.. ' | add-file -path ' .. quote_pipeline_arg ( folder )
2026-01-07 05:09:59 -08:00
local function run_pipeline_direct ( )
M.run_pipeline ( pipeline_cmd , nil , function ( _ , err )
if err then
mp.osd_message ( ' Download failed: ' .. tostring ( err ) , 5 )
end
end )
end
if not _run_pipeline_detached ( pipeline_cmd , function ( )
run_pipeline_direct ( )
end ) then
run_pipeline_direct ( )
2025-12-18 22:50:21 -08:00
end
mp.osd_message ( ' Download started ' , 3 )
_pending_download = nil
end )
2025-12-17 17:42:46 -08:00
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
2026-01-12 17:55:04 -08:00
-- Ref: mpv_lua_api.py was removed in favor of pipeline_helper (run_pipeline_via_ipc_response).
-- This placeholder comment ensures we don't have code shifting issues.
2025-11-27 10:59:01 -08:00
2026-01-03 03:37:48 -08:00
-- Run a Medeia pipeline command via the Python pipeline helper (IPC request/response).
2026-01-07 05:09:59 -08:00
-- Calls the callback with stdout on success or error message on failure.
function M . run_pipeline ( pipeline_cmd , seeds , cb )
2026-02-03 17:14:11 -08:00
_lua_log ( ' M.run_pipeline called with cmd: ' .. tostring ( pipeline_cmd ) )
2026-01-07 05:09:59 -08:00
cb = cb or function ( ) end
2026-01-03 03:37:48 -08:00
pipeline_cmd = trim ( tostring ( pipeline_cmd or ' ' ) )
if pipeline_cmd == ' ' then
2026-02-03 17:14:11 -08:00
_lua_log ( ' M.run_pipeline: empty command ' )
2026-01-07 05:09:59 -08:00
cb ( nil , ' empty pipeline command ' )
return
2026-01-03 03:37:48 -08:00
end
ensure_mpv_ipc_server ( )
2026-02-04 16:59:04 -08:00
-- Use shorter timeout for .mpv -url commands since they just queue the URL (non-blocking)
-- The actual URL resolution happens asynchronously in MPV itself
local timeout_seconds = pipeline_cmd : match ( ' %.mpv%s+%-url ' ) and 10 or 30
2026-02-03 17:14:11 -08:00
_run_pipeline_request_async ( pipeline_cmd , seeds , timeout_seconds , function ( resp , err )
_lua_log ( ' M.run_pipeline callback fired: resp= ' .. tostring ( resp ) .. ' , err= ' .. tostring ( err ) )
2026-01-07 05:09:59 -08:00
if resp and resp.success then
2026-02-03 17:14:11 -08:00
_lua_log ( ' M.run_pipeline: success ' )
2026-01-07 05:09:59 -08:00
cb ( resp.stdout or ' ' , nil )
return
2026-01-03 03:37:48 -08:00
end
2026-01-07 05:09:59 -08:00
local details = err or ' '
if details == ' ' and type ( resp ) == ' table ' then
if resp.error and tostring ( resp.error ) ~= ' ' then
details = tostring ( resp.error )
elseif resp.stderr and tostring ( resp.stderr ) ~= ' ' then
details = tostring ( resp.stderr )
end
end
if details == ' ' then
details = ' unknown '
end
_lua_log ( ' pipeline failed cmd= ' .. tostring ( pipeline_cmd ) .. ' err= ' .. details )
cb ( nil , details )
end )
2026-01-03 03:37:48 -08:00
end
2025-11-27 10:59:01 -08:00
-- Helper to run pipeline and parse JSON output
2026-01-07 05:09:59 -08:00
function M . run_pipeline_json ( pipeline_cmd , seeds , cb )
cb = cb or function ( ) end
if not pipeline_cmd : match ( ' output%-json$ ' ) then
pipeline_cmd = pipeline_cmd .. ' | output-json '
end
M.run_pipeline ( pipeline_cmd , seeds , function ( output , err )
if output then
local ok , data = pcall ( utils.parse_json , output )
if ok then
cb ( data , nil )
return
end
_lua_log ( ' Failed to parse JSON: ' .. output )
cb ( nil , ' malformed JSON response ' )
return
2025-11-27 10:59:01 -08:00
end
2026-01-07 05:09:59 -08:00
cb ( nil , err )
end )
2025-11-27 10:59:01 -08:00
end
-- Command: Get info for current file
function M . get_file_info ( )
2026-01-07 05:09:59 -08:00
local path = mp.get_property ( ' path ' )
if not path then
return
2025-11-27 10:59:01 -08:00
end
2026-01-07 05:09:59 -08:00
local seed = { { path = path } }
M.run_pipeline_json ( ' get-metadata ' , seed , function ( data , err )
if data then
_lua_log ( ' Metadata: ' .. utils.format_json ( data ) )
mp.osd_message ( ' Metadata loaded (check console) ' , 3 )
return
end
if err then
mp.osd_message ( ' Failed to load metadata: ' .. tostring ( err ) , 3 )
end
end )
2025-11-27 10:59:01 -08:00
end
-- Command: Delete current file
function M . delete_current_file ( )
2026-01-07 05:09:59 -08:00
local path = mp.get_property ( ' path ' )
if not path then
return
end
2025-11-27 10:59:01 -08:00
local seed = { { path = path } }
2026-01-07 05:09:59 -08:00
M.run_pipeline ( ' delete-file ' , seed , function ( _ , err )
if err then
mp.osd_message ( ' Delete failed: ' .. tostring ( err ) , 3 )
return
end
mp.osd_message ( ' File deleted ' , 3 )
mp.command ( ' playlist-next ' )
end )
2025-11-27 10:59:01 -08:00
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 ( )
2026-02-03 17:14:11 -08:00
_lua_log ( ' open_load_url_prompt called ' )
2025-12-17 17:42:46 -08:00
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
2026-02-03 17:14:11 -08:00
_lua_log ( ' open_load_url_prompt: sending menu to uosc ' )
2025-12-18 22:50:21 -08:00
mp.commandv ( ' script-message-to ' , ' uosc ' , ' open-menu ' , json )
else
2026-02-03 17:14:11 -08:00
_lua_log ( ' menu: uosc not available; cannot open-menu for load-url ' )
2025-12-18 22:50:21 -08:00
end
2025-12-17 17:42:46 -08:00
end
2025-12-23 16:36:39 -08:00
-- Open the command submenu with tailored cmdlets (screenshot, clip, trim prompt)
function M . open_cmd_menu ( )
local items = {
{
title = ' Screenshot ' ,
hint = ' Capture a screenshot ' ,
value = { ' script-message-to ' , mp.get_script_name ( ) , ' medios-cmd-exec ' , utils.format_json ( { cmd = ' screenshot ' } ) } ,
} ,
{
title = ' Capture clip marker ' ,
hint = ' Place a clip marker at current time ' ,
value = { ' script-message-to ' , mp.get_script_name ( ) , ' medios-cmd-exec ' , utils.format_json ( { cmd = ' clip ' } ) } ,
} ,
{
title = ' Trim file ' ,
hint = ' Trim current file (prompt for range) ' ,
value = { ' script-message-to ' , mp.get_script_name ( ) , ' medios-cmd-exec ' , utils.format_json ( { cmd = ' trim ' } ) } ,
} ,
}
local menu_data = {
type = CMD_MENU_TYPE ,
title = ' Cmd ' ,
search_style = ' palette ' ,
search_debounce = ' submit ' ,
footnote = ' Type to filter or pick a command ' ,
items = items ,
}
local json = utils.format_json ( menu_data )
if ensure_uosc_loaded ( ) then
mp.commandv ( ' script-message-to ' , ' uosc ' , ' open-menu ' , json )
else
_lua_log ( ' menu: uosc not available; cannot open cmd menu ' )
end
end
-- Prompt for trim range via an input box and callback
local function _start_trim_with_range ( range )
2025-12-27 03:13:16 -08:00
_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
2025-12-23 16:36:39 -08:00
range = trim ( tostring ( range or ' ' ) )
2025-12-27 03:13:16 -08:00
_lua_log ( ' trim: after_trim range= ' .. tostring ( range ) )
2025-12-23 16:36:39 -08:00
if range == ' ' then
mp.osd_message ( ' Trim cancelled (no range provided) ' , 3 )
2025-12-27 03:13:16 -08:00
_lua_log ( ' trim: CANCELLED - empty range ' )
2025-12-23 16:36:39 -08:00
return
end
local target = _current_target ( )
if not target or target == ' ' then
mp.osd_message ( ' No file to trim ' , 3 )
2025-12-27 03:13:16 -08:00
_lua_log ( ' trim: FAILED - no target ' )
2025-12-23 16:36:39 -08:00
return
end
2025-12-27 03:13:16 -08:00
_lua_log ( ' trim: target= ' .. tostring ( target ) )
2025-12-23 16:36:39 -08:00
local store_hash = _extract_store_hash ( target )
2025-12-27 03:13:16 -08:00
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 ' ) )
2025-12-23 16:36:39 -08:00
local stream = trim ( tostring ( mp.get_property ( ' stream-open-filename ' ) or ' ' ) )
if stream == ' ' then
stream = tostring ( target )
end
2025-12-27 03:13:16 -08:00
_lua_log ( ' trim: stream= ' .. tostring ( stream ) )
2025-12-23 16:36:39 -08:00
2025-12-27 03:13:16 -08:00
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 ' ) )
2025-12-23 16:36:39 -08:00
if store_hash then
2025-12-27 03:13:16 -08:00
-- Original file is from a store - set relationship to it
_lua_log ( ' trim: building store file pipeline (original from store) ' )
if selected_store then
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 " ' .. selected_store .. ' " ' ..
' | add-relationship -store " ' .. selected_store .. ' " ' ..
' -to-hash ' .. quote_pipeline_arg ( store_hash.hash )
else
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
2025-12-23 16:36:39 -08:00
else
2025-12-27 03:13:16 -08:00
-- 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 )
2025-12-23 16:36:39 -08:00
else
2025-12-27 03:13:16 -08:00
mp.osd_message ( ' Trim complete: ' .. output_path , 5 )
_lua_log ( ' trim: no store selected, trim complete at ' .. output_path )
return
2025-12-23 16:36:39 -08:00
end
end
2025-12-27 03:13:16 -08:00
if not pipeline_cmd or pipeline_cmd == ' ' then
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 )
2026-01-12 17:55:04 -08:00
_lua_log ( ' trim: === CALLING PIPELINE HELPER FOR UPLOAD === ' )
2025-12-27 03:13:16 -08:00
2026-01-12 17:55:04 -08:00
-- Optimization: use persistent pipeline helper
local response = run_pipeline_via_ipc_response ( pipeline_cmd , nil , 60 )
if not response then
response = { success = false , error = " Timeout or IPC error " }
end
2025-12-27 03:13:16 -08:00
_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 )
2025-12-23 16:36:39 -08:00
end
end
function M . open_trim_prompt ( )
2025-12-27 03:13:16 -08:00
_lua_log ( ' === OPEN_TRIM_PROMPT called ' )
2025-12-23 16:36:39 -08:00
local marker_range = _get_trim_range_from_clip_markers ( )
2025-12-27 03:13:16 -08:00
_lua_log ( ' trim_prompt: marker_range= ' .. tostring ( marker_range or ' NONE ' ) )
2025-12-23 16:36:39 -08:00
if marker_range then
2025-12-27 03:13:16 -08:00
_lua_log ( ' trim_prompt: using auto-detected markers, starting trim ' )
mp.osd_message ( ' Using clip markers: ' .. marker_range , 2 )
2025-12-23 16:36:39 -08:00
_start_trim_with_range ( marker_range )
return
end
2025-12-27 03:13:16 -08:00
_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) '
2025-12-23 16:36:39 -08:00
local menu_data = {
type = TRIM_PROMPT_MENU_TYPE ,
title = ' Trim file ' ,
search_style = ' palette ' ,
search_debounce = ' submit ' ,
on_search = ' callback ' ,
2025-12-27 03:13:16 -08:00
footnote = " Enter time range (e.g. '00:03:45-00:03:55' or '1h3m-1h10m30s') and press Enter " .. store_hint ,
2025-12-23 16:36:39 -08:00
callback = { mp.get_script_name ( ) , ' medios-trim-run ' } ,
items = {
{
title = ' Enter range... ' ,
hint = ' Type range and press Enter ' ,
value = { ' script-message-to ' , mp.get_script_name ( ) , ' medios-trim-run ' } ,
}
}
}
local json = utils.format_json ( menu_data )
if ensure_uosc_loaded ( ) then
mp.commandv ( ' script-message-to ' , ' uosc ' , ' open-menu ' , json )
else
_lua_log ( ' menu: uosc not available; cannot open trim prompt ' )
end
end
-- Handlers for the command submenu
mp.register_script_message ( ' medios-open-cmd ' , function ( )
M.open_cmd_menu ( )
end )
mp.register_script_message ( ' medios-cmd-exec ' , function ( json )
local ok , ev = pcall ( utils.parse_json , json )
if not ok or type ( ev ) ~= ' table ' then
return
end
local cmd = trim ( tostring ( ev.cmd or ' ' ) )
if cmd == ' screenshot ' then
_capture_screenshot ( )
elseif cmd == ' clip ' then
_capture_clip ( )
elseif cmd == ' trim ' then
M.open_trim_prompt ( )
else
mp.osd_message ( ' Unknown cmd ' .. tostring ( cmd ) , 2 )
end
end )
mp.register_script_message ( ' medios-trim-run ' , function ( json )
local ok , ev = pcall ( utils.parse_json , json )
local range = nil
if ok and type ( ev ) == ' table ' then
if ev.type == ' search ' then
range = trim ( tostring ( ev.query or ' ' ) )
end
end
_start_trim_with_range ( range )
end )
2025-12-17 17:42:46 -08:00
mp.register_script_message ( ' medios-load-url ' , function ( )
2026-02-03 17:14:11 -08:00
_lua_log ( ' medios-load-url handler called ' )
2026-02-03 18:43:54 -08:00
-- Close the main menu first
if ensure_uosc_loaded ( ) then
_lua_log ( ' medios-load-url: closing main menu before opening Load URL prompt ' )
mp.commandv ( ' script-message-to ' , ' uosc ' , ' close-menu ' )
end
2025-12-17 17:42:46 -08:00
M.open_load_url_prompt ( )
end )
2026-02-03 17:14:11 -08:00
mp.register_script_message ( ' medios-start-helper ' , function ( )
-- Asynchronously start the pipeline helper without blocking the menu.
attempt_start_pipeline_helper_async ( function ( success )
if success then
mp.osd_message ( ' Pipeline helper started ' , 2 )
else
mp.osd_message ( ' Failed to start pipeline helper (check logs) ' , 3 )
end
end )
end )
2025-12-17 17:42:46 -08:00
mp.register_script_message ( ' medios-load-url-event ' , function ( json )
2026-02-03 18:46:13 -08:00
_lua_log ( ' [LOAD-URL] Event handler called with: ' .. tostring ( json or ' nil ' ) )
2025-12-17 17:42:46 -08:00
local ok , event = pcall ( utils.parse_json , json )
if not ok or type ( event ) ~= ' table ' then
2026-02-03 18:46:13 -08:00
_lua_log ( ' [LOAD-URL] Failed to parse JSON: ' .. tostring ( json ) )
2026-02-03 17:14:11 -08:00
mp.osd_message ( ' Failed to parse URL ' , 2 )
if ensure_uosc_loaded ( ) then
2026-02-03 18:46:13 -08:00
_lua_log ( ' [LOAD-URL] Closing menu due to parse error ' )
2026-02-03 17:14:11 -08:00
mp.commandv ( ' script-message-to ' , ' uosc ' , ' close-menu ' , LOAD_URL_MENU_TYPE )
end
2025-12-17 17:42:46 -08:00
return
end
2026-02-03 18:46:13 -08:00
_lua_log ( ' [LOAD-URL] Parsed event: type= ' .. tostring ( event.type ) .. ' , query= ' .. tostring ( event.query ) )
2025-12-17 17:42:46 -08:00
if event.type ~= ' search ' then
2026-02-03 18:46:13 -08:00
_lua_log ( ' [LOAD-URL] Event type is not search: ' .. tostring ( event.type ) )
2026-02-03 17:14:11 -08:00
if ensure_uosc_loaded ( ) then
2026-02-03 18:46:13 -08:00
_lua_log ( ' [LOAD-URL] Closing menu due to type mismatch ' )
2026-02-03 17:14:11 -08:00
mp.commandv ( ' script-message-to ' , ' uosc ' , ' close-menu ' , LOAD_URL_MENU_TYPE )
end
2025-12-17 17:42:46 -08:00
return
end
local url = trim ( tostring ( event.query or ' ' ) )
2026-02-03 18:46:13 -08:00
_lua_log ( ' [LOAD-URL] Trimmed URL: " ' .. url .. ' " ' )
2025-12-17 17:42:46 -08:00
if url == ' ' then
2026-02-03 18:46:13 -08:00
_lua_log ( ' [LOAD-URL] URL is empty ' )
2026-02-04 16:59:04 -08:00
_log_all ( ' ERROR ' , ' Load URL failed: URL is empty ' )
2026-02-03 17:14:11 -08:00
mp.osd_message ( ' URL is empty ' , 2 )
if ensure_uosc_loaded ( ) then
2026-02-03 18:46:13 -08:00
_lua_log ( ' [LOAD-URL] Closing menu due to empty URL ' )
2026-02-03 17:14:11 -08:00
mp.commandv ( ' script-message-to ' , ' uosc ' , ' close-menu ' , LOAD_URL_MENU_TYPE )
end
2025-12-17 17:42:46 -08:00
return
end
2026-02-03 17:14:11 -08:00
mp.osd_message ( ' Loading URL... ' , 1 )
2026-02-04 16:59:04 -08:00
_log_all ( ' INFO ' , ' Load URL started: ' .. url )
2026-02-03 18:46:13 -08:00
_lua_log ( ' [LOAD-URL] Starting to load: ' .. url )
2026-02-03 17:14:11 -08:00
local function close_menu ( )
2026-02-03 18:46:13 -08:00
_lua_log ( ' [LOAD-URL] Closing menu ' )
2026-02-03 17:14:11 -08:00
if ensure_uosc_loaded ( ) then
2026-02-03 18:46:13 -08:00
_lua_log ( ' [LOAD-URL] Sending close-menu command to UOSC ' )
2026-02-03 17:14:11 -08:00
mp.commandv ( ' script-message-to ' , ' uosc ' , ' close-menu ' , LOAD_URL_MENU_TYPE )
2026-02-03 18:46:13 -08:00
else
_lua_log ( ' [LOAD-URL] UOSC not loaded, cannot close menu ' )
2026-02-03 17:14:11 -08:00
end
end
-- First, always try direct loadfile. This is the fastest path.
local can_direct = _url_can_direct_load ( url )
2026-02-03 18:46:13 -08:00
_lua_log ( ' [LOAD-URL] Checking if URL can be loaded directly: ' .. tostring ( can_direct ) )
2026-02-03 17:14:11 -08:00
if can_direct then
2026-02-03 18:46:13 -08:00
_lua_log ( ' [LOAD-URL] Attempting direct loadfile ' )
2026-02-03 17:14:11 -08:00
local ok_load = pcall ( mp.commandv , ' loadfile ' , url , ' replace ' )
if ok_load then
2026-02-03 18:46:13 -08:00
_lua_log ( ' [LOAD-URL] Direct loadfile command sent successfully ' )
2026-02-04 16:59:04 -08:00
_log_all ( ' INFO ' , ' Load URL succeeded via direct load ' )
2026-02-03 17:14:11 -08:00
mp.osd_message ( ' URL loaded ' , 2 )
close_menu ( )
return
else
2026-02-03 18:46:13 -08:00
_lua_log ( ' [LOAD-URL] Direct loadfile command failed ' )
2026-02-04 16:59:04 -08:00
_log_all ( ' ERROR ' , ' Load URL failed: direct loadfile command failed ' )
2026-02-03 17:14:11 -08:00
mp.osd_message ( ' Load URL failed (direct) ' , 3 )
close_menu ( )
return
end
end
-- Complex streams (YouTube, DASH, etc.) need the pipeline helper.
2026-02-03 18:46:13 -08:00
_lua_log ( ' [LOAD-URL] URL requires pipeline helper for processing ' )
2025-12-18 22:50:21 -08:00
ensure_mpv_ipc_server ( )
2026-02-03 17:14:11 -08:00
local helper_ready = ensure_pipeline_helper_running ( )
2026-02-03 18:46:13 -08:00
_lua_log ( ' [LOAD-URL] Pipeline helper ready: ' .. tostring ( helper_ready ) )
2026-02-03 17:14:11 -08:00
if not helper_ready then
2026-02-04 16:59:04 -08:00
_lua_log ( ' [LOAD-URL] Pipeline helper not available, attempting to start... ' )
_log_all ( ' WARN ' , ' Pipeline helper not running, attempting auto-start ' )
mp.osd_message ( ' Starting pipeline helper... ' , 2 )
-- Attempt to start the helper asynchronously
attempt_start_pipeline_helper_async ( function ( success )
if success then
_lua_log ( ' [LOAD-URL] Helper started successfully, retry Load URL from menu ' )
_log_all ( ' INFO ' , ' Pipeline helper started successfully ' )
mp.osd_message ( ' Helper started! Try Load URL again ' , 2 )
else
_lua_log ( ' [LOAD-URL] Failed to start helper ' )
_log_all ( ' ERROR ' , ' Failed to start pipeline helper ' )
mp.osd_message ( ' Could not start pipeline helper ' , 3 )
end
close_menu ( )
end )
2026-02-03 17:14:11 -08:00
return
end
-- Use pipeline to download/prepare the URL
2026-01-07 05:09:59 -08:00
local pipeline_cmd = ' .mpv -url ' .. quote_pipeline_arg ( url ) .. ' -play '
2026-02-03 18:46:13 -08:00
_lua_log ( ' [LOAD-URL] Sending to pipeline: ' .. pipeline_cmd )
2026-02-04 16:59:04 -08:00
_lua_log ( ' [LOAD-URL] Pipeline helper ready: ' .. tostring ( _is_pipeline_helper_ready ( ) ) )
-- Add a timeout message after a delay to give user feedback
local timeout_timer = nil
timeout_timer = mp.add_timeout ( 5 , function ( )
if timeout_timer then
mp.osd_message ( ' Still loading... (helper may be resolving URL) ' , 2 )
_log_all ( ' WARN ' , ' Load URL still processing after 5 seconds ' )
_lua_log ( ' [LOAD-URL] Timeout message shown (helper still processing) ' )
end
end )
2026-02-03 17:14:11 -08:00
M.run_pipeline ( pipeline_cmd , nil , function ( resp , err )
2026-02-04 16:59:04 -08:00
if timeout_timer then
timeout_timer : kill ( )
timeout_timer = nil
end
2026-02-03 18:46:13 -08:00
_lua_log ( ' [LOAD-URL] Pipeline callback received: resp= ' .. tostring ( resp ) .. ' , err= ' .. tostring ( err ) )
2026-01-07 05:09:59 -08:00
if err then
2026-02-03 18:46:13 -08:00
_lua_log ( ' [LOAD-URL] Pipeline error: ' .. tostring ( err ) )
2026-02-04 16:59:04 -08:00
_log_all ( ' ERROR ' , ' Load URL pipeline failed: ' .. tostring ( err ) )
2026-01-07 05:09:59 -08:00
mp.osd_message ( ' Load URL failed: ' .. tostring ( err ) , 3 )
2026-02-03 17:14:11 -08:00
close_menu ( )
2026-01-07 05:09:59 -08:00
return
end
2026-02-03 18:46:13 -08:00
_lua_log ( ' [LOAD-URL] URL loaded successfully ' )
2026-02-04 16:59:04 -08:00
_log_all ( ' INFO ' , ' Load URL succeeded ' )
2026-02-03 17:14:11 -08:00
mp.osd_message ( ' URL loaded ' , 2 )
close_menu ( )
2026-01-07 05:09:59 -08:00
end )
2025-12-17 17:42:46 -08:00
end )
2025-11-27 10:59:01 -08:00
-- Menu integration with UOSC
function M . show_menu ( )
2026-02-03 18:26:36 -08:00
_lua_log ( ' [MENU] M.show_menu called ' )
-- Build menu items
2026-02-03 18:43:54 -08:00
-- Note: UOSC expects command strings, not arrays
2026-02-03 17:14:11 -08:00
local items = {
2026-02-03 18:43:54 -08:00
{ title = " Load URL " , value = " script-message medios-load-url " } ,
2026-02-03 17:14:11 -08:00
{ title = " Get Metadata " , value = " script-binding medios-info " , hint = " Ctrl+i " } ,
{ title = " Delete File " , value = " script-binding medios-delete " , hint = " Ctrl+Del " } ,
2026-02-03 18:43:54 -08:00
{ title = " Cmd " , value = " script-message medios-open-cmd " , hint = " screenshot/trim/etc " } ,
{ title = " Download " , value = " script-message medios-download-current " } ,
{ title = " Change Format " , value = " script-message medios-change-format-current " } ,
2026-02-03 17:14:11 -08:00
}
2026-02-03 18:26:36 -08:00
-- Conditionally add "Start Helper" if not running
2026-02-03 17:14:11 -08:00
if not _is_pipeline_helper_ready ( ) then
2026-02-03 18:43:54 -08:00
table.insert ( items , { title = " Start Helper " , hint = " (pipeline actions) " , value = " script-message medios-start-helper " } )
2026-02-03 17:14:11 -08:00
end
2026-02-03 18:26:36 -08:00
_lua_log ( ' [MENU] Built ' .. # items .. ' menu items ' )
-- Check UOSC availability
local uosc_ready = ensure_uosc_loaded ( )
_lua_log ( ' [MENU] ensure_uosc_loaded returned: ' .. tostring ( uosc_ready ) )
if not uosc_ready then
_lua_log ( ' [MENU] ERROR: uosc not available; menu cannot open ' )
mp.osd_message ( ' Menu unavailable (uosc not loaded) ' , 3 )
return
end
-- Format menu for UOSC
2025-11-27 10:59:01 -08:00
local menu_data = {
title = " Medios Macina " ,
2026-02-03 17:14:11 -08:00
items = items ,
2025-11-27 10:59:01 -08:00
}
2026-02-03 17:14:11 -08:00
2025-11-27 10:59:01 -08:00
local json = utils.format_json ( menu_data )
2026-02-03 18:26:36 -08:00
_lua_log ( ' [MENU] Sending menu JSON to uosc: ' .. string.sub ( json , 1 , 200 ) .. ' ... ' )
-- Try to open menu via uosc script message
-- Note: UOSC expects JSON data as a string parameter
local ok , err = pcall ( function ( )
-- Method 1: Try commandv with individual arguments
2025-12-18 22:50:21 -08:00
mp.commandv ( ' script-message-to ' , ' uosc ' , ' open-menu ' , json )
2026-02-03 18:26:36 -08:00
end )
if not ok then
_lua_log ( ' [MENU] Method 1 failed: ' .. tostring ( err ) )
-- Method 2: Try command with full string
local cmd = ' script-message-to uosc open-menu ' .. json
local ok2 , err2 = pcall ( function ( )
mp.command ( cmd )
end )
if not ok2 then
_lua_log ( ' [MENU] Method 2 failed: ' .. tostring ( err2 ) )
mp.osd_message ( ' Menu error ' , 3 )
else
_lua_log ( ' [MENU] Menu command sent via method 2 ' )
end
2025-12-18 22:50:21 -08:00
else
2026-02-03 18:26:36 -08:00
_lua_log ( ' [MENU] Menu command sent successfully ' )
2025-12-18 22:50:21 -08:00
end
2025-11-27 10:59:01 -08:00
end
2026-02-03 18:26:36 -08:00
-- Keybindings with logging wrappers
mp.add_key_binding ( " m " , " medios-menu " , function ( )
2026-02-03 18:29:29 -08:00
_lua_log ( ' [KEY] m pressed ' )
2026-02-03 18:26:36 -08:00
M.show_menu ( )
end )
2026-02-03 18:35:36 -08:00
mp.add_key_binding ( " z " , " medios-menu-alt " , function ( )
_lua_log ( ' [KEY] z pressed (alternative menu trigger) ' )
M.show_menu ( )
end )
2026-02-03 18:29:29 -08:00
-- NOTE: mbtn_right is claimed by UOSC globally, so we can't override it here.
-- Instead, use script-message handler below for alternative routing.
2025-11-27 10:59:01 -08:00
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 )
2026-02-03 18:29:29 -08:00
-- Script message handler for input.conf routing (right-click via input.conf)
mp.register_script_message ( ' medios-show-menu ' , function ( )
_lua_log ( ' [input.conf] medios-show-menu called ' )
M.show_menu ( )
end )
2025-12-23 16:36:39 -08:00
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 )
pcall ( _lua_log , ' medeia-lua loaded version= ' .. MEDEIA_LUA_VERSION )
2026-02-03 18:36:06 -08:00
-- Try to re-register right-click after UOSC loads (might override its binding)
mp.add_timeout ( 1.0 , function ( )
_lua_log ( ' [KEY] attempting to re-register mbtn_right after UOSC loaded ' )
pcall ( function ( )
mp.add_key_binding ( " mbtn_right " , " medios-menu-right-click-late " , function ( )
_lua_log ( ' [KEY] mbtn_right pressed (late registration attempt) ' )
M.show_menu ( )
end , { repeatable = false } )
end )
end )
2025-12-27 06:05:07 -08:00
-- Load optional modules (kept in separate files).
pcall ( function ( )
local script_dir = mp.get_script_directory ( ) or ' '
local candidates = { }
if script_dir ~= ' ' then
table.insert ( candidates , script_dir .. ' /sleep_timer.lua ' )
table.insert ( candidates , script_dir .. ' /LUA/sleep_timer.lua ' )
table.insert ( candidates , script_dir .. ' /../sleep_timer.lua ' )
end
table.insert ( candidates , ' C:/medios/Medios-Macina/MPV/LUA/sleep_timer.lua ' )
for _ , p in ipairs ( candidates ) do
local ok , chunk = pcall ( loadfile , p )
if ok and chunk then
pcall ( chunk )
break
end
end
end )
2025-12-17 17:42:46 -08:00
end )
2025-11-27 10:59:01 -08:00
return M