f
This commit is contained in:
261
MPV/LUA/main.lua
261
MPV/LUA/main.lua
@@ -275,6 +275,12 @@ local opts = {
|
||||
cli_path = nil -- Will be auto-detected if nil
|
||||
}
|
||||
|
||||
-- Read script options from script-opts/medeia.conf when available
|
||||
pcall(function()
|
||||
local mpopts = require('mp.options')
|
||||
mpopts.read_options(opts, 'medeia')
|
||||
end)
|
||||
|
||||
local function find_file_upwards(start_dir, relative_path, max_levels)
|
||||
local dir = start_dir
|
||||
local levels = max_levels or 6
|
||||
@@ -397,6 +403,10 @@ local _last_ipc_error = ''
|
||||
local _last_ipc_last_req_json = ''
|
||||
local _last_ipc_last_resp_json = ''
|
||||
|
||||
-- Debounce helper start attempts (window in seconds)
|
||||
local _helper_start_debounce_ts = 0
|
||||
local HELPER_START_DEBOUNCE = 2.0
|
||||
|
||||
local function _is_pipeline_helper_ready()
|
||||
local ready = mp.get_property(PIPELINE_READY_PROP)
|
||||
if ready == nil or ready == '' then
|
||||
@@ -410,7 +420,7 @@ local function _is_pipeline_helper_ready()
|
||||
return false
|
||||
end
|
||||
|
||||
-- Back-compat: older helpers may set "1". New helpers set unix timestamps.
|
||||
-- Only support unix timestamp heartbeats from current helper version
|
||||
local n = tonumber(s)
|
||||
if n and n > 1000000000 then
|
||||
local now = (os and os.time) and os.time() or nil
|
||||
@@ -424,7 +434,7 @@ local function _is_pipeline_helper_ready()
|
||||
return age <= 10
|
||||
end
|
||||
|
||||
-- If it's some other non-empty value, treat as ready.
|
||||
-- Non-empty value treated as ready
|
||||
return true
|
||||
end
|
||||
|
||||
@@ -463,11 +473,80 @@ local function ensure_mpv_ipc_server()
|
||||
return (now and now ~= '') and true or false
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
|
||||
local function ensure_pipeline_helper_running()
|
||||
-- IMPORTANT: do NOT spawn Python from inside mpv.
|
||||
-- The Python side (MPV.mpv_ipc) starts pipeline_helper.py using Windows
|
||||
-- no-console flags; spawning here can flash a console window.
|
||||
return _is_pipeline_helper_ready() and true or false
|
||||
-- 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()
|
||||
end
|
||||
|
||||
local _ipc_async_busy = false
|
||||
@@ -569,6 +648,8 @@ local function _run_helper_request_async(req, timeout_seconds, cb)
|
||||
end
|
||||
|
||||
local function _run_helper_request_response(req, timeout_seconds)
|
||||
-- Legacy synchronous wrapper for compatibility with run_pipeline_via_ipc_response.
|
||||
-- TODO: Migrate all callers to async _run_helper_request_async and remove this.
|
||||
_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 '')
|
||||
@@ -650,6 +731,46 @@ local function run_pipeline_via_ipc_response(pipeline_cmd, seeds, timeout_second
|
||||
return _run_helper_request_response(req, timeout_seconds)
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
local function quote_pipeline_arg(s)
|
||||
-- Ensure URLs with special characters (e.g. &, #) survive pipeline parsing.
|
||||
s = tostring(s or '')
|
||||
@@ -1483,7 +1604,6 @@ function FileState.new()
|
||||
return setmetatable({
|
||||
url = nil,
|
||||
formats = nil,
|
||||
formats_table = nil, -- back-compat alias
|
||||
}, FileState)
|
||||
end
|
||||
|
||||
@@ -2187,16 +2307,22 @@ end
|
||||
-- Run a Medeia pipeline command via the Python pipeline helper (IPC request/response).
|
||||
-- Calls the callback with stdout on success or error message on failure.
|
||||
function M.run_pipeline(pipeline_cmd, seeds, cb)
|
||||
_lua_log('M.run_pipeline called with cmd: ' .. tostring(pipeline_cmd))
|
||||
cb = cb or function() end
|
||||
pipeline_cmd = trim(tostring(pipeline_cmd or ''))
|
||||
if pipeline_cmd == '' then
|
||||
_lua_log('M.run_pipeline: empty command')
|
||||
cb(nil, 'empty pipeline command')
|
||||
return
|
||||
end
|
||||
ensure_mpv_ipc_server()
|
||||
|
||||
_run_pipeline_request_async(pipeline_cmd, seeds, 30, function(resp, err)
|
||||
-- Use longer timeout for .mpv -url commands since they may involve downloading
|
||||
local timeout_seconds = pipeline_cmd:match('%.mpv%s+%-url') and 120 or 30
|
||||
_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))
|
||||
if resp and resp.success then
|
||||
_lua_log('M.run_pipeline: success')
|
||||
cb(resp.stdout or '', nil)
|
||||
return
|
||||
end
|
||||
@@ -2279,6 +2405,7 @@ end
|
||||
|
||||
-- Command: Load a URL via pipeline (Ctrl+Enter in prompt)
|
||||
function M.open_load_url_prompt()
|
||||
_lua_log('open_load_url_prompt called')
|
||||
local menu_data = {
|
||||
type = LOAD_URL_MENU_TYPE,
|
||||
title = 'Load URL',
|
||||
@@ -2292,9 +2419,10 @@ function M.open_load_url_prompt()
|
||||
|
||||
local json = utils.format_json(menu_data)
|
||||
if ensure_uosc_loaded() then
|
||||
_lua_log('open_load_url_prompt: sending menu to uosc')
|
||||
mp.commandv('script-message-to', 'uosc', 'open-menu', json)
|
||||
else
|
||||
_lua_log('menu: uosc not available; cannot open-menu')
|
||||
_lua_log('menu: uosc not available; cannot open-menu for load-url')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2602,52 +2730,131 @@ mp.register_script_message('medios-trim-run', function(json)
|
||||
end)
|
||||
|
||||
mp.register_script_message('medios-load-url', function()
|
||||
_lua_log('medios-load-url handler called')
|
||||
M.open_load_url_prompt()
|
||||
end)
|
||||
|
||||
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)
|
||||
|
||||
mp.register_script_message('medios-load-url-event', function(json)
|
||||
_lua_log('Load URL event handler called with: ' .. tostring(json or ''))
|
||||
local ok, event = pcall(utils.parse_json, json)
|
||||
if not ok or type(event) ~= 'table' then
|
||||
_lua_log('Load URL: failed to parse JSON: ' .. tostring(json))
|
||||
mp.osd_message('Failed to parse URL', 2)
|
||||
if ensure_uosc_loaded() then
|
||||
mp.commandv('script-message-to', 'uosc', 'close-menu', LOAD_URL_MENU_TYPE)
|
||||
end
|
||||
return
|
||||
end
|
||||
if event.type ~= 'search' then
|
||||
_lua_log('Load URL: event type is ' .. tostring(event.type) .. ', expected search')
|
||||
if ensure_uosc_loaded() then
|
||||
mp.commandv('script-message-to', 'uosc', 'close-menu', LOAD_URL_MENU_TYPE)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
local url = trim(tostring(event.query or ''))
|
||||
if url == '' then
|
||||
mp.osd_message('URL is empty', 2)
|
||||
if ensure_uosc_loaded() then
|
||||
mp.commandv('script-message-to', 'uosc', 'close-menu', LOAD_URL_MENU_TYPE)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
ensure_mpv_ipc_server()
|
||||
local pipeline_cmd = '.mpv -url ' .. quote_pipeline_arg(url) .. ' -play'
|
||||
M.run_pipeline(pipeline_cmd, nil, function(_, err)
|
||||
if err then
|
||||
mp.osd_message('Load URL failed: ' .. tostring(err), 3)
|
||||
return
|
||||
end
|
||||
mp.osd_message('Loading URL...', 1)
|
||||
_lua_log('Load URL: ' .. url)
|
||||
|
||||
local function close_menu()
|
||||
_lua_log('Load URL: closing menu')
|
||||
if ensure_uosc_loaded() then
|
||||
mp.commandv('script-message-to', 'uosc', 'close-menu', LOAD_URL_MENU_TYPE)
|
||||
else
|
||||
_lua_log('menu: uosc not available; cannot close-menu')
|
||||
end
|
||||
end
|
||||
|
||||
-- First, always try direct loadfile. This is the fastest path.
|
||||
local can_direct = _url_can_direct_load(url)
|
||||
_lua_log('Load URL: can_direct_load=' .. tostring(can_direct))
|
||||
|
||||
if can_direct then
|
||||
_lua_log('Load URL: attempting direct loadfile')
|
||||
local ok_load = pcall(mp.commandv, 'loadfile', url, 'replace')
|
||||
if ok_load then
|
||||
_lua_log('Load URL: direct loadfile succeeded')
|
||||
mp.osd_message('URL loaded', 2)
|
||||
close_menu()
|
||||
return
|
||||
else
|
||||
_lua_log('Load URL: direct loadfile failed')
|
||||
mp.osd_message('Load URL failed (direct)', 3)
|
||||
close_menu()
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
-- Complex streams (YouTube, DASH, etc.) need the pipeline helper.
|
||||
_lua_log('Load URL: URL needs pipeline helper')
|
||||
ensure_mpv_ipc_server()
|
||||
local helper_ready = ensure_pipeline_helper_running()
|
||||
_lua_log('Load URL: helper_ready=' .. tostring(helper_ready))
|
||||
|
||||
if not helper_ready then
|
||||
mp.osd_message('Pipeline helper not running (try right-click menu)', 3)
|
||||
close_menu()
|
||||
return
|
||||
end
|
||||
|
||||
-- Use pipeline to download/prepare the URL
|
||||
local pipeline_cmd = '.mpv -url ' .. quote_pipeline_arg(url) .. ' -play'
|
||||
_lua_log('Load URL: executing pipeline command: ' .. pipeline_cmd)
|
||||
M.run_pipeline(pipeline_cmd, nil, function(resp, err)
|
||||
_lua_log('Load URL: pipeline callback fired. resp=' .. tostring(resp) .. ', err=' .. tostring(err))
|
||||
if err then
|
||||
_lua_log('Load URL: pipeline error: ' .. tostring(err))
|
||||
mp.osd_message('Load URL failed: ' .. tostring(err), 3)
|
||||
close_menu()
|
||||
return
|
||||
end
|
||||
_lua_log('Load URL: URL loaded successfully via pipeline')
|
||||
mp.osd_message('URL loaded', 2)
|
||||
close_menu()
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
-- Menu integration with UOSC
|
||||
function M.show_menu()
|
||||
_lua_log('M.show_menu called')
|
||||
local items = {
|
||||
{ title = "Get Metadata", value = "script-binding medios-info", hint = "Ctrl+i" },
|
||||
{ title = "Delete File", value = "script-binding medios-delete", hint = "Ctrl+Del" },
|
||||
{ title = "Load URL", value = {"script-message-to", mp.get_script_name(), "medios-load-url"} },
|
||||
{ title = "Cmd", value = {"script-message-to", mp.get_script_name(), "medios-open-cmd"}, hint = "Run quick commands (screenshot, trim, etc)" },
|
||||
{ title = "Download", value = {"script-message-to", mp.get_script_name(), "medios-download-current"} },
|
||||
{ title = "Change Format", value = {"script-message-to", mp.get_script_name(), "medios-change-format-current"} },
|
||||
}
|
||||
|
||||
-- Only show "Start Helper" if helper is not running (conditional menu item)
|
||||
if not _is_pipeline_helper_ready() then
|
||||
table.insert(items, { title = "Start Helper", hint = "(for pipeline actions)", value = {"script-message-to", mp.get_script_name(), "medios-start-helper"} })
|
||||
end
|
||||
|
||||
local menu_data = {
|
||||
title = "Medios Macina",
|
||||
items = {
|
||||
{ title = "Get Metadata", value = "script-binding medios-info", hint = "Ctrl+i" },
|
||||
{ title = "Delete File", value = "script-binding medios-delete", hint = "Ctrl+Del" },
|
||||
{ title = "Load URL", value = {"script-message-to", mp.get_script_name(), "medios-load-url"} },
|
||||
{ title = "Cmd", value = {"script-message-to", mp.get_script_name(), "medios-open-cmd"}, hint = "Run quick commands (screenshot, trim, etc)" },
|
||||
{ title = "Download", value = {"script-message-to", mp.get_script_name(), "medios-download-current"} },
|
||||
{ title = "Change Format", value = {"script-message-to", mp.get_script_name(), "medios-change-format-current"} },
|
||||
}
|
||||
items = items,
|
||||
}
|
||||
|
||||
|
||||
local json = utils.format_json(menu_data)
|
||||
if ensure_uosc_loaded() then
|
||||
mp.commandv('script-message-to', 'uosc', 'open-menu', json)
|
||||
|
||||
@@ -7,6 +7,7 @@ This is the central hub for all Python-mpv IPC communication. The Lua script
|
||||
should use the Python CLI, which uses this module to manage mpv connections.
|
||||
"""
|
||||
|
||||
import ctypes
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
@@ -30,6 +31,23 @@ _LYRIC_LOG_FH: Optional[Any] = None
|
||||
_MPV_AVAILABILITY_CACHE: Optional[Tuple[bool, Optional[str]]] = None
|
||||
|
||||
|
||||
def _windows_pipe_available(path: str) -> bool:
|
||||
"""Check if a Windows named pipe is ready without raising."""
|
||||
if platform.system() != "Windows":
|
||||
return False
|
||||
if not path:
|
||||
return False
|
||||
try:
|
||||
kernel32 = ctypes.windll.kernel32
|
||||
WaitNamedPipeW = kernel32.WaitNamedPipeW
|
||||
WaitNamedPipeW.argtypes = [ctypes.c_wchar_p, ctypes.c_uint32]
|
||||
WaitNamedPipeW.restype = ctypes.c_bool
|
||||
# Timeout 0 ensures we don't block.
|
||||
return bool(WaitNamedPipeW(path, 0))
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _windows_pythonw_exe(python_exe: Optional[str]) -> Optional[str]:
|
||||
"""Return a pythonw.exe adjacent to python.exe if available (Windows only)."""
|
||||
if platform.system() != "Windows":
|
||||
@@ -970,6 +988,11 @@ class MPVIPCClient:
|
||||
try:
|
||||
if self.is_windows:
|
||||
# Windows named pipes
|
||||
if not _windows_pipe_available(self.socket_path):
|
||||
if not self.silent:
|
||||
debug("Named pipe not available yet: %s" % self.socket_path)
|
||||
return False
|
||||
|
||||
try:
|
||||
# Try to open the named pipe
|
||||
self.sock = open(self.socket_path, "r+b", buffering=0)
|
||||
|
||||
@@ -562,26 +562,19 @@ def _run_op(op: str, data: Any) -> Dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def _helper_log_path() -> Path:
|
||||
try:
|
||||
d = _repo_root() / "Log"
|
||||
d.mkdir(parents=True, exist_ok=True)
|
||||
return d / "medeia-mpv-helper.log"
|
||||
except Exception:
|
||||
return Path(tempfile.gettempdir()) / "medeia-mpv-helper.log"
|
||||
|
||||
|
||||
def _append_helper_log(text: str) -> None:
|
||||
"""Log to database instead of file. This provides unified logging with rest of system."""
|
||||
payload = (text or "").rstrip()
|
||||
if not payload:
|
||||
return
|
||||
try:
|
||||
path = _helper_log_path()
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(path, "a", encoding="utf-8", errors="replace") as fh:
|
||||
fh.write(payload + "\n")
|
||||
# Try database logging first (best practice: unified logging)
|
||||
from SYS.database import log_to_db
|
||||
log_to_db("INFO", "mpv", payload)
|
||||
except Exception:
|
||||
return
|
||||
# Fallback to stderr if database unavailable
|
||||
import sys
|
||||
print(f"[mpv-helper] {payload}", file=sys.stderr)
|
||||
|
||||
|
||||
def _acquire_ipc_lock(ipc_path: str) -> Optional[Any]:
|
||||
|
||||
Reference in New Issue
Block a user