diff --git a/README.md b/README.md
index 1559d2c3..ca9820ea 100644
--- a/README.md
+++ b/README.md
@@ -27,7 +27,7 @@
Rofi
-
+
[viu-showcase-rofi.webm](https://github.com/user-attachments/assets/01f197d9-5ac9-45e6-a00b-8e8cd5ab459c)
@@ -51,7 +51,7 @@
> [!IMPORTANT]
> This project scrapes public-facing websites for its streaming / downloading capabilities and primarily acts as an anilist, jikan and many other media apis tui client. The developer(s) of this application have no affiliation with these content providers. This application hosts zero content and is intended for educational and personal use only. Use at your own risk.
->
+>
> [**Read the Full Disclaimer**](DISCLAIMER.md)
## Core Features
@@ -74,6 +74,7 @@ For the best experience, please install these external tools:
* **Required for Streaming:**
* [**mpv**](https://mpv.io/installation/) - The primary and recommended media player.
+ * [**IINA**](https://iina.io/) - A recommended macOS player if you want a native app built on top of mpv.
* **Recommended for UI & Previews:**
* [**fzf**](https://github.com/junegunn/fzf) - For the best fuzzy-finder interface.
* [**chafa**](https://github.com/hpjansson/chafa) or [**kitty's icat**](https://sw.kovidgoyal.net/kitty/kittens/icat/) - For image previews in the terminal.
@@ -102,7 +103,7 @@ The easiest way to get started is to download a pre-built, self-contained binary
```bash
# Option 1: System-wide installation (requires sudo)
sudo mv viu-linux-x86_64 /usr/local/bin/viu
-
+
# Option 2: User directory installation
mkdir -p ~/.local/bin
mv viu-linux-x86_64 ~/.local/bin/viu
@@ -133,7 +134,7 @@ uv tool install "viu-media[notifications]" # For desktop notifications
Platform-Specific and Alternative Installers
-
+
#### Nix / NixOS
##### Ephemeral / One-Off Run (No Installation)
```bash
@@ -164,7 +165,7 @@ uv tool install "viu-media[notifications]" # For desktop notifications
```
#### Termux
You may have to have rust installed see this issue: https://github.com/pydantic/pydantic-core/issues/1012#issuecomment-2511269688.
-
+
```bash
# Recommended (with pip due to more control)
pkg install python
@@ -241,7 +242,7 @@ https://github.com/user-attachments/assets/0c628421-a439-4dea-91bb-7153e8f20ccf
```bash
pipx install "viu-media[standard]"
```
-
+
#### Using pip
```bash
pip install "viu-media[standard]"
@@ -250,7 +251,7 @@ https://github.com/user-attachments/assets/0c628421-a439-4dea-91bb-7153e8f20ccf
Building from Source
-
+
Requires [Git](https://git-scm.com/), [Python 3.10+](https://www.python.org/), and [uv](https://astral.sh/blog/uv).
```bash
git clone https://github.com/viu-media/Viu.git --depth 1
@@ -383,7 +384,7 @@ auto_select_anime_result = True ; Automatically select the best search match.
# [stream] Section: Controls playback and streaming.
[stream]
-player = mpv ; The media player to use (mpv, vlc).
+player = mpv ; The media player to use (mpv, vlc, iina).
quality = 1080 ; Preferred stream quality (1080, 720, 480, 360).
translation_type = sub ; Preferred audio/subtitle type (sub, dub).
auto_next = False ; Automatically play the next episode.
@@ -452,7 +453,7 @@ 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
diff --git a/viu_media/cli/service/player/service.py b/viu_media/cli/service/player/service.py
index ed153a95..b96b982e 100644
--- a/viu_media/cli/service/player/service.py
+++ b/viu_media/cli/service/player/service.py
@@ -2,7 +2,6 @@
from typing import Optional
from ....core.config import AppConfig
-from ....core.exceptions import ViuError
from ....libs.media_api.types import MediaItem
from ....libs.player.base import BasePlayer
from ....libs.player.params import PlayerParams
@@ -63,5 +62,9 @@ def _play_with_ipc(
return MpvIPCPlayer(self.app_config.stream).play(
self.player, params, self.provider, anime, registry, media_item
)
- else:
- raise ViuError("Not implemented")
+
+ logger.warning(
+ "IPC is only supported for mpv; falling back to standard playback for player %s",
+ self.app_config.stream.player,
+ )
+ return self.player.play(params)
diff --git a/viu_media/core/config/__init__.py b/viu_media/core/config/__init__.py
index 4ae6df5e..14450cd1 100644
--- a/viu_media/core/config/__init__.py
+++ b/viu_media/core/config/__init__.py
@@ -9,6 +9,7 @@
RofiConfig,
StreamConfig,
VlcConfig,
+ IinaConfig,
)
__all__ = [
@@ -17,6 +18,7 @@
"RofiConfig",
"VlcConfig",
"MpvConfig",
+ "IinaConfig",
"AnilistConfig",
"StreamConfig",
"GeneralConfig",
diff --git a/viu_media/core/config/defaults.py b/viu_media/core/config/defaults.py
index 4cc0aa82..a567caaa 100644
--- a/viu_media/core/config/defaults.py
+++ b/viu_media/core/config/defaults.py
@@ -86,6 +86,10 @@ def STREAM_USE_IPC():
# VlcConfig
VLC_ARGS = ""
+# IinaConfig
+IINA_ARGS = ""
+
+
# AnilistConfig
ANILIST_PER_PAGE = 15
ANILIST_SORT_BY = "SEARCH_MATCH"
diff --git a/viu_media/core/config/descriptions.py b/viu_media/core/config/descriptions.py
index 1de2dc56..f4fc8ccd 100644
--- a/viu_media/core/config/descriptions.py
+++ b/viu_media/core/config/descriptions.py
@@ -96,6 +96,10 @@
# VlcConfig
VLC_ARGS = "Comma-separated arguments to pass to the Vlc player."
+# IinaConfig
+IINA_ARGS = "Comma-separated arguments to pass to the IINA player."
+
+
# AnilistConfig
ANILIST_PER_PAGE = "Number of items to fetch per page from AniList."
ANILIST_SORT_BY = "Default sort order for AniList search results."
@@ -133,6 +137,7 @@
APP_ROFI = "Settings for the Rofi selector interface."
APP_MPV = "Configuration for the MPV media player."
APP_VLC = "Configuration for the VLC media player."
+APP_IINA = "Configuration for the IINA media player."
APP_MEDIA_REGISTRY = "Configuration for the media registry."
APP_SESSIONS = "Configuration for sessions."
diff --git a/viu_media/core/config/model.py b/viu_media/core/config/model.py
index eb81deb7..8f29f85c 100644
--- a/viu_media/core/config/model.py
+++ b/viu_media/core/config/model.py
@@ -229,7 +229,7 @@ class GeneralConfig(BaseModel):
class StreamConfig(BaseModel):
"""Configuration specific to video streaming and playback."""
- player: Literal["mpv", "vlc"] = Field(
+ player: Literal["mpv", "vlc", "iina"] = Field(
default=defaults.STREAM_PLAYER,
description=desc.STREAM_PLAYER,
)
@@ -390,6 +390,12 @@ class VlcConfig(OtherConfig):
args: str = Field(default=defaults.VLC_ARGS, description=desc.VLC_ARGS)
+class IinaConfig(OtherConfig):
+ """Configuration specific to the IINA player integration."""
+
+ args: str = Field(default=defaults.IINA_ARGS, description=desc.IINA_ARGS)
+
+
class AnilistConfig(OtherConfig):
"""Configuration for interacting with the AniList API."""
@@ -535,6 +541,7 @@ class AppConfig(BaseModel):
)
mpv: MpvConfig = Field(default_factory=MpvConfig, description=desc.APP_MPV)
vlc: VlcConfig = Field(default_factory=VlcConfig, description=desc.APP_VLC)
+ iina: IinaConfig = Field(default_factory=IinaConfig, description=desc.APP_IINA)
media_registry: MediaRegistryConfig = Field(
default_factory=MediaRegistryConfig, description=desc.APP_MEDIA_REGISTRY
)
diff --git a/viu_media/libs/player/iina/__init__.py b/viu_media/libs/player/iina/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/viu_media/libs/player/iina/player.py b/viu_media/libs/player/iina/player.py
new file mode 100644
index 00000000..0eaf0905
--- /dev/null
+++ b/viu_media/libs/player/iina/player.py
@@ -0,0 +1,161 @@
+"""
+IINA player integration for Viu.
+
+This module provides the IinaPlayer class,
+which implements the BasePlayer interface for the IINA media player.
+"""
+
+import logging
+import shutil
+import subprocess
+from pathlib import Path
+
+from ....core.config import IinaConfig
+from ....core.constants import PLATFORM
+from ....core.exceptions import ViuError
+from ....core.patterns import TORRENT_REGEX
+from ....core.utils import detect
+from ..base import BasePlayer
+from ..params import PlayerParams
+from ..types import PlayerResult
+
+logger = logging.getLogger(__name__)
+
+IINA_APP_EXECUTABLES = (
+ Path("/Applications/IINA.app/Contents/MacOS/iina-cli"),
+ Path.home() / "Applications/IINA.app/Contents/MacOS/iina-cli",
+)
+
+
+class IinaPlayer(BasePlayer):
+ """
+ IINA player implementation for Viu.
+
+ Provides playback functionality using the IINA media player.
+ """
+
+ def __init__(self, config: IinaConfig):
+ """
+ Initialize the IINA player with the given configuration.
+
+ Args:
+ config: IinaConfig object containing IINA-specific configuration.
+ """
+ self.config = config
+ self.executable = self._find_executable()
+
+ def play(self, params: PlayerParams) -> PlayerResult:
+ """
+ Play the given media URL using IINA player.
+
+ Args:
+ params: PlayerParams object containing playback parameters.
+
+ Returns:
+ PlayerResult: Information about the playback session.
+
+ Raises:
+ ViuError: If IINA is not supported on the current platform,
+ if syncplay is requested, if URL is a torrent,
+ or if IINA executable is not found.
+ """
+ if PLATFORM != "darwin":
+ raise ViuError("IINA is only supported on macOS.")
+
+ if params.syncplay:
+ raise ViuError("Viu's IINA integration does not support Syncplay.")
+
+ if TORRENT_REGEX.search(params.url):
+ raise ViuError("Unable to play torrents with IINA.")
+
+ if not self.executable:
+ raise ViuError(
+ "IINA executable not found. Install IINA or expose `iina-cli` in PATH."
+ )
+
+ args = self._build_iina_command(params)
+
+ subprocess.run(args, check=False, env=detect.get_clean_env())
+ return PlayerResult(episode=params.episode)
+
+ def play_with_ipc(self, params: PlayerParams, socket_path: str):
+ raise NotImplementedError("play_with_ipc is not implemented for IINA player.")
+
+ def _find_executable(self) -> str | None:
+ """
+ Find the IINA executable path.
+
+ First checks if 'iina-cli' is in PATH, then checks common macOS application paths.
+
+ Returns:
+ str | None: The path to the IINA executable, or None if not found.
+ """
+ executable = shutil.which("iina-cli")
+ if executable:
+ return executable
+
+ for app_executable in IINA_APP_EXECUTABLES:
+ if app_executable.exists():
+ return str(app_executable)
+
+ return None
+
+ def _build_iina_command(self, params: PlayerParams) -> list[str]:
+ """
+ Build the command line arguments for launching IINA.
+
+ Args:
+ params: PlayerParams object containing playback parameters.
+
+ Returns:
+ list[str]: The command line arguments for IINA.
+ """
+ assert self.executable is not None
+ args = [self.executable]
+ args.append(params.url)
+
+ if mpv_args := self._create_iina_mpv_options(params):
+ args.append("--")
+ args.extend(mpv_args)
+
+ logger.debug("Starting IINA with args: %s", args)
+ return args
+
+ def _create_iina_mpv_options(self, params: PlayerParams) -> list[str]:
+ """
+ Create MPV options for IINA based on the player parameters.
+
+ Args:
+ params: PlayerParams object containing playback parameters.
+
+ Returns:
+ list[str]: List of MPV command line options.
+ """
+ mpv_args = []
+
+ if params.title:
+ mpv_args.append(f"--force-media-title={params.title}")
+ if params.subtitles:
+ for sub in params.subtitles:
+ mpv_args.append(f"--sub-file={sub}")
+ if params.start_time:
+ mpv_args.append(f"--start={params.start_time}")
+ if params.headers:
+ header_str = ",".join(f"{k}:{v}" for k, v in params.headers.items())
+ mpv_args.append(f"--http-header-fields={header_str}")
+ if self.config.args:
+ mpv_args.extend(
+ arg.strip() for arg in self.config.args.split(",") if arg.strip()
+ )
+
+ return mpv_args
+
+
+if __name__ == "__main__":
+ from ....core.constants import APP_ASCII_ART
+
+ print(APP_ASCII_ART)
+ url = input("Enter the url you would like to stream: ")
+ iina = IinaPlayer(IinaConfig())
+ player_result = iina.play(PlayerParams(episode="", query="", url=url, title=""))
+ print(player_result)
diff --git a/viu_media/libs/player/player.py b/viu_media/libs/player/player.py
index a5a74811..2c3afd1d 100644
--- a/viu_media/libs/player/player.py
+++ b/viu_media/libs/player/player.py
@@ -7,7 +7,7 @@
from ...core.config import AppConfig
from .base import BasePlayer
-PLAYERS = ["mpv", "vlc", "syncplay"]
+PLAYERS = ["mpv", "vlc", "iina", "syncplay"]
class PlayerFactory:
@@ -45,6 +45,12 @@ def create(config: AppConfig) -> BasePlayer:
from .vlc.player import VlcPlayer
return VlcPlayer(config.vlc)
+
+ elif player_name == "iina":
+ from .iina.player import IinaPlayer
+
+ return IinaPlayer(config.iina)
+
raise NotImplementedError(
f"Configuration logic for player '{player_name}' not implemented in factory."
)