This commit is contained in:
2026-03-21 17:23:26 -07:00
parent 4160e1cca4
commit f09f66ff9a
2 changed files with 156 additions and 37 deletions

View File

@@ -847,8 +847,9 @@ local _last_ipc_error = ''
local _last_ipc_last_req_json = '' local _last_ipc_last_req_json = ''
local _last_ipc_last_resp_json = '' local _last_ipc_last_resp_json = ''
-- Debounce helper start attempts (window in seconds) -- Debounce helper start attempts (window in seconds).
local _helper_start_debounce_ts = 0 -- Initialize below zero so the very first startup attempt is never rejected.
local _helper_start_debounce_ts = -1000
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 -- Track ready-heartbeat freshness so stale or non-timestamp values don't mask a stopped helper
@@ -891,10 +892,12 @@ local function _is_pipeline_helper_ready()
if age <= HELPER_READY_STALE_SECONDS then if age <= HELPER_READY_STALE_SECONDS then
return true return true
end end
return false
end end
end end
-- Fall back to the last time we observed a new value so stale data does not appear fresh. -- Fall back only for non-timestamp values so stale helper timestamps from a
-- previous session do not look fresh right after Lua reload.
if _helper_ready_last_seen_ts > 0 and (now - _helper_ready_last_seen_ts) <= HELPER_READY_STALE_SECONDS then if _helper_ready_last_seen_ts > 0 and (now - _helper_ready_last_seen_ts) <= HELPER_READY_STALE_SECONDS then
return true return true
end end
@@ -963,13 +966,18 @@ local function attempt_start_pipeline_helper_async(callback)
-- Debounce: don't spawn multiple helpers in quick succession -- Debounce: don't spawn multiple helpers in quick succession
local now = mp.get_time() local now = mp.get_time()
if (now - _helper_start_debounce_ts) < HELPER_START_DEBOUNCE then if _helper_start_debounce_ts > -1 and (now - _helper_start_debounce_ts) < HELPER_START_DEBOUNCE then
_lua_log('attempt_start_pipeline_helper_async: debounced (recent attempt)') _lua_log('attempt_start_pipeline_helper_async: debounced (recent attempt)')
callback(false) callback(false)
return return
end end
_helper_start_debounce_ts = now _helper_start_debounce_ts = now
-- Clear any stale ready heartbeat from an earlier helper instance before spawning.
pcall(mp.set_property, PIPELINE_READY_PROP, '')
_helper_ready_last_value = ''
_helper_ready_last_seen_ts = 0
local python = _resolve_python_exe(true) local python = _resolve_python_exe(true)
if not python or python == '' then if not python or python == '' then
_lua_log('attempt_start_pipeline_helper_async: no python executable available') _lua_log('attempt_start_pipeline_helper_async: no python executable available')
@@ -2137,7 +2145,8 @@ local function _capture_screenshot()
end end
if selected_store == '' then if selected_store == '' then
mp.osd_message('Select a store first (Store button)', 2) _pending_screenshot = { path = out_path }
_open_store_picker_for_pending_screenshot()
return return
end end
@@ -2614,6 +2623,7 @@ _refresh_store_cache = function(timeout_seconds, on_complete)
local prev_count = (type(_cached_store_names) == 'table') and #_cached_store_names or 0 local prev_count = (type(_cached_store_names) == 'table') and #_cached_store_names or 0
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 cached_json = mp.get_property('user-data/medeia-store-choices-cached') 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)) _lua_log('stores: cache_read cached_json=' .. tostring(cached_json) .. ' len=' .. tostring(cached_json and #cached_json or 0))
@@ -2632,8 +2642,15 @@ _refresh_store_cache = function(timeout_seconds, on_complete)
out[#out + 1] = name out[#out + 1] = name
end end
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 _cached_store_names = out
_store_cache_loaded = true _store_cache_loaded = (#out > 0) or _store_cache_loaded
local preview = '' local preview = ''
if #out > 0 then if #out > 0 then
preview = table.concat(out, ', ') preview = table.concat(out, ', ')
@@ -2694,8 +2711,13 @@ _refresh_store_cache = function(timeout_seconds, on_complete)
out[#out + 1] = name out[#out + 1] = name
end 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 _cached_store_names = out
_store_cache_loaded = true _store_cache_loaded = (#out > 0) or _store_cache_loaded
local preview = '' local preview = ''
if #out > 0 then if #out > 0 then
preview = table.concat(out, ', ') preview = table.concat(out, ', ')
@@ -2703,6 +2725,7 @@ _refresh_store_cache = function(timeout_seconds, on_complete)
_lua_log('stores: loaded ' .. tostring(#out) .. ' stores via helper request: ' .. tostring(preview)) _lua_log('stores: loaded ' .. tostring(#out) .. ' stores via helper request: ' .. tostring(preview))
success = true success = true
changed = (#out ~= prev_count) or (_store_names_key(out) ~= prev_key) changed = (#out ~= prev_count) or (_store_names_key(out) ~= prev_key)
end
else else
_lua_log( _lua_log(
'stores: failed to load store choices via helper; success=' 'stores: failed to load store choices via helper; success='

View File

@@ -63,7 +63,7 @@ if _ROOT not in sys.path:
sys.path.insert(0, _ROOT) sys.path.insert(0, _ROOT)
from MPV.mpv_ipc import MPVIPCClient # noqa: E402 from MPV.mpv_ipc import MPVIPCClient # noqa: E402
from SYS.config import load_config # noqa: E402 from SYS.config import load_config, reload_config # noqa: E402
from SYS.logger import set_debug, debug, set_thread_stream # noqa: E402 from SYS.logger import set_debug, debug, set_thread_stream # noqa: E402
from SYS.repl_queue import enqueue_repl_command # noqa: E402 from SYS.repl_queue import enqueue_repl_command # noqa: E402
from SYS.utils import format_bytes # noqa: E402 from SYS.utils import format_bytes # noqa: E402
@@ -80,6 +80,73 @@ _HELPER_LOG_BACKLOG_LIMIT = 200
_ASYNC_PIPELINE_JOBS: Dict[str, Dict[str, Any]] = {} _ASYNC_PIPELINE_JOBS: Dict[str, Dict[str, Any]] = {}
_ASYNC_PIPELINE_JOBS_LOCK = threading.Lock() _ASYNC_PIPELINE_JOBS_LOCK = threading.Lock()
_ASYNC_PIPELINE_JOB_TTL_SECONDS = 900.0 _ASYNC_PIPELINE_JOB_TTL_SECONDS = 900.0
_STORE_CHOICES_CACHE: list[str] = []
_STORE_CHOICES_CACHE_LOCK = threading.Lock()
def _normalize_store_choices(values: Any) -> list[str]:
out: list[str] = []
seen: set[str] = set()
if not isinstance(values, (list, tuple, set)):
return out
for item in values:
text = str(item or "").strip()
if not text:
continue
key = text.casefold()
if key in seen:
continue
seen.add(key)
out.append(text)
return sorted(out, key=str.casefold)
def _get_cached_store_choices() -> list[str]:
with _STORE_CHOICES_CACHE_LOCK:
return list(_STORE_CHOICES_CACHE)
def _set_cached_store_choices(choices: Any) -> list[str]:
normalized = _normalize_store_choices(choices)
with _STORE_CHOICES_CACHE_LOCK:
_STORE_CHOICES_CACHE[:] = normalized
return list(_STORE_CHOICES_CACHE)
def _publish_store_choices_cache(ipc_path: str, choices: Any) -> None:
cached = _normalize_store_choices(choices)
if not ipc_path or not cached:
return
payload = json.dumps(
{
"success": True,
"choices": cached,
},
ensure_ascii=False,
)
client = MPVIPCClient(socket_path=ipc_path, timeout=0.75, silent=True)
try:
client.send_command_no_wait(
[
"set_property_string",
"user-data/medeia-store-choices-cached",
payload,
]
)
finally:
try:
client.disconnect()
except Exception:
pass
def _load_store_choices_from_config(*, force_reload: bool = False) -> list[str]:
from Store.registry import list_configured_backend_names # noqa: WPS433
cfg = reload_config() if force_reload else load_config()
return _normalize_store_choices(list_configured_backend_names(cfg or {}))
def _prune_async_pipeline_jobs(now: Optional[float] = None) -> None: def _prune_async_pipeline_jobs(now: Optional[float] = None) -> None:
@@ -632,17 +699,42 @@ def _run_op(op: str, data: Any) -> Dict[str, Any]:
"store_choices", "store_choices",
"get-store-choices", "get-store-choices",
"get_store_choices"}: "get_store_choices"}:
cached_choices = _get_cached_store_choices()
refresh = False
if isinstance(data, dict):
refresh = bool(data.get("refresh") or data.get("reload"))
if cached_choices and not refresh:
try: try:
from SYS.config import reload_config # noqa: WPS433 _publish_store_choices_cache(os.environ.get("MEDEIA_MPV_IPC", ""), cached_choices)
from Store import Store # noqa: WPS433 except Exception:
pass
debug(f"[store-choices] using cached choices={len(cached_choices)}")
return {
"success": True,
"stdout": "",
"stderr": "",
"error": None,
"table": None,
"choices": cached_choices,
}
try:
config_root = _runtime_config_root() config_root = _runtime_config_root()
cfg = reload_config() choices = _load_store_choices_from_config(force_reload=refresh)
storage = Store(config=cfg, suppress_debug=True) if not choices and cached_choices:
backends = storage.list_backends() or [] choices = cached_choices
choices = sorted({str(n) debug(
for n in backends if str(n).strip()}) f"[store-choices] config returned empty; falling back to cached choices={len(choices)}"
)
if choices:
choices = _set_cached_store_choices(choices)
try:
_publish_store_choices_cache(os.environ.get("MEDEIA_MPV_IPC", ""), choices)
except Exception:
pass
debug(f"[store-choices] config_dir={config_root} choices={len(choices)}") debug(f"[store-choices] config_dir={config_root} choices={len(choices)}")
@@ -655,6 +747,22 @@ def _run_op(op: str, data: Any) -> Dict[str, Any]:
"choices": choices, "choices": choices,
} }
except Exception as exc: except Exception as exc:
if cached_choices:
debug(
f"[store-choices] refresh failed; returning cached choices={len(cached_choices)} error={type(exc).__name__}: {exc}"
)
try:
_publish_store_choices_cache(os.environ.get("MEDEIA_MPV_IPC", ""), cached_choices)
except Exception:
pass
return {
"success": True,
"stdout": "",
"stderr": "",
"error": None,
"table": None,
"choices": cached_choices,
}
return { return {
"success": False, "success": False,
"stdout": "", "stdout": "",
@@ -1529,6 +1637,7 @@ def main(argv: Optional[list[str]] = None) -> int:
dict) else None dict) else None
) )
if isinstance(startup_choices, list): if isinstance(startup_choices, list):
startup_choices = _set_cached_store_choices(startup_choices)
preview = ", ".join(str(x) for x in startup_choices[:50]) preview = ", ".join(str(x) for x in startup_choices[:50])
_append_helper_log( _append_helper_log(
f"[helper] startup store-choices count={len(startup_choices)} items={preview}" f"[helper] startup store-choices count={len(startup_choices)} items={preview}"
@@ -1536,20 +1645,7 @@ def main(argv: Optional[list[str]] = None) -> int:
# Publish to a cached property for Lua to read without IPC request. # Publish to a cached property for Lua to read without IPC request.
try: try:
cached_json = json.dumps( _publish_store_choices_cache(str(args.ipc), startup_choices)
{
"success": True,
"choices": startup_choices
},
ensure_ascii=False
)
client.send_command_no_wait(
[
"set_property_string",
"user-data/medeia-store-choices-cached",
cached_json
]
)
_append_helper_log( _append_helper_log(
"[helper] published store-choices to user-data/medeia-store-choices-cached" "[helper] published store-choices to user-data/medeia-store-choices-cached"
) )