diff --git a/MPV/LUA/main.lua b/MPV/LUA/main.lua index 98ed348..6cc5944 100644 --- a/MPV/LUA/main.lua +++ b/MPV/LUA/main.lua @@ -4,7 +4,7 @@ local msg = require 'mp.msg' 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. 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 _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) store = trim(tostring(store or '')) store = store:gsub('^"', ''):gsub('"$', '') @@ -2715,7 +2830,34 @@ end _pick_folder_windows = function() -- 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({ -- Hide the PowerShell console window (dialog still shows). 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 had_previous = prev_count > 0 - 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)) + local function apply_store_choices(resp, source) + 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 function handle_cached(resp) - 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))) - return false + local out = {} + for _, v in ipairs(resp.choices) do + local name = trim(tostring(v or '')) + if name ~= '' then + out[#out + 1] = name end - - local out = {} - for _, v in ipairs(resp.choices) do - 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)) + end + if #out == 0 and had_previous then + _lua_log('stores: ignoring empty ' .. tostring(source) .. ' payload; keeping previous store list') if type(on_complete) == 'function' then - on_complete(true, _store_names_key(out) ~= prev_key) + on_complete(true, false) end return true end - 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 handle_cached(cached_resp) then - return true - end - end - else - _lua_log('stores: cache_parse failed ok=' .. tostring(ok) .. ' resp=' .. tostring(cached_resp)) + _cached_store_names = out + _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 - 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 - if not _is_pipeline_helper_ready() then - if not _store_cache_retry_pending then - _store_cache_retry_pending = true - _lua_log('stores: helper not ready; deferring dynamic store refresh') - 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') + local function request_helper_store_choices() + if not _is_pipeline_helper_ready() then + _lua_log('stores: helper not ready; skipping helper store refresh fallback') + if type(on_complete) == 'function' then + on_complete(false, false) + end + return false end - return false - end - _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) - local success = false - local changed = false - 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 + _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) + if apply_store_choices(resp, 'helper fallback') then + return 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( 'stores: failed to load store choices via helper; success=' .. tostring(resp and resp.success or false) @@ -2875,11 +2970,51 @@ _refresh_store_cache = function(timeout_seconds, on_complete) .. ' error=' .. 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 - if type(on_complete) == 'function' then - on_complete(success, changed) - end - 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 end @@ -3271,22 +3406,8 @@ _refresh_current_store_url_status = function(reason) local generation = _begin_current_store_url_status(store, url, 'pending') if not _is_pipeline_helper_ready() then - _lua_log('store-check: helper not ready; deferring reason=' .. tostring(reason) .. ' store=' .. tostring(store)) - attempt_start_pipeline_helper_async(function(success) - 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) + _lua_log('store-check: helper not ready; skipping lookup reason=' .. tostring(reason) .. ' store=' .. tostring(store)) + _set_current_store_url_status(store, url, 'idle') return end