diff --git a/MPV/LUA/main.lua b/MPV/LUA/main.lua index 4e3d2d6..58f810b 100644 --- a/MPV/LUA/main.lua +++ b/MPV/LUA/main.lua @@ -847,8 +847,9 @@ local _last_ipc_error = '' local _last_ipc_last_req_json = '' local _last_ipc_last_resp_json = '' --- Debounce helper start attempts (window in seconds) -local _helper_start_debounce_ts = 0 +-- Debounce helper start attempts (window in seconds). +-- Initialize below zero so the very first startup attempt is never rejected. +local _helper_start_debounce_ts = -1000 local HELPER_START_DEBOUNCE = 2.0 -- 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 return true end + return false 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 return true end @@ -963,12 +966,17 @@ local function attempt_start_pipeline_helper_async(callback) -- Debounce: don't spawn multiple helpers in quick succession 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)') callback(false) return end _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) if not python or python == '' then @@ -2137,7 +2145,8 @@ local function _capture_screenshot() end 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 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_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)) @@ -2632,8 +2642,15 @@ _refresh_store_cache = function(timeout_seconds, on_complete) 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 = true + _store_cache_loaded = (#out > 0) or _store_cache_loaded local preview = '' if #out > 0 then preview = table.concat(out, ', ') @@ -2694,15 +2711,21 @@ _refresh_store_cache = function(timeout_seconds, on_complete) out[#out + 1] = name end end - _cached_store_names = out - _store_cache_loaded = true - local preview = '' - if #out > 0 then - preview = table.concat(out, ', ') + 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 - _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) else _lua_log( 'stores: failed to load store choices via helper; success=' diff --git a/MPV/pipeline_helper.py b/MPV/pipeline_helper.py index ff39bc7..aa2bfaa 100644 --- a/MPV/pipeline_helper.py +++ b/MPV/pipeline_helper.py @@ -63,7 +63,7 @@ if _ROOT not in sys.path: sys.path.insert(0, _ROOT) 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.repl_queue import enqueue_repl_command # 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_LOCK = threading.Lock() _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: @@ -632,17 +699,42 @@ def _run_op(op: str, data: Any) -> Dict[str, Any]: "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: + _publish_store_choices_cache(os.environ.get("MEDEIA_MPV_IPC", ""), cached_choices) + 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: - from SYS.config import reload_config # noqa: WPS433 - from Store import Store # noqa: WPS433 - config_root = _runtime_config_root() - cfg = reload_config() + choices = _load_store_choices_from_config(force_reload=refresh) - storage = Store(config=cfg, suppress_debug=True) - backends = storage.list_backends() or [] - choices = sorted({str(n) - for n in backends if str(n).strip()}) + if not choices and cached_choices: + choices = cached_choices + debug( + 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)}") @@ -655,6 +747,22 @@ def _run_op(op: str, data: Any) -> Dict[str, Any]: "choices": choices, } 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 { "success": False, "stdout": "", @@ -1529,6 +1637,7 @@ def main(argv: Optional[list[str]] = None) -> int: dict) else None ) if isinstance(startup_choices, list): + startup_choices = _set_cached_store_choices(startup_choices) preview = ", ".join(str(x) for x in startup_choices[:50]) _append_helper_log( 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. try: - cached_json = json.dumps( - { - "success": True, - "choices": startup_choices - }, - ensure_ascii=False - ) - client.send_command_no_wait( - [ - "set_property_string", - "user-data/medeia-store-choices-cached", - cached_json - ] - ) + _publish_store_choices_cache(str(args.ipc), startup_choices) _append_helper_log( "[helper] published store-choices to user-data/medeia-store-choices-cached" )