mirror of
https://github.com/Benexl/FastAnime.git
synced 2025-12-16 09:30:49 -08:00
Compare commits
37 Commits
v3.2.6
...
feature/pl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f99ff546d5 | ||
|
|
88b707e060 | ||
|
|
eaedf3268d | ||
|
|
ade0465ea4 | ||
|
|
5e82db4ea8 | ||
|
|
a10e56cb6f | ||
|
|
fbd95e1966 | ||
|
|
d37a441ccf | ||
|
|
cbc1ceccbb | ||
|
|
249a207cad | ||
|
|
c8a42c4920 | ||
|
|
de8b6b7f2f | ||
|
|
54e0942233 | ||
|
|
8ea0c121c2 | ||
|
|
eddaad64e7 | ||
|
|
43be7a52cf | ||
|
|
b689760a25 | ||
|
|
e53246b79b | ||
|
|
b0fc94cdc5 | ||
|
|
449f6c1e59 | ||
|
|
ab4734b79d | ||
|
|
93d0f6a1a5 | ||
|
|
19c75c48b2 | ||
|
|
5341b0a844 | ||
|
|
24e7e6a16b | ||
|
|
4b310e60b8 | ||
|
|
4d50cffd86 | ||
|
|
f6fedf0500 | ||
|
|
7b431450fe | ||
|
|
66b247330b | ||
|
|
c6b8cfc294 | ||
|
|
6895426d67 | ||
|
|
cc69dc35f6 | ||
|
|
ed81f37ae4 | ||
|
|
c6858b00c4 | ||
|
|
a44034a5d4 | ||
|
|
f768518721 |
15
.github/FUNDING.yml
vendored
15
.github/FUNDING.yml
vendored
@@ -1,15 +0,0 @@
|
|||||||
# These are supported funding model platforms
|
|
||||||
|
|
||||||
github: benexl # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
|
||||||
patreon: # Replace with a single Patreon username
|
|
||||||
open_collective: # Replace with a single Open Collective username
|
|
||||||
ko_fi: benexl # Replace with a single Ko-fi username
|
|
||||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
|
||||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
|
||||||
liberapay: # Replace with a single Liberapay username
|
|
||||||
issuehunt: # Replace with a single IssueHunt username
|
|
||||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
|
||||||
polar: # Replace with a single Polar username
|
|
||||||
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
|
|
||||||
thanks_dev: # Replace with a single thanks.dev username
|
|
||||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
|
||||||
7
.github/workflows/test.yml
vendored
7
.github/workflows/test.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
|||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.10", "3.11"] # List the Python versions you want to test
|
python-version: ["3.11", "3.12"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -22,6 +22,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
|
- name: Install dbus-python build dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get -y install libdbus-1-dev libglib2.0-dev
|
||||||
|
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@v3
|
uses: astral-sh/setup-uv@v3
|
||||||
with:
|
with:
|
||||||
|
|||||||
1
.repomixignore
Normal file
1
.repomixignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
**/generated/**/*
|
||||||
319
PLUGINS.md
Normal file
319
PLUGINS.md
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
# Viu Plugin Development Guide
|
||||||
|
|
||||||
|
This guide explains how to create plugins for viu, the terminal-based anime streaming tool.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Viu supports four types of plugins:
|
||||||
|
|
||||||
|
- **Providers**: Add support for new anime streaming websites
|
||||||
|
- **Players**: Add support for new media players
|
||||||
|
- **Selectors**: Add support for new interactive selection tools
|
||||||
|
- **Commands**: Add new CLI commands to viu
|
||||||
|
|
||||||
|
## Plugin Structure
|
||||||
|
|
||||||
|
Every plugin must be a Git repository with the following structure:
|
||||||
|
|
||||||
|
```
|
||||||
|
your-plugin-repo/
|
||||||
|
├── plugin.info.toml # Plugin metadata (required)
|
||||||
|
├── your_module.py # Your plugin implementation
|
||||||
|
├── config.toml # Default configuration (optional)
|
||||||
|
├── requirements.txt # Dependencies (optional)
|
||||||
|
├── utils.py # Additional modules (optional)
|
||||||
|
├── helpers/ # Subdirectories supported (optional)
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ └── parser.py
|
||||||
|
└── README.md # Documentation (recommended)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multi-File Plugins
|
||||||
|
|
||||||
|
Viu supports plugins with multiple Python files. You can organize your plugin code across multiple modules and import between them normally:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# In your main plugin file
|
||||||
|
from utils import helper_function
|
||||||
|
from helpers.parser import ResponseParser
|
||||||
|
|
||||||
|
class MyProvider(BaseAnimeProvider):
|
||||||
|
def __init__(self, client, **config):
|
||||||
|
self.parser = ResponseParser()
|
||||||
|
# ... rest of implementation
|
||||||
|
```
|
||||||
|
|
||||||
|
The plugin system automatically adds your plugin directory to Python's import path during loading, so relative imports work as expected.
|
||||||
|
|
||||||
|
### Plugin Manifest (`plugin.info.toml`)
|
||||||
|
|
||||||
|
Every plugin repository must contain a `plugin.info.toml` file at its root:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[plugin]
|
||||||
|
name = "My Awesome Plugin"
|
||||||
|
version = "1.0.0"
|
||||||
|
description = "Adds support for Example Anime Site"
|
||||||
|
author = "Your Name"
|
||||||
|
homepage = "https://github.com/yourname/viu-example-plugin"
|
||||||
|
requires_python = ">=3.11"
|
||||||
|
|
||||||
|
[components]
|
||||||
|
# Specify which components your plugin provides
|
||||||
|
provider = "example_provider:ExampleProvider" # format: module:class
|
||||||
|
# player = "my_player:MyPlayer" # (if providing a player)
|
||||||
|
# selector = "my_selector:MySelector" # (if providing a selector)
|
||||||
|
# command = "my_command:my_command_func" # (if providing a command)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Provider Plugins
|
||||||
|
|
||||||
|
Provider plugins add support for new anime streaming websites.
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
Your provider class must inherit from `BaseAnimeProvider` and implement:
|
||||||
|
|
||||||
|
- `search(query: str) -> SearchResults`
|
||||||
|
- `get(anime_id: str) -> Anime`
|
||||||
|
- `episode_streams(anime_id: str, episode: str) -> List[Server]`
|
||||||
|
|
||||||
|
### Example Provider Plugin
|
||||||
|
|
||||||
|
**plugin.info.toml:**
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[plugin]
|
||||||
|
name = "Example Anime Provider"
|
||||||
|
version = "1.0.0"
|
||||||
|
description = "Adds support for example.anime.site"
|
||||||
|
|
||||||
|
[components]
|
||||||
|
provider = "example_provider:ExampleProvider"
|
||||||
|
```
|
||||||
|
|
||||||
|
**example_provider.py:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
from typing import List
|
||||||
|
from httpx import Client
|
||||||
|
|
||||||
|
# These imports work because viu adds the plugin path to sys.path
|
||||||
|
from viu_media.libs.provider.anime.base import BaseAnimeProvider
|
||||||
|
from viu_media.libs.provider.anime.types import SearchResults, Anime, Server
|
||||||
|
|
||||||
|
class ExampleProvider(BaseAnimeProvider):
|
||||||
|
HEADERS = {
|
||||||
|
"Referer": "https://example.anime.site/",
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, client: Client, **config):
|
||||||
|
self.client = client
|
||||||
|
# Access plugin configuration
|
||||||
|
self.timeout = config.get("timeout", 30)
|
||||||
|
self.preferred_quality = config.get("preferred_quality", "720p")
|
||||||
|
|
||||||
|
def search(self, query: str) -> SearchResults:
|
||||||
|
# Implement search logic
|
||||||
|
response = self.client.get(f"https://example.anime.site/search?q={query}")
|
||||||
|
# Parse response and return SearchResults
|
||||||
|
return SearchResults(...)
|
||||||
|
|
||||||
|
def get(self, anime_id: str) -> Anime:
|
||||||
|
# Implement anime details fetching
|
||||||
|
response = self.client.get(f"https://example.anime.site/anime/{anime_id}")
|
||||||
|
# Parse response and return Anime
|
||||||
|
return Anime(...)
|
||||||
|
|
||||||
|
def episode_streams(self, anime_id: str, episode: str) -> List[Server]:
|
||||||
|
# Implement stream URL extraction
|
||||||
|
response = self.client.get(f"https://example.anime.site/watch/{anime_id}/{episode}")
|
||||||
|
# Parse response and return list of Server objects
|
||||||
|
return [Server(...)]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Player Plugins
|
||||||
|
|
||||||
|
Player plugins add support for new media players.
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
Your player class must inherit from `BasePlayer` and implement:
|
||||||
|
|
||||||
|
- `play(media_url: str, **kwargs) -> None`
|
||||||
|
|
||||||
|
### Example Player Plugin
|
||||||
|
|
||||||
|
**plugin.info.toml:**
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[plugin]
|
||||||
|
name = "Custom Player"
|
||||||
|
version = "1.0.0"
|
||||||
|
description = "Adds support for my custom media player"
|
||||||
|
|
||||||
|
[components]
|
||||||
|
player = "custom_player:CustomPlayer"
|
||||||
|
```
|
||||||
|
|
||||||
|
**custom_player.py:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
import subprocess
|
||||||
|
from viu_media.libs.player.base import BasePlayer
|
||||||
|
|
||||||
|
class CustomPlayer(BasePlayer):
|
||||||
|
def __init__(self, **config):
|
||||||
|
self.executable = config.get("executable", "my-player")
|
||||||
|
self.extra_args = config.get("extra_args", [])
|
||||||
|
|
||||||
|
def play(self, media_url: str, **kwargs) -> None:
|
||||||
|
cmd = [self.executable] + self.extra_args + [media_url]
|
||||||
|
subprocess.run(cmd)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Selector Plugins
|
||||||
|
|
||||||
|
Selector plugins add support for new interactive selection tools.
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
Your selector class must inherit from `BaseSelector` and implement:
|
||||||
|
|
||||||
|
- `choose(choices: List[str], **kwargs) -> str`
|
||||||
|
- `confirm(message: str, **kwargs) -> bool`
|
||||||
|
- `ask(message: str, **kwargs) -> str`
|
||||||
|
|
||||||
|
## Command Plugins
|
||||||
|
|
||||||
|
Command plugins add new CLI commands to viu.
|
||||||
|
|
||||||
|
### Example Command Plugin
|
||||||
|
|
||||||
|
**plugin.info.toml:**
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[plugin]
|
||||||
|
name = "My Command"
|
||||||
|
version = "1.0.0"
|
||||||
|
description = "Adds a custom command to viu"
|
||||||
|
|
||||||
|
[components]
|
||||||
|
command = "my_command:my_command"
|
||||||
|
```
|
||||||
|
|
||||||
|
**my_command.py:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
import click
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
@click.argument("arg1")
|
||||||
|
def my_command(arg1: str):
|
||||||
|
"""My custom command description."""
|
||||||
|
click.echo(f"Hello from plugin command with arg: {arg1}")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Plugin Configuration
|
||||||
|
|
||||||
|
Plugins can include a default configuration file (`config.toml`) in their repository root. When a plugin is installed, this default configuration is automatically copied to the user's `~/.config/viu/plugins.config.toml` file.
|
||||||
|
|
||||||
|
**Example `config.toml` in plugin repository:**
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# Default configuration for My Plugin
|
||||||
|
[my-plugin-name]
|
||||||
|
timeout = 30
|
||||||
|
preferred_quality = "720p"
|
||||||
|
custom_option = "default_value"
|
||||||
|
```
|
||||||
|
|
||||||
|
**After installation, users can customize by editing `~/.config/viu/plugins.config.toml`:**
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[my-plugin-name]
|
||||||
|
timeout = 60 # Customized value
|
||||||
|
preferred_quality = "1080p" # Customized value
|
||||||
|
custom_option = "my_value" # Customized value
|
||||||
|
```
|
||||||
|
|
||||||
|
Access this configuration in your plugin constructor via the `**config` parameter.
|
||||||
|
|
||||||
|
## Installation and Usage
|
||||||
|
|
||||||
|
### For Plugin Developers
|
||||||
|
|
||||||
|
1. Create your plugin repository following the structure above
|
||||||
|
2. Test your plugin locally
|
||||||
|
3. Publish your repository on GitHub/GitLab
|
||||||
|
4. Share the installation command with users
|
||||||
|
|
||||||
|
### For Users
|
||||||
|
|
||||||
|
Install a plugin:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
viu plugin add --type provider myplugin github:user/viu-myplugin
|
||||||
|
```
|
||||||
|
|
||||||
|
Configure the plugin by editing `~/.config/viu/plugins.config.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[myplugin]
|
||||||
|
option1 = "value1"
|
||||||
|
option2 = "value2"
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the plugin:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
viu --provider myplugin search "anime name"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
If your plugin requires additional Python packages, include a `requirements.txt` file in your repository root. Users will need to install these manually:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Error Handling**: Implement proper error handling and logging
|
||||||
|
2. **Configuration**: Make your plugin configurable through the config system
|
||||||
|
3. **Documentation**: Include a README.md with usage instructions
|
||||||
|
4. **Testing**: Test your plugin thoroughly before publishing
|
||||||
|
5. **Versioning**: Use semantic versioning for your plugin releases
|
||||||
|
6. **Compatibility**: Specify minimum Python version requirements
|
||||||
|
|
||||||
|
## Plugin Management Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install a plugin
|
||||||
|
viu plugin add --type provider myplugin github:user/viu-myplugin
|
||||||
|
|
||||||
|
# List installed plugins
|
||||||
|
viu plugin list
|
||||||
|
viu plugin list --type provider
|
||||||
|
|
||||||
|
# Update a plugin
|
||||||
|
viu plugin update --type provider myplugin
|
||||||
|
|
||||||
|
# Remove a plugin
|
||||||
|
viu plugin remove --type provider myplugin
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example Plugins
|
||||||
|
|
||||||
|
Check out these example plugin repositories:
|
||||||
|
|
||||||
|
- [Example Provider Plugin](https://github.com/example/viu-example-provider)
|
||||||
|
- [Example Player Plugin](https://github.com/example/viu-example-player)
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For plugin development support:
|
||||||
|
|
||||||
|
- Open an issue in the main viu repository
|
||||||
|
- Join the Discord server: https://discord.gg/C4rhMA4mmK
|
||||||
59
README.md
59
README.md
@@ -8,8 +8,8 @@
|
|||||||
</p>
|
</p>
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
[](https://pypi.org/project/viu_cli/)
|
[](https://pypi.org/project/viu-media/)
|
||||||
[](https://pypi.org/project/viu_cli/)
|
[](https://pypi.org/project/viu-media/)
|
||||||
[](https://github.com/Benexl/Viu/actions)
|
[](https://github.com/Benexl/Viu/actions)
|
||||||
[](https://discord.gg/HBEmAwvbHV)
|
[](https://discord.gg/HBEmAwvbHV)
|
||||||
[](https://github.com/Benexl/Viu/issues)
|
[](https://github.com/Benexl/Viu/issues)
|
||||||
@@ -23,47 +23,6 @@
|
|||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>
|
|
||||||
<b>Screenshots</b>
|
|
||||||
</summary>
|
|
||||||
<b>Fzf:</b>
|
|
||||||
<img width="1346" height="710" alt="250815_13h29m15s_screenshot" src="https://github.com/user-attachments/assets/d8fb8473-a0fe-47b1-b112-5cd8bec51937" />
|
|
||||||
<img width="1346" height="710" alt="250815_13h29m43s_screenshot" src="https://github.com/user-attachments/assets/16a2555d-f81e-4044-9e65-e61205dfe899" />
|
|
||||||
<img width="1346" height="710" alt="250815_13h30m09s_screenshot" src="https://github.com/user-attachments/assets/f521670a-c04f-4f5e-a62a-6c849fbf49bd" />
|
|
||||||
<img width="1346" height="710" alt="250815_13h30m33s_screenshot" src="https://github.com/user-attachments/assets/27fd2ef9-ec1f-4677-b816-038eaaca1391" />
|
|
||||||
<img width="1346" height="710" alt="250815_13h31m07s_screenshot" src="https://github.com/user-attachments/assets/6a64aa99-507e-449a-9e4a-9daa4fe496a3" />
|
|
||||||
<img width="1346" height="710" alt="250815_13h31m44s_screenshot" src="https://github.com/user-attachments/assets/a2896d1f-0e23-4ff3-b0c6-121d21a9f99a" />
|
|
||||||
|
|
||||||
<b>Rofi:</b>
|
|
||||||
<img width="1366" height="729" alt="250815_13h23m12s_screenshot" src="https://github.com/user-attachments/assets/6d18d950-11e5-41fc-a7fe-1f9eaa481e46" />
|
|
||||||
<img width="1366" height="765" alt="250815_13h24m09s_screenshot" src="https://github.com/user-attachments/assets/af852fee-17bf-4f24-ada9-7cf0e6f3451c" />
|
|
||||||
<img width="1366" height="768" alt="250815_13h24m57s_screenshot" src="https://github.com/user-attachments/assets/d3b4e2ab-10bd-40ae-88ed-0720b57957c1" />
|
|
||||||
<img width="1366" height="735" alt="250815_13h26m47s_screenshot" src="https://github.com/user-attachments/assets/64682b09-c88e-4d4c-ae26-a3aa34dd08a1" />
|
|
||||||
<img width="1366" height="768" alt="250815_13h28m05s_screenshot" src="https://github.com/user-attachments/assets/d6cd6931-0113-462c-86bb-abe6f3e12d68" />
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>
|
|
||||||
<b>Riced Preview Examples</b>
|
|
||||||
</summary>
|
|
||||||
|
|
||||||
**Anilist Results Menu (FZF):**
|
|
||||||

|
|
||||||
|
|
||||||
**Episodes Menu with Preview (FZF):**
|
|
||||||

|
|
||||||
|
|
||||||
**No Image Preview Mode:**
|
|
||||||

|
|
||||||
|
|
||||||
**Desktop Notifications + Episodes Menu:**
|
|
||||||

|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## Core Features
|
## Core Features
|
||||||
|
|
||||||
@@ -98,13 +57,13 @@ The best way to install Viu is with [**uv**](https://github.com/astral-sh/uv), a
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install with all optional features for the full experience
|
# Install with all optional features for the full experience
|
||||||
uv tool install "viu_cli[standard]"
|
uv tool install "viu-media[standard]"
|
||||||
|
|
||||||
# Or, pick and choose the extras you need:
|
# Or, pick and choose the extras you need:
|
||||||
uv tool install viu_cli # Core functionality only
|
uv tool install viu-media # Core functionality only
|
||||||
uv tool install "viu_cli[download]" # For advanced downloading with yt-dlp
|
uv tool install "viu-media[download]" # For advanced downloading with yt-dlp
|
||||||
uv tool install "viu_cli[discord]" # For Discord Rich Presence
|
uv tool install "viu-media[discord]" # For Discord Rich Presence
|
||||||
uv tool install "viu_cli[notifications]" # For desktop notifications
|
uv tool install "viu-media[notifications]" # For desktop notifications
|
||||||
```
|
```
|
||||||
|
|
||||||
### Other Installation Methods
|
### Other Installation Methods
|
||||||
@@ -129,12 +88,12 @@ uv tool install "viu_cli[notifications]" # For desktop notifications
|
|||||||
|
|
||||||
#### Using pipx (for isolated environments)
|
#### Using pipx (for isolated environments)
|
||||||
```bash
|
```bash
|
||||||
pipx install "viu_cli[standard]"
|
pipx install "viu-media[standard]"
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Using pip
|
#### Using pip
|
||||||
```bash
|
```bash
|
||||||
pip install "viu_cli[standard]"
|
pip install "viu-media[standard]"
|
||||||
```
|
```
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
|||||||
15
examples/plugins/player/config.toml
Normal file
15
examples/plugins/player/config.toml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
[multi-file-provider]
|
||||||
|
# Base URL for the anime site
|
||||||
|
base_url = "https://multifile.example.site"
|
||||||
|
|
||||||
|
# Request timeout in seconds
|
||||||
|
timeout = 30
|
||||||
|
|
||||||
|
# Preferred video quality
|
||||||
|
preferred_quality = "720p"
|
||||||
|
|
||||||
|
# Maximum number of search results
|
||||||
|
max_results = 25
|
||||||
|
|
||||||
|
# Enable debug logging
|
||||||
|
debug = false
|
||||||
169
examples/plugins/player/player.py
Normal file
169
examples/plugins/player/player.py
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
"""
|
||||||
|
VLC player integration for Viu.
|
||||||
|
|
||||||
|
This module provides the VlcPlayer class, which implements the BasePlayer interface for the VLC media player.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from viu_media.core.config import VlcConfig
|
||||||
|
from viu_media.core.exceptions import ViuError
|
||||||
|
from viu_media.core.patterns import TORRENT_REGEX, YOUTUBE_REGEX
|
||||||
|
from viu_media.core.utils import detect
|
||||||
|
from viu_media.libs.player.base import BasePlayer
|
||||||
|
from viu_media.libs.player.params import PlayerParams
|
||||||
|
from viu_media.libs.player.types import PlayerResult
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class VlcPlayer(BasePlayer):
|
||||||
|
"""
|
||||||
|
VLC player implementation for Viu.
|
||||||
|
|
||||||
|
Provides playback functionality using the VLC media player, supporting desktop, mobile, and torrent scenarios.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, config: VlcConfig):
|
||||||
|
"""
|
||||||
|
Initialize the VlcPlayer with the given VLC configuration.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config: VlcConfig object containing VLC-specific settings.
|
||||||
|
"""
|
||||||
|
self.config = config
|
||||||
|
self.executable = shutil.which("vlc")
|
||||||
|
|
||||||
|
def play(self, params: PlayerParams) -> PlayerResult:
|
||||||
|
"""
|
||||||
|
Play the given media using VLC, handling desktop, mobile, and torrent scenarios.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
params: PlayerParams object containing playback parameters.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PlayerResult: Information about the playback session.
|
||||||
|
"""
|
||||||
|
if not self.executable:
|
||||||
|
raise ViuError("VLC executable not found in PATH.")
|
||||||
|
|
||||||
|
if TORRENT_REGEX.match(params.url) and detect.is_running_in_termux():
|
||||||
|
return self._play_on_mobile(params)
|
||||||
|
else:
|
||||||
|
return self._play_on_desktop(params)
|
||||||
|
|
||||||
|
def play_with_ipc(self, params: PlayerParams, socket_path: str) -> subprocess.Popen:
|
||||||
|
"""
|
||||||
|
Not implemented for VLC player.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError("play_with_ipc is not implemented for VLC player.")
|
||||||
|
|
||||||
|
def _play_on_mobile(self, params: PlayerParams) -> PlayerResult:
|
||||||
|
"""
|
||||||
|
Play media on a mobile device using Android intents.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
params: PlayerParams object containing playback parameters.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PlayerResult: Information about the playback session.
|
||||||
|
"""
|
||||||
|
if YOUTUBE_REGEX.match(params.url):
|
||||||
|
args = [
|
||||||
|
"nohup",
|
||||||
|
"am",
|
||||||
|
"start",
|
||||||
|
"--user",
|
||||||
|
"0",
|
||||||
|
"-a",
|
||||||
|
"android.intent.action.VIEW",
|
||||||
|
"-d",
|
||||||
|
params.url,
|
||||||
|
"-n",
|
||||||
|
"com.google.android.youtube/.UrlActivity",
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
args = [
|
||||||
|
"nohup",
|
||||||
|
"am",
|
||||||
|
"start",
|
||||||
|
"--user",
|
||||||
|
"0",
|
||||||
|
"-a",
|
||||||
|
"android.intent.action.VIEW",
|
||||||
|
"-d",
|
||||||
|
params.url,
|
||||||
|
"-n",
|
||||||
|
"org.videolan.vlc/org.videolan.vlc.gui.video.VideoPlayerActivity",
|
||||||
|
"-e",
|
||||||
|
"title",
|
||||||
|
params.title,
|
||||||
|
]
|
||||||
|
|
||||||
|
subprocess.run(args)
|
||||||
|
|
||||||
|
return PlayerResult(episode=params.episode)
|
||||||
|
|
||||||
|
def _play_on_desktop(self, params: PlayerParams) -> PlayerResult:
|
||||||
|
"""
|
||||||
|
Play media on a desktop environment using VLC.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
params: PlayerParams object containing playback parameters.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PlayerResult: Information about the playback session.
|
||||||
|
"""
|
||||||
|
if TORRENT_REGEX.search(params.url):
|
||||||
|
return self._stream_on_desktop_with_webtorrent_cli(params)
|
||||||
|
|
||||||
|
args = [self.executable, params.url]
|
||||||
|
if params.subtitles:
|
||||||
|
for sub in params.subtitles:
|
||||||
|
args.extend(["--sub-file", sub])
|
||||||
|
break
|
||||||
|
if params.title:
|
||||||
|
args.extend(["--video-title", params.title])
|
||||||
|
|
||||||
|
if self.config.args:
|
||||||
|
args.extend(self.config.args.split(","))
|
||||||
|
|
||||||
|
subprocess.run(args, encoding="utf-8")
|
||||||
|
return PlayerResult(episode=params.episode)
|
||||||
|
|
||||||
|
def _stream_on_desktop_with_webtorrent_cli(
|
||||||
|
self, params: PlayerParams
|
||||||
|
) -> PlayerResult:
|
||||||
|
"""
|
||||||
|
Stream torrent media using the webtorrent CLI and VLC.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
params: PlayerParams object containing playback parameters.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PlayerResult: Information about the playback session.
|
||||||
|
"""
|
||||||
|
WEBTORRENT_CLI = shutil.which("webtorrent")
|
||||||
|
if not WEBTORRENT_CLI:
|
||||||
|
raise ViuError("Please Install webtorrent cli inorder to stream torrents")
|
||||||
|
|
||||||
|
args = [WEBTORRENT_CLI, params.url, "--vlc"]
|
||||||
|
|
||||||
|
if self.config.args:
|
||||||
|
args.append("--player-args")
|
||||||
|
args.extend(self.config.args.split(","))
|
||||||
|
|
||||||
|
subprocess.run(args)
|
||||||
|
return PlayerResult(episode=params.episode)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
from viu_media.core.constants import APP_ASCII_ART
|
||||||
|
|
||||||
|
print(APP_ASCII_ART)
|
||||||
|
url = input("Enter the url you would like to stream: ")
|
||||||
|
vlc = VlcPlayer(VlcConfig())
|
||||||
|
player_result = vlc.play(PlayerParams(url=url, title="", query="", episode=""))
|
||||||
|
print(player_result)
|
||||||
9
examples/plugins/player/plugin.info.toml
Normal file
9
examples/plugins/player/plugin.info.toml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
[plugin]
|
||||||
|
name = "Multi-File Provider Plugin"
|
||||||
|
version = "1.0.0"
|
||||||
|
description = "A demo plugin with multiple Python files"
|
||||||
|
author = "Viu Developer"
|
||||||
|
requires_python = ">=3.11"
|
||||||
|
|
||||||
|
[components]
|
||||||
|
player = "player:VlcPlayer"
|
||||||
18
examples/plugins/provider/config.toml
Normal file
18
examples/plugins/provider/config.toml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Default configuration for Example Provider Plugin
|
||||||
|
# This file is automatically copied to ~/.config/viu/plugins.config.toml during installation
|
||||||
|
|
||||||
|
[example-provider]
|
||||||
|
# Request timeout in seconds
|
||||||
|
timeout = 30
|
||||||
|
|
||||||
|
# Preferred video quality
|
||||||
|
preferred_quality = "720p"
|
||||||
|
|
||||||
|
# Maximum number of search results to return
|
||||||
|
max_results = 20
|
||||||
|
|
||||||
|
# Custom headers (optional)
|
||||||
|
# custom_header = "value"
|
||||||
|
|
||||||
|
# Enable debug logging for this plugin
|
||||||
|
# debug = false
|
||||||
100
examples/plugins/provider/mappers.py
Normal file
100
examples/plugins/provider/mappers.py
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from viu_media.libs.provider.anime.types import (
|
||||||
|
Anime,
|
||||||
|
AnimeEpisodeInfo,
|
||||||
|
AnimeEpisodes,
|
||||||
|
EpisodeStream,
|
||||||
|
MediaTranslationType,
|
||||||
|
PageInfo,
|
||||||
|
SearchResult,
|
||||||
|
SearchResults,
|
||||||
|
Server,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .types import (
|
||||||
|
AnimePaheAnimePage,
|
||||||
|
AnimePaheSearchPage,
|
||||||
|
)
|
||||||
|
|
||||||
|
translation_type_map = {
|
||||||
|
"sub": MediaTranslationType.SUB,
|
||||||
|
"dub": MediaTranslationType.DUB,
|
||||||
|
"raw": MediaTranslationType.RAW,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def map_to_search_results(data: AnimePaheSearchPage) -> SearchResults:
|
||||||
|
results = []
|
||||||
|
for result in data["data"]:
|
||||||
|
results.append(
|
||||||
|
SearchResult(
|
||||||
|
id=result["session"],
|
||||||
|
title=result["title"],
|
||||||
|
episodes=AnimeEpisodes(
|
||||||
|
sub=list(map(str, range(1, result["episodes"] + 1))),
|
||||||
|
dub=list(map(str, range(1, result["episodes"] + 1))),
|
||||||
|
raw=list(map(str, range(1, result["episodes"] + 1))),
|
||||||
|
),
|
||||||
|
media_type=result["type"],
|
||||||
|
score=result["score"],
|
||||||
|
status=result["status"],
|
||||||
|
season=result["season"],
|
||||||
|
poster=result["poster"],
|
||||||
|
year=str(result["year"]),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return SearchResults(
|
||||||
|
page_info=PageInfo(
|
||||||
|
total=data["total"],
|
||||||
|
per_page=data["per_page"],
|
||||||
|
current_page=data["current_page"],
|
||||||
|
),
|
||||||
|
results=results,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def map_to_anime_result(
|
||||||
|
search_result: SearchResult, anime: AnimePaheAnimePage
|
||||||
|
) -> Anime:
|
||||||
|
episodes_info = []
|
||||||
|
episodes = []
|
||||||
|
anime["data"] = sorted(anime["data"], key=lambda k: float(k["episode"]))
|
||||||
|
for ep_info in anime["data"]:
|
||||||
|
episodes.append(str(ep_info["episode"]))
|
||||||
|
episodes_info.append(
|
||||||
|
AnimeEpisodeInfo(
|
||||||
|
id=str(ep_info["id"]),
|
||||||
|
session_id=ep_info["session"],
|
||||||
|
episode=str(ep_info["episode"]),
|
||||||
|
title=ep_info["title"],
|
||||||
|
poster=ep_info["snapshot"],
|
||||||
|
duration=str(ep_info["duration"]),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return Anime(
|
||||||
|
id=search_result.id,
|
||||||
|
title=search_result.title,
|
||||||
|
episodes=AnimeEpisodes(
|
||||||
|
sub=episodes,
|
||||||
|
dub=episodes,
|
||||||
|
),
|
||||||
|
year=str(search_result.year),
|
||||||
|
poster=search_result.poster,
|
||||||
|
episodes_info=episodes_info,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def map_to_server(
|
||||||
|
episode: AnimeEpisodeInfo, translation_type: Any, quality: Any, stream_link: Any
|
||||||
|
) -> Server:
|
||||||
|
links = [
|
||||||
|
EpisodeStream(
|
||||||
|
link=stream_link,
|
||||||
|
quality=quality,
|
||||||
|
translation_type=translation_type_map[translation_type],
|
||||||
|
)
|
||||||
|
]
|
||||||
|
return Server(name="kwik", links=links, episode_title=episode.title)
|
||||||
10
examples/plugins/provider/plugin.info.toml
Normal file
10
examples/plugins/provider/plugin.info.toml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[plugin]
|
||||||
|
name = "Example Provider Plugin"
|
||||||
|
version = "1.0.0"
|
||||||
|
description = "A demo provider plugin for testing the viu plugin system"
|
||||||
|
author = "Viu Developer"
|
||||||
|
homepage = "https://github.com/example/viu-example-plugin"
|
||||||
|
requires_python = ">=3.11"
|
||||||
|
|
||||||
|
[components]
|
||||||
|
provider = "example_provider:ExampleProvider"
|
||||||
207
examples/plugins/provider/provider.py
Normal file
207
examples/plugins/provider/provider.py
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
import logging
|
||||||
|
from functools import lru_cache
|
||||||
|
from typing import Iterator, Optional
|
||||||
|
|
||||||
|
from viu_media.libs.provider.anime.base import BaseAnimeProvider
|
||||||
|
from viu_media.libs.provider.anime.params import (
|
||||||
|
AnimeParams,
|
||||||
|
EpisodeStreamsParams,
|
||||||
|
SearchParams,
|
||||||
|
)
|
||||||
|
from viu_media.libs.provider.anime.types import (
|
||||||
|
Anime,
|
||||||
|
AnimeEpisodeInfo,
|
||||||
|
SearchResult,
|
||||||
|
SearchResults,
|
||||||
|
Server,
|
||||||
|
)
|
||||||
|
from viu_media.libs.provider.anime.utils.debug import debug_provider
|
||||||
|
|
||||||
|
from .constants import (
|
||||||
|
ANIMEPAHE_BASE,
|
||||||
|
ANIMEPAHE_ENDPOINT,
|
||||||
|
JUICY_STREAM_REGEX,
|
||||||
|
REQUEST_HEADERS,
|
||||||
|
SERVER_HEADERS,
|
||||||
|
)
|
||||||
|
from .extractor import process_animepahe_embed_page
|
||||||
|
from .mappers import map_to_anime_result, map_to_search_results, map_to_server
|
||||||
|
from .types import AnimePaheAnimePage, AnimePaheSearchPage
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AnimePahe(BaseAnimeProvider):
|
||||||
|
HEADERS = REQUEST_HEADERS
|
||||||
|
|
||||||
|
@debug_provider
|
||||||
|
def search(self, params: SearchParams) -> SearchResults | None:
|
||||||
|
return self._search(params)
|
||||||
|
|
||||||
|
@lru_cache()
|
||||||
|
def _search(self, params: SearchParams) -> SearchResults | None:
|
||||||
|
url_params = {"m": "search", "q": params.query}
|
||||||
|
response = self.client.get(ANIMEPAHE_ENDPOINT, params=url_params)
|
||||||
|
response.raise_for_status()
|
||||||
|
data: AnimePaheSearchPage = response.json()
|
||||||
|
if not data.get("data"):
|
||||||
|
return
|
||||||
|
return map_to_search_results(data)
|
||||||
|
|
||||||
|
@debug_provider
|
||||||
|
def get(self, params: AnimeParams) -> Anime | None:
|
||||||
|
return self._get_anime(params)
|
||||||
|
|
||||||
|
@lru_cache()
|
||||||
|
def _get_anime(self, params: AnimeParams) -> Anime | None:
|
||||||
|
page = 1
|
||||||
|
standardized_episode_number = 0
|
||||||
|
|
||||||
|
search_result = self._get_search_result(params)
|
||||||
|
if not search_result:
|
||||||
|
logger.error(f"No search result found for ID {params.id}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
anime: Optional[AnimePaheAnimePage] = None
|
||||||
|
|
||||||
|
has_next_page = True
|
||||||
|
while has_next_page:
|
||||||
|
logger.debug(f"Loading page: {page}")
|
||||||
|
_anime_page = self._anime_page_loader(
|
||||||
|
m="release",
|
||||||
|
id=params.id,
|
||||||
|
sort="episode_asc",
|
||||||
|
page=page,
|
||||||
|
)
|
||||||
|
|
||||||
|
has_next_page = True if _anime_page["next_page_url"] else False
|
||||||
|
page += 1
|
||||||
|
if not anime:
|
||||||
|
anime = _anime_page
|
||||||
|
else:
|
||||||
|
anime["data"].extend(_anime_page["data"])
|
||||||
|
|
||||||
|
if anime:
|
||||||
|
for episode in anime.get("data", []):
|
||||||
|
if episode["episode"] % 1 == 0:
|
||||||
|
standardized_episode_number += 1
|
||||||
|
episode.update({"episode": standardized_episode_number})
|
||||||
|
else:
|
||||||
|
standardized_episode_number += episode["episode"] % 1
|
||||||
|
episode.update({"episode": standardized_episode_number})
|
||||||
|
standardized_episode_number = int(standardized_episode_number)
|
||||||
|
|
||||||
|
return map_to_anime_result(search_result, anime)
|
||||||
|
|
||||||
|
@lru_cache()
|
||||||
|
def _get_search_result(self, params: AnimeParams) -> Optional[SearchResult]:
|
||||||
|
search_results = self._search(SearchParams(query=params.query))
|
||||||
|
if not search_results or not search_results.results:
|
||||||
|
logger.error(f"No search results found for ID {params.id}")
|
||||||
|
return None
|
||||||
|
for search_result in search_results.results:
|
||||||
|
if search_result.id == params.id:
|
||||||
|
return search_result
|
||||||
|
|
||||||
|
@lru_cache()
|
||||||
|
def _anime_page_loader(self, m, id, sort, page) -> AnimePaheAnimePage:
|
||||||
|
url_params = {
|
||||||
|
"m": m,
|
||||||
|
"id": id,
|
||||||
|
"sort": sort,
|
||||||
|
"page": page,
|
||||||
|
}
|
||||||
|
response = self.client.get(ANIMEPAHE_ENDPOINT, params=url_params)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
@debug_provider
|
||||||
|
def episode_streams(self, params: EpisodeStreamsParams) -> Iterator[Server] | None:
|
||||||
|
from viu_media.libs.provider.scraping.html_parser import (
|
||||||
|
extract_attributes,
|
||||||
|
get_element_by_id,
|
||||||
|
get_elements_html_by_class,
|
||||||
|
)
|
||||||
|
|
||||||
|
episode = self._get_episode_info(params)
|
||||||
|
if not episode:
|
||||||
|
logger.error(
|
||||||
|
f"Episode {params.episode} doesn't exist for anime {params.anime_id}"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
url = f"{ANIMEPAHE_BASE}/play/{params.anime_id}/{episode.session_id}"
|
||||||
|
response = self.client.get(url, follow_redirects=True)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
c = get_element_by_id("resolutionMenu", response.text)
|
||||||
|
if not c:
|
||||||
|
logger.error("Resolution menu not found in the response")
|
||||||
|
return
|
||||||
|
resolutionMenuItems = get_elements_html_by_class("dropdown-item", c)
|
||||||
|
res_dicts = [extract_attributes(item) for item in resolutionMenuItems]
|
||||||
|
quality = None
|
||||||
|
translation_type = None
|
||||||
|
stream_link = None
|
||||||
|
|
||||||
|
# TODO: better document the scraping process
|
||||||
|
for res_dict in res_dicts:
|
||||||
|
# the actual attributes are data attributes in the original html 'prefixed with data-'
|
||||||
|
embed_url = res_dict["src"]
|
||||||
|
data_audio = "dub" if res_dict["audio"] == "eng" else "sub"
|
||||||
|
|
||||||
|
if data_audio != params.translation_type:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not embed_url:
|
||||||
|
logger.warning("embed url not found please report to the developers")
|
||||||
|
continue
|
||||||
|
|
||||||
|
embed_response = self.client.get(
|
||||||
|
embed_url,
|
||||||
|
headers={
|
||||||
|
"User-Agent": self.client.headers["User-Agent"],
|
||||||
|
**SERVER_HEADERS,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
embed_response.raise_for_status()
|
||||||
|
embed_page = embed_response.text
|
||||||
|
|
||||||
|
decoded_js = process_animepahe_embed_page(embed_page)
|
||||||
|
if not decoded_js:
|
||||||
|
logger.error("failed to decode embed page")
|
||||||
|
continue
|
||||||
|
juicy_stream = JUICY_STREAM_REGEX.search(decoded_js)
|
||||||
|
if not juicy_stream:
|
||||||
|
logger.error("failed to find juicy stream")
|
||||||
|
continue
|
||||||
|
juicy_stream = juicy_stream.group(1)
|
||||||
|
quality = res_dict["resolution"]
|
||||||
|
translation_type = data_audio
|
||||||
|
stream_link = juicy_stream
|
||||||
|
|
||||||
|
if translation_type and quality and stream_link:
|
||||||
|
yield map_to_server(episode, translation_type, quality, stream_link)
|
||||||
|
|
||||||
|
@lru_cache()
|
||||||
|
def _get_episode_info(
|
||||||
|
self, params: EpisodeStreamsParams
|
||||||
|
) -> Optional[AnimeEpisodeInfo]:
|
||||||
|
anime_info = self._get_anime(
|
||||||
|
AnimeParams(id=params.anime_id, query=params.query)
|
||||||
|
)
|
||||||
|
if not anime_info:
|
||||||
|
logger.error(f"No anime info for {params.anime_id}")
|
||||||
|
return
|
||||||
|
if not anime_info.episodes_info:
|
||||||
|
logger.error(f"No episodes info for {params.anime_id}")
|
||||||
|
return
|
||||||
|
for episode in anime_info.episodes_info:
|
||||||
|
if episode.episode == params.episode:
|
||||||
|
return episode
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
from viu_media.libs.provider.anime.utils.debug import test_anime_provider
|
||||||
|
|
||||||
|
test_anime_provider(AnimePahe)
|
||||||
@@ -67,8 +67,6 @@
|
|||||||
# Needs to be adapted for the nix derivation build
|
# Needs to be adapted for the nix derivation build
|
||||||
doCheck = false;
|
doCheck = false;
|
||||||
|
|
||||||
pythonImportsCheck = [ "viu" ];
|
|
||||||
|
|
||||||
meta = {
|
meta = {
|
||||||
description = "Your browser anime experience from the terminal";
|
description = "Your browser anime experience from the terminal";
|
||||||
homepage = "https://github.com/Benexl/Viu";
|
homepage = "https://github.com/Benexl/Viu";
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "viu_cli"
|
name = "viu-media"
|
||||||
version = "3.2.6"
|
version = "3.2.7"
|
||||||
description = "A browser anime site experience from the terminal"
|
description = "A browser anime site experience from the terminal"
|
||||||
license = "UNLICENSE"
|
license = "UNLICENSE"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"click>=8.1.7",
|
"click>=8.1.7",
|
||||||
"httpx>=0.28.1",
|
"httpx>=0.28.1",
|
||||||
"inquirerpy>=0.3.4",
|
"inquirerpy>=0.3.4",
|
||||||
"pydantic>=2.11.7",
|
"pydantic>=2.11.7",
|
||||||
"rich>=13.9.2",
|
"rich>=13.9.2",
|
||||||
|
"tomli-w>=1.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
viu = 'viu_cli:Cli'
|
viu = 'viu_media:Cli'
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
standard = [
|
standard = [
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"venvPath": ".",
|
"venvPath": ".",
|
||||||
"venv": ".venv",
|
"venv": ".venv",
|
||||||
"pythonVersion": "3.10"
|
"pythonVersion": "3.11"
|
||||||
}
|
}
|
||||||
|
|||||||
2
tox.ini
2
tox.ini
@@ -1,7 +1,7 @@
|
|||||||
[tox]
|
[tox]
|
||||||
requires =
|
requires =
|
||||||
tox>=4
|
tox>=4
|
||||||
env_list = lint, pyright, py{310,311}
|
env_list = lint, pyright, py{311,312}
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
description = run unit tests
|
description = run unit tests
|
||||||
|
|||||||
2
fa → viu
2
fa → viu
@@ -3,4 +3,4 @@ provider_type=$1
|
|||||||
provider_name=$2
|
provider_name=$2
|
||||||
[ -z "$provider_type" ] && echo "Please specify provider type" && exit
|
[ -z "$provider_type" ] && echo "Please specify provider type" && exit
|
||||||
[ -z "$provider_name" ] && echo "Please specify provider type" && exit
|
[ -z "$provider_name" ] && echo "Please specify provider type" && exit
|
||||||
uv run python -m viu_cli.libs.provider.${provider_type}.${provider_name}.provider
|
uv run python -m viu_media.libs.provider.${provider_type}.${provider_name}.provider
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
"""Update command for Viu CLI."""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
import click
|
|
||||||
from rich import print
|
|
||||||
from rich.console import Console
|
|
||||||
from rich.markdown import Markdown
|
|
||||||
|
|
||||||
from ..utils.update import check_for_updates, update_app
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from ...core.config import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
@click.command(
|
|
||||||
help="Update Viu to the latest version",
|
|
||||||
short_help="Update Viu",
|
|
||||||
epilog="""
|
|
||||||
\b
|
|
||||||
\b\bExamples:
|
|
||||||
# Check for updates and update if available
|
|
||||||
viu update
|
|
||||||
\b
|
|
||||||
# Force update even if already up to date
|
|
||||||
viu update --force
|
|
||||||
\b
|
|
||||||
# Only check for updates without updating
|
|
||||||
viu update --check-only
|
|
||||||
\b
|
|
||||||
# Show release notes for the latest version
|
|
||||||
viu update --release-notes
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
@click.option(
|
|
||||||
"--force",
|
|
||||||
"-f",
|
|
||||||
is_flag=True,
|
|
||||||
help="Force update even if already up to date",
|
|
||||||
)
|
|
||||||
@click.option(
|
|
||||||
"--check-only",
|
|
||||||
"-c",
|
|
||||||
is_flag=True,
|
|
||||||
help="Only check for updates without updating",
|
|
||||||
)
|
|
||||||
@click.option(
|
|
||||||
"--release-notes",
|
|
||||||
"-r",
|
|
||||||
is_flag=True,
|
|
||||||
help="Show release notes for the latest version",
|
|
||||||
)
|
|
||||||
@click.pass_context
|
|
||||||
@click.pass_obj
|
|
||||||
def update(
|
|
||||||
config: "AppConfig",
|
|
||||||
ctx: click.Context,
|
|
||||||
force: bool,
|
|
||||||
check_only: bool,
|
|
||||||
release_notes: bool,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
Update Viu to the latest version.
|
|
||||||
|
|
||||||
This command checks for available updates and optionally updates
|
|
||||||
the application to the latest version from the configured sources
|
|
||||||
(pip, uv, pipx, git, or nix depending on installation method).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
config: The application configuration object
|
|
||||||
ctx: The click context containing CLI options
|
|
||||||
force: Whether to force update even if already up to date
|
|
||||||
check_only: Whether to only check for updates without updating
|
|
||||||
release_notes: Whether to show release notes for the latest version
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
if release_notes:
|
|
||||||
print("[cyan]Fetching latest release notes...[/]")
|
|
||||||
is_latest, release_json = check_for_updates()
|
|
||||||
|
|
||||||
if not release_json:
|
|
||||||
print(
|
|
||||||
"[yellow]Could not fetch release information. Please check your internet connection.[/]"
|
|
||||||
)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
version = release_json.get("tag_name", "unknown")
|
|
||||||
release_name = release_json.get("name", version)
|
|
||||||
release_body = release_json.get("body", "No release notes available.")
|
|
||||||
published_at = release_json.get("published_at", "unknown")
|
|
||||||
|
|
||||||
console = Console()
|
|
||||||
|
|
||||||
print(f"[bold cyan]Release: {release_name}[/]")
|
|
||||||
print(f"[dim]Version: {version}[/]")
|
|
||||||
print(f"[dim]Published: {published_at}[/]")
|
|
||||||
print()
|
|
||||||
|
|
||||||
# Display release notes as markdown if available
|
|
||||||
if release_body.strip():
|
|
||||||
markdown = Markdown(release_body)
|
|
||||||
console.print(markdown)
|
|
||||||
else:
|
|
||||||
print("[dim]No release notes available for this version.[/]")
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
elif check_only:
|
|
||||||
print("[cyan]Checking for updates...[/]")
|
|
||||||
is_latest, release_json = check_for_updates()
|
|
||||||
|
|
||||||
if not release_json:
|
|
||||||
print(
|
|
||||||
"[yellow]Could not check for updates. Please check your internet connection.[/]"
|
|
||||||
)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if is_latest:
|
|
||||||
print("[green]Viu is up to date![/]")
|
|
||||||
print(
|
|
||||||
f"[dim]Current version: {release_json.get('tag_name', 'unknown')}[/]"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
latest_version = release_json.get("tag_name", "unknown")
|
|
||||||
print(f"[yellow]Update available: {latest_version}[/]")
|
|
||||||
print("[dim]Run 'viu update' to update[/]")
|
|
||||||
sys.exit(1)
|
|
||||||
else:
|
|
||||||
print("[cyan]Checking for updates and updating if necessary...[/]")
|
|
||||||
success, release_json = update_app(force=force)
|
|
||||||
|
|
||||||
if not release_json:
|
|
||||||
print(
|
|
||||||
"[red]Could not check for updates. Please check your internet connection.[/]"
|
|
||||||
)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if success:
|
|
||||||
latest_version = release_json.get("tag_name", "unknown")
|
|
||||||
print(f"[green]Successfully updated to version {latest_version}![/]")
|
|
||||||
else:
|
|
||||||
if force:
|
|
||||||
print(
|
|
||||||
"[red]Update failed. Please check the error messages above.[/]"
|
|
||||||
)
|
|
||||||
sys.exit(1)
|
|
||||||
# If not forced and update failed, it might be because already up to date
|
|
||||||
# The update_app function already prints appropriate messages
|
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
print("\n[yellow]Update cancelled by user.[/]")
|
|
||||||
sys.exit(1)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[red]An error occurred during update: {e}[/]")
|
|
||||||
# Get trace option from parent context
|
|
||||||
trace = ctx.parent.params.get("trace", False) if ctx.parent else False
|
|
||||||
if trace:
|
|
||||||
raise
|
|
||||||
sys.exit(1)
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
from .generate import generate_config_ini_from_app_model
|
|
||||||
from .loader import ConfigLoader
|
|
||||||
|
|
||||||
__all__ = ["ConfigLoader", "generate_config_ini_from_app_model"]
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
from enum import Enum
|
|
||||||
from typing import Dict, Optional, Union
|
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, Field
|
|
||||||
|
|
||||||
from ...libs.media_api.params import MediaSearchParams, UserMediaListSearchParams
|
|
||||||
from ...libs.media_api.types import MediaItem, PageInfo
|
|
||||||
from ...libs.provider.anime.types import Anime, SearchResults, Server
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: is internal directive a good name
|
|
||||||
class InternalDirective(Enum):
|
|
||||||
MAIN = "MAIN"
|
|
||||||
|
|
||||||
BACK = "BACK"
|
|
||||||
|
|
||||||
BACKX2 = "BACKX2"
|
|
||||||
|
|
||||||
BACKX3 = "BACKX3"
|
|
||||||
|
|
||||||
BACKX4 = "BACKX4"
|
|
||||||
|
|
||||||
EXIT = "EXIT"
|
|
||||||
|
|
||||||
CONFIG_EDIT = "CONFIG_EDIT"
|
|
||||||
|
|
||||||
RELOAD = "RELOAD"
|
|
||||||
|
|
||||||
|
|
||||||
class MenuName(Enum):
|
|
||||||
MAIN = "MAIN"
|
|
||||||
AUTH = "AUTH"
|
|
||||||
EPISODES = "EPISODES"
|
|
||||||
RESULTS = "RESULTS"
|
|
||||||
SERVERS = "SERVERS"
|
|
||||||
WATCH_HISTORY = "WATCH_HISTORY"
|
|
||||||
PROVIDER_SEARCH = "PROVIDER_SEARCH"
|
|
||||||
PLAYER_CONTROLS = "PLAYER_CONTROLS"
|
|
||||||
USER_MEDIA_LIST = "USER_MEDIA_LIST"
|
|
||||||
SESSION_MANAGEMENT = "SESSION_MANAGEMENT"
|
|
||||||
MEDIA_ACTIONS = "MEDIA_ACTIONS"
|
|
||||||
DOWNLOADS = "DOWNLOADS"
|
|
||||||
DYNAMIC_SEARCH = "DYNAMIC_SEARCH"
|
|
||||||
MEDIA_REVIEW = "MEDIA_REVIEW"
|
|
||||||
MEDIA_CHARACTERS = "MEDIA_CHARACTERS"
|
|
||||||
MEDIA_AIRING_SCHEDULE = "MEDIA_AIRING_SCHEDULE"
|
|
||||||
PLAY_DOWNLOADS = "PLAY_DOWNLOADS"
|
|
||||||
DOWNLOADS_PLAYER_CONTROLS = "DOWNLOADS_PLAYER_CONTROLS"
|
|
||||||
DOWNLOAD_EPISODES = "DOWNLOAD_EPISODES"
|
|
||||||
|
|
||||||
|
|
||||||
class StateModel(BaseModel):
|
|
||||||
model_config = ConfigDict(frozen=True)
|
|
||||||
|
|
||||||
|
|
||||||
class MediaApiState(StateModel):
|
|
||||||
search_result: Optional[Dict[int, MediaItem]] = None
|
|
||||||
search_params: Optional[Union[MediaSearchParams, UserMediaListSearchParams]] = None
|
|
||||||
page_info: Optional[PageInfo] = None
|
|
||||||
media_id: Optional[int] = None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def media_item(self) -> Optional[MediaItem]:
|
|
||||||
if self.search_result and self.media_id:
|
|
||||||
return self.search_result[self.media_id]
|
|
||||||
|
|
||||||
|
|
||||||
class ProviderState(StateModel):
|
|
||||||
search_results: Optional[SearchResults] = None
|
|
||||||
anime: Optional[Anime] = None
|
|
||||||
episode: Optional[str] = None
|
|
||||||
servers: Optional[Dict[str, Server]] = None
|
|
||||||
server_name: Optional[str] = None
|
|
||||||
start_time: Optional[str] = None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def server(self) -> Optional[Server]:
|
|
||||||
if self.servers and self.server_name:
|
|
||||||
return self.servers[self.server_name]
|
|
||||||
|
|
||||||
|
|
||||||
class State(StateModel):
|
|
||||||
menu_name: MenuName
|
|
||||||
provider: ProviderState = Field(default_factory=ProviderState)
|
|
||||||
media_api: MediaApiState = Field(default_factory=MediaApiState)
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
if sys.version_info < (3, 10):
|
if sys.version_info < (3, 11):
|
||||||
raise ImportError(
|
raise ImportError(
|
||||||
"You are using an unsupported version of Python. Only Python versions 3.10 and above are supported by Viu"
|
"You are using an unsupported version of Python. Only Python versions 3.10 and above are supported by Viu"
|
||||||
) # noqa: F541
|
) # noqa: F541
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
██╗░░░██╗██╗██╗░░░██╗
|
██╗░░░██╗██╗██╗░░░██╗
|
||||||
██║░░░██║██║██║░░░██║
|
██║░░░██║██║██║░░░██║
|
||||||
╚██╗░██╔╝██║██║░░░██║
|
╚██╗░██╔╝██║██║░░░██║
|
||||||
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 276 KiB After Width: | Height: | Size: 276 KiB |
@@ -6,7 +6,7 @@ import click
|
|||||||
from click.core import ParameterSource
|
from click.core import ParameterSource
|
||||||
|
|
||||||
from ..core.config import AppConfig
|
from ..core.config import AppConfig
|
||||||
from ..core.constants import PROJECT_NAME, USER_CONFIG, __version__
|
from ..core.constants import CLI_NAME, USER_CONFIG, __version__
|
||||||
from .config import ConfigLoader
|
from .config import ConfigLoader
|
||||||
from .options import options_from_model
|
from .options import options_from_model
|
||||||
from .utils.exception import setup_exceptions_handler
|
from .utils.exception import setup_exceptions_handler
|
||||||
@@ -39,15 +39,16 @@ commands = {
|
|||||||
"worker": "worker.worker",
|
"worker": "worker.worker",
|
||||||
"queue": "queue.queue",
|
"queue": "queue.queue",
|
||||||
"completions": "completions.completions",
|
"completions": "completions.completions",
|
||||||
|
"plugin": "plugin.plugin",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@click.group(
|
@click.group(
|
||||||
cls=LazyGroup,
|
cls=LazyGroup,
|
||||||
root="viu_cli.cli.commands",
|
root="viu_media.cli.commands",
|
||||||
invoke_without_command=True,
|
invoke_without_command=True,
|
||||||
lazy_subcommands=commands,
|
lazy_subcommands=commands,
|
||||||
context_settings=dict(auto_envvar_prefix=PROJECT_NAME),
|
context_settings=dict(auto_envvar_prefix=CLI_NAME),
|
||||||
)
|
)
|
||||||
@click.version_option(__version__, "--version")
|
@click.version_option(__version__, "--version")
|
||||||
@click.option("--no-config", is_flag=True, help="Don't load the user config file.")
|
@click.option("--no-config", is_flag=True, help="Don't load the user config file.")
|
||||||
@@ -108,6 +109,49 @@ def cli(ctx: click.Context, **options: "Unpack[Options]"):
|
|||||||
else loader.load(cli_overrides)
|
else loader.load(cli_overrides)
|
||||||
)
|
)
|
||||||
ctx.obj = config
|
ctx.obj = config
|
||||||
|
|
||||||
|
if config.general.check_for_updates:
|
||||||
|
import time
|
||||||
|
|
||||||
|
from ..core.constants import APP_CACHE_DIR
|
||||||
|
|
||||||
|
last_updated_at_file = APP_CACHE_DIR / "last_update"
|
||||||
|
should_check_for_update = False
|
||||||
|
if last_updated_at_file.exists():
|
||||||
|
try:
|
||||||
|
last_updated_at_time = float(
|
||||||
|
last_updated_at_file.read_text(encoding="utf-8")
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
time.time() - last_updated_at_time
|
||||||
|
) > config.general.update_check_interval * 3600:
|
||||||
|
should_check_for_update = True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to check for update: {e}")
|
||||||
|
|
||||||
|
else:
|
||||||
|
should_check_for_update = True
|
||||||
|
if should_check_for_update:
|
||||||
|
last_updated_at_file.write_text(str(time.time()), encoding="utf-8")
|
||||||
|
from .service.feedback import FeedbackService
|
||||||
|
from .utils.update import check_for_updates, print_release_json, update_app
|
||||||
|
|
||||||
|
feedback = FeedbackService(config)
|
||||||
|
feedback.info("Checking for updates...")
|
||||||
|
is_latest, release_json = check_for_updates()
|
||||||
|
if not is_latest:
|
||||||
|
from ..libs.selectors.selector import create_selector
|
||||||
|
|
||||||
|
selector = create_selector(config)
|
||||||
|
if release_json and selector.confirm(
|
||||||
|
"Theres an update available would you like to see the release notes before deciding to update?"
|
||||||
|
):
|
||||||
|
print_release_json(release_json)
|
||||||
|
selector.ask("Enter to continue...")
|
||||||
|
if selector.confirm("Would you like to update?"):
|
||||||
|
update_app()
|
||||||
|
|
||||||
if ctx.invoked_subcommand is None:
|
if ctx.invoked_subcommand is None:
|
||||||
from .commands.anilist import cmd
|
from .commands.anilist import cmd
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ commands = {
|
|||||||
@click.group(
|
@click.group(
|
||||||
cls=LazyGroup,
|
cls=LazyGroup,
|
||||||
name="anilist",
|
name="anilist",
|
||||||
root="viu_cli.cli.commands.anilist.commands",
|
root="viu_media.cli.commands.anilist.commands",
|
||||||
invoke_without_command=True,
|
invoke_without_command=True,
|
||||||
help="A beautiful interface that gives you access to a commplete streaming experience",
|
help="A beautiful interface that gives you access to a commplete streaming experience",
|
||||||
short_help="Access all streaming options",
|
short_help="Access all streaming options",
|
||||||
@@ -45,7 +45,9 @@ def auth(config: AppConfig, status: bool, logout: bool):
|
|||||||
open_success = webbrowser.open(ANILIST_AUTH, new=2)
|
open_success = webbrowser.open(ANILIST_AUTH, new=2)
|
||||||
if open_success:
|
if open_success:
|
||||||
feedback.info("Your browser has been opened to obtain an AniList token.")
|
feedback.info("Your browser has been opened to obtain an AniList token.")
|
||||||
feedback.info(f"or you can visit the site manually [magenta][link={ANILIST_AUTH}]here[/link][/magenta].")
|
feedback.info(
|
||||||
|
f"or you can visit the site manually [magenta][link={ANILIST_AUTH}]here[/link][/magenta]."
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
feedback.warning(
|
feedback.warning(
|
||||||
f"Failed to open the browser. Please visit the site manually [magenta][link={ANILIST_AUTH}]here[/link][/magenta]."
|
f"Failed to open the browser. Please visit the site manually [magenta][link={ANILIST_AUTH}]here[/link][/magenta]."
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
from typing import TYPE_CHECKING, Dict, List
|
from typing import TYPE_CHECKING, Dict, List
|
||||||
|
|
||||||
import click
|
import click
|
||||||
from viu_cli.cli.utils.completion import anime_titles_shell_complete
|
from viu_media.cli.utils.completion import anime_titles_shell_complete
|
||||||
from viu_cli.core.config import AppConfig
|
from viu_media.core.config import AppConfig
|
||||||
from viu_cli.core.exceptions import ViuError
|
from viu_media.core.exceptions import ViuError
|
||||||
from viu_cli.libs.media_api.types import (
|
from viu_media.libs.media_api.types import (
|
||||||
MediaFormat,
|
MediaFormat,
|
||||||
MediaGenre,
|
MediaGenre,
|
||||||
MediaItem,
|
MediaItem,
|
||||||
@@ -112,15 +112,15 @@ if TYPE_CHECKING:
|
|||||||
)
|
)
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def download(config: AppConfig, **options: "Unpack[DownloadOptions]"):
|
def download(config: AppConfig, **options: "Unpack[DownloadOptions]"):
|
||||||
from viu_cli.cli.service.download.service import DownloadService
|
from viu_media.cli.service.download.service import DownloadService
|
||||||
from viu_cli.cli.service.feedback import FeedbackService
|
from viu_media.cli.service.feedback import FeedbackService
|
||||||
from viu_cli.cli.service.registry import MediaRegistryService
|
from viu_media.cli.service.registry import MediaRegistryService
|
||||||
from viu_cli.cli.service.watch_history import WatchHistoryService
|
from viu_media.cli.service.watch_history import WatchHistoryService
|
||||||
from viu_cli.cli.utils.parser import parse_episode_range
|
from viu_media.cli.utils.parser import parse_episode_range
|
||||||
from viu_cli.libs.media_api.api import create_api_client
|
from viu_media.libs.media_api.api import create_api_client
|
||||||
from viu_cli.libs.media_api.params import MediaSearchParams
|
from viu_media.libs.media_api.params import MediaSearchParams
|
||||||
from viu_cli.libs.provider.anime.provider import create_provider
|
from viu_media.libs.provider.anime.provider import create_provider
|
||||||
from viu_cli.libs.selectors import create_selector
|
from viu_media.libs.selectors import create_selector
|
||||||
from rich.progress import Progress
|
from rich.progress import Progress
|
||||||
|
|
||||||
feedback = FeedbackService(config)
|
feedback = FeedbackService(config)
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import json
|
import json
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import click
|
import click
|
||||||
from viu_cli.core.config import AppConfig
|
from viu_media.core.config import AppConfig
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.table import Table
|
from rich.table import Table
|
||||||
|
|
||||||
@@ -11,8 +11,8 @@ def notifications(config: AppConfig):
|
|||||||
Displays unread notifications from AniList.
|
Displays unread notifications from AniList.
|
||||||
Running this command will also mark the notifications as read on the AniList website.
|
Running this command will also mark the notifications as read on the AniList website.
|
||||||
"""
|
"""
|
||||||
from viu_cli.cli.service.feedback import FeedbackService
|
from viu_media.cli.service.feedback import FeedbackService
|
||||||
from viu_cli.libs.media_api.api import create_api_client
|
from viu_media.libs.media_api.api import create_api_client
|
||||||
|
|
||||||
from ....service.auth import AuthService
|
from ....service.auth import AuthService
|
||||||
|
|
||||||
@@ -251,18 +251,14 @@ def search(config: AppConfig, **options: "Unpack[SearchOptions]"):
|
|||||||
and start_date_lesser is not None
|
and start_date_lesser is not None
|
||||||
and start_date_greater > start_date_lesser
|
and start_date_greater > start_date_lesser
|
||||||
):
|
):
|
||||||
raise ViuError(
|
raise ViuError("Start date greater cannot be later than start date lesser")
|
||||||
"Start date greater cannot be later than start date lesser"
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
end_date_greater is not None
|
end_date_greater is not None
|
||||||
and end_date_lesser is not None
|
and end_date_lesser is not None
|
||||||
and end_date_greater > end_date_lesser
|
and end_date_greater > end_date_lesser
|
||||||
):
|
):
|
||||||
raise ViuError(
|
raise ViuError("End date greater cannot be later than end date lesser")
|
||||||
"End date greater cannot be later than end date lesser"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Build search parameters
|
# Build search parameters
|
||||||
search_params = MediaSearchParams(
|
search_params = MediaSearchParams(
|
||||||
@@ -3,7 +3,7 @@ from typing import TYPE_CHECKING
|
|||||||
import click
|
import click
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from viu_cli.core.config import AppConfig
|
from viu_media.core.config import AppConfig
|
||||||
|
|
||||||
|
|
||||||
@click.command(help="Print out your anilist stats")
|
@click.command(help="Print out your anilist stats")
|
||||||
@@ -72,7 +72,7 @@ def config(
|
|||||||
):
|
):
|
||||||
from ...core.constants import USER_CONFIG
|
from ...core.constants import USER_CONFIG
|
||||||
from ..config.editor import InteractiveConfigEditor
|
from ..config.editor import InteractiveConfigEditor
|
||||||
from ..config.generate import generate_config_ini_from_app_model
|
from ..config.generate import generate_config_toml_from_app_model
|
||||||
|
|
||||||
if path:
|
if path:
|
||||||
print(USER_CONFIG)
|
print(USER_CONFIG)
|
||||||
@@ -81,9 +81,9 @@ def config(
|
|||||||
from rich.syntax import Syntax
|
from rich.syntax import Syntax
|
||||||
|
|
||||||
console = Console()
|
console = Console()
|
||||||
config_ini = generate_config_ini_from_app_model(user_config)
|
config_toml = generate_config_toml_from_app_model(user_config)
|
||||||
syntax = Syntax(
|
syntax = Syntax(
|
||||||
config_ini,
|
config_toml,
|
||||||
"ini",
|
"ini",
|
||||||
theme=user_config.general.pygment_style,
|
theme=user_config.general.pygment_style,
|
||||||
line_numbers=True,
|
line_numbers=True,
|
||||||
@@ -99,12 +99,14 @@ def config(
|
|||||||
elif interactive:
|
elif interactive:
|
||||||
editor = InteractiveConfigEditor(current_config=user_config)
|
editor = InteractiveConfigEditor(current_config=user_config)
|
||||||
new_config = editor.run()
|
new_config = editor.run()
|
||||||
with open(USER_CONFIG, "w", encoding="utf-8") as file:
|
USER_CONFIG.write_text(
|
||||||
file.write(generate_config_ini_from_app_model(new_config))
|
generate_config_toml_from_app_model(new_config), encoding="utf-8"
|
||||||
|
)
|
||||||
click.echo(f"Configuration saved successfully to {USER_CONFIG}")
|
click.echo(f"Configuration saved successfully to {USER_CONFIG}")
|
||||||
elif update:
|
elif update:
|
||||||
with open(USER_CONFIG, "w", encoding="utf-8") as file:
|
USER_CONFIG.write_text(
|
||||||
file.write(generate_config_ini_from_app_model(user_config))
|
generate_config_toml_from_app_model(user_config), encoding="utf-8"
|
||||||
|
)
|
||||||
print("update successfull")
|
print("update successfull")
|
||||||
else:
|
else:
|
||||||
click.edit(filename=str(USER_CONFIG))
|
click.edit(filename=str(USER_CONFIG))
|
||||||
@@ -123,9 +125,9 @@ def _generate_desktop_entry():
|
|||||||
from rich.prompt import Confirm
|
from rich.prompt import Confirm
|
||||||
|
|
||||||
from ...core.constants import (
|
from ...core.constants import (
|
||||||
|
CLI_NAME,
|
||||||
ICON_PATH,
|
ICON_PATH,
|
||||||
PLATFORM,
|
PLATFORM,
|
||||||
PROJECT_NAME,
|
|
||||||
USER_APPLICATIONS,
|
USER_APPLICATIONS,
|
||||||
__version__,
|
__version__,
|
||||||
)
|
)
|
||||||
@@ -149,7 +151,7 @@ def _generate_desktop_entry():
|
|||||||
desktop_entry = dedent(
|
desktop_entry = dedent(
|
||||||
f"""
|
f"""
|
||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Name={PROJECT_NAME.title()}
|
Name={CLI_NAME.title()}
|
||||||
Type=Application
|
Type=Application
|
||||||
version={__version__}
|
version={__version__}
|
||||||
Path={Path().home()}
|
Path={Path().home()}
|
||||||
@@ -160,7 +162,7 @@ def _generate_desktop_entry():
|
|||||||
Categories=Entertainment
|
Categories=Entertainment
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
desktop_entry_path = USER_APPLICATIONS / f"{PROJECT_NAME}.desktop"
|
desktop_entry_path = USER_APPLICATIONS / f"{CLI_NAME}.desktop"
|
||||||
if desktop_entry_path.exists():
|
if desktop_entry_path.exists():
|
||||||
if not Confirm.ask(
|
if not Confirm.ask(
|
||||||
f"The file already exists {desktop_entry_path}; or would you like to rewrite it",
|
f"The file already exists {desktop_entry_path}; or would you like to rewrite it",
|
||||||
@@ -11,7 +11,7 @@ if TYPE_CHECKING:
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TypedDict
|
from typing import TypedDict
|
||||||
|
|
||||||
from viu_cli.cli.service.feedback.service import FeedbackService
|
from viu_media.cli.service.feedback.service import FeedbackService
|
||||||
from typing_extensions import Unpack
|
from typing_extensions import Unpack
|
||||||
|
|
||||||
from ...libs.provider.anime.base import BaseAnimeProvider
|
from ...libs.provider.anime.base import BaseAnimeProvider
|
||||||
@@ -103,7 +103,7 @@ if TYPE_CHECKING:
|
|||||||
)
|
)
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def download(config: AppConfig, **options: "Unpack[Options]"):
|
def download(config: AppConfig, **options: "Unpack[Options]"):
|
||||||
from viu_cli.cli.service.feedback.service import FeedbackService
|
from viu_media.cli.service.feedback.service import FeedbackService
|
||||||
|
|
||||||
from ...core.exceptions import ViuError
|
from ...core.exceptions import ViuError
|
||||||
from ...libs.provider.anime.params import (
|
from ...libs.provider.anime.params import (
|
||||||
5
viu_media/cli/commands/plugin/__init__.py
Normal file
5
viu_media/cli/commands/plugin/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
"""Plugin management commands for viu."""
|
||||||
|
|
||||||
|
from .cmd import plugin
|
||||||
|
|
||||||
|
__all__ = ["plugin"]
|
||||||
24
viu_media/cli/commands/plugin/cmd.py
Normal file
24
viu_media/cli/commands/plugin/cmd.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
"""Main plugin command group."""
|
||||||
|
|
||||||
|
import click
|
||||||
|
|
||||||
|
from ...utils.lazyloader import LazyGroup
|
||||||
|
|
||||||
|
lazy_subcommands = {
|
||||||
|
"add": "add.add",
|
||||||
|
"remove": "remove.remove",
|
||||||
|
"list": "list_plugins.list_plugins",
|
||||||
|
"update": "update.update",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@click.group(
|
||||||
|
name="plugin",
|
||||||
|
cls=LazyGroup,
|
||||||
|
root="viu_media.cli.commands.plugin.commands",
|
||||||
|
lazy_subcommands=lazy_subcommands,
|
||||||
|
help="Manage viu plugins (providers, players, selectors, commands)"
|
||||||
|
)
|
||||||
|
def plugin() -> None:
|
||||||
|
"""Manage viu plugins."""
|
||||||
|
pass
|
||||||
1
viu_media/cli/commands/plugin/commands/__init__.py
Normal file
1
viu_media/cli/commands/plugin/commands/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Plugin command implementations."""
|
||||||
54
viu_media/cli/commands/plugin/commands/add.py
Normal file
54
viu_media/cli/commands/plugin/commands/add.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
"""Add plugin command."""
|
||||||
|
|
||||||
|
import click
|
||||||
|
from rich.console import Console
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
|
from viu_media.core.plugins.manager import PluginError, plugin_manager, ComponentType
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
@click.option(
|
||||||
|
"--type",
|
||||||
|
"plugin_type",
|
||||||
|
type=click.Choice(["provider", "player", "selector", "command"]),
|
||||||
|
required=True,
|
||||||
|
help="Type of plugin to install"
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--force",
|
||||||
|
is_flag=True,
|
||||||
|
help="Force installation, overwriting existing plugin"
|
||||||
|
)
|
||||||
|
@click.argument("name")
|
||||||
|
@click.argument("source")
|
||||||
|
def add(plugin_type: str, name: str, source: str, force: bool) -> None:
|
||||||
|
"""Install a plugin from a Git repository.
|
||||||
|
|
||||||
|
NAME: Local name for the plugin
|
||||||
|
SOURCE: Git source (e.g., 'github:user/repo' or full URL)
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
viu plugin add --type provider gogoanime github:user/viu-gogoanime
|
||||||
|
viu plugin add --type player custom-mpv https://github.com/user/viu-mpv-plugin
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
console.print(f"Installing {plugin_type} plugin '{name}' from {source}...")
|
||||||
|
plugin_manager.add_plugin(cast(ComponentType, plugin_type), name, source, force=force)
|
||||||
|
console.print(f"✅ Successfully installed plugin '{name}'", style="green")
|
||||||
|
|
||||||
|
# Show configuration hint
|
||||||
|
from viu_media.core.constants import PLUGINS_CONFIG
|
||||||
|
console.print(
|
||||||
|
f"\n💡 Configure the plugin by editing: {PLUGINS_CONFIG}",
|
||||||
|
style="blue"
|
||||||
|
)
|
||||||
|
|
||||||
|
except PluginError as e:
|
||||||
|
console.print(f"❌ Failed to install plugin: {e}", style="red")
|
||||||
|
raise click.ClickException(str(e))
|
||||||
|
except Exception as e:
|
||||||
|
console.print(f"❌ Unexpected error: {e}", style="red")
|
||||||
|
raise click.ClickException(f"Unexpected error: {e}")
|
||||||
74
viu_media/cli/commands/plugin/commands/list_plugins.py
Normal file
74
viu_media/cli/commands/plugin/commands/list_plugins.py
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
"""List plugins command."""
|
||||||
|
|
||||||
|
import click
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.table import Table
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
|
from viu_media.core.plugins.manager import plugin_manager, ComponentType
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
|
||||||
|
|
||||||
|
@click.command(name="list")
|
||||||
|
@click.option(
|
||||||
|
"--type",
|
||||||
|
"plugin_type",
|
||||||
|
type=click.Choice(["provider", "player", "selector", "command"]),
|
||||||
|
help="Filter by plugin type"
|
||||||
|
)
|
||||||
|
def list_plugins(plugin_type: str) -> None:
|
||||||
|
"""List installed plugins.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
viu plugin list
|
||||||
|
viu plugin list --type provider
|
||||||
|
"""
|
||||||
|
all_plugins = plugin_manager.list_plugins()
|
||||||
|
|
||||||
|
# Filter by type if specified
|
||||||
|
if plugin_type:
|
||||||
|
plugins_to_show = {cast(ComponentType, plugin_type): all_plugins[cast(ComponentType, plugin_type)]}
|
||||||
|
else:
|
||||||
|
plugins_to_show = all_plugins
|
||||||
|
|
||||||
|
# Count total plugins
|
||||||
|
total_count = sum(len(plugins) for plugins in plugins_to_show.values())
|
||||||
|
|
||||||
|
if total_count == 0:
|
||||||
|
if plugin_type:
|
||||||
|
console.print(f"No {plugin_type} plugins installed.", style="yellow")
|
||||||
|
else:
|
||||||
|
console.print("No plugins installed.", style="yellow")
|
||||||
|
console.print("Install plugins with: viu plugin add --type <type> <name> <source>")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create table
|
||||||
|
table = Table(title="Installed Plugins")
|
||||||
|
table.add_column("Type", style="cyan")
|
||||||
|
table.add_column("Name", style="green")
|
||||||
|
table.add_column("Version", style="yellow")
|
||||||
|
table.add_column("Source", style="blue")
|
||||||
|
table.add_column("Path", style="magenta")
|
||||||
|
|
||||||
|
# Add rows
|
||||||
|
for component_type, plugins in plugins_to_show.items():
|
||||||
|
for name, plugin_info in plugins.items():
|
||||||
|
table.add_row(
|
||||||
|
component_type,
|
||||||
|
name,
|
||||||
|
plugin_info.version or "unknown",
|
||||||
|
plugin_info.source,
|
||||||
|
str(plugin_info.path)
|
||||||
|
)
|
||||||
|
|
||||||
|
console.print(table)
|
||||||
|
console.print(f"\nTotal: {total_count} plugin(s)")
|
||||||
|
|
||||||
|
# Show configuration hint if plugins exist
|
||||||
|
if total_count > 0:
|
||||||
|
from viu_media.core.constants import PLUGINS_CONFIG
|
||||||
|
console.print(
|
||||||
|
f"\n💡 Configure plugins by editing: {PLUGINS_CONFIG}",
|
||||||
|
style="blue"
|
||||||
|
)
|
||||||
43
viu_media/cli/commands/plugin/commands/remove.py
Normal file
43
viu_media/cli/commands/plugin/commands/remove.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
"""Remove plugin command."""
|
||||||
|
|
||||||
|
import click
|
||||||
|
from rich.console import Console
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
|
from viu_media.core.plugins.manager import PluginError, PluginNotFoundError, plugin_manager, ComponentType
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
@click.option(
|
||||||
|
"--type",
|
||||||
|
"plugin_type",
|
||||||
|
type=click.Choice(["provider", "player", "selector", "command"]),
|
||||||
|
required=True,
|
||||||
|
help="Type of plugin to remove"
|
||||||
|
)
|
||||||
|
@click.argument("name")
|
||||||
|
def remove(plugin_type: str, name: str) -> None:
|
||||||
|
"""Remove an installed plugin.
|
||||||
|
|
||||||
|
NAME: Name of the plugin to remove
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
viu plugin remove --type provider gogoanime
|
||||||
|
viu plugin remove --type player custom-mpv
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
console.print(f"Removing {plugin_type} plugin '{name}'...")
|
||||||
|
plugin_manager.remove_plugin(cast(ComponentType, plugin_type), name)
|
||||||
|
console.print(f"✅ Successfully removed plugin '{name}'", style="green")
|
||||||
|
|
||||||
|
except PluginNotFoundError as e:
|
||||||
|
console.print(f"❌ Plugin not found: {e}", style="red")
|
||||||
|
raise click.ClickException(str(e))
|
||||||
|
except PluginError as e:
|
||||||
|
console.print(f"❌ Failed to remove plugin: {e}", style="red")
|
||||||
|
raise click.ClickException(str(e))
|
||||||
|
except Exception as e:
|
||||||
|
console.print(f"❌ Unexpected error: {e}", style="red")
|
||||||
|
raise click.ClickException(f"Unexpected error: {e}")
|
||||||
43
viu_media/cli/commands/plugin/commands/update.py
Normal file
43
viu_media/cli/commands/plugin/commands/update.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
"""Update plugin command."""
|
||||||
|
|
||||||
|
import click
|
||||||
|
from rich.console import Console
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
|
from viu_media.core.plugins.manager import PluginError, PluginNotFoundError, plugin_manager, ComponentType
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
@click.option(
|
||||||
|
"--type",
|
||||||
|
"plugin_type",
|
||||||
|
type=click.Choice(["provider", "player", "selector", "command"]),
|
||||||
|
required=True,
|
||||||
|
help="Type of plugin to update"
|
||||||
|
)
|
||||||
|
@click.argument("name")
|
||||||
|
def update(plugin_type: str, name: str) -> None:
|
||||||
|
"""Update an installed plugin by pulling from Git.
|
||||||
|
|
||||||
|
NAME: Name of the plugin to update
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
viu plugin update --type provider gogoanime
|
||||||
|
viu plugin update --type player custom-mpv
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
console.print(f"Updating {plugin_type} plugin '{name}'...")
|
||||||
|
plugin_manager.update_plugin(cast(ComponentType, plugin_type), name)
|
||||||
|
console.print(f"✅ Successfully updated plugin '{name}'", style="green")
|
||||||
|
|
||||||
|
except PluginNotFoundError as e:
|
||||||
|
console.print(f"❌ Plugin not found: {e}", style="red")
|
||||||
|
raise click.ClickException(str(e))
|
||||||
|
except PluginError as e:
|
||||||
|
console.print(f"❌ Failed to update plugin: {e}", style="red")
|
||||||
|
raise click.ClickException(str(e))
|
||||||
|
except Exception as e:
|
||||||
|
console.print(f"❌ Unexpected error: {e}", style="red")
|
||||||
|
raise click.ClickException(f"Unexpected error: {e}")
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import click
|
import click
|
||||||
from viu_cli.core.config import AppConfig
|
from viu_media.core.config import AppConfig
|
||||||
from viu_cli.core.exceptions import ViuError
|
from viu_media.core.exceptions import ViuError
|
||||||
from viu_cli.libs.media_api.types import (
|
from viu_media.libs.media_api.types import (
|
||||||
MediaFormat,
|
MediaFormat,
|
||||||
MediaGenre,
|
MediaGenre,
|
||||||
MediaItem,
|
MediaItem,
|
||||||
@@ -33,8 +33,12 @@ from viu_cli.libs.media_api.types import (
|
|||||||
@click.option(
|
@click.option(
|
||||||
"--genres-not", multiple=True, type=click.Choice([g.value for g in MediaGenre])
|
"--genres-not", multiple=True, type=click.Choice([g.value for g in MediaGenre])
|
||||||
)
|
)
|
||||||
@click.option("--tags", "-T", multiple=True, type=click.Choice([t.value for t in MediaTag]))
|
@click.option(
|
||||||
@click.option("--tags-not", multiple=True, type=click.Choice([t.value for t in MediaTag]))
|
"--tags", "-T", multiple=True, type=click.Choice([t.value for t in MediaTag])
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--tags-not", multiple=True, type=click.Choice([t.value for t in MediaTag])
|
||||||
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--media-format",
|
"--media-format",
|
||||||
"-f",
|
"-f",
|
||||||
@@ -72,14 +76,14 @@ def queue(config: AppConfig, **options):
|
|||||||
and queue the specified episode range for background download.
|
and queue the specified episode range for background download.
|
||||||
The background worker should be running to process the queue.
|
The background worker should be running to process the queue.
|
||||||
"""
|
"""
|
||||||
from viu_cli.cli.service.download.service import DownloadService
|
from viu_media.cli.service.download.service import DownloadService
|
||||||
from viu_cli.cli.service.feedback import FeedbackService
|
from viu_media.cli.service.feedback import FeedbackService
|
||||||
from viu_cli.cli.service.registry import MediaRegistryService
|
from viu_media.cli.service.registry import MediaRegistryService
|
||||||
from viu_cli.cli.utils.parser import parse_episode_range
|
from viu_media.cli.utils.parser import parse_episode_range
|
||||||
from viu_cli.libs.media_api.params import MediaSearchParams
|
from viu_media.libs.media_api.params import MediaSearchParams
|
||||||
from viu_cli.libs.media_api.api import create_api_client
|
from viu_media.libs.media_api.api import create_api_client
|
||||||
from viu_cli.libs.provider.anime.provider import create_provider
|
from viu_media.libs.provider.anime.provider import create_provider
|
||||||
from viu_cli.libs.selectors import create_selector
|
from viu_media.libs.selectors import create_selector
|
||||||
from rich.progress import Progress
|
from rich.progress import Progress
|
||||||
|
|
||||||
feedback = FeedbackService(config)
|
feedback = FeedbackService(config)
|
||||||
@@ -13,7 +13,7 @@ commands = {
|
|||||||
@click.group(
|
@click.group(
|
||||||
cls=LazyGroup,
|
cls=LazyGroup,
|
||||||
name="queue",
|
name="queue",
|
||||||
root="viu_cli.cli.commands.queue.commands",
|
root="viu_media.cli.commands.queue.commands",
|
||||||
invoke_without_command=False,
|
invoke_without_command=False,
|
||||||
help="Manage the download queue (add, list, resume, clear).",
|
help="Manage the download queue (add, list, resume, clear).",
|
||||||
short_help="Manage the download queue.",
|
short_help="Manage the download queue.",
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import click
|
import click
|
||||||
from viu_cli.core.config import AppConfig
|
from viu_media.core.config import AppConfig
|
||||||
from viu_cli.core.exceptions import ViuError
|
from viu_media.core.exceptions import ViuError
|
||||||
from viu_cli.libs.media_api.types import (
|
from viu_media.libs.media_api.types import (
|
||||||
MediaFormat,
|
MediaFormat,
|
||||||
MediaGenre,
|
MediaGenre,
|
||||||
MediaItem,
|
MediaItem,
|
||||||
@@ -70,14 +70,14 @@ from viu_cli.libs.media_api.types import (
|
|||||||
)
|
)
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def add(config: AppConfig, **options):
|
def add(config: AppConfig, **options):
|
||||||
from viu_cli.cli.service.download import DownloadService
|
from viu_media.cli.service.download import DownloadService
|
||||||
from viu_cli.cli.service.feedback import FeedbackService
|
from viu_media.cli.service.feedback import FeedbackService
|
||||||
from viu_cli.cli.service.registry import MediaRegistryService
|
from viu_media.cli.service.registry import MediaRegistryService
|
||||||
from viu_cli.cli.utils.parser import parse_episode_range
|
from viu_media.cli.utils.parser import parse_episode_range
|
||||||
from viu_cli.libs.media_api.api import create_api_client
|
from viu_media.libs.media_api.api import create_api_client
|
||||||
from viu_cli.libs.media_api.params import MediaSearchParams
|
from viu_media.libs.media_api.params import MediaSearchParams
|
||||||
from viu_cli.libs.provider.anime.provider import create_provider
|
from viu_media.libs.provider.anime.provider import create_provider
|
||||||
from viu_cli.libs.selectors import create_selector
|
from viu_media.libs.selectors import create_selector
|
||||||
from rich.progress import Progress
|
from rich.progress import Progress
|
||||||
|
|
||||||
feedback = FeedbackService(config)
|
feedback = FeedbackService(config)
|
||||||
@@ -149,7 +149,7 @@ def add(config: AppConfig, **options):
|
|||||||
}
|
}
|
||||||
preview_command = None
|
preview_command = None
|
||||||
if config.general.preview != "none":
|
if config.general.preview != "none":
|
||||||
from viu_cli.cli.utils.preview import create_preview_context
|
from viu_media.cli.utils.preview import create_preview_context
|
||||||
|
|
||||||
with create_preview_context() as preview_ctx:
|
with create_preview_context() as preview_ctx:
|
||||||
preview_command = preview_ctx.get_anime_preview(
|
preview_command = preview_ctx.get_anime_preview(
|
||||||
@@ -1,14 +1,17 @@
|
|||||||
import click
|
import click
|
||||||
from viu_cli.core.config import AppConfig
|
from viu_media.core.config import AppConfig
|
||||||
|
|
||||||
|
|
||||||
@click.command(name="clear", help="Clear queued items from the registry (QUEUED -> NOT_DOWNLOADED).")
|
@click.command(
|
||||||
|
name="clear",
|
||||||
|
help="Clear queued items from the registry (QUEUED -> NOT_DOWNLOADED).",
|
||||||
|
)
|
||||||
@click.option("--force", is_flag=True, help="Do not prompt for confirmation.")
|
@click.option("--force", is_flag=True, help="Do not prompt for confirmation.")
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def clear_cmd(config: AppConfig, force: bool):
|
def clear_cmd(config: AppConfig, force: bool):
|
||||||
from viu_cli.cli.service.feedback import FeedbackService
|
from viu_media.cli.service.feedback import FeedbackService
|
||||||
from viu_cli.cli.service.registry import MediaRegistryService
|
from viu_media.cli.service.registry import MediaRegistryService
|
||||||
from viu_cli.cli.service.registry.models import DownloadStatus
|
from viu_media.cli.service.registry.models import DownloadStatus
|
||||||
|
|
||||||
feedback = FeedbackService(config)
|
feedback = FeedbackService(config)
|
||||||
registry = MediaRegistryService(config.general.media_api, config.media_registry)
|
registry = MediaRegistryService(config.general.media_api, config.media_registry)
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import click
|
import click
|
||||||
from viu_cli.core.config import AppConfig
|
from viu_media.core.config import AppConfig
|
||||||
|
|
||||||
|
|
||||||
@click.command(name="list", help="List items in the download queue and their statuses.")
|
@click.command(name="list", help="List items in the download queue and their statuses.")
|
||||||
@@ -10,9 +10,9 @@ from viu_cli.core.config import AppConfig
|
|||||||
@click.option("--detailed", is_flag=True)
|
@click.option("--detailed", is_flag=True)
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def list_cmd(config: AppConfig, status: str | None, detailed: bool | None):
|
def list_cmd(config: AppConfig, status: str | None, detailed: bool | None):
|
||||||
from viu_cli.cli.service.feedback import FeedbackService
|
from viu_media.cli.service.feedback import FeedbackService
|
||||||
from viu_cli.cli.service.registry import MediaRegistryService
|
from viu_media.cli.service.registry import MediaRegistryService
|
||||||
from viu_cli.cli.service.registry.models import DownloadStatus
|
from viu_media.cli.service.registry.models import DownloadStatus
|
||||||
|
|
||||||
feedback = FeedbackService(config)
|
feedback = FeedbackService(config)
|
||||||
registry = MediaRegistryService(config.general.media_api, config.media_registry)
|
registry = MediaRegistryService(config.general.media_api, config.media_registry)
|
||||||
@@ -1,15 +1,17 @@
|
|||||||
import click
|
import click
|
||||||
from viu_cli.core.config import AppConfig
|
from viu_media.core.config import AppConfig
|
||||||
|
|
||||||
|
|
||||||
@click.command(name="resume", help="Submit any queued or in-progress downloads to the worker.")
|
@click.command(
|
||||||
|
name="resume", help="Submit any queued or in-progress downloads to the worker."
|
||||||
|
)
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def resume(config: AppConfig):
|
def resume(config: AppConfig):
|
||||||
from viu_cli.cli.service.download.service import DownloadService
|
from viu_media.cli.service.download.service import DownloadService
|
||||||
from viu_cli.cli.service.feedback import FeedbackService
|
from viu_media.cli.service.feedback import FeedbackService
|
||||||
from viu_cli.cli.service.registry import MediaRegistryService
|
from viu_media.cli.service.registry import MediaRegistryService
|
||||||
from viu_cli.libs.media_api.api import create_api_client
|
from viu_media.libs.media_api.api import create_api_client
|
||||||
from viu_cli.libs.provider.anime.provider import create_provider
|
from viu_media.libs.provider.anime.provider import create_provider
|
||||||
|
|
||||||
feedback = FeedbackService(config)
|
feedback = FeedbackService(config)
|
||||||
media_api = create_api_client(config.general.media_api, config)
|
media_api = create_api_client(config.general.media_api, config)
|
||||||
@@ -19,7 +19,7 @@ commands = {
|
|||||||
@click.group(
|
@click.group(
|
||||||
cls=LazyGroup,
|
cls=LazyGroup,
|
||||||
name="registry",
|
name="registry",
|
||||||
root="viu_cli.cli.commands.registry.commands",
|
root="viu_media.cli.commands.registry.commands",
|
||||||
invoke_without_command=True,
|
invoke_without_command=True,
|
||||||
help="Manage your local media registry - sync, search, backup and maintain your anime database",
|
help="Manage your local media registry - sync, search, backup and maintain your anime database",
|
||||||
short_help="Local media registry management",
|
short_help="Local media registry management",
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user