j
This commit is contained in:
@@ -22,7 +22,7 @@
|
|||||||
"((1fichier\\.com|megadl\\.fr|alterupload\\.com|cjoint\\.net|desfichiers\\.com|dfichiers\\.com|mesfichiers\\.org|piecejointe\\.net|pjointe\\.com|tenvoi\\.com|dl4free\\.com)/\\?[a-zA-Z0-9]{5,30}(&pw=[^&]+)?)"
|
"((1fichier\\.com|megadl\\.fr|alterupload\\.com|cjoint\\.net|desfichiers\\.com|dfichiers\\.com|mesfichiers\\.org|piecejointe\\.net|pjointe\\.com|tenvoi\\.com|dl4free\\.com)/\\?[a-zA-Z0-9]{5,30}(&pw=[^&]+)?)"
|
||||||
],
|
],
|
||||||
"regexp": "((1fichier\\.com|megadl\\.fr|alterupload\\.com|cjoint\\.net|desfichiers\\.com|dfichiers\\.com|mesfichiers\\.org|piecejointe\\.net|pjointe\\.com|tenvoi\\.com|dl4free\\.com)/\\?[a-zA-Z0-9]{5,30}(&pw=[^&]+)?)",
|
"regexp": "((1fichier\\.com|megadl\\.fr|alterupload\\.com|cjoint\\.net|desfichiers\\.com|dfichiers\\.com|mesfichiers\\.org|piecejointe\\.net|pjointe\\.com|tenvoi\\.com|dl4free\\.com)/\\?[a-zA-Z0-9]{5,30}(&pw=[^&]+)?)",
|
||||||
"status": true
|
"status": false
|
||||||
},
|
},
|
||||||
"rapidgator": {
|
"rapidgator": {
|
||||||
"name": "rapidgator",
|
"name": "rapidgator",
|
||||||
@@ -92,7 +92,7 @@
|
|||||||
"(hitfile\\.net/[a-z0-9A-Z]{4,9})"
|
"(hitfile\\.net/[a-z0-9A-Z]{4,9})"
|
||||||
],
|
],
|
||||||
"regexp": "(hitf\\.(to|cc)/([a-z0-9A-Z]{4,9}))|(htfl\\.(net|to|cc)/([a-z0-9A-Z]{4,9}))|(hitfile\\.(net)/download/free/([a-z0-9A-Z]{4,9}))|((hitfile\\.net/[a-z0-9A-Z]{4,9}))",
|
"regexp": "(hitf\\.(to|cc)/([a-z0-9A-Z]{4,9}))|(htfl\\.(net|to|cc)/([a-z0-9A-Z]{4,9}))|(hitfile\\.(net)/download/free/([a-z0-9A-Z]{4,9}))|((hitfile\\.net/[a-z0-9A-Z]{4,9}))",
|
||||||
"status": false
|
"status": true
|
||||||
},
|
},
|
||||||
"mega": {
|
"mega": {
|
||||||
"name": "mega",
|
"name": "mega",
|
||||||
@@ -495,7 +495,7 @@
|
|||||||
"(katfile\\.com/[0-9a-zA-Z]{12})"
|
"(katfile\\.com/[0-9a-zA-Z]{12})"
|
||||||
],
|
],
|
||||||
"regexp": "(katfile\\.(cloud|online)/([0-9a-zA-Z]{12}))|((katfile\\.com/[0-9a-zA-Z]{12}))",
|
"regexp": "(katfile\\.(cloud|online)/([0-9a-zA-Z]{12}))|((katfile\\.com/[0-9a-zA-Z]{12}))",
|
||||||
"status": true
|
"status": false
|
||||||
},
|
},
|
||||||
"mediafire": {
|
"mediafire": {
|
||||||
"name": "mediafire",
|
"name": "mediafire",
|
||||||
|
|||||||
644
MPV/LUA/main.lua
644
MPV/LUA/main.lua
@@ -51,34 +51,16 @@ local PIPELINE_REQ_PROP = 'user-data/medeia-pipeline-request'
|
|||||||
local PIPELINE_RESP_PROP = 'user-data/medeia-pipeline-response'
|
local PIPELINE_RESP_PROP = 'user-data/medeia-pipeline-response'
|
||||||
local PIPELINE_READY_PROP = 'user-data/medeia-pipeline-ready'
|
local PIPELINE_READY_PROP = 'user-data/medeia-pipeline-ready'
|
||||||
|
|
||||||
-- Dedicated Lua log (next to mpv log-file) because mp.msg output is not always
|
-- Dedicated Lua log: write directly to logs.db database for unified logging
|
||||||
-- included in --log-file depending on msg-level and build.
|
-- Fallback to stderr if database unavailable
|
||||||
local function _lua_log(text)
|
local function _lua_log(text)
|
||||||
local payload = (text and tostring(text) or '')
|
local payload = (text and tostring(text) or '')
|
||||||
if payload == '' then
|
if payload == '' then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
local dir = ''
|
|
||||||
|
|
||||||
-- Prefer a stable repo-root Log/ folder based on the script directory.
|
-- Attempt to find repo root for database access
|
||||||
do
|
local repo_root = ''
|
||||||
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
|
|
||||||
|
|
||||||
-- Prefer repo-root Log/ for consistency with Python helper logs.
|
|
||||||
do
|
do
|
||||||
local function find_up(start_dir, relative_path, max_levels)
|
local function find_up(start_dir, relative_path, max_levels)
|
||||||
local d = start_dir
|
local d = start_dir
|
||||||
@@ -102,110 +84,33 @@ local function _lua_log(text)
|
|||||||
local base = mp.get_script_directory() or utils.getcwd() or ''
|
local base = mp.get_script_directory() or utils.getcwd() or ''
|
||||||
if base ~= '' then
|
if base ~= '' then
|
||||||
local cli = find_up(base, 'CLI.py', 8)
|
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
|
|
||||||
|
|
||||||
_lua_log('medeia lua loaded version=' .. tostring(MEDEIA_LUA_VERSION) .. ' script=' .. tostring(mp.get_script_name()))
|
|
||||||
|
|
||||||
-- 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
|
if cli and cli ~= '' then
|
||||||
repo_root = cli:match('(.*)[/\\]') or ''
|
repo_root = cli:match('(.*)[/\\]') or ''
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if repo_root == '' then
|
-- Write to logs.db via Python subprocess (non-blocking, async)
|
||||||
return -- Can't find repo root, skip logging to database
|
if repo_root ~= '' then
|
||||||
|
local python = (opts and opts.python_path) and tostring(opts.python_path) or 'python'
|
||||||
|
local db_path = (repo_root .. '/logs.db'):gsub('\\', '/')
|
||||||
|
local msg = payload:gsub('\\', '\\\\'):gsub("'", "\\'")
|
||||||
|
|
||||||
|
local script = string.format(
|
||||||
|
"import sqlite3; p='%s'; c=sqlite3.connect(p); c.execute(\"CREATE TABLE IF NOT EXISTS logs (id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, level TEXT, module TEXT, message TEXT)\"); c.execute(\"INSERT INTO logs (level,module,message) VALUES (?,?,?)\", ('DEBUG','mpv','%s')); c.commit(); c.close()",
|
||||||
|
db_path,
|
||||||
|
msg
|
||||||
|
)
|
||||||
|
|
||||||
|
pcall(function()
|
||||||
|
mp.command_native_async({ name = 'subprocess', args = { python, '-c', script }, cwd = nil }, function() end)
|
||||||
|
end)
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
-- Combined log: to file + database (for persistence and debugging)
|
_lua_log('medeia lua loaded version=' .. tostring(MEDEIA_LUA_VERSION) .. ' script=' .. tostring(mp.get_script_name()))
|
||||||
|
|
||||||
|
-- Combined log: to database (primary) + _lua_log (which also writes to db)
|
||||||
local function _log_all(level, text)
|
local function _log_all(level, text)
|
||||||
if not text or text == '' then
|
if not text or text == '' then
|
||||||
return
|
return
|
||||||
@@ -213,11 +118,8 @@ local function _log_all(level, text)
|
|||||||
level = tostring(level or 'INFO'):upper()
|
level = tostring(level or 'INFO'):upper()
|
||||||
text = tostring(text)
|
text = tostring(text)
|
||||||
|
|
||||||
-- Log to file
|
-- Log with level prefix via _lua_log (which writes to database)
|
||||||
_lua_log('[' .. level .. '] ' .. text)
|
_lua_log('[' .. level .. '] ' .. text)
|
||||||
|
|
||||||
-- Log to database (async, non-blocking)
|
|
||||||
_log_to_db(level, text)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function ensure_uosc_loaded()
|
local function ensure_uosc_loaded()
|
||||||
@@ -344,8 +246,8 @@ end
|
|||||||
-- Default to visible unless user overrides.
|
-- Default to visible unless user overrides.
|
||||||
lyric_set_visible(true)
|
lyric_set_visible(true)
|
||||||
|
|
||||||
-- Configuration
|
-- Configuration (global so _lua_log can see python_path early)
|
||||||
local opts = {
|
opts = {
|
||||||
python_path = "python",
|
python_path = "python",
|
||||||
cli_path = nil -- Will be auto-detected if nil
|
cli_path = nil -- Will be auto-detected if nil
|
||||||
}
|
}
|
||||||
@@ -375,6 +277,9 @@ local function find_file_upwards(start_dir, relative_path, max_levels)
|
|||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Forward declaration (defined later) used by helper auto-start.
|
||||||
|
local _resolve_python_exe
|
||||||
|
|
||||||
local _cached_store_names = {}
|
local _cached_store_names = {}
|
||||||
local _store_cache_loaded = false
|
local _store_cache_loaded = false
|
||||||
|
|
||||||
@@ -482,35 +387,55 @@ local _last_ipc_last_resp_json = ''
|
|||||||
local _helper_start_debounce_ts = 0
|
local _helper_start_debounce_ts = 0
|
||||||
local HELPER_START_DEBOUNCE = 2.0
|
local HELPER_START_DEBOUNCE = 2.0
|
||||||
|
|
||||||
|
-- Track ready-heartbeat freshness so stale or non-timestamp values don't mask a stopped helper
|
||||||
|
local _helper_ready_last_value = ''
|
||||||
|
local _helper_ready_last_seen_ts = 0
|
||||||
|
local HELPER_READY_STALE_SECONDS = 10.0
|
||||||
|
|
||||||
local function _is_pipeline_helper_ready()
|
local function _is_pipeline_helper_ready()
|
||||||
local ready = mp.get_property(PIPELINE_READY_PROP)
|
local ready = mp.get_property(PIPELINE_READY_PROP)
|
||||||
if ready == nil or ready == '' then
|
if ready == nil or ready == '' then
|
||||||
ready = mp.get_property_native(PIPELINE_READY_PROP)
|
ready = mp.get_property_native(PIPELINE_READY_PROP)
|
||||||
end
|
end
|
||||||
if not ready then
|
if not ready then
|
||||||
|
_helper_ready_last_value = ''
|
||||||
|
_helper_ready_last_seen_ts = 0
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
local s = tostring(ready)
|
local s = tostring(ready)
|
||||||
if s == '' or s == '0' then
|
if s == '' or s == '0' then
|
||||||
|
_helper_ready_last_value = s
|
||||||
|
_helper_ready_last_seen_ts = 0
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Only support unix timestamp heartbeats from current helper version
|
local now = mp.get_time() or 0
|
||||||
local n = tonumber(s)
|
if s ~= _helper_ready_last_value then
|
||||||
if n and n > 1000000000 then
|
_helper_ready_last_value = s
|
||||||
local now = (os and os.time) and os.time() or nil
|
_helper_ready_last_seen_ts = now
|
||||||
if not now then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
local age = now - n
|
|
||||||
if age < 0 then
|
|
||||||
age = 0
|
|
||||||
end
|
|
||||||
return age <= 10
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Non-empty value treated as ready
|
-- Prefer timestamp heartbeats from modern helpers.
|
||||||
return true
|
local n = tonumber(s)
|
||||||
|
if n and n > 1000000000 then
|
||||||
|
local os_now = (os and os.time) and os.time() or nil
|
||||||
|
if os_now then
|
||||||
|
local age = os_now - n
|
||||||
|
if age < 0 then
|
||||||
|
age = 0
|
||||||
|
end
|
||||||
|
if age <= HELPER_READY_STALE_SECONDS then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Fall back to the last time we observed a new value so stale data does not appear fresh.
|
||||||
|
if _helper_ready_last_seen_ts > 0 and (now - _helper_ready_last_seen_ts) <= HELPER_READY_STALE_SECONDS then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
local function get_mpv_ipc_path()
|
local function get_mpv_ipc_path()
|
||||||
@@ -630,6 +555,29 @@ local _ipc_async_queue = {}
|
|||||||
local function _run_helper_request_async(req, timeout_seconds, cb)
|
local function _run_helper_request_async(req, timeout_seconds, cb)
|
||||||
cb = cb or function() end
|
cb = cb or function() end
|
||||||
|
|
||||||
|
if type(req) ~= 'table' then
|
||||||
|
_lua_log('ipc-async: invalid request')
|
||||||
|
cb(nil, 'invalid request')
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Assign id and label early for logging
|
||||||
|
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-async: queuing request id=' .. id .. ' ' .. label .. ' (busy=' .. tostring(_ipc_async_busy) .. ', queue_size=' .. tostring(#_ipc_async_queue) .. ')')
|
||||||
if _ipc_async_busy then
|
if _ipc_async_busy then
|
||||||
_ipc_async_queue[#_ipc_async_queue + 1] = { req = req, timeout = timeout_seconds, cb = cb }
|
_ipc_async_queue[#_ipc_async_queue + 1] = { req = req, timeout = timeout_seconds, cb = cb }
|
||||||
return
|
return
|
||||||
@@ -637,6 +585,36 @@ local function _run_helper_request_async(req, timeout_seconds, cb)
|
|||||||
_ipc_async_busy = true
|
_ipc_async_busy = true
|
||||||
|
|
||||||
local function done(resp, err)
|
local function done(resp, err)
|
||||||
|
local err_text = err and tostring(err) or ''
|
||||||
|
local is_timeout = err_text:find('timeout waiting response', 1, true) ~= nil
|
||||||
|
local retry_count = type(req) == 'table' and tonumber(req._retry or 0) or 0
|
||||||
|
local is_retryable = is_timeout and type(req) == 'table'
|
||||||
|
and tostring(req.op or '') == 'ytdlp-formats' and retry_count < 1
|
||||||
|
|
||||||
|
if is_retryable then
|
||||||
|
req._retry = retry_count + 1
|
||||||
|
req.id = nil
|
||||||
|
_ipc_async_busy = false
|
||||||
|
_lua_log('ipc-async: timeout on ytdlp-formats; restarting helper and retrying (attempt ' .. tostring(req._retry) .. ')')
|
||||||
|
pcall(mp.set_property, PIPELINE_READY_PROP, '')
|
||||||
|
attempt_start_pipeline_helper_async(function(success)
|
||||||
|
if success then
|
||||||
|
_lua_log('ipc-async: helper restart succeeded; retrying ytdlp-formats')
|
||||||
|
else
|
||||||
|
_lua_log('ipc-async: helper restart failed; retrying anyway')
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
mp.add_timeout(0.3, function()
|
||||||
|
_run_helper_request_async(req, timeout_seconds, cb)
|
||||||
|
end)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if err then
|
||||||
|
_lua_log('ipc-async: done id=' .. tostring(id) .. ' ERROR: ' .. tostring(err))
|
||||||
|
else
|
||||||
|
_lua_log('ipc-async: done id=' .. tostring(id) .. ' success=' .. tostring(resp and resp.success))
|
||||||
|
end
|
||||||
_ipc_async_busy = false
|
_ipc_async_busy = false
|
||||||
cb(resp, err)
|
cb(resp, err)
|
||||||
|
|
||||||
@@ -655,71 +633,86 @@ local function _run_helper_request_async(req, timeout_seconds, cb)
|
|||||||
end
|
end
|
||||||
|
|
||||||
ensure_mpv_ipc_server()
|
ensure_mpv_ipc_server()
|
||||||
if not ensure_pipeline_helper_running() then
|
|
||||||
done(nil, 'helper not running')
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Assign id.
|
local function send_request_payload()
|
||||||
local id = tostring(req.id or '')
|
_lua_log('ipc-async: send request id=' .. tostring(id) .. ' ' .. label)
|
||||||
if id == '' then
|
local req_json = utils.format_json(req)
|
||||||
id = tostring(math.floor(mp.get_time() * 1000)) .. '-' .. tostring(math.random(100000, 999999))
|
_last_ipc_last_req_json = req_json
|
||||||
req.id = id
|
|
||||||
end
|
|
||||||
|
|
||||||
local label = ''
|
mp.set_property(PIPELINE_RESP_PROP, '')
|
||||||
if req.op then
|
mp.set_property(PIPELINE_REQ_PROP, req_json)
|
||||||
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 deadline = mp.get_time() + (timeout_seconds or 5)
|
||||||
local ready_deadline = mp.get_time() + 3.0
|
local poll_timer
|
||||||
local ready_timer
|
poll_timer = mp.add_periodic_timer(0.05, function()
|
||||||
ready_timer = mp.add_periodic_timer(0.05, function()
|
if mp.get_time() >= deadline then
|
||||||
if _is_pipeline_helper_ready() then
|
poll_timer:kill()
|
||||||
ready_timer:kill()
|
done(nil, 'timeout waiting response (' .. label .. ')')
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
_lua_log('ipc-async: send request id=' .. tostring(id) .. ' ' .. label)
|
local resp_json = mp.get_property(PIPELINE_RESP_PROP)
|
||||||
local req_json = utils.format_json(req)
|
if resp_json and resp_json ~= '' then
|
||||||
_last_ipc_last_req_json = req_json
|
_last_ipc_last_resp_json = resp_json
|
||||||
|
local ok, resp = pcall(utils.parse_json, resp_json)
|
||||||
mp.set_property(PIPELINE_RESP_PROP, '')
|
if ok and resp and resp.id == id then
|
||||||
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()
|
poll_timer:kill()
|
||||||
done(nil, 'timeout waiting response (' .. label .. ')')
|
_lua_log('ipc-async: got response id=' .. tostring(id) .. ' success=' .. tostring(resp.success))
|
||||||
return
|
done(resp, nil)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
local resp_json = mp.get_property(PIPELINE_RESP_PROP)
|
local function wait_for_helper_ready(timeout, on_ready)
|
||||||
if resp_json and resp_json ~= '' then
|
local deadline = mp.get_time() + (timeout or 3.0)
|
||||||
_last_ipc_last_resp_json = resp_json
|
local ready_timer
|
||||||
local ok, resp = pcall(utils.parse_json, resp_json)
|
ready_timer = mp.add_periodic_timer(0.05, function()
|
||||||
if ok and resp and resp.id == id then
|
if _is_pipeline_helper_ready() then
|
||||||
poll_timer:kill()
|
ready_timer:kill()
|
||||||
_lua_log('ipc-async: got response id=' .. tostring(id) .. ' success=' .. tostring(resp.success))
|
on_ready()
|
||||||
done(resp, nil)
|
return
|
||||||
end
|
end
|
||||||
end
|
if mp.get_time() >= deadline then
|
||||||
end)
|
ready_timer:kill()
|
||||||
|
done(nil, 'helper not ready')
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function ensure_helper_and_send()
|
||||||
|
if _is_pipeline_helper_ready() then
|
||||||
|
wait_for_helper_ready(3.0, send_request_payload)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
if mp.get_time() >= ready_deadline then
|
_lua_log('ipc-async: helper not ready, auto-starting before request id=' .. id)
|
||||||
ready_timer:kill()
|
attempt_start_pipeline_helper_async(function(success)
|
||||||
done(nil, 'helper not ready')
|
if not success then
|
||||||
return
|
_lua_log('ipc-async: helper auto-start failed while handling request id=' .. id)
|
||||||
end
|
else
|
||||||
end)
|
_lua_log('ipc-async: helper auto-start triggered by request id=' .. id)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
local helper_deadline = mp.get_time() + 6.0
|
||||||
|
local helper_timer
|
||||||
|
helper_timer = mp.add_periodic_timer(0.1, function()
|
||||||
|
if _is_pipeline_helper_ready() then
|
||||||
|
helper_timer:kill()
|
||||||
|
wait_for_helper_ready(3.0, send_request_payload)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if mp.get_time() >= helper_deadline then
|
||||||
|
helper_timer:kill()
|
||||||
|
done(nil, 'helper not running')
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
ensure_helper_and_send()
|
||||||
end
|
end
|
||||||
|
|
||||||
local function _run_helper_request_response(req, timeout_seconds)
|
local function _run_helper_request_response(req, timeout_seconds)
|
||||||
@@ -830,13 +823,14 @@ local function _url_can_direct_load(url)
|
|||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
local function _try_direct_loadfile(url)
|
local function _try_direct_loadfile(url, force)
|
||||||
-- Attempt to load URL directly via mpv without pipeline.
|
-- Attempt to load URL directly via mpv without pipeline.
|
||||||
-- Returns (success: bool, loaded: bool) where:
|
-- Returns (success: bool, loaded: bool) where:
|
||||||
-- - success=true, loaded=true: URL loaded successfully
|
-- - success=true, loaded=true: URL loaded successfully
|
||||||
-- - success=true, loaded=false: URL not suitable for direct load
|
-- - success=true, loaded=false: URL not suitable for direct load (when not forced)
|
||||||
-- - success=false: loadfile command failed
|
-- - success=false: loadfile command failed
|
||||||
if not _url_can_direct_load(url) then
|
force = force and true or false
|
||||||
|
if not force and not _url_can_direct_load(url) then
|
||||||
_lua_log('_try_direct_loadfile: URL not suitable for direct load: ' .. url)
|
_lua_log('_try_direct_loadfile: URL not suitable for direct load: ' .. url)
|
||||||
return true, false -- Not suitable, but not an error
|
return true, false -- Not suitable, but not an error
|
||||||
end
|
end
|
||||||
@@ -858,7 +852,7 @@ local function _is_windows()
|
|||||||
return sep == '\\'
|
return sep == '\\'
|
||||||
end
|
end
|
||||||
|
|
||||||
local function _resolve_python_exe(prefer_no_console)
|
_resolve_python_exe = function(prefer_no_console)
|
||||||
local python = (opts and opts.python_path) and tostring(opts.python_path) or 'python'
|
local python = (opts and opts.python_path) and tostring(opts.python_path) or 'python'
|
||||||
if (not prefer_no_console) or (not _is_windows()) then
|
if (not prefer_no_console) or (not _is_windows()) then
|
||||||
return python
|
return python
|
||||||
@@ -1711,6 +1705,75 @@ local function _is_http_url(u)
|
|||||||
return u:match('^https?://') ~= nil
|
return u:match('^https?://') ~= nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local _ytdlp_domains_cached = nil
|
||||||
|
|
||||||
|
local function _is_ytdlp_url(u)
|
||||||
|
if not u or type(u) ~= 'string' then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
local low = trim(u:lower())
|
||||||
|
if not low:match('^https?://') then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Fast exclusions for things we know are not meant for yt-dlp format switching
|
||||||
|
if low:find('/get_files/file', 1, true) then return false end
|
||||||
|
if low:find('tidal.com/manifest', 1, true) then return false end
|
||||||
|
if low:find('alldebrid.com/f/', 1, true) then return false end
|
||||||
|
|
||||||
|
-- Try to use the cached domain list from the pipeline helper
|
||||||
|
local domains_str = mp.get_property('user-data/medeia-ytdlp-domains-cached') or ''
|
||||||
|
if domains_str ~= '' then
|
||||||
|
if not _ytdlp_domains_cached then
|
||||||
|
_ytdlp_domains_cached = {}
|
||||||
|
for d in domains_str:gmatch('%S+') do
|
||||||
|
_ytdlp_domains_cached[d] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local host = low:match('^https?://([^/]+)')
|
||||||
|
if host then
|
||||||
|
-- Remove port if present
|
||||||
|
host = host:match('^([^:]+)')
|
||||||
|
|
||||||
|
-- Check direct match or parent domain matches
|
||||||
|
local parts = {}
|
||||||
|
for p in host:gmatch('[^%.]+') do
|
||||||
|
table.insert(parts, p)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Check from full domain down to top-level (e.g. m.youtube.com, youtube.com)
|
||||||
|
for i = 1, #parts - 1 do
|
||||||
|
local candidate = table.concat(parts, '.', i)
|
||||||
|
if _ytdlp_domains_cached[candidate] then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Fallback/Hardcoded: Probable video/audio sites for which Change Format is actually useful
|
||||||
|
local patterns = {
|
||||||
|
'youtube%.com', 'youtu%.be', 'vimeo%.com', 'twitch%.tv',
|
||||||
|
'soundcloud%.com', 'bandcamp%.com', 'bilibili%.com',
|
||||||
|
'dailymotion%.com', 'pixiv%.net', 'twitter%.com',
|
||||||
|
'x%.com', 'instagram%.com', 'tiktok%.com', 'reddit%.com',
|
||||||
|
'facebook%.com', 'fb%.watch'
|
||||||
|
}
|
||||||
|
for _, p in ipairs(patterns) do
|
||||||
|
if low:match(p) then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- If we have formats already cached for this URL, it's definitely supported
|
||||||
|
if _get_cached_formats_table(u) then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
local function _cache_formats_for_url(url, tbl)
|
local function _cache_formats_for_url(url, tbl)
|
||||||
if type(url) ~= 'string' or url == '' then
|
if type(url) ~= 'string' or url == '' then
|
||||||
return
|
return
|
||||||
@@ -1741,59 +1804,97 @@ end
|
|||||||
|
|
||||||
function FileState:fetch_formats(cb)
|
function FileState:fetch_formats(cb)
|
||||||
local url = tostring(self.url or '')
|
local url = tostring(self.url or '')
|
||||||
|
_lua_log('fetch-formats: started for url=' .. url)
|
||||||
if url == '' or not _is_http_url(url) then
|
if url == '' or not _is_http_url(url) then
|
||||||
|
_lua_log('fetch-formats: skipped (not a url)')
|
||||||
if cb then cb(false, 'not a url') end
|
if cb then cb(false, 'not a url') end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
if _extract_store_hash(url) then
|
if _extract_store_hash(url) then
|
||||||
|
_lua_log('fetch-formats: skipped (store-hash)')
|
||||||
if cb then cb(false, 'store-hash url') end
|
if cb then cb(false, 'store-hash url') end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local cached = _get_cached_formats_table(url)
|
local cached = _get_cached_formats_table(url)
|
||||||
if type(cached) == 'table' then
|
if type(cached) == 'table' then
|
||||||
|
_lua_log('fetch-formats: using cached table')
|
||||||
self:set_formats(url, cached)
|
self:set_formats(url, cached)
|
||||||
if cb then cb(true, nil) end
|
if cb then cb(true, nil) end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
if _formats_inflight[url] then
|
local function _perform_request()
|
||||||
|
if _formats_inflight[url] then
|
||||||
|
_lua_log('fetch-formats: already inflight, adding waiter')
|
||||||
|
_formats_waiters[url] = _formats_waiters[url] or {}
|
||||||
|
if cb then table.insert(_formats_waiters[url], cb) end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
_lua_log('fetch-formats: initiating IPC request')
|
||||||
|
_formats_inflight[url] = true
|
||||||
_formats_waiters[url] = _formats_waiters[url] or {}
|
_formats_waiters[url] = _formats_waiters[url] or {}
|
||||||
if cb then table.insert(_formats_waiters[url], cb) end
|
if cb then table.insert(_formats_waiters[url], cb) end
|
||||||
return
|
|
||||||
end
|
|
||||||
_formats_inflight[url] = true
|
|
||||||
_formats_waiters[url] = _formats_waiters[url] or {}
|
|
||||||
if cb then table.insert(_formats_waiters[url], cb) end
|
|
||||||
|
|
||||||
_run_helper_request_async({ op = 'ytdlp-formats', data = { url = url } }, 90, function(resp, err)
|
_run_helper_request_async({ op = 'ytdlp-formats', data = { url = url } }, 90, function(resp, err)
|
||||||
_formats_inflight[url] = nil
|
_lua_log('fetch-formats: IPC callback received id=' .. tostring(resp and resp.id) .. ' err=' .. tostring(err))
|
||||||
|
_formats_inflight[url] = nil
|
||||||
|
|
||||||
local ok = false
|
local ok = false
|
||||||
local reason = err
|
local reason = err
|
||||||
if resp and resp.success and type(resp.table) == 'table' then
|
if resp and resp.success and type(resp.table) == 'table' then
|
||||||
ok = true
|
ok = true
|
||||||
reason = nil
|
reason = nil
|
||||||
self:set_formats(url, resp.table)
|
self:set_formats(url, resp.table)
|
||||||
_cache_formats_for_url(url, resp.table)
|
_cache_formats_for_url(url, resp.table)
|
||||||
_lua_log('formats: cached ' .. tostring((resp.table.rows and #resp.table.rows) or 0) .. ' rows for url')
|
_lua_log('formats: cached ' .. tostring((resp.table.rows and #resp.table.rows) or 0) .. ' rows for url')
|
||||||
else
|
else
|
||||||
if type(resp) == 'table' then
|
_lua_log('fetch-formats: request failed success=' .. tostring(resp and resp.success))
|
||||||
if resp.error and tostring(resp.error) ~= '' then
|
if type(resp) == 'table' then
|
||||||
reason = tostring(resp.error)
|
if resp.error and tostring(resp.error) ~= '' then
|
||||||
elseif resp.stderr and tostring(resp.stderr) ~= '' then
|
reason = tostring(resp.error)
|
||||||
reason = tostring(resp.stderr)
|
elseif resp.stderr and tostring(resp.stderr) ~= '' then
|
||||||
|
reason = tostring(resp.stderr)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
local waiters = _formats_waiters[url] or {}
|
local waiters = _formats_waiters[url] or {}
|
||||||
_formats_waiters[url] = nil
|
_lua_log('fetch-formats: calling ' .. tostring(#waiters) .. ' waiters with ok=' .. tostring(ok) .. ' reason=' .. tostring(reason))
|
||||||
for _, fn in ipairs(waiters) do
|
_formats_waiters[url] = nil
|
||||||
pcall(fn, ok, reason)
|
for _, fn in ipairs(waiters) do
|
||||||
end
|
pcall(fn, ok, reason)
|
||||||
end)
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
if not ensure_pipeline_helper_running() then
|
||||||
|
_lua_log('fetch-formats: helper not running, waiting before requesting')
|
||||||
|
local deadline = mp.get_time() + 6.0
|
||||||
|
local waiter
|
||||||
|
waiter = mp.add_periodic_timer(0.1, function()
|
||||||
|
if _is_pipeline_helper_ready() then
|
||||||
|
waiter:kill()
|
||||||
|
_lua_log('fetch-formats: helper became ready, continuing request')
|
||||||
|
_perform_request()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if mp.get_time() >= deadline then
|
||||||
|
waiter:kill()
|
||||||
|
_lua_log('fetch-formats: helper still not ready after timeout')
|
||||||
|
if cb then cb(false, 'helper not running') end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
attempt_start_pipeline_helper_async(function(success)
|
||||||
|
if not success then
|
||||||
|
_lua_log('fetch-formats: helper auto-start failed')
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
_perform_request()
|
||||||
end
|
end
|
||||||
|
|
||||||
local function _prefetch_formats_for_url(url)
|
local function _prefetch_formats_for_url(url)
|
||||||
@@ -1847,6 +1948,38 @@ local function _debug_dump_formatted_formats(url, tbl, items)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function _show_format_list_osd(items, max_items)
|
||||||
|
if type(items) ~= 'table' then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local total = #items
|
||||||
|
if total == 0 then
|
||||||
|
mp.osd_message('No formats available', 4)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local limit = max_items or 8
|
||||||
|
if limit < 1 then
|
||||||
|
limit = 1
|
||||||
|
end
|
||||||
|
local lines = {}
|
||||||
|
for i = 1, math.min(total, limit) do
|
||||||
|
local it = items[i] or {}
|
||||||
|
local title = tostring(it.title or '')
|
||||||
|
local hint = tostring(it.hint or '')
|
||||||
|
if hint ~= '' then
|
||||||
|
lines[#lines + 1] = title .. ' — ' .. hint
|
||||||
|
else
|
||||||
|
lines[#lines + 1] = title
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if total > limit then
|
||||||
|
lines[#lines + 1] = '… +' .. tostring(total - limit) .. ' more'
|
||||||
|
end
|
||||||
|
if #lines > 0 then
|
||||||
|
mp.osd_message(table.concat(lines, '\n'), 6)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
local function _current_ytdl_format_string()
|
local function _current_ytdl_format_string()
|
||||||
-- Preferred: mpv exposes the active ytdl format string.
|
-- Preferred: mpv exposes the active ytdl format string.
|
||||||
local fmt = trim(tostring(mp.get_property_native('ytdl-format') or ''))
|
local fmt = trim(tostring(mp.get_property_native('ytdl-format') or ''))
|
||||||
@@ -2087,6 +2220,7 @@ mp.register_script_message('medios-change-format-current', function()
|
|||||||
end
|
end
|
||||||
|
|
||||||
_debug_dump_formatted_formats(url, cached_tbl, items)
|
_debug_dump_formatted_formats(url, cached_tbl, items)
|
||||||
|
_show_format_list_osd(items, 8)
|
||||||
_uosc_open_list_picker(DOWNLOAD_FORMAT_MENU_TYPE, 'Change format', items)
|
_uosc_open_list_picker(DOWNLOAD_FORMAT_MENU_TYPE, 'Change format', items)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -2155,6 +2289,7 @@ mp.register_script_message('medios-change-format-current', function()
|
|||||||
|
|
||||||
_pending_format_change.formats_table = tbl
|
_pending_format_change.formats_table = tbl
|
||||||
_debug_dump_formatted_formats(url, tbl, items)
|
_debug_dump_formatted_formats(url, tbl, items)
|
||||||
|
_show_format_list_osd(items, 8)
|
||||||
_uosc_open_list_picker(DOWNLOAD_FORMAT_MENU_TYPE, 'Change format', items)
|
_uosc_open_list_picker(DOWNLOAD_FORMAT_MENU_TYPE, 'Change format', items)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
@@ -2392,9 +2527,10 @@ function M.run_pipeline(pipeline_cmd, seeds, cb)
|
|||||||
end
|
end
|
||||||
ensure_mpv_ipc_server()
|
ensure_mpv_ipc_server()
|
||||||
|
|
||||||
-- Use shorter timeout for .mpv -url commands since they just queue the URL (non-blocking)
|
-- Use a longer timeout for `.mpv -url` commands to avoid races with slow helper starts.
|
||||||
-- The actual URL resolution happens asynchronously in MPV itself
|
local lower_cmd = pipeline_cmd:lower()
|
||||||
local timeout_seconds = pipeline_cmd:match('%.mpv%s+%-url') and 10 or 30
|
local is_mpv_load = lower_cmd:match('%.mpv%s+%-url') ~= nil
|
||||||
|
local timeout_seconds = is_mpv_load and 45 or 30
|
||||||
_run_pipeline_request_async(pipeline_cmd, seeds, timeout_seconds, function(resp, err)
|
_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))
|
_lua_log('M.run_pipeline callback fired: resp=' .. tostring(resp) .. ', err=' .. tostring(err))
|
||||||
if resp and resp.success then
|
if resp and resp.success then
|
||||||
@@ -2879,22 +3015,18 @@ mp.register_script_message('medios-load-url-event', function(json)
|
|||||||
local can_direct = _url_can_direct_load(url)
|
local can_direct = _url_can_direct_load(url)
|
||||||
_lua_log('[LOAD-URL] Checking if URL can be loaded directly: ' .. tostring(can_direct))
|
_lua_log('[LOAD-URL] Checking if URL can be loaded directly: ' .. tostring(can_direct))
|
||||||
|
|
||||||
if can_direct then
|
local direct_ok, direct_loaded = _try_direct_loadfile(url, false)
|
||||||
_lua_log('[LOAD-URL] Attempting direct loadfile')
|
if direct_ok and direct_loaded then
|
||||||
local ok_load = pcall(mp.commandv, 'loadfile', url, 'replace')
|
_lua_log('[LOAD-URL] Direct loadfile command sent successfully (forced)')
|
||||||
if ok_load then
|
_log_all('INFO', 'Load URL succeeded via direct load')
|
||||||
_lua_log('[LOAD-URL] Direct loadfile command sent successfully')
|
mp.osd_message('URL loaded', 2)
|
||||||
_log_all('INFO', 'Load URL succeeded via direct load')
|
close_menu()
|
||||||
mp.osd_message('URL loaded', 2)
|
return
|
||||||
close_menu()
|
end
|
||||||
return
|
if direct_ok then
|
||||||
else
|
_lua_log('[LOAD-URL] Direct loadfile command did not load the URL; falling back to helper')
|
||||||
_lua_log('[LOAD-URL] Direct loadfile command failed')
|
else
|
||||||
_log_all('ERROR', 'Load URL failed: direct loadfile command failed')
|
_lua_log('[LOAD-URL] Direct loadfile command failed; falling back to helper')
|
||||||
mp.osd_message('Load URL failed (direct)', 3)
|
|
||||||
close_menu()
|
|
||||||
return
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Complex streams (YouTube, DASH, etc.) need the pipeline helper.
|
-- Complex streams (YouTube, DASH, etc.) need the pipeline helper.
|
||||||
@@ -2956,6 +3088,15 @@ mp.register_script_message('medios-load-url-event', function(json)
|
|||||||
_lua_log('[LOAD-URL] URL loaded successfully')
|
_lua_log('[LOAD-URL] URL loaded successfully')
|
||||||
_log_all('INFO', 'Load URL succeeded')
|
_log_all('INFO', 'Load URL succeeded')
|
||||||
mp.osd_message('URL loaded', 2)
|
mp.osd_message('URL loaded', 2)
|
||||||
|
|
||||||
|
-- Prefetch formats for yt-dlp URLs so "Change Format" menu is instant
|
||||||
|
if _is_ytdlp_url(url) then
|
||||||
|
_lua_log('[LOAD-URL] URL is yt-dlp compatible, prefetching formats in background')
|
||||||
|
mp.add_timeout(0.5, function()
|
||||||
|
_prefetch_formats_for_url(url)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
close_menu()
|
close_menu()
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
@@ -2964,6 +3105,9 @@ end)
|
|||||||
function M.show_menu()
|
function M.show_menu()
|
||||||
_lua_log('[MENU] M.show_menu called')
|
_lua_log('[MENU] M.show_menu called')
|
||||||
|
|
||||||
|
local target = _current_target()
|
||||||
|
_lua_log('[MENU] current target: ' .. tostring(target))
|
||||||
|
|
||||||
-- Build menu items
|
-- Build menu items
|
||||||
-- Note: UOSC expects command strings, not arrays
|
-- Note: UOSC expects command strings, not arrays
|
||||||
local items = {
|
local items = {
|
||||||
@@ -2972,12 +3116,10 @@ function M.show_menu()
|
|||||||
{ title = "Delete File", value = "script-binding medios-delete", hint = "Ctrl+Del" },
|
{ title = "Delete File", value = "script-binding medios-delete", hint = "Ctrl+Del" },
|
||||||
{ title = "Cmd", value = "script-message medios-open-cmd", hint = "screenshot/trim/etc" },
|
{ title = "Cmd", value = "script-message medios-open-cmd", hint = "screenshot/trim/etc" },
|
||||||
{ title = "Download", value = "script-message medios-download-current" },
|
{ title = "Download", value = "script-message medios-download-current" },
|
||||||
{ title = "Change Format", value = "script-message medios-change-format-current" },
|
|
||||||
}
|
}
|
||||||
|
|
||||||
-- Conditionally add "Start Helper" if not running
|
if _is_ytdlp_url(target) then
|
||||||
if not _is_pipeline_helper_ready() then
|
table.insert(items, { title = "Change Format", value = "script-message medios-change-format-current" })
|
||||||
table.insert(items, { title = "Start Helper", hint = "(pipeline actions)", value = "script-message medios-start-helper" })
|
|
||||||
end
|
end
|
||||||
|
|
||||||
_lua_log('[MENU] Built ' .. #items .. ' menu items')
|
_lua_log('[MENU] Built ' .. #items .. ' menu items')
|
||||||
@@ -3058,6 +3200,14 @@ mp.add_timeout(0, function()
|
|||||||
pcall(ensure_mpv_ipc_server)
|
pcall(ensure_mpv_ipc_server)
|
||||||
pcall(_lua_log, 'medeia-lua loaded version=' .. MEDEIA_LUA_VERSION)
|
pcall(_lua_log, 'medeia-lua loaded version=' .. MEDEIA_LUA_VERSION)
|
||||||
|
|
||||||
|
attempt_start_pipeline_helper_async(function(success)
|
||||||
|
if success then
|
||||||
|
_lua_log('helper-auto-start succeeded')
|
||||||
|
else
|
||||||
|
_lua_log('helper-auto-start failed')
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
-- Try to re-register right-click after UOSC loads (might override its binding)
|
-- Try to re-register right-click after UOSC loads (might override its binding)
|
||||||
mp.add_timeout(1.0, function()
|
mp.add_timeout(1.0, function()
|
||||||
_lua_log('[KEY] attempting to re-register mbtn_right after UOSC loaded')
|
_lua_log('[KEY] attempting to re-register mbtn_right after UOSC loaded')
|
||||||
|
|||||||
@@ -376,31 +376,14 @@ def _run_op(op: str, data: Any) -> Dict[str, Any]:
|
|||||||
"table": None,
|
"table": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Fast gate: only for streaming URLs yt-dlp knows about.
|
|
||||||
try:
|
try:
|
||||||
from tool.ytdlp import is_url_supported_by_ytdlp # noqa: WPS433
|
from tool.ytdlp import list_formats, is_browseable_format # noqa: WPS433
|
||||||
|
|
||||||
if not is_url_supported_by_ytdlp(url):
|
|
||||||
return {
|
|
||||||
"success": False,
|
|
||||||
"stdout": "",
|
|
||||||
"stderr": "",
|
|
||||||
"error": "URL not supported by yt-dlp",
|
|
||||||
"table": None,
|
|
||||||
}
|
|
||||||
except Exception:
|
|
||||||
# If probing support fails, still attempt extraction and let yt-dlp decide.
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
import yt_dlp # type: ignore
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"stdout": "",
|
"stdout": "",
|
||||||
"stderr": "",
|
"stderr": "",
|
||||||
"error":
|
"error": f"yt-dlp tool unavailable: {type(exc).__name__}: {exc}",
|
||||||
f"yt-dlp module not available: {type(exc).__name__}: {exc}",
|
|
||||||
"table": None,
|
"table": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -415,96 +398,27 @@ def _run_op(op: str, data: Any) -> Dict[str, Any]:
|
|||||||
except Exception:
|
except Exception:
|
||||||
cookiefile = None
|
cookiefile = None
|
||||||
|
|
||||||
ydl_opts: Dict[str,
|
|
||||||
Any] = {
|
|
||||||
"quiet": True,
|
|
||||||
"no_warnings": True,
|
|
||||||
"socket_timeout": 20,
|
|
||||||
"retries": 2,
|
|
||||||
"skip_download": True,
|
|
||||||
# Avoid accidentally expanding huge playlists on load.
|
|
||||||
"noplaylist": True,
|
|
||||||
"noprogress": True,
|
|
||||||
}
|
|
||||||
if cookiefile:
|
|
||||||
ydl_opts["cookiefile"] = cookiefile
|
|
||||||
|
|
||||||
def _format_bytes(n: Any) -> str:
|
def _format_bytes(n: Any) -> str:
|
||||||
"""Format bytes using centralized utility."""
|
"""Format bytes using centralized utility."""
|
||||||
return format_bytes(n)
|
return format_bytes(n)
|
||||||
|
|
||||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl: # type: ignore[attr-defined]
|
formats = list_formats(
|
||||||
info = ydl.extract_info(url, download=False)
|
url,
|
||||||
|
no_playlist=True,
|
||||||
|
cookiefile=cookiefile,
|
||||||
|
timeout_seconds=25,
|
||||||
|
)
|
||||||
|
|
||||||
# Debug: dump a short summary of the format list to the helper log.
|
if formats is None:
|
||||||
try:
|
|
||||||
formats_any = info.get("formats") if isinstance(info, dict) else None
|
|
||||||
count = len(formats_any) if isinstance(formats_any, list) else 0
|
|
||||||
_append_helper_log(
|
|
||||||
f"[ytdlp-formats] extracted formats count={count} url={url}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if isinstance(formats_any, list) and formats_any:
|
|
||||||
limit = 60
|
|
||||||
for i, f in enumerate(formats_any[:limit], start=1):
|
|
||||||
if not isinstance(f, dict):
|
|
||||||
continue
|
|
||||||
fid = str(f.get("format_id") or "")
|
|
||||||
ext = str(f.get("ext") or "")
|
|
||||||
note = f.get("format_note") or f.get("format") or ""
|
|
||||||
vcodec = str(f.get("vcodec") or "")
|
|
||||||
acodec = str(f.get("acodec") or "")
|
|
||||||
size = f.get("filesize") or f.get("filesize_approx")
|
|
||||||
res = str(f.get("resolution") or "")
|
|
||||||
if not res:
|
|
||||||
try:
|
|
||||||
w = f.get("width")
|
|
||||||
h = f.get("height")
|
|
||||||
if w and h:
|
|
||||||
res = f"{int(w)}x{int(h)}"
|
|
||||||
elif h:
|
|
||||||
res = f"{int(h)}p"
|
|
||||||
except Exception:
|
|
||||||
res = ""
|
|
||||||
_append_helper_log(
|
|
||||||
f"[ytdlp-format {i:02d}] id={fid} ext={ext} res={res} note={note} codecs={vcodec}/{acodec} size={size}"
|
|
||||||
)
|
|
||||||
if count > limit:
|
|
||||||
_append_helper_log(
|
|
||||||
f"[ytdlp-formats] (truncated; total={count})"
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Optional: dump the full extracted JSON for inspection.
|
|
||||||
try:
|
|
||||||
dump = os.environ.get("MEDEIA_MPV_YTDLP_DUMP", "").strip()
|
|
||||||
if dump and dump != "0" and isinstance(info, dict):
|
|
||||||
h = hashlib.sha1(url.encode("utf-8",
|
|
||||||
errors="replace")).hexdigest()[:10]
|
|
||||||
out_path = _repo_root() / "Log" / f"ytdlp-probe-{h}.json"
|
|
||||||
out_path.write_text(
|
|
||||||
json.dumps(info,
|
|
||||||
ensure_ascii=False,
|
|
||||||
indent=2),
|
|
||||||
encoding="utf-8",
|
|
||||||
errors="replace",
|
|
||||||
)
|
|
||||||
_append_helper_log(f"[ytdlp-formats] wrote probe json: {out_path}")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if not isinstance(info, dict):
|
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"stdout": "",
|
"stdout": "",
|
||||||
"stderr": "",
|
"stderr": "",
|
||||||
"error": "yt-dlp returned non-dict info",
|
"error": "yt-dlp format probe failed or timed out",
|
||||||
"table": None,
|
"table": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
formats = info.get("formats")
|
if not formats:
|
||||||
if not isinstance(formats, list) or not formats:
|
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
"stdout": "",
|
"stdout": "",
|
||||||
@@ -516,6 +430,48 @@ def _run_op(op: str, data: Any) -> Dict[str, Any]:
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
browseable = [f for f in formats if is_browseable_format(f)]
|
||||||
|
if browseable:
|
||||||
|
formats = browseable
|
||||||
|
|
||||||
|
# Debug: dump a short summary of the format list to the helper log.
|
||||||
|
try:
|
||||||
|
count = len(formats)
|
||||||
|
_append_helper_log(
|
||||||
|
f"[ytdlp-formats] extracted formats count={count} url={url}"
|
||||||
|
)
|
||||||
|
|
||||||
|
limit = 60
|
||||||
|
for i, f in enumerate(formats[:limit], start=1):
|
||||||
|
if not isinstance(f, dict):
|
||||||
|
continue
|
||||||
|
fid = str(f.get("format_id") or "")
|
||||||
|
ext = str(f.get("ext") or "")
|
||||||
|
note = f.get("format_note") or f.get("format") or ""
|
||||||
|
vcodec = str(f.get("vcodec") or "")
|
||||||
|
acodec = str(f.get("acodec") or "")
|
||||||
|
size = f.get("filesize") or f.get("filesize_approx")
|
||||||
|
res = str(f.get("resolution") or "")
|
||||||
|
if not res:
|
||||||
|
try:
|
||||||
|
w = f.get("width")
|
||||||
|
h = f.get("height")
|
||||||
|
if w and h:
|
||||||
|
res = f"{int(w)}x{int(h)}"
|
||||||
|
elif h:
|
||||||
|
res = f"{int(h)}p"
|
||||||
|
except Exception:
|
||||||
|
res = ""
|
||||||
|
_append_helper_log(
|
||||||
|
f"[ytdlp-format {i:02d}] id={fid} ext={ext} res={res} note={note} codecs={vcodec}/{acodec} size={size}"
|
||||||
|
)
|
||||||
|
if count > limit:
|
||||||
|
_append_helper_log(
|
||||||
|
f"[ytdlp-formats] (truncated; total={count})"
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
rows = []
|
rows = []
|
||||||
for fmt in formats:
|
for fmt in formats:
|
||||||
if not isinstance(fmt, dict):
|
if not isinstance(fmt, dict):
|
||||||
@@ -540,8 +496,15 @@ def _run_op(op: str, data: Any) -> Dict[str, Any]:
|
|||||||
ext = str(fmt.get("ext") or "").strip()
|
ext = str(fmt.get("ext") or "").strip()
|
||||||
size = _format_bytes(fmt.get("filesize") or fmt.get("filesize_approx"))
|
size = _format_bytes(fmt.get("filesize") or fmt.get("filesize_approx"))
|
||||||
|
|
||||||
|
vcodec = str(fmt.get("vcodec") or "none")
|
||||||
|
acodec = str(fmt.get("acodec") or "none")
|
||||||
|
selection_id = format_id
|
||||||
|
if vcodec != "none" and acodec == "none":
|
||||||
|
selection_id = f"{format_id}+ba"
|
||||||
|
|
||||||
# Build selection args compatible with MPV Lua picker.
|
# Build selection args compatible with MPV Lua picker.
|
||||||
selection_args = ["-query", f"format:{format_id}"]
|
# Use -format instead of -query so Lua can extract the ID easily.
|
||||||
|
selection_args = ["-format", selection_id]
|
||||||
|
|
||||||
rows.append(
|
rows.append(
|
||||||
{
|
{
|
||||||
@@ -962,6 +925,28 @@ def main(argv: Optional[list[str]] = None) -> int:
|
|||||||
f"[helper] failed to publish config temp: {type(exc).__name__}: {exc}"
|
f"[helper] failed to publish config temp: {type(exc).__name__}: {exc}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Publish yt-dlp supported domains for Lua menu filtering
|
||||||
|
try:
|
||||||
|
from tool.ytdlp import _build_supported_domains
|
||||||
|
domains = sorted(list(_build_supported_domains()))
|
||||||
|
if domains:
|
||||||
|
# We join them into a space-separated string for Lua to parse easily
|
||||||
|
domains_str = " ".join(domains)
|
||||||
|
client.send_command_no_wait(
|
||||||
|
[
|
||||||
|
"set_property_string",
|
||||||
|
"user-data/medeia-ytdlp-domains-cached",
|
||||||
|
domains_str
|
||||||
|
]
|
||||||
|
)
|
||||||
|
_append_helper_log(
|
||||||
|
f"[helper] published {len(domains)} ytdlp domains for Lua menu filtering"
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
_append_helper_log(
|
||||||
|
f"[helper] failed to publish ytdlp domains: {type(exc).__name__}: {exc}"
|
||||||
|
)
|
||||||
|
|
||||||
last_seen_id: Optional[str] = None
|
last_seen_id: Optional[str] = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ osd-bar=no
|
|||||||
ytdl=yes
|
ytdl=yes
|
||||||
# uosc will draw its own window controls and border if you disable window border
|
# uosc will draw its own window controls and border if you disable window border
|
||||||
border=no
|
border=no
|
||||||
|
cache=yes
|
||||||
|
cache-secs=30
|
||||||
|
demuxer-max-bytes=200MiB
|
||||||
|
demuxer-max-back-bytes=100MiB
|
||||||
|
|
||||||
# Ensure uosc texture/icon fonts are discoverable by libass.
|
# Ensure uosc texture/icon fonts are discoverable by libass.
|
||||||
osd-fonts-dir=~~/scripts/uosc/fonts
|
osd-fonts-dir=~~/scripts/uosc/fonts
|
||||||
|
|||||||
@@ -2068,13 +2068,13 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
|||||||
"If you need full [main2] logs, restart mpv so it starts with --log-file."
|
"If you need full [main2] logs, restart mpv so it starts with --log-file."
|
||||||
)
|
)
|
||||||
|
|
||||||
# Print database logs for mpv module (helper output)
|
# Print database logs for mpv module (helper + lua output)
|
||||||
try:
|
try:
|
||||||
import sqlite3
|
import sqlite3
|
||||||
log_db_path = str((Path(__file__).resolve().parent.parent / "logs.db"))
|
log_db_path = str((Path(__file__).resolve().parent.parent / "logs.db"))
|
||||||
conn = sqlite3.connect(log_db_path, timeout=5.0)
|
conn = sqlite3.connect(log_db_path, timeout=5.0)
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
query = "SELECT level, module, message FROM logs WHERE module = 'mpv'"
|
query = "SELECT timestamp, level, module, message FROM logs WHERE module = 'mpv'"
|
||||||
params: List[str] = []
|
params: List[str] = []
|
||||||
if log_filter_text:
|
if log_filter_text:
|
||||||
query += " AND LOWER(message) LIKE ?"
|
query += " AND LOWER(message) LIKE ?"
|
||||||
@@ -2085,57 +2085,24 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
|||||||
cur.close()
|
cur.close()
|
||||||
conn.close()
|
conn.close()
|
||||||
if log_filter_text:
|
if log_filter_text:
|
||||||
print(f"Helper logs from database (mpv module, filtered by '{log_filter_text}', most recent first):")
|
print(f"MPV logs from database (mpv module, filtered by '{log_filter_text}', most recent first):")
|
||||||
else:
|
else:
|
||||||
print("Helper logs from database (mpv module, most recent first):")
|
print("MPV logs from database (mpv module, most recent first):")
|
||||||
if mpv_logs:
|
if mpv_logs:
|
||||||
for level, module, message in mpv_logs:
|
for timestamp, level, module, message in mpv_logs:
|
||||||
print(f"[{level}] {message}")
|
ts = str(timestamp or "").strip()
|
||||||
|
if ts:
|
||||||
|
print(f"[{ts}] [{level}] {message}")
|
||||||
|
else:
|
||||||
|
print(f"[{level}] {message}")
|
||||||
else:
|
else:
|
||||||
if log_filter_text:
|
if log_filter_text:
|
||||||
print(f"(no helper logs found matching '{log_filter_text}')")
|
print(f"(no mpv logs found matching '{log_filter_text}')")
|
||||||
else:
|
else:
|
||||||
print("(no helper logs found)")
|
print("(no mpv logs found)")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
debug(f"Could not fetch database logs: {e}")
|
debug(f"Could not fetch database logs: {e}")
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Also print the helper log tail (this captures Python helper output that won't
|
|
||||||
# necessarily show up in MPV's own log-file).
|
|
||||||
try:
|
|
||||||
helper_path = _helper_log_file()
|
|
||||||
helper_tail = _tail_text_file(str(helper_path), max_lines=200)
|
|
||||||
filtered_helper = _apply_log_filter(helper_tail, log_filter_text)
|
|
||||||
print(f"Helper log file: {str(helper_path)}")
|
|
||||||
if filtered_helper:
|
|
||||||
print("Helper log (tail):")
|
|
||||||
for ln in filtered_helper:
|
|
||||||
print(ln)
|
|
||||||
else:
|
|
||||||
if log_filter_text:
|
|
||||||
print(f"(no helper file logs found matching '{log_filter_text}')")
|
|
||||||
else:
|
|
||||||
print("Helper log (tail): <empty>")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Also print the Lua-side log tail (mp.msg output isn't always written to mpv's log-file).
|
|
||||||
try:
|
|
||||||
lua_path = _lua_log_file()
|
|
||||||
lua_tail = _tail_text_file(str(lua_path), max_lines=200)
|
|
||||||
filtered_lua = _apply_log_filter(lua_tail, log_filter_text)
|
|
||||||
print(f"Lua log file: {str(lua_path)}")
|
|
||||||
if filtered_lua:
|
|
||||||
print("Lua log (tail):")
|
|
||||||
for ln in filtered_lua:
|
|
||||||
print(ln)
|
|
||||||
else:
|
|
||||||
if log_filter_text:
|
|
||||||
print(f"(no lua file logs found matching '{log_filter_text}')")
|
|
||||||
else:
|
|
||||||
print("Lua log (tail): <empty>")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
try:
|
try:
|
||||||
|
|||||||
Reference in New Issue
Block a user