This commit is contained in:
2026-03-22 02:09:27 -07:00
parent 6d1a4d8bfc
commit 210f1ae4c0

View File

@@ -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,108 +2905,61 @@ _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 not resp or type(resp) ~= 'table' or type(resp.choices) ~= 'table' then
_lua_log('stores: ' .. tostring(source) .. ' result missing choices table; resp_type=' .. tostring(type(resp)))
return false
end
if cached_json and cached_json ~= '' then local out = {}
local function handle_cached(resp) for _, v in ipairs(resp.choices) do
if not resp or type(resp) ~= 'table' or type(resp.choices) ~= 'table' then local name = trim(tostring(v or ''))
_lua_log('stores: cache_parse result missing choices table; resp_type=' .. tostring(type(resp))) if name ~= '' then
return false out[#out + 1] = name
end end
end
local out = {} if #out == 0 and had_previous then
for _, v in ipairs(resp.choices) do _lua_log('stores: ignoring empty ' .. tostring(source) .. ' payload; keeping previous store list')
local name = trim(tostring(v or ''))
if name ~= '' then
out[#out + 1] = name
end
end
if #out == 0 and had_previous then
_lua_log('stores: ignoring empty cache payload; keeping previous store list')
if type(on_complete) == 'function' then
on_complete(true, false)
end
return true
end
_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 from cache: ' .. 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, false)
end end
return true return true
end end
local ok, cached_resp = pcall(utils.parse_json, cached_json) _cached_store_names = out
_lua_log('stores: cache_parse ok=' .. tostring(ok) .. ' resp_type=' .. tostring(type(cached_resp))) _store_cache_loaded = (#out > 0) or _store_cache_loaded
if ok then
if type(cached_resp) == 'string' then local payload = utils.format_json({ choices = out })
_lua_log('stores: cache_parse returned string, trying again...') if type(payload) == 'string' and payload ~= '' then
ok, cached_resp = pcall(utils.parse_json, cached_resp) pcall(mp.set_property, 'user-data/medeia-store-choices-cached', payload)
_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 end
else
_lua_log('stores: cache_empty cached_json=' .. tostring(cached_json)) local preview = ''
if #out > 0 then
preview = table.concat(out, ', ')
end
_lua_log('stores: loaded ' .. tostring(#out) .. ' stores from ' .. tostring(source) .. ': ' .. tostring(preview))
if type(on_complete) == 'function' then
on_complete(true, _store_names_key(out) ~= prev_key)
end
return true
end end
if not _is_pipeline_helper_ready() then local function request_helper_store_choices()
if not _store_cache_retry_pending then if not _is_pipeline_helper_ready() then
_store_cache_retry_pending = true _lua_log('stores: helper not ready; skipping helper store refresh fallback')
_lua_log('stores: helper not ready; deferring dynamic store refresh') if type(on_complete) == 'function' then
attempt_start_pipeline_helper_async(function(success) on_complete(false, false)
_lua_log('stores: deferred helper start success=' .. tostring(success)) end
end) return false
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
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 '')
) )
if type(on_complete) == 'function' then
on_complete(false, false)
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 end
if type(on_complete) == 'function' then else
on_complete(success, changed) _lua_log('stores: cache_empty cached_json=' .. tostring(cached_json))
end end
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