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=[^&]+)?)"
|
||||
],
|
||||
"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": {
|
||||
"name": "rapidgator",
|
||||
@@ -92,7 +92,7 @@
|
||||
"(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": {
|
||||
"name": "mega",
|
||||
@@ -495,7 +495,7 @@
|
||||
"(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": {
|
||||
"name": "mediafire",
|
||||
|
||||
502
MPV/LUA/main.lua
502
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_READY_PROP = 'user-data/medeia-pipeline-ready'
|
||||
|
||||
-- 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.
|
||||
-- Dedicated Lua log: write directly to logs.db database for unified logging
|
||||
-- Fallback to stderr if database unavailable
|
||||
local function _lua_log(text)
|
||||
local payload = (text and tostring(text) or '')
|
||||
if payload == '' then
|
||||
return
|
||||
end
|
||||
local dir = ''
|
||||
|
||||
-- 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
|
||||
|
||||
-- Prefer repo-root Log/ for consistency with Python helper logs.
|
||||
-- Attempt to find repo root for database access
|
||||
local repo_root = ''
|
||||
do
|
||||
local function find_up(start_dir, relative_path, max_levels)
|
||||
local d = start_dir
|
||||
@@ -102,110 +84,33 @@ local function _lua_log(text)
|
||||
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
|
||||
|
||||
_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
|
||||
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
|
||||
-- Write to logs.db via Python subprocess (non-blocking, async)
|
||||
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'
|
||||
local db_path = (repo_root .. '/logs.db'):gsub('\\', '/')
|
||||
local msg = payload:gsub('\\', '\\\\'):gsub("'", "\\'")
|
||||
|
||||
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
|
||||
"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
|
||||
|
||||
-- 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)
|
||||
if not text or text == '' then
|
||||
return
|
||||
@@ -213,11 +118,8 @@ local function _log_all(level, text)
|
||||
level = tostring(level or 'INFO'):upper()
|
||||
text = tostring(text)
|
||||
|
||||
-- Log to file
|
||||
-- Log with level prefix via _lua_log (which writes to database)
|
||||
_lua_log('[' .. level .. '] ' .. text)
|
||||
|
||||
-- Log to database (async, non-blocking)
|
||||
_log_to_db(level, text)
|
||||
end
|
||||
|
||||
local function ensure_uosc_loaded()
|
||||
@@ -344,8 +246,8 @@ end
|
||||
-- Default to visible unless user overrides.
|
||||
lyric_set_visible(true)
|
||||
|
||||
-- Configuration
|
||||
local opts = {
|
||||
-- Configuration (global so _lua_log can see python_path early)
|
||||
opts = {
|
||||
python_path = "python",
|
||||
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
|
||||
end
|
||||
|
||||
-- Forward declaration (defined later) used by helper auto-start.
|
||||
local _resolve_python_exe
|
||||
|
||||
local _cached_store_names = {}
|
||||
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 = 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 ready = mp.get_property(PIPELINE_READY_PROP)
|
||||
if ready == nil or ready == '' then
|
||||
ready = mp.get_property_native(PIPELINE_READY_PROP)
|
||||
end
|
||||
if not ready then
|
||||
_helper_ready_last_value = ''
|
||||
_helper_ready_last_seen_ts = 0
|
||||
return false
|
||||
end
|
||||
local s = tostring(ready)
|
||||
if s == '' or s == '0' then
|
||||
_helper_ready_last_value = s
|
||||
_helper_ready_last_seen_ts = 0
|
||||
return false
|
||||
end
|
||||
|
||||
-- Only support unix timestamp heartbeats from current helper version
|
||||
local now = mp.get_time() or 0
|
||||
if s ~= _helper_ready_last_value then
|
||||
_helper_ready_last_value = s
|
||||
_helper_ready_last_seen_ts = now
|
||||
end
|
||||
|
||||
-- Prefer timestamp heartbeats from modern helpers.
|
||||
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
|
||||
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
|
||||
return age <= 10
|
||||
if age <= HELPER_READY_STALE_SECONDS then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Non-empty value treated as ready
|
||||
-- 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
|
||||
|
||||
local function get_mpv_ipc_path()
|
||||
@@ -630,6 +555,29 @@ local _ipc_async_queue = {}
|
||||
local function _run_helper_request_async(req, timeout_seconds, cb)
|
||||
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
|
||||
_ipc_async_queue[#_ipc_async_queue + 1] = { req = req, timeout = timeout_seconds, cb = cb }
|
||||
return
|
||||
@@ -637,6 +585,36 @@ local function _run_helper_request_async(req, timeout_seconds, cb)
|
||||
_ipc_async_busy = true
|
||||
|
||||
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
|
||||
cb(resp, err)
|
||||
|
||||
@@ -655,34 +633,8 @@ local function _run_helper_request_async(req, timeout_seconds, cb)
|
||||
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()
|
||||
|
||||
local function send_request_payload()
|
||||
_lua_log('ipc-async: send request id=' .. tostring(id) .. ' ' .. label)
|
||||
local req_json = utils.format_json(req)
|
||||
_last_ipc_last_req_json = req_json
|
||||
@@ -710,16 +662,57 @@ local function _run_helper_request_async(req, timeout_seconds, cb)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
if mp.get_time() >= ready_deadline then
|
||||
local function wait_for_helper_ready(timeout, on_ready)
|
||||
local deadline = mp.get_time() + (timeout or 3.0)
|
||||
local ready_timer
|
||||
ready_timer = mp.add_periodic_timer(0.05, function()
|
||||
if _is_pipeline_helper_ready() then
|
||||
ready_timer:kill()
|
||||
on_ready()
|
||||
return
|
||||
end
|
||||
if mp.get_time() >= deadline then
|
||||
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
|
||||
end
|
||||
|
||||
_lua_log('ipc-async: helper not ready, auto-starting before request id=' .. id)
|
||||
attempt_start_pipeline_helper_async(function(success)
|
||||
if not success then
|
||||
_lua_log('ipc-async: helper auto-start failed while handling request id=' .. id)
|
||||
else
|
||||
_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
|
||||
|
||||
local function _run_helper_request_response(req, timeout_seconds)
|
||||
@@ -830,13 +823,14 @@ local function _url_can_direct_load(url)
|
||||
return true
|
||||
end
|
||||
|
||||
local function _try_direct_loadfile(url)
|
||||
local function _try_direct_loadfile(url, force)
|
||||
-- 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=true, loaded=false: URL not suitable for direct load (when not forced)
|
||||
-- - 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)
|
||||
return true, false -- Not suitable, but not an error
|
||||
end
|
||||
@@ -858,7 +852,7 @@ local function _is_windows()
|
||||
return sep == '\\'
|
||||
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'
|
||||
if (not prefer_no_console) or (not _is_windows()) then
|
||||
return python
|
||||
@@ -1711,6 +1705,75 @@ local function _is_http_url(u)
|
||||
return u:match('^https?://') ~= nil
|
||||
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)
|
||||
if type(url) ~= 'string' or url == '' then
|
||||
return
|
||||
@@ -1741,33 +1804,41 @@ end
|
||||
|
||||
function FileState:fetch_formats(cb)
|
||||
local url = tostring(self.url or '')
|
||||
_lua_log('fetch-formats: started for url=' .. url)
|
||||
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
|
||||
return
|
||||
end
|
||||
|
||||
if _extract_store_hash(url) then
|
||||
_lua_log('fetch-formats: skipped (store-hash)')
|
||||
if cb then cb(false, 'store-hash url') end
|
||||
return
|
||||
end
|
||||
|
||||
local cached = _get_cached_formats_table(url)
|
||||
if type(cached) == 'table' then
|
||||
_lua_log('fetch-formats: using cached table')
|
||||
self:set_formats(url, cached)
|
||||
if cb then cb(true, nil) end
|
||||
return
|
||||
end
|
||||
|
||||
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 {}
|
||||
if cb then table.insert(_formats_waiters[url], cb) end
|
||||
|
||||
_run_helper_request_async({ op = 'ytdlp-formats', data = { url = url } }, 90, function(resp, err)
|
||||
_lua_log('fetch-formats: IPC callback received id=' .. tostring(resp and resp.id) .. ' err=' .. tostring(err))
|
||||
_formats_inflight[url] = nil
|
||||
|
||||
local ok = false
|
||||
@@ -1779,6 +1850,7 @@ function FileState:fetch_formats(cb)
|
||||
_cache_formats_for_url(url, resp.table)
|
||||
_lua_log('formats: cached ' .. tostring((resp.table.rows and #resp.table.rows) or 0) .. ' rows for url')
|
||||
else
|
||||
_lua_log('fetch-formats: request failed success=' .. tostring(resp and resp.success))
|
||||
if type(resp) == 'table' then
|
||||
if resp.error and tostring(resp.error) ~= '' then
|
||||
reason = tostring(resp.error)
|
||||
@@ -1789,11 +1861,40 @@ function FileState:fetch_formats(cb)
|
||||
end
|
||||
|
||||
local waiters = _formats_waiters[url] or {}
|
||||
_lua_log('fetch-formats: calling ' .. tostring(#waiters) .. ' waiters with ok=' .. tostring(ok) .. ' reason=' .. tostring(reason))
|
||||
_formats_waiters[url] = nil
|
||||
for _, fn in ipairs(waiters) do
|
||||
pcall(fn, ok, reason)
|
||||
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
|
||||
|
||||
local function _prefetch_formats_for_url(url)
|
||||
@@ -1847,6 +1948,38 @@ local function _debug_dump_formatted_formats(url, tbl, items)
|
||||
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()
|
||||
-- Preferred: mpv exposes the active ytdl format string.
|
||||
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
|
||||
|
||||
_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)
|
||||
return
|
||||
end
|
||||
@@ -2155,6 +2289,7 @@ mp.register_script_message('medios-change-format-current', function()
|
||||
|
||||
_pending_format_change.formats_table = tbl
|
||||
_debug_dump_formatted_formats(url, tbl, items)
|
||||
_show_format_list_osd(items, 8)
|
||||
_uosc_open_list_picker(DOWNLOAD_FORMAT_MENU_TYPE, 'Change format', items)
|
||||
end)
|
||||
end
|
||||
@@ -2392,9 +2527,10 @@ function M.run_pipeline(pipeline_cmd, seeds, cb)
|
||||
end
|
||||
ensure_mpv_ipc_server()
|
||||
|
||||
-- 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
|
||||
-- Use a longer timeout for `.mpv -url` commands to avoid races with slow helper starts.
|
||||
local lower_cmd = pipeline_cmd:lower()
|
||||
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)
|
||||
_lua_log('M.run_pipeline callback fired: resp=' .. tostring(resp) .. ', err=' .. tostring(err))
|
||||
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)
|
||||
_lua_log('[LOAD-URL] Checking if URL can be loaded directly: ' .. 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 command sent successfully')
|
||||
local direct_ok, direct_loaded = _try_direct_loadfile(url, false)
|
||||
if direct_ok and direct_loaded then
|
||||
_lua_log('[LOAD-URL] Direct loadfile command sent successfully (forced)')
|
||||
_log_all('INFO', 'Load URL succeeded via direct load')
|
||||
mp.osd_message('URL loaded', 2)
|
||||
close_menu()
|
||||
return
|
||||
else
|
||||
_lua_log('[LOAD-URL] Direct loadfile command failed')
|
||||
_log_all('ERROR', 'Load URL failed: direct loadfile command failed')
|
||||
mp.osd_message('Load URL failed (direct)', 3)
|
||||
close_menu()
|
||||
return
|
||||
end
|
||||
if direct_ok then
|
||||
_lua_log('[LOAD-URL] Direct loadfile command did not load the URL; falling back to helper')
|
||||
else
|
||||
_lua_log('[LOAD-URL] Direct loadfile command failed; falling back to helper')
|
||||
end
|
||||
|
||||
-- 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')
|
||||
_log_all('INFO', 'Load URL succeeded')
|
||||
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()
|
||||
end)
|
||||
end)
|
||||
@@ -2964,6 +3105,9 @@ end)
|
||||
function M.show_menu()
|
||||
_lua_log('[MENU] M.show_menu called')
|
||||
|
||||
local target = _current_target()
|
||||
_lua_log('[MENU] current target: ' .. tostring(target))
|
||||
|
||||
-- Build menu items
|
||||
-- Note: UOSC expects command strings, not arrays
|
||||
local items = {
|
||||
@@ -2972,12 +3116,10 @@ function M.show_menu()
|
||||
{ title = "Delete File", value = "script-binding medios-delete", hint = "Ctrl+Del" },
|
||||
{ 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" },
|
||||
}
|
||||
|
||||
-- Conditionally add "Start Helper" if not running
|
||||
if not _is_pipeline_helper_ready() then
|
||||
table.insert(items, { title = "Start Helper", hint = "(pipeline actions)", value = "script-message medios-start-helper" })
|
||||
if _is_ytdlp_url(target) then
|
||||
table.insert(items, { title = "Change Format", value = "script-message medios-change-format-current" })
|
||||
end
|
||||
|
||||
_lua_log('[MENU] Built ' .. #items .. ' menu items')
|
||||
@@ -3058,6 +3200,14 @@ mp.add_timeout(0, function()
|
||||
pcall(ensure_mpv_ipc_server)
|
||||
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)
|
||||
mp.add_timeout(1.0, function()
|
||||
_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,
|
||||
}
|
||||
|
||||
# Fast gate: only for streaming URLs yt-dlp knows about.
|
||||
try:
|
||||
from tool.ytdlp import is_url_supported_by_ytdlp # 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
|
||||
from tool.ytdlp import list_formats, is_browseable_format # noqa: WPS433
|
||||
except Exception as exc:
|
||||
return {
|
||||
"success": False,
|
||||
"stdout": "",
|
||||
"stderr": "",
|
||||
"error":
|
||||
f"yt-dlp module not available: {type(exc).__name__}: {exc}",
|
||||
"error": f"yt-dlp tool unavailable: {type(exc).__name__}: {exc}",
|
||||
"table": None,
|
||||
}
|
||||
|
||||
@@ -415,38 +398,51 @@ def _run_op(op: str, data: Any) -> Dict[str, Any]:
|
||||
except Exception:
|
||||
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:
|
||||
"""Format bytes using centralized utility."""
|
||||
return format_bytes(n)
|
||||
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl: # type: ignore[attr-defined]
|
||||
info = ydl.extract_info(url, download=False)
|
||||
formats = list_formats(
|
||||
url,
|
||||
no_playlist=True,
|
||||
cookiefile=cookiefile,
|
||||
timeout_seconds=25,
|
||||
)
|
||||
|
||||
if formats is None:
|
||||
return {
|
||||
"success": False,
|
||||
"stdout": "",
|
||||
"stderr": "",
|
||||
"error": "yt-dlp format probe failed or timed out",
|
||||
"table": None,
|
||||
}
|
||||
|
||||
if not formats:
|
||||
return {
|
||||
"success": True,
|
||||
"stdout": "",
|
||||
"stderr": "",
|
||||
"error": None,
|
||||
"table": {
|
||||
"title": "Formats",
|
||||
"rows": []
|
||||
},
|
||||
}
|
||||
|
||||
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:
|
||||
formats_any = info.get("formats") if isinstance(info, dict) else None
|
||||
count = len(formats_any) if isinstance(formats_any, list) else 0
|
||||
count = len(formats)
|
||||
_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):
|
||||
for i, f in enumerate(formats[:limit], start=1):
|
||||
if not isinstance(f, dict):
|
||||
continue
|
||||
fid = str(f.get("format_id") or "")
|
||||
@@ -476,46 +472,6 @@ def _run_op(op: str, data: Any) -> Dict[str, Any]:
|
||||
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 {
|
||||
"success": False,
|
||||
"stdout": "",
|
||||
"stderr": "",
|
||||
"error": "yt-dlp returned non-dict info",
|
||||
"table": None,
|
||||
}
|
||||
|
||||
formats = info.get("formats")
|
||||
if not isinstance(formats, list) or not formats:
|
||||
return {
|
||||
"success": True,
|
||||
"stdout": "",
|
||||
"stderr": "",
|
||||
"error": None,
|
||||
"table": {
|
||||
"title": "Formats",
|
||||
"rows": []
|
||||
},
|
||||
}
|
||||
|
||||
rows = []
|
||||
for fmt in formats:
|
||||
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()
|
||||
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.
|
||||
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(
|
||||
{
|
||||
@@ -962,6 +925,28 @@ def main(argv: Optional[list[str]] = None) -> int:
|
||||
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
|
||||
|
||||
try:
|
||||
|
||||
@@ -4,6 +4,10 @@ osd-bar=no
|
||||
ytdl=yes
|
||||
# uosc will draw its own window controls and border if you disable window border
|
||||
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.
|
||||
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."
|
||||
)
|
||||
|
||||
# Print database logs for mpv module (helper output)
|
||||
# Print database logs for mpv module (helper + lua output)
|
||||
try:
|
||||
import sqlite3
|
||||
log_db_path = str((Path(__file__).resolve().parent.parent / "logs.db"))
|
||||
conn = sqlite3.connect(log_db_path, timeout=5.0)
|
||||
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] = []
|
||||
if log_filter_text:
|
||||
query += " AND LOWER(message) LIKE ?"
|
||||
@@ -2085,57 +2085,24 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
cur.close()
|
||||
conn.close()
|
||||
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:
|
||||
print("Helper logs from database (mpv module, most recent first):")
|
||||
print("MPV logs from database (mpv module, most recent first):")
|
||||
if mpv_logs:
|
||||
for level, module, message in mpv_logs:
|
||||
for timestamp, level, module, message in mpv_logs:
|
||||
ts = str(timestamp or "").strip()
|
||||
if ts:
|
||||
print(f"[{ts}] [{level}] {message}")
|
||||
else:
|
||||
print(f"[{level}] {message}")
|
||||
else:
|
||||
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:
|
||||
print("(no helper logs found)")
|
||||
print("(no mpv logs found)")
|
||||
except Exception as e:
|
||||
debug(f"Could not fetch database logs: {e}")
|
||||
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:
|
||||
pass
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user