From 9d9fa55b699fb2f2aba6ca8ffca507e04122a10f Mon Sep 17 00:00:00 2001 From: Benexl Date: Sun, 28 Dec 2025 17:54:14 +0300 Subject: [PATCH 1/4] chore: bump version --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 18e187b..96cbcc7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "viu-media" -version = "3.3.4" +version = "3.3.5" description = "A browser anime site experience from the terminal" license = "UNLICENSE" readme = "README.md" diff --git a/uv.lock b/uv.lock index c433f12..724b548 100644 --- a/uv.lock +++ b/uv.lock @@ -3743,7 +3743,7 @@ wheels = [ [[package]] name = "viu-media" -version = "3.3.4" +version = "3.3.5" source = { editable = "." } dependencies = [ { name = "click" }, From df8e925eec7953bf8ce39fce52e9ab81c9a91a52 Mon Sep 17 00:00:00 2001 From: Benedict Xavier <81157281+Benexl@users.noreply.github.com> Date: Mon, 29 Dec 2025 00:32:33 +0300 Subject: [PATCH 2/4] Update README with project reference and contribution info Added a project reference and updated contributing section. --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index a8c9927..45a9ea8 100644 --- a/README.md +++ b/README.md @@ -362,6 +362,9 @@ You can run the background worker as a systemd service for persistence. systemctl --user daemon-reload systemctl --user enable --now viu-worker.service ``` + +## Project using it +**[Inazuma](https://github.com/viu-media/Inazuma)** - official gui wrapper over viu built in kivymd ## Contributing From bcc5e7df8e2cf655360f5029cbc796c1946815ee Mon Sep 17 00:00:00 2001 From: Benexl Date: Mon, 29 Dec 2025 11:53:02 +0300 Subject: [PATCH 3/4] feat: allow disabling of initial config creation --- viu_media/cli/config/loader.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/viu_media/cli/config/loader.py b/viu_media/cli/config/loader.py index 81b5bd6..6307c84 100644 --- a/viu_media/cli/config/loader.py +++ b/viu_media/cli/config/loader.py @@ -71,7 +71,7 @@ class ConfigLoader: return app_config - def load(self, update: Dict = {}) -> AppConfig: + def load(self, update: Dict = {}, allow_setup=True) -> AppConfig: """ Loads the configuration and returns a populated, validated AppConfig object. @@ -84,7 +84,7 @@ class ConfigLoader: Raises: ConfigError: If the configuration file contains validation or parsing errors. """ - if not self.config_path.exists(): + if not self.config_path.exists() and allow_setup: return self._handle_first_run() try: From 4dc15eec50c6e3c14606786fc9d64ad11ac9373a Mon Sep 17 00:00:00 2001 From: Zen Date: Tue, 30 Dec 2025 12:39:45 +0800 Subject: [PATCH 4/4] fix: IPC socket for windows --- viu_media/cli/service/player/ipc/mpv.py | 62 ++++++++++++++++++++++--- 1 file changed, 56 insertions(+), 6 deletions(-) diff --git a/viu_media/cli/service/player/ipc/mpv.py b/viu_media/cli/service/player/ipc/mpv.py index ba25ce0..f719350 100644 --- a/viu_media/cli/service/player/ipc/mpv.py +++ b/viu_media/cli/service/player/ipc/mpv.py @@ -5,6 +5,7 @@ This provides advanced features like episode navigation, quality switching, and import json import logging +import os import socket import subprocess import tempfile @@ -43,7 +44,7 @@ class MPVIPCClient: def __init__(self, socket_path: str): self.socket_path = socket_path - self.socket: Optional[socket.socket] = None + self.socket: Optional[Any] = None self._request_id_counter = 0 self._lock = threading.Lock() @@ -55,13 +56,54 @@ class MPVIPCClient: self._response_dict: Dict[int, Any] = {} self._response_events: Dict[int, threading.Event] = {} + @staticmethod + def _is_windows_named_pipe(path: str) -> bool: + return path.startswith("\\\\.\\pipe\\") + + @staticmethod + def _supports_unix_sockets() -> bool: + return hasattr(socket, "AF_UNIX") + + @staticmethod + def _open_windows_named_pipe(path: str): + # MPV's JSON IPC on Windows uses named pipes like: \\.\pipe\mpvpipe + # Opening the pipe as a binary file supports read/write. + f = open(path, "r+b", buffering=0) + + class _PipeConn: + def __init__(self, fileobj): + self._f = fileobj + + def recv(self, n: int) -> bytes: + return self._f.read(n) + + def sendall(self, data: bytes) -> None: + self._f.write(data) + self._f.flush() + + def close(self) -> None: + self._f.close() + + return _PipeConn(f) + def connect(self, timeout: float = 5.0) -> None: """Connect to MPV IPC socket and start the reader thread.""" start_time = time.time() while time.time() - start_time < timeout: try: - self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - self.socket.connect(self.socket_path) + if self._supports_unix_sockets() and not self._is_windows_named_pipe( + self.socket_path + ): + self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + self.socket.connect(self.socket_path) + else: + if os.name != "nt" or not self._is_windows_named_pipe(self.socket_path): + raise MPVIPCError( + "MPV IPC requires Unix domain sockets (AF_UNIX) or a Windows named pipe path " + "like \\\\.\\pipe\\mpvpipe. Got: " + f"{self.socket_path}" + ) + self.socket = self._open_windows_named_pipe(self.socket_path) logger.info(f"Connected to MPV IPC socket at {self.socket_path}") self._start_reader_thread() return @@ -329,8 +371,12 @@ class MpvIPCPlayer(BaseIPCPlayer): def _start_mpv_process(self, player: BasePlayer, params: PlayerParams) -> None: """Start MPV process with IPC enabled.""" - temp_dir = Path(tempfile.gettempdir()) - self.socket_path = str(temp_dir / f"mpv_ipc_{time.time()}.sock") + if hasattr(socket, "AF_UNIX"): + temp_dir = Path(tempfile.gettempdir()) + self.socket_path = str(temp_dir / f"mpv_ipc_{time.time()}.sock") + else: + # Windows MPV IPC uses named pipes. + self.socket_path = f"\\\\.\\pipe\\mpv_ipc_{int(time.time() * 1000)}" self.mpv_process = player.play_with_ipc(params, self.socket_path) time.sleep(1.0) @@ -480,7 +526,11 @@ class MpvIPCPlayer(BaseIPCPlayer): self.mpv_process.wait(timeout=3) except subprocess.TimeoutExpired: self.mpv_process.kill() - if self.socket_path and Path(self.socket_path).exists(): + if ( + self.socket_path + and not self.socket_path.startswith("\\\\.\\pipe\\") + and Path(self.socket_path).exists() + ): Path(self.socket_path).unlink(missing_ok=True) def _get_episode(