dfslkjelf

This commit is contained in:
nose
2025-12-18 22:50:21 -08:00
parent 76691dbbf5
commit d637532237
16 changed files with 2587 additions and 299 deletions

View File

@@ -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)