Skip to content
This repository was archived by the owner on May 2, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@

<details>
<summary>Rofi</summary>

[viu-showcase-rofi.webm](https://github.com/user-attachments/assets/01f197d9-5ac9-45e6-a00b-8e8cd5ab459c)

</details>
Expand All @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -133,7 +134,7 @@ uv tool install "viu-media[notifications]" # For desktop notifications

<details>
<summary><b>Platform-Specific and Alternative Installers</b></summary>

#### Nix / NixOS
##### Ephemeral / One-Off Run (No Installation)
```bash
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]"
Expand All @@ -250,7 +251,7 @@ https://github.com/user-attachments/assets/0c628421-a439-4dea-91bb-7153e8f20ccf

<details>
<summary><b>Building from Source</b></summary>

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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand Down
9 changes: 6 additions & 3 deletions viu_media/cli/service/player/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
2 changes: 2 additions & 0 deletions viu_media/core/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
RofiConfig,
StreamConfig,
VlcConfig,
IinaConfig,
)

__all__ = [
Expand All @@ -17,6 +18,7 @@
"RofiConfig",
"VlcConfig",
"MpvConfig",
"IinaConfig",
"AnilistConfig",
"StreamConfig",
"GeneralConfig",
Expand Down
4 changes: 4 additions & 0 deletions viu_media/core/config/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ def STREAM_USE_IPC():
# VlcConfig
VLC_ARGS = ""

# IinaConfig
IINA_ARGS = ""


# AnilistConfig
ANILIST_PER_PAGE = 15
ANILIST_SORT_BY = "SEARCH_MATCH"
Expand Down
5 changes: 5 additions & 0 deletions viu_media/core/config/descriptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down Expand Up @@ -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."

Expand Down
9 changes: 8 additions & 1 deletion viu_media/core/config/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down Expand Up @@ -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."""

Expand Down Expand Up @@ -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
)
Expand Down
Empty file.
161 changes: 161 additions & 0 deletions viu_media/libs/player/iina/player.py
Original file line number Diff line number Diff line change
@@ -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):
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

play_with_ipc override doesn’t match the BasePlayer interface: it’s missing the -> subprocess.Popen return annotation and currently has an untyped signature. This can trigger pyright’s incompatible override checks and is inconsistent with VlcPlayer/MpvPlayer. Update the method signature to match the base class (and keep raising NotImplementedError if IPC isn’t supported).

Suggested change
def play_with_ipc(self, params: PlayerParams, socket_path: str):
def play_with_ipc(
self, params: PlayerParams, socket_path: str
) -> subprocess.Popen:

Copilot uses AI. Check for mistakes.
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)
8 changes: 7 additions & 1 deletion viu_media/libs/player/player.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from ...core.config import AppConfig
from .base import BasePlayer

PLAYERS = ["mpv", "vlc", "syncplay"]
PLAYERS = ["mpv", "vlc", "iina", "syncplay"]
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PLAYERS includes "syncplay", but the factory has no branch to construct a Syncplay player, and StreamConfig.player doesn’t allow "syncplay". Keeping it in this allowlist makes the validation/error messaging misleading. Either remove "syncplay" from PLAYERS or implement/allow it consistently across config + factory.

Suggested change
PLAYERS = ["mpv", "vlc", "iina", "syncplay"]
PLAYERS = ["mpv", "vlc", "iina"]

Copilot uses AI. Check for mistakes.


class PlayerFactory:
Expand Down Expand Up @@ -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."
)
Expand Down