dfslkjelf
This commit is contained in:
255
MPV/mpv_ipc.py
255
MPV/mpv_ipc.py
@@ -198,11 +198,13 @@ class MPV:
|
||||
ipc_path: Optional[str] = None,
|
||||
lua_script_path: Optional[str | Path] = None,
|
||||
timeout: float = 5.0,
|
||||
check_mpv: bool = True,
|
||||
) -> None:
|
||||
|
||||
ok, reason = _check_mpv_availability()
|
||||
if not ok:
|
||||
raise MPVIPCError(reason or "MPV unavailable")
|
||||
if bool(check_mpv):
|
||||
ok, reason = _check_mpv_availability()
|
||||
if not ok:
|
||||
raise MPVIPCError(reason or "MPV unavailable")
|
||||
|
||||
self.timeout = timeout
|
||||
self.ipc_path = ipc_path or get_ipc_pipe_path()
|
||||
@@ -246,6 +248,66 @@ class MPV:
|
||||
resp = self.send({"command": ["set_property", name, value]})
|
||||
return bool(resp and resp.get("error") == "success")
|
||||
|
||||
def download(
|
||||
self,
|
||||
*,
|
||||
url: str,
|
||||
fmt: str,
|
||||
store: Optional[str] = None,
|
||||
path: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Download a URL using the same pipeline semantics as the MPV UI.
|
||||
|
||||
This is intended as a stable Python entrypoint for "button actions".
|
||||
It does not require mpv.exe availability (set check_mpv=False if needed).
|
||||
"""
|
||||
url = str(url or "").strip()
|
||||
fmt = str(fmt or "").strip()
|
||||
store = str(store or "").strip() if store is not None else None
|
||||
path = str(path or "").strip() if path is not None else None
|
||||
|
||||
if not url:
|
||||
return {"success": False, "stdout": "", "stderr": "", "error": "Missing url"}
|
||||
if not fmt:
|
||||
return {"success": False, "stdout": "", "stderr": "", "error": "Missing fmt"}
|
||||
if bool(store) == bool(path):
|
||||
return {
|
||||
"success": False,
|
||||
"stdout": "",
|
||||
"stderr": "",
|
||||
"error": "Provide exactly one of store or path",
|
||||
}
|
||||
|
||||
# Ensure any in-process cmdlets that talk to MPV pick up this IPC path.
|
||||
try:
|
||||
os.environ["MEDEIA_MPV_IPC"] = str(self.ipc_path)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _q(s: str) -> str:
|
||||
return '"' + s.replace('\\', '\\\\').replace('"', '\\"') + '"'
|
||||
|
||||
pipeline = f"download-media -url {_q(url)} -format {_q(fmt)}"
|
||||
if store:
|
||||
pipeline += f" | add-file -store {_q(store)}"
|
||||
else:
|
||||
pipeline += f" | add-file -path {_q(path or '')}"
|
||||
|
||||
try:
|
||||
from TUI.pipeline_runner import PipelineExecutor # noqa: WPS433
|
||||
|
||||
executor = PipelineExecutor()
|
||||
result = executor.run_pipeline(pipeline)
|
||||
return {
|
||||
"success": bool(getattr(result, "success", False)),
|
||||
"stdout": getattr(result, "stdout", "") or "",
|
||||
"stderr": getattr(result, "stderr", "") or "",
|
||||
"error": getattr(result, "error", None),
|
||||
"pipeline": pipeline,
|
||||
}
|
||||
except Exception as exc:
|
||||
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)
|
||||
if resp is None:
|
||||
@@ -427,13 +489,62 @@ class MPV:
|
||||
http_header_fields: Optional[str] = None,
|
||||
detached: bool = True,
|
||||
) -> None:
|
||||
# uosc reads its config from "~~/script-opts/uosc.conf".
|
||||
# With --no-config, mpv makes ~~ expand to an empty string, so uosc can't load.
|
||||
# Instead, point mpv at a repo-controlled config directory.
|
||||
try:
|
||||
repo_root = Path(__file__).resolve().parent.parent
|
||||
except Exception:
|
||||
repo_root = Path.cwd()
|
||||
|
||||
portable_config_dir = repo_root / "MPV" / "portable_config"
|
||||
try:
|
||||
(portable_config_dir / "script-opts").mkdir(parents=True, exist_ok=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Ensure uosc.conf is available at the location uosc expects.
|
||||
try:
|
||||
src_uosc_conf = repo_root / "MPV" / "LUA" / "uosc" / "uosc.conf"
|
||||
dst_uosc_conf = portable_config_dir / "script-opts" / "uosc.conf"
|
||||
if src_uosc_conf.exists():
|
||||
# Only seed a default config if the user doesn't already have one.
|
||||
if not dst_uosc_conf.exists():
|
||||
dst_uosc_conf.write_bytes(src_uosc_conf.read_bytes())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
cmd: List[str] = [
|
||||
"mpv",
|
||||
f"--config-dir={str(portable_config_dir)}",
|
||||
# Allow mpv to auto-load scripts from <config-dir>/scripts/ (e.g., thumbfast).
|
||||
"--load-scripts=yes",
|
||||
"--osc=no",
|
||||
"--load-console=no",
|
||||
"--load-commands=no",
|
||||
"--load-select=no",
|
||||
"--load-context-menu=no",
|
||||
"--load-positioning=no",
|
||||
"--load-stats-overlay=no",
|
||||
"--load-auto-profiles=no",
|
||||
"--ytdl=yes",
|
||||
f"--input-ipc-server={self.ipc_path}",
|
||||
"--idle=yes",
|
||||
"--force-window=yes",
|
||||
]
|
||||
|
||||
# uosc and other scripts are expected to be auto-loaded from portable_config/scripts.
|
||||
# We keep the back-compat fallback only if the user hasn't installed uosc.lua there.
|
||||
try:
|
||||
uosc_entry = portable_config_dir / "scripts" / "uosc.lua"
|
||||
if not uosc_entry.exists() and self.lua_script_path:
|
||||
lua_dir = Path(self.lua_script_path).resolve().parent
|
||||
uosc_main = lua_dir / "uosc" / "scripts" / "uosc" / "main.lua"
|
||||
if uosc_main.exists():
|
||||
cmd.append(f"--script={str(uosc_main)}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Always load the bundled Lua script at startup.
|
||||
if self.lua_script_path and os.path.exists(self.lua_script_path):
|
||||
cmd.append(f"--script={self.lua_script_path}")
|
||||
@@ -519,6 +630,112 @@ class MPVIPCClient:
|
||||
self.sock: socket.socket | BinaryIO | None = None
|
||||
self.is_windows = platform.system() == "Windows"
|
||||
self.silent = bool(silent)
|
||||
self._recv_buffer: bytes = b""
|
||||
|
||||
def _write_payload(self, payload: str) -> None:
|
||||
if not self.sock:
|
||||
if not self.connect():
|
||||
raise MPVIPCError("Not connected")
|
||||
|
||||
if self.is_windows:
|
||||
pipe = cast(BinaryIO, self.sock)
|
||||
pipe.write(payload.encode("utf-8"))
|
||||
pipe.flush()
|
||||
else:
|
||||
sock_obj = cast(socket.socket, self.sock)
|
||||
sock_obj.sendall(payload.encode("utf-8"))
|
||||
|
||||
def _readline(self, *, timeout: Optional[float] = None) -> Optional[bytes]:
|
||||
if not self.sock:
|
||||
if not self.connect():
|
||||
return None
|
||||
|
||||
effective_timeout = self.timeout if timeout is None else float(timeout)
|
||||
deadline = _time.time() + max(0.0, effective_timeout)
|
||||
|
||||
if self.is_windows:
|
||||
try:
|
||||
pipe = cast(BinaryIO, self.sock)
|
||||
return pipe.readline()
|
||||
except (OSError, IOError):
|
||||
return None
|
||||
|
||||
# Unix: buffer until newline.
|
||||
sock_obj = cast(socket.socket, self.sock)
|
||||
while True:
|
||||
nl = self._recv_buffer.find(b"\n")
|
||||
if nl != -1:
|
||||
line = self._recv_buffer[: nl + 1]
|
||||
self._recv_buffer = self._recv_buffer[nl + 1 :]
|
||||
return line
|
||||
|
||||
remaining = deadline - _time.time()
|
||||
if remaining <= 0:
|
||||
return None
|
||||
|
||||
try:
|
||||
# Temporarily narrow timeout for this read.
|
||||
old_timeout = sock_obj.gettimeout()
|
||||
try:
|
||||
sock_obj.settimeout(remaining)
|
||||
chunk = sock_obj.recv(4096)
|
||||
finally:
|
||||
sock_obj.settimeout(old_timeout)
|
||||
except socket.timeout:
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
if not chunk:
|
||||
# EOF
|
||||
return b""
|
||||
self._recv_buffer += chunk
|
||||
|
||||
def read_message(self, *, timeout: Optional[float] = None) -> Optional[Dict[str, Any]]:
|
||||
"""Read the next JSON message/event from MPV.
|
||||
|
||||
Returns:
|
||||
- dict: parsed JSON message/event
|
||||
- {"event": "__eof__"} if the stream ended
|
||||
- None on timeout / no data
|
||||
"""
|
||||
raw = self._readline(timeout=timeout)
|
||||
if raw is None:
|
||||
return None
|
||||
if raw == b"":
|
||||
return {"event": "__eof__"}
|
||||
try:
|
||||
return json.loads(raw.decode("utf-8", errors="replace").strip())
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def send_command_no_wait(self, command_data: Dict[str, Any] | List[Any]) -> Optional[int]:
|
||||
"""Send a command to mpv without waiting for its response.
|
||||
|
||||
This is important for long-running event loops (helpers) so we don't
|
||||
consume/lose async events (like property-change) while waiting.
|
||||
"""
|
||||
try:
|
||||
request: Dict[str, Any]
|
||||
if isinstance(command_data, list):
|
||||
request = {"command": command_data}
|
||||
else:
|
||||
request = dict(command_data)
|
||||
|
||||
if "request_id" not in request:
|
||||
request["request_id"] = int(_time.time() * 1000) % 100000
|
||||
|
||||
payload = json.dumps(request) + "\n"
|
||||
self._write_payload(payload)
|
||||
return int(request["request_id"])
|
||||
except Exception as exc:
|
||||
if not self.silent:
|
||||
debug(f"Error sending no-wait command to MPV: {exc}")
|
||||
try:
|
||||
self.disconnect()
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
def connect(self) -> bool:
|
||||
"""Connect to mpv IPC socket.
|
||||
@@ -592,44 +809,22 @@ class MPVIPCClient:
|
||||
_debug(f"[IPC] Sending: {payload.strip()}")
|
||||
|
||||
# Send command
|
||||
if self.is_windows:
|
||||
pipe = cast(BinaryIO, self.sock)
|
||||
pipe.write(payload.encode("utf-8"))
|
||||
pipe.flush()
|
||||
else:
|
||||
sock_obj = cast(socket.socket, self.sock)
|
||||
sock_obj.sendall(payload.encode("utf-8"))
|
||||
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.
|
||||
start_time = _time.time()
|
||||
while _time.time() - start_time < self.timeout:
|
||||
response_data = b""
|
||||
if self.is_windows:
|
||||
try:
|
||||
pipe = cast(BinaryIO, self.sock)
|
||||
response_data = pipe.readline()
|
||||
except (OSError, IOError):
|
||||
return None
|
||||
else:
|
||||
try:
|
||||
# This is simplistic for Unix socket (might not get full line)
|
||||
# But for now assuming MPV sends line-buffered JSON
|
||||
sock_obj = cast(socket.socket, self.sock)
|
||||
chunk = sock_obj.recv(4096)
|
||||
if not chunk:
|
||||
break
|
||||
response_data = chunk
|
||||
# TODO: Handle partial lines if needed
|
||||
except socket.timeout:
|
||||
return None
|
||||
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').strip().split('\n')
|
||||
lines = response_data.decode('utf-8', errors='replace').strip().split('\n')
|
||||
for line in lines:
|
||||
if not line: continue
|
||||
resp = json.loads(line)
|
||||
|
||||
Reference in New Issue
Block a user