dfd
This commit is contained in:
293
MPV/LUA/main.lua
293
MPV/LUA/main.lua
@@ -4,7 +4,7 @@ local msg = require 'mp.msg'
|
|||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
local MEDEIA_LUA_VERSION = '2026-03-22.1'
|
local MEDEIA_LUA_VERSION = '2026-03-22.3'
|
||||||
|
|
||||||
-- Expose a tiny breadcrumb for debugging which script version is loaded.
|
-- Expose a tiny breadcrumb for debugging which script version is loaded.
|
||||||
pcall(mp.set_property, 'user-data/medeia-lua-version', MEDEIA_LUA_VERSION)
|
pcall(mp.set_property, 'user-data/medeia-lua-version', MEDEIA_LUA_VERSION)
|
||||||
@@ -750,6 +750,121 @@ local _refresh_current_store_url_status
|
|||||||
local _skip_next_store_check_url = ''
|
local _skip_next_store_check_url = ''
|
||||||
local _pick_folder_windows
|
local _pick_folder_windows
|
||||||
|
|
||||||
|
function M._load_store_choices_direct_async(cb)
|
||||||
|
cb = cb or function() end
|
||||||
|
|
||||||
|
local refresh_state = M._store_direct_refresh_state or { inflight = false, callbacks = {} }
|
||||||
|
M._store_direct_refresh_state = refresh_state
|
||||||
|
|
||||||
|
if refresh_state.inflight then
|
||||||
|
table.insert(refresh_state.callbacks, cb)
|
||||||
|
_lua_log('stores: direct config load join existing request')
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local python = _resolve_python_exe(false)
|
||||||
|
if not python or python == '' then
|
||||||
|
cb(nil, 'no python executable available')
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local repo_root = _detect_repo_root()
|
||||||
|
if not repo_root or repo_root == '' then
|
||||||
|
cb(nil, 'repo root not found')
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local function finish(resp, err)
|
||||||
|
local callbacks = refresh_state.callbacks or {}
|
||||||
|
refresh_state.callbacks = {}
|
||||||
|
refresh_state.inflight = false
|
||||||
|
for _, pending_cb in ipairs(callbacks) do
|
||||||
|
pcall(pending_cb, resp, err)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local bootstrap = table.concat({
|
||||||
|
'import json, os, sys',
|
||||||
|
'root = sys.argv[1]',
|
||||||
|
'if root:',
|
||||||
|
' os.chdir(root)',
|
||||||
|
' sys.path.insert(0, root) if root not in sys.path else None',
|
||||||
|
'from SYS.logger import set_thread_stream',
|
||||||
|
'set_thread_stream(sys.stderr)',
|
||||||
|
'from SYS.config import load_config',
|
||||||
|
'from Store.registry import list_configured_backend_names',
|
||||||
|
'config = load_config()',
|
||||||
|
'choices = list_configured_backend_names(config) or []',
|
||||||
|
'sys.stdout.write(json.dumps({"choices": choices}, ensure_ascii=False))',
|
||||||
|
}, '\n')
|
||||||
|
|
||||||
|
refresh_state.inflight = true
|
||||||
|
refresh_state.callbacks = { cb }
|
||||||
|
_lua_log('stores: spawning direct config scan python=' .. tostring(python) .. ' root=' .. tostring(repo_root))
|
||||||
|
|
||||||
|
mp.command_native_async(
|
||||||
|
{
|
||||||
|
name = 'subprocess',
|
||||||
|
args = { python, '-c', bootstrap, repo_root },
|
||||||
|
capture_stdout = true,
|
||||||
|
capture_stderr = true,
|
||||||
|
playback_only = false,
|
||||||
|
},
|
||||||
|
function(success, result, err)
|
||||||
|
if not success then
|
||||||
|
finish(nil, tostring(err or 'subprocess failed'))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if type(result) ~= 'table' then
|
||||||
|
finish(nil, 'invalid subprocess result')
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local status = tonumber(result.status or 0) or 0
|
||||||
|
local stdout = trim(tostring(result.stdout or ''))
|
||||||
|
local stderr = trim(tostring(result.stderr or ''))
|
||||||
|
if status ~= 0 then
|
||||||
|
local detail = stderr
|
||||||
|
if detail == '' then
|
||||||
|
detail = tostring(result.error or ('direct store scan exited with status ' .. tostring(status)))
|
||||||
|
end
|
||||||
|
finish(nil, detail)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if stdout == '' then
|
||||||
|
local detail = stderr
|
||||||
|
if detail == '' then
|
||||||
|
detail = 'direct store scan returned no output'
|
||||||
|
end
|
||||||
|
finish(nil, detail)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local ok, resp = pcall(utils.parse_json, stdout)
|
||||||
|
if ok and type(resp) == 'string' then
|
||||||
|
ok, resp = pcall(utils.parse_json, resp)
|
||||||
|
end
|
||||||
|
if not ok or type(resp) ~= 'table' then
|
||||||
|
local stdout_preview = stdout
|
||||||
|
if #stdout_preview > 200 then
|
||||||
|
stdout_preview = stdout_preview:sub(1, 200) .. '...'
|
||||||
|
end
|
||||||
|
local stderr_preview = stderr
|
||||||
|
if #stderr_preview > 200 then
|
||||||
|
stderr_preview = stderr_preview:sub(1, 200) .. '...'
|
||||||
|
end
|
||||||
|
_lua_log('stores: direct config parse failed stdout=' .. tostring(stdout_preview) .. ' stderr=' .. tostring(stderr_preview))
|
||||||
|
finish(nil, 'failed to parse direct store scan output')
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
finish(resp, nil)
|
||||||
|
end
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
local function _normalize_store_name(store)
|
local function _normalize_store_name(store)
|
||||||
store = trim(tostring(store or ''))
|
store = trim(tostring(store or ''))
|
||||||
store = store:gsub('^"', ''):gsub('"$', '')
|
store = store:gsub('^"', ''):gsub('"$', '')
|
||||||
@@ -2715,7 +2830,34 @@ end
|
|||||||
|
|
||||||
_pick_folder_windows = function()
|
_pick_folder_windows = function()
|
||||||
-- Native folder picker via PowerShell + WinForms.
|
-- Native folder picker via PowerShell + WinForms.
|
||||||
local ps = [[Add-Type -AssemblyName System.Windows.Forms; $d = New-Object System.Windows.Forms.FolderBrowserDialog; $d.Description = 'Select download folder'; $d.ShowNewFolderButton = $true; if ($d.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { $d.SelectedPath }]]
|
local ps = [[
|
||||||
|
Add-Type -AssemblyName System.Windows.Forms
|
||||||
|
Add-Type -AssemblyName System.Drawing
|
||||||
|
[System.Windows.Forms.Application]::EnableVisualStyles()
|
||||||
|
$owner = New-Object System.Windows.Forms.Form
|
||||||
|
$owner.Text = 'medeia-folder-owner'
|
||||||
|
$owner.StartPosition = [System.Windows.Forms.FormStartPosition]::Manual
|
||||||
|
$owner.Location = New-Object System.Drawing.Point(-32000, -32000)
|
||||||
|
$owner.Size = New-Object System.Drawing.Size(1, 1)
|
||||||
|
$owner.ShowInTaskbar = $false
|
||||||
|
$owner.TopMost = $true
|
||||||
|
$owner.Opacity = 0
|
||||||
|
$owner.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::FixedToolWindow
|
||||||
|
$owner.Add_Shown({ $owner.Activate() })
|
||||||
|
$null = $owner.Show()
|
||||||
|
$d = New-Object System.Windows.Forms.FolderBrowserDialog
|
||||||
|
$d.Description = 'Select download folder'
|
||||||
|
$d.ShowNewFolderButton = $true
|
||||||
|
try {
|
||||||
|
if ($d.ShowDialog($owner) -eq [System.Windows.Forms.DialogResult]::OK) {
|
||||||
|
$d.SelectedPath
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
$d.Dispose()
|
||||||
|
$owner.Close()
|
||||||
|
$owner.Dispose()
|
||||||
|
}
|
||||||
|
]]
|
||||||
local res = utils.subprocess({
|
local res = utils.subprocess({
|
||||||
-- Hide the PowerShell console window (dialog still shows).
|
-- Hide the PowerShell console window (dialog still shows).
|
||||||
args = { 'powershell', '-NoProfile', '-WindowStyle', 'Hidden', '-STA', '-ExecutionPolicy', 'Bypass', '-Command', ps },
|
args = { 'powershell', '-NoProfile', '-WindowStyle', 'Hidden', '-STA', '-ExecutionPolicy', 'Bypass', '-Command', ps },
|
||||||
@@ -2763,13 +2905,9 @@ _refresh_store_cache = function(timeout_seconds, on_complete)
|
|||||||
local prev_key = _store_names_key(_cached_store_names)
|
local prev_key = _store_names_key(_cached_store_names)
|
||||||
local had_previous = prev_count > 0
|
local had_previous = prev_count > 0
|
||||||
|
|
||||||
local cached_json = mp.get_property('user-data/medeia-store-choices-cached')
|
local function apply_store_choices(resp, source)
|
||||||
_lua_log('stores: cache_read cached_json=' .. tostring(cached_json) .. ' len=' .. tostring(cached_json and #cached_json or 0))
|
|
||||||
|
|
||||||
if cached_json and cached_json ~= '' then
|
|
||||||
local function handle_cached(resp)
|
|
||||||
if not resp or type(resp) ~= 'table' or type(resp.choices) ~= 'table' then
|
if not resp or type(resp) ~= 'table' or type(resp.choices) ~= 'table' then
|
||||||
_lua_log('stores: cache_parse result missing choices table; resp_type=' .. tostring(type(resp)))
|
_lua_log('stores: ' .. tostring(source) .. ' result missing choices table; resp_type=' .. tostring(type(resp)))
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -2781,90 +2919,47 @@ _refresh_store_cache = function(timeout_seconds, on_complete)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
if #out == 0 and had_previous then
|
if #out == 0 and had_previous then
|
||||||
_lua_log('stores: ignoring empty cache payload; keeping previous store list')
|
_lua_log('stores: ignoring empty ' .. tostring(source) .. ' payload; keeping previous store list')
|
||||||
if type(on_complete) == 'function' then
|
if type(on_complete) == 'function' then
|
||||||
on_complete(true, false)
|
on_complete(true, false)
|
||||||
end
|
end
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
_cached_store_names = out
|
_cached_store_names = out
|
||||||
_store_cache_loaded = (#out > 0) or _store_cache_loaded
|
_store_cache_loaded = (#out > 0) or _store_cache_loaded
|
||||||
|
|
||||||
|
local payload = utils.format_json({ choices = out })
|
||||||
|
if type(payload) == 'string' and payload ~= '' then
|
||||||
|
pcall(mp.set_property, 'user-data/medeia-store-choices-cached', payload)
|
||||||
|
end
|
||||||
|
|
||||||
local preview = ''
|
local preview = ''
|
||||||
if #out > 0 then
|
if #out > 0 then
|
||||||
preview = table.concat(out, ', ')
|
preview = table.concat(out, ', ')
|
||||||
end
|
end
|
||||||
_lua_log('stores: loaded ' .. tostring(#out) .. ' stores from cache: ' .. tostring(preview))
|
_lua_log('stores: loaded ' .. tostring(#out) .. ' stores from ' .. tostring(source) .. ': ' .. tostring(preview))
|
||||||
if type(on_complete) == 'function' then
|
if type(on_complete) == 'function' then
|
||||||
on_complete(true, _store_names_key(out) ~= prev_key)
|
on_complete(true, _store_names_key(out) ~= prev_key)
|
||||||
end
|
end
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
local ok, cached_resp = pcall(utils.parse_json, cached_json)
|
local function request_helper_store_choices()
|
||||||
_lua_log('stores: cache_parse ok=' .. tostring(ok) .. ' resp_type=' .. tostring(type(cached_resp)))
|
|
||||||
if ok then
|
|
||||||
if type(cached_resp) == 'string' then
|
|
||||||
_lua_log('stores: cache_parse returned string, trying again...')
|
|
||||||
ok, cached_resp = pcall(utils.parse_json, cached_resp)
|
|
||||||
_lua_log('stores: cache_parse retry ok=' .. tostring(ok) .. ' resp_type=' .. tostring(type(cached_resp)))
|
|
||||||
end
|
|
||||||
if ok then
|
|
||||||
if handle_cached(cached_resp) then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
else
|
|
||||||
_lua_log('stores: cache_parse failed ok=' .. tostring(ok) .. ' resp=' .. tostring(cached_resp))
|
|
||||||
end
|
|
||||||
else
|
|
||||||
_lua_log('stores: cache_empty cached_json=' .. tostring(cached_json))
|
|
||||||
end
|
|
||||||
|
|
||||||
if not _is_pipeline_helper_ready() then
|
if not _is_pipeline_helper_ready() then
|
||||||
if not _store_cache_retry_pending then
|
_lua_log('stores: helper not ready; skipping helper store refresh fallback')
|
||||||
_store_cache_retry_pending = true
|
if type(on_complete) == 'function' then
|
||||||
_lua_log('stores: helper not ready; deferring dynamic store refresh')
|
on_complete(false, false)
|
||||||
attempt_start_pipeline_helper_async(function(success)
|
|
||||||
_lua_log('stores: deferred helper start success=' .. tostring(success))
|
|
||||||
end)
|
|
||||||
mp.add_timeout(1.0, function()
|
|
||||||
_store_cache_retry_pending = false
|
|
||||||
_refresh_store_cache(timeout_seconds, on_complete)
|
|
||||||
end)
|
|
||||||
else
|
|
||||||
_lua_log('stores: helper not ready; refresh already deferred')
|
|
||||||
end
|
end
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
_lua_log('stores: requesting store-choices via helper (fallback)')
|
_lua_log('stores: requesting store-choices via helper fallback')
|
||||||
_run_helper_request_async({ op = 'store-choices' }, math.max(timeout_seconds or 0, 6.0), function(resp, err)
|
_run_helper_request_async({ op = 'store-choices' }, math.max(timeout_seconds or 0, 6.0), function(resp, err)
|
||||||
local success = false
|
if apply_store_choices(resp, 'helper fallback') then
|
||||||
local changed = false
|
return
|
||||||
if resp and resp.success and type(resp.choices) == 'table' then
|
|
||||||
local out = {}
|
|
||||||
for _, v in ipairs(resp.choices) do
|
|
||||||
local name = trim(tostring(v or ''))
|
|
||||||
if name ~= '' then
|
|
||||||
out[#out + 1] = name
|
|
||||||
end
|
end
|
||||||
end
|
|
||||||
if #out == 0 and had_previous then
|
|
||||||
_lua_log('stores: helper returned empty store list; keeping previous store list')
|
|
||||||
success = true
|
|
||||||
changed = false
|
|
||||||
else
|
|
||||||
_cached_store_names = out
|
|
||||||
_store_cache_loaded = (#out > 0) or _store_cache_loaded
|
|
||||||
local preview = ''
|
|
||||||
if #out > 0 then
|
|
||||||
preview = table.concat(out, ', ')
|
|
||||||
end
|
|
||||||
_lua_log('stores: loaded ' .. tostring(#out) .. ' stores via helper request: ' .. tostring(preview))
|
|
||||||
success = true
|
|
||||||
changed = (#out ~= prev_count) or (_store_names_key(out) ~= prev_key)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
_lua_log(
|
_lua_log(
|
||||||
'stores: failed to load store choices via helper; success='
|
'stores: failed to load store choices via helper; success='
|
||||||
.. tostring(resp and resp.success or false)
|
.. tostring(resp and resp.success or false)
|
||||||
@@ -2875,11 +2970,51 @@ _refresh_store_cache = function(timeout_seconds, on_complete)
|
|||||||
.. ' error='
|
.. ' error='
|
||||||
.. tostring(resp and resp.error or err or '')
|
.. tostring(resp and resp.error or err or '')
|
||||||
)
|
)
|
||||||
end
|
|
||||||
if type(on_complete) == 'function' then
|
if type(on_complete) == 'function' then
|
||||||
on_complete(success, changed)
|
on_complete(false, false)
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local cached_json = mp.get_property('user-data/medeia-store-choices-cached')
|
||||||
|
_lua_log('stores: cache_read cached_json=' .. tostring(cached_json) .. ' len=' .. tostring(cached_json and #cached_json or 0))
|
||||||
|
|
||||||
|
if cached_json and cached_json ~= '' then
|
||||||
|
local ok, cached_resp = pcall(utils.parse_json, cached_json)
|
||||||
|
_lua_log('stores: cache_parse ok=' .. tostring(ok) .. ' resp_type=' .. tostring(type(cached_resp)))
|
||||||
|
if ok then
|
||||||
|
if type(cached_resp) == 'string' then
|
||||||
|
_lua_log('stores: cache_parse returned string, trying again...')
|
||||||
|
ok, cached_resp = pcall(utils.parse_json, cached_resp)
|
||||||
|
_lua_log('stores: cache_parse retry ok=' .. tostring(ok) .. ' resp_type=' .. tostring(type(cached_resp)))
|
||||||
|
end
|
||||||
|
if ok then
|
||||||
|
if apply_store_choices(cached_resp, 'cache') then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
_lua_log('stores: cache_parse failed ok=' .. tostring(ok) .. ' resp=' .. tostring(cached_resp))
|
||||||
|
end
|
||||||
|
else
|
||||||
|
_lua_log('stores: cache_empty cached_json=' .. tostring(cached_json))
|
||||||
|
end
|
||||||
|
|
||||||
|
if not _store_cache_retry_pending then
|
||||||
|
_store_cache_retry_pending = true
|
||||||
|
M._load_store_choices_direct_async(function(resp, err)
|
||||||
|
_store_cache_retry_pending = false
|
||||||
|
if apply_store_choices(resp, 'direct config') then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
_lua_log('stores: direct config load failed error=' .. tostring(err or 'unknown'))
|
||||||
|
request_helper_store_choices()
|
||||||
|
end)
|
||||||
|
else
|
||||||
|
_lua_log('stores: direct config refresh already pending')
|
||||||
|
end
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -3271,22 +3406,8 @@ _refresh_current_store_url_status = function(reason)
|
|||||||
|
|
||||||
local generation = _begin_current_store_url_status(store, url, 'pending')
|
local generation = _begin_current_store_url_status(store, url, 'pending')
|
||||||
if not _is_pipeline_helper_ready() then
|
if not _is_pipeline_helper_ready() then
|
||||||
_lua_log('store-check: helper not ready; deferring reason=' .. tostring(reason) .. ' store=' .. tostring(store))
|
_lua_log('store-check: helper not ready; skipping lookup reason=' .. tostring(reason) .. ' store=' .. tostring(store))
|
||||||
attempt_start_pipeline_helper_async(function(success)
|
_set_current_store_url_status(store, url, 'idle')
|
||||||
if _current_store_url_status.generation ~= generation then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
if not success then
|
|
||||||
_set_current_store_url_status(store, url, 'error', 'helper not running')
|
|
||||||
return
|
|
||||||
end
|
|
||||||
mp.add_timeout(0.1, function()
|
|
||||||
if _current_store_url_status.generation ~= generation then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
_refresh_current_store_url_status((reason ~= '' and (reason .. '-retry')) or 'retry')
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user