df
Some checks failed
smoke-mm / Install & smoke test mm --help (push) Has been cancelled

This commit is contained in:
2025-12-29 17:05:03 -08:00
parent 226de9316a
commit c019c00aed
104 changed files with 19669 additions and 12954 deletions

View File

@@ -133,9 +133,7 @@ def _windows_list_lyric_helper_pids(ipc_path: str) -> List[int]:
# Use CIM to query command lines; output as JSON for robust parsing.
# Note: `ConvertTo-Json` returns a number for single item, array for many, or null.
ps_script = (
"$ipc = "
+ json.dumps(ipc_path)
+ "; "
"$ipc = " + json.dumps(ipc_path) + "; "
"Get-CimInstance Win32_Process | "
"Where-Object { $_.CommandLine -and $_.CommandLine -match ' -m\\s+MPV\\.lyric(\\s|$)' -and $_.CommandLine -match ('--ipc\\s+' + [regex]::Escape($ipc)) } | "
"Select-Object -ExpandProperty ProcessId | ConvertTo-Json -Compress"
@@ -201,6 +199,7 @@ def _windows_kill_pids(pids: List[int]) -> None:
class MPVIPCError(Exception):
"""Raised when MPV IPC communication fails."""
pass
@@ -248,7 +247,9 @@ class MPV:
finally:
client.disconnect()
def send(self, command: Dict[str, Any] | List[Any], silent: bool = False) -> Optional[Dict[str, Any]]:
def send(
self, command: Dict[str, Any] | List[Any], silent: bool = False
) -> Optional[Dict[str, Any]]:
client = self.client(silent=bool(silent))
try:
if not client.connect():
@@ -308,7 +309,7 @@ class MPV:
pass
def _q(s: str) -> str:
return '"' + s.replace('\\', '\\\\').replace('"', '\\"') + '"'
return '"' + s.replace("\\", "\\\\").replace('"', '\\"') + '"'
pipeline = f"download-media -url {_q(url)} -format {_q(fmt)}"
if store:
@@ -329,10 +330,18 @@ class MPV:
"pipeline": pipeline,
}
except Exception as exc:
return {"success": False, "stdout": "", "stderr": "", "error": f"{type(exc).__name__}: {exc}", "pipeline": pipeline}
return {
"success": False,
"stdout": "",
"stderr": "",
"error": f"{type(exc).__name__}: {exc}",
"pipeline": pipeline,
}
def get_playlist(self, silent: bool = False) -> Optional[List[Dict[str, Any]]]:
resp = self.send({"command": ["get_property", "playlist"], "request_id": 100}, silent=silent)
resp = self.send(
{"command": ["get_property", "playlist"], "request_id": 100}, silent=silent
)
if resp is None:
return None
if resp.get("error") == "success":
@@ -467,7 +476,11 @@ class MPV:
env["PYTHONUNBUFFERED"] = "1"
try:
existing_pp = env.get("PYTHONPATH")
env["PYTHONPATH"] = str(repo_root) if not existing_pp else (str(repo_root) + os.pathsep + str(existing_pp))
env["PYTHONPATH"] = (
str(repo_root)
if not existing_pp
else (str(repo_root) + os.pathsep + str(existing_pp))
)
except Exception:
pass
kwargs["env"] = env
@@ -486,7 +499,13 @@ class MPV:
except Exception:
flags |= 0x08000000
kwargs["creationflags"] = flags
kwargs.update({k: v for k, v in _windows_hidden_subprocess_kwargs().items() if k != "creationflags"})
kwargs.update(
{
k: v
for k, v in _windows_hidden_subprocess_kwargs().items()
if k != "creationflags"
}
)
_LYRIC_PROCESS = subprocess.Popen(cmd, **kwargs)
debug(f"Lyric loader started (log={log_path})")
@@ -608,10 +627,22 @@ class MPV:
flags |= 0x08000000
kwargs["creationflags"] = flags
# startupinfo is harmless for GUI apps; helps hide flashes for console-subsystem builds.
kwargs.update({k: v for k, v in _windows_hidden_subprocess_kwargs().items() if k != "creationflags"})
kwargs.update(
{
k: v
for k, v in _windows_hidden_subprocess_kwargs().items()
if k != "creationflags"
}
)
debug("Starting MPV")
subprocess.Popen(cmd, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, **kwargs)
subprocess.Popen(
cmd,
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
**kwargs,
)
# Start the persistent pipeline helper eagerly so MPV Lua can issue
# non-blocking requests (e.g., format list prefetch) without needing
@@ -634,7 +665,11 @@ class MPV:
helper_env = os.environ.copy()
try:
existing_pp = helper_env.get("PYTHONPATH")
helper_env["PYTHONPATH"] = str(repo_root) if not existing_pp else (str(repo_root) + os.pathsep + str(existing_pp))
helper_env["PYTHONPATH"] = (
str(repo_root)
if not existing_pp
else (str(repo_root) + os.pathsep + str(existing_pp))
)
except Exception:
pass
@@ -650,7 +685,13 @@ class MPV:
except Exception:
flags |= 0x08000000
helper_kwargs["creationflags"] = flags
helper_kwargs.update({k: v for k, v in _windows_hidden_subprocess_kwargs().items() if k != "creationflags"})
helper_kwargs.update(
{
k: v
for k, v in _windows_hidden_subprocess_kwargs().items()
if k != "creationflags"
}
)
helper_kwargs["cwd"] = str(repo_root)
helper_kwargs["env"] = helper_env
@@ -668,10 +709,10 @@ class MPV:
def get_ipc_pipe_path() -> str:
"""Get the fixed IPC pipe/socket path for persistent MPV connection.
Uses a fixed name so all playback sessions connect to the same MPV
window/process instead of creating new instances.
Returns:
Path to IPC pipe (Windows) or socket (Linux/macOS)
"""
@@ -680,7 +721,7 @@ def get_ipc_pipe_path() -> str:
return str(override)
system = platform.system()
if system == "Windows":
return f"\\\\.\\pipe\\{FIXED_IPC_PIPE_NAME}"
elif system == "Darwin": # macOS
@@ -695,7 +736,7 @@ def _unwrap_memory_target(text: Optional[str]) -> Optional[str]:
return text
for line in text.splitlines():
line = line.strip()
if not line or line.startswith('#') or line.startswith('memory://'):
if not line or line.startswith("#") or line.startswith("memory://"):
continue
return line
return text
@@ -703,14 +744,16 @@ def _unwrap_memory_target(text: Optional[str]) -> Optional[str]:
class MPVIPCClient:
"""Client for communicating with mpv via IPC socket/pipe.
This is the unified interface for all Python code to communicate with mpv.
It handles platform-specific differences (Windows named pipes vs Unix sockets).
"""
def __init__(self, socket_path: Optional[str] = None, timeout: float = 5.0, silent: bool = False):
def __init__(
self, socket_path: Optional[str] = None, timeout: float = 5.0, silent: bool = False
):
"""Initialize MPV IPC client.
Args:
socket_path: Path to IPC socket/pipe. If None, uses the fixed persistent path.
timeout: Socket timeout in seconds.
@@ -826,10 +869,10 @@ class MPVIPCClient:
except Exception:
pass
return None
def connect(self) -> bool:
"""Connect to mpv IPC socket.
Returns:
True if connection successful, False otherwise.
"""
@@ -838,7 +881,7 @@ class MPVIPCClient:
# Windows named pipes
try:
# Try to open the named pipe
self.sock = open(self.socket_path, 'r+b', buffering=0)
self.sock = open(self.socket_path, "r+b", buffering=0)
return True
except (OSError, IOError) as exc:
if not self.silent:
@@ -866,20 +909,20 @@ class MPVIPCClient:
debug(f"Failed to connect to MPV IPC: {exc}")
self.sock = None
return False
def send_command(self, command_data: Dict[str, Any] | List[Any]) -> Optional[Dict[str, Any]]:
"""Send a command to mpv and get response.
Args:
command_data: Command dict (e.g. {"command": [...]}) or list (e.g. ["loadfile", ...])
Returns:
Response dict with 'error' key (value 'success' on success), or None on error.
"""
if not self.sock:
if not self.connect():
return None
try:
# Format command as JSON (mpv IPC protocol)
request: Dict[str, Any]
@@ -887,20 +930,21 @@ class MPVIPCClient:
request = {"command": command_data}
else:
request = command_data
# Add request_id if not present to match response
if "request_id" not in request:
request["request_id"] = int(_time.time() * 1000) % 100000
payload = json.dumps(request) + "\n"
# Debug: log the command being sent
from SYS.logger import debug as _debug
_debug(f"[IPC] Sending: {payload.strip()}")
# Send command
self._write_payload(payload)
# Receive response
# We need to read lines until we find the one with matching request_id
# or until timeout/error. MPV might send events in between.
@@ -909,20 +953,22 @@ class MPVIPCClient:
response_data = self._readline(timeout=self.timeout)
if response_data is None:
return None
if not response_data:
break
try:
lines = response_data.decode('utf-8', errors='replace').strip().split('\n')
lines = response_data.decode("utf-8", errors="replace").strip().split("\n")
for line in lines:
if not line: continue
if not line:
continue
resp = json.loads(line)
# Debug: log responses
from SYS.logger import debug as _debug
_debug(f"[IPC] Received: {line}")
# Check if this is the response to our request
if resp.get("request_id") == request.get("request_id"):
return resp
@@ -940,13 +986,13 @@ class MPVIPCClient:
debug(f"[MPV error] {resp}")
except json.JSONDecodeError:
pass
return None
except Exception as exc:
debug(f"Error sending command to MPV: {exc}")
self.disconnect()
return None
def disconnect(self) -> None:
"""Disconnect from mpv IPC socket."""
if self.sock:
@@ -955,17 +1001,16 @@ class MPVIPCClient:
except Exception:
pass
self.sock = None
def __del__(self) -> None:
"""Cleanup on object destruction."""
self.disconnect()
def __enter__(self):
"""Context manager entry."""
self.connect()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""Context manager exit."""
self.disconnect()