This commit is contained in:
2026-03-21 22:56:37 -07:00
parent b183167a64
commit f8c98b39bd
4 changed files with 304 additions and 82 deletions

View File

@@ -20,7 +20,7 @@ This helper is intentionally minimal: one request at a time, last-write-wins.
from __future__ import annotations
MEDEIA_MPV_HELPER_VERSION = "2026-03-22.2"
MEDEIA_MPV_HELPER_VERSION = "2026-03-22.4"
import argparse
import json
@@ -194,20 +194,35 @@ def _append_prefixed_log_lines(prefix: str, text: Any, *, max_lines: int = 40) -
break
def _start_ready_heartbeat(ipc_path: str, stop_event: threading.Event) -> threading.Thread:
def _start_ready_heartbeat(
ipc_path: str,
stop_event: threading.Event,
mark_alive: Optional[Callable[[str], None]] = None,
note_ipc_unavailable: Optional[Callable[[str], None]] = None,
) -> threading.Thread:
"""Keep READY_PROP fresh even when the main loop blocks on Windows pipes."""
def _heartbeat_loop() -> None:
hb_client = MPVIPCClient(socket_path=ipc_path, timeout=0.5, silent=True)
while not stop_event.is_set():
try:
if hb_client.sock is None and not hb_client.connect():
stop_event.wait(0.25)
continue
was_disconnected = hb_client.sock is None
if was_disconnected:
if not hb_client.connect():
if note_ipc_unavailable is not None:
note_ipc_unavailable("heartbeat-connect")
stop_event.wait(0.25)
continue
if mark_alive is not None:
mark_alive("heartbeat-connect")
hb_client.send_command_no_wait(
["set_property_string", READY_PROP, str(int(time.time()))]
)
if mark_alive is not None:
mark_alive("heartbeat-send")
except Exception:
if note_ipc_unavailable is not None:
note_ipc_unavailable("heartbeat-send")
try:
hb_client.disconnect()
except Exception:
@@ -1207,6 +1222,8 @@ def _start_request_poll_loop(
ipc_path: str,
stop_event: threading.Event,
handle_request: Callable[[Any, str], bool],
mark_alive: Optional[Callable[[str], None]] = None,
note_ipc_unavailable: Optional[Callable[[str], None]] = None,
) -> threading.Thread:
"""Poll the request property on a separate IPC connection.
@@ -1219,12 +1236,20 @@ def _start_request_poll_loop(
poll_client = MPVIPCClient(socket_path=ipc_path, timeout=0.75, silent=True)
while not stop_event.is_set():
try:
if poll_client.sock is None and not poll_client.connect():
stop_event.wait(0.10)
continue
was_disconnected = poll_client.sock is None
if was_disconnected:
if not poll_client.connect():
if note_ipc_unavailable is not None:
note_ipc_unavailable("request-poll-connect")
stop_event.wait(0.10)
continue
if mark_alive is not None:
mark_alive("request-poll-connect")
resp = poll_client.send_command(["get_property", REQUEST_PROP])
if not resp:
if note_ipc_unavailable is not None:
note_ipc_unavailable("request-poll-read")
try:
poll_client.disconnect()
except Exception:
@@ -1232,10 +1257,14 @@ def _start_request_poll_loop(
stop_event.wait(0.10)
continue
if mark_alive is not None:
mark_alive("request-poll-read")
if resp.get("error") == "success":
handle_request(resp.get("data"), "poll")
stop_event.wait(0.05)
except Exception:
if note_ipc_unavailable is not None:
note_ipc_unavailable("request-poll-exception")
try:
poll_client.disconnect()
except Exception:
@@ -1386,9 +1415,48 @@ def main(argv: Optional[list[str]] = None) -> int:
seen_request_ttl_seconds = 180.0
request_processing_lock = threading.Lock()
command_client_lock = threading.Lock()
stop_event = threading.Event()
ipc_loss_grace_seconds = 4.0
ipc_lost_since: Optional[float] = None
ipc_connected_once = False
shutdown_reason = ""
shutdown_reason_lock = threading.Lock()
_send_helper_command = lambda _command, _label='': False
_publish_store_choices_cached_property = lambda _choices: None
def _request_shutdown(reason: str) -> None:
nonlocal shutdown_reason
message = str(reason or "").strip() or "unknown"
with shutdown_reason_lock:
if shutdown_reason:
return
shutdown_reason = message
_append_helper_log(f"[helper] shutdown requested: {message}")
stop_event.set()
def _mark_ipc_alive(source: str = "") -> None:
nonlocal ipc_lost_since, ipc_connected_once
if ipc_lost_since is not None and source:
_append_helper_log(f"[helper] ipc restored via {source}")
ipc_connected_once = True
ipc_lost_since = None
def _note_ipc_unavailable(source: str) -> None:
nonlocal ipc_lost_since
if stop_event.is_set() or not ipc_connected_once:
return
now = time.time()
if ipc_lost_since is None:
ipc_lost_since = now
_append_helper_log(
f"[helper] ipc unavailable via {source}; waiting {ipc_loss_grace_seconds:.1f}s for reconnect"
)
return
if (now - ipc_lost_since) >= ipc_loss_grace_seconds:
_request_shutdown(
f"mpv ipc unavailable for {now - ipc_lost_since:.2f}s via {source}"
)
def _write_error_log(text: str, *, req_id: str) -> Optional[str]:
try:
error_log_dir.mkdir(parents=True, exist_ok=True)
@@ -1593,6 +1661,7 @@ def main(argv: Optional[list[str]] = None) -> int:
while True:
try:
if client.connect():
_mark_ipc_alive("startup-connect")
break
except Exception as exc:
last_connect_error = f"{type(exc).__name__}: {exc}"
@@ -1611,26 +1680,32 @@ def main(argv: Optional[list[str]] = None) -> int:
def _send_helper_command(command: Any, label: str = "") -> bool:
with command_client_lock:
try:
if command_client.sock is None and not command_client.connect():
_append_helper_log(
f"[helper-ipc] connect failed label={label or '?'}"
)
return False
if command_client.sock is None:
if not command_client.connect():
_append_helper_log(
f"[helper-ipc] connect failed label={label or '?'}"
)
_note_ipc_unavailable(f"helper-command-connect:{label or '?' }")
return False
_mark_ipc_alive(f"helper-command-connect:{label or '?'}")
rid = command_client.send_command_no_wait(command)
if rid is None:
_append_helper_log(
f"[helper-ipc] send failed label={label or '?'}"
)
_note_ipc_unavailable(f"helper-command-send:{label or '?'}")
try:
command_client.disconnect()
except Exception:
pass
return False
_mark_ipc_alive(f"helper-command-send:{label or '?'}")
return True
except Exception as exc:
_append_helper_log(
f"[helper-ipc] exception label={label or '?'} error={type(exc).__name__}: {exc}"
)
_note_ipc_unavailable(f"helper-command-exception:{label or '?'}")
try:
command_client.disconnect()
except Exception:
@@ -1740,9 +1815,19 @@ def main(argv: Optional[list[str]] = None) -> int:
except Exception:
pass
heartbeat_stop = threading.Event()
_start_ready_heartbeat(str(args.ipc), heartbeat_stop)
_start_request_poll_loop(str(args.ipc), heartbeat_stop, _process_request)
_start_ready_heartbeat(
str(args.ipc),
stop_event,
_mark_ipc_alive,
_note_ipc_unavailable,
)
_start_request_poll_loop(
str(args.ipc),
stop_event,
_process_request,
_mark_ipc_alive,
_note_ipc_unavailable,
)
# Pre-compute store choices at startup and publish to a cached property so Lua
# can read immediately without waiting for a request/response cycle (which may timeout).
@@ -1824,67 +1909,91 @@ def main(argv: Optional[list[str]] = None) -> int:
except Exception:
pass
while True:
msg = client.read_message(timeout=0.25)
if msg is None:
# Keep READY fresh even when idle (Lua may clear it on timeouts).
_touch_ready()
time.sleep(0.02)
continue
try:
while not stop_event.is_set():
msg = client.read_message(timeout=0.25)
if msg is None:
if client.sock is None:
_note_ipc_unavailable("main-read")
else:
_mark_ipc_alive("main-idle")
# Keep READY fresh even when idle (Lua may clear it on timeouts).
_touch_ready()
time.sleep(0.02)
continue
if msg.get("event") == "__eof__":
try:
_flush_mpv_repeat()
except Exception:
pass
heartbeat_stop.set()
return 0
_mark_ipc_alive("main-read")
if msg.get("event") == "log-message":
try:
level = str(msg.get("level") or "")
prefix = str(msg.get("prefix") or "")
text = str(msg.get("text") or "").rstrip()
if msg.get("event") == "__eof__":
_request_shutdown("mpv closed ipc stream")
break
if not text:
continue
if msg.get("event") == "log-message":
try:
level = str(msg.get("level") or "")
prefix = str(msg.get("prefix") or "")
text = str(msg.get("text") or "").rstrip()
# Filter excessive noise unless debug is enabled.
if not debug_enabled:
lower_prefix = prefix.lower()
if "quic" in lower_prefix and "DEBUG:" in text:
if not text:
continue
# Suppress progress-bar style lines (keep true errors).
if ("ETA" in text or "%" in text) and ("ERROR:" not in text
and "WARNING:" not in text):
# Typical yt-dlp progress bar line.
if text.lstrip().startswith("["):
# Filter excessive noise unless debug is enabled.
if not debug_enabled:
lower_prefix = prefix.lower()
if "quic" in lower_prefix and "DEBUG:" in text:
continue
# Suppress progress-bar style lines (keep true errors).
if ("ETA" in text or "%" in text) and ("ERROR:" not in text
and "WARNING:" not in text):
# Typical yt-dlp progress bar line.
if text.lstrip().startswith("["):
continue
line = f"[mpv {level}] {prefix} {text}".strip()
line = f"[mpv {level}] {prefix} {text}".strip()
now = time.time()
if last_mpv_line == line and (now - last_mpv_ts) < 2.0:
last_mpv_count += 1
now = time.time()
if last_mpv_line == line and (now - last_mpv_ts) < 2.0:
last_mpv_count += 1
last_mpv_ts = now
continue
_flush_mpv_repeat()
last_mpv_line = line
last_mpv_count = 1
last_mpv_ts = now
continue
_append_helper_log(line)
except Exception:
pass
continue
_flush_mpv_repeat()
last_mpv_line = line
last_mpv_count = 1
last_mpv_ts = now
_append_helper_log(line)
if msg.get("event") != "property-change":
continue
if msg.get("id") != OBS_ID_REQUEST:
continue
_process_request(msg.get("data"), "observe")
finally:
stop_event.set()
try:
_flush_mpv_repeat()
except Exception:
pass
if shutdown_reason:
try:
_append_helper_log(f"[helper] exiting reason={shutdown_reason}")
except Exception:
pass
continue
try:
command_client.disconnect()
except Exception:
pass
try:
client.disconnect()
except Exception:
pass
if msg.get("event") != "property-change":
continue
if msg.get("id") != OBS_ID_REQUEST:
continue
_process_request(msg.get("data"), "observe")
return 0
if __name__ == "__main__":