update
This commit is contained in:
@@ -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__":
|
||||
|
||||
Reference in New Issue
Block a user