df
Some checks failed
smoke-mm / Install & smoke test mm --help (push) Has been cancelled
Some checks failed
smoke-mm / Install & smoke test mm --help (push) Has been cancelled
This commit is contained in:
135
MPV/mpv_ipc.py
135
MPV/mpv_ipc.py
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user