Compare commits

...

57 Commits

Author SHA1 Message Date
Benexl
f99ff546d5 feat(plugins): init examples 2025-08-18 16:20:48 +03:00
Benexl
88b707e060 feat: plugins system 2025-08-18 16:02:37 +03:00
Benexl
eaedf3268d feat(config): switch to toml format 2025-08-18 14:06:31 +03:00
Benexl
ade0465ea4 chore: set py version for pyright 2025-08-18 13:24:50 +03:00
Benexl
5e82db4ea8 chore: add repomixignore 2025-08-18 13:23:48 +03:00
Benexl
a10e56cb6f refactor:set min supported python version to 3.11 2025-08-18 13:19:56 +03:00
Benexl
fbd95e1966 feat(config-loader): allow env vars 2025-08-18 13:04:00 +03:00
Benexl
d37a441ccf fix(state): check for is None instead 2025-08-18 12:33:15 +03:00
Benexl
cbc1ceccbb feat(cli): auto check for updates 2025-08-18 02:14:56 +03:00
Benexl
249a207cad fix(update-command): use viu-media when updating 2025-08-18 01:28:59 +03:00
Benexl
c8a42c4920 Update README.md 2025-08-18 01:16:47 +03:00
Benexl
de8b6b7f2f chore: bump version 2025-08-18 01:15:00 +03:00
Benexl
54e0942233 chore: update uv.lock 2025-08-18 01:12:10 +03:00
Benexl
8ea0c121c2 chore: viu_media 2025-08-18 01:08:27 +03:00
Benexl
eddaad64e7 chore: viu media is better 2025-08-18 01:07:36 +03:00
Benexl
43be7a52cf chore(envrc): check if nix command is available 2025-08-18 00:31:05 +03:00
Benexl
b689760a25 Merge pull request #129 from s-weigand/fix-ci
🚇🩹 Fix test CI workflow
2025-08-17 20:25:55 +03:00
Benexl
e53246b79b feat(interactive-state): media api state should come second 2025-08-17 19:45:59 +03:00
Benexl
b0fc94cdc5 style: ruff format 2025-08-17 19:40:53 +03:00
Benexl
449f6c1e59 feat(interactive-state): create accessors that ensure values exist 2025-08-17 19:38:55 +03:00
Benexl
ab4734b79d fix(session): allow offline viewing by wrapping authenticate in try block 2025-08-17 17:49:38 +03:00
Benexl
93d0f6a1a5 refactor: fa to viu 2025-08-17 17:22:38 +03:00
Benexl
19c75c48b2 Merge pull request #128 from s-weigand/improve-title-matching
👌 Make finding best_match_title more robust
2025-08-17 16:49:32 +03:00
Benexl
5341b0a844 Update README.md 2025-08-17 16:40:26 +03:00
Benexl
24e7e6a16b Update README.md 2025-08-17 16:36:52 +03:00
s-weigand
4b310e60b8 Revert " Run on feature-branch"
This reverts commit c6b8cfc294.
2025-08-17 13:42:43 +02:00
s-weigand
4d50cffd86 🧹 Ignore blank except ruff rule 2025-08-17 13:14:32 +02:00
s-weigand
f6fedf0500 🧹 Remove unused TYPE_CHECKING import 2025-08-17 13:13:11 +02:00
s-weigand
7b431450fe 🩹 Relock uv.lock file due to changed package name 2025-08-17 13:09:52 +02:00
s-weigand
66b247330b 🚇🩹 Install libglib2.0-dev 2025-08-17 12:56:03 +02:00
s-weigand
c6b8cfc294 Run on feature-branch 2025-08-17 12:49:05 +02:00
s-weigand
6895426d67 🚇🩹 Install dbus-python build dependencies 2025-08-17 12:48:29 +02:00
s-weigand
cc69dc35f6 👌 Make finding best_match_title more robust 2025-08-17 12:34:25 +02:00
Benexl
ed81f37ae4 Merge pull request #126 from blob5/master
Build failure on nixOS. ModuleNotFoundError: No module named 'viu'
2025-08-16 23:47:43 +03:00
Senna
c6858b00c4 remove pythonImportsCheck 2025-08-16 22:08:06 +02:00
Benexl
a44034a5d4 chore: remove 2025-08-16 21:47:44 +03:00
Benexl
f768518721 Update README.md 2025-08-16 19:48:57 +03:00
Benexl
97f5bb9cb3 chore: bump 2025-08-16 19:45:25 +03:00
Benexl
b09fdbf69b chore: update deps 2025-08-16 19:44:49 +03:00
Benexl
071c46cad9 chore: bump version 2025-08-16 19:32:23 +03:00
Benexl
5d32503ff9 chore: update publish.yml 2025-08-16 19:31:28 +03:00
Benexl
e67532c496 chore: bump version 2025-08-16 19:19:32 +03:00
Benexl
819012897d Update README.md 2025-08-16 19:17:44 +03:00
Benexl
c4f78b12a4 revert 2025-08-16 19:16:11 +03:00
Benexl
8aacbcc35b Update README.md 2025-08-16 19:11:21 +03:00
Benexl
5976ab43b2 chore: correct package issues 2025-08-16 19:08:39 +03:00
Benexl
99c67a4bc0 fix: publish.yml 2025-08-16 19:00:44 +03:00
Benexl
34851fd3e4 chore: update publish.yml 2025-08-16 18:58:59 +03:00
Benexl
e74b5977bb chore: update workflow 2025-08-16 18:56:31 +03:00
Benexl
0650f45fba revert 2025-08-16 18:55:33 +03:00
Benexl
0c8f2a70ba chore: update project name 2025-08-16 18:50:42 +03:00
Benexl
ce7cd98783 feat(constants): _LOWER 2025-08-16 16:57:08 +03:00
Benexl
ec14c40c77 feat(config-generator): include computed fields + show defaults 2025-08-16 16:54:47 +03:00
Benexl
6a6e03c744 feat(cli): invoke the default media api on no subcommand 2025-08-16 16:32:07 +03:00
Benexl
26de1a0fb4 feat(cli): register completions cmd 2025-08-16 16:21:41 +03:00
Benexl
e49fb4898c refactor: rename to viu 2025-08-16 16:18:41 +03:00
Benexl
e2407d4948 Update README.md 2025-08-15 13:45:52 +03:00
301 changed files with 6430 additions and 1569 deletions

4
.envrc
View File

@@ -1 +1,3 @@
use flake
if command -v nix >/dev/null;then
use flake
fi

15
.github/FUNDING.yml vendored
View File

@@ -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']

View File

@@ -2,29 +2,29 @@
description: "Generate a new 'click' command following the project's lazy-loading pattern and service architecture."
tools: ['codebase']
---
# FastAnime: CLI Command Generation Mode
# viu: CLI Command Generation Mode
You are an expert on the `fastanime` CLI structure, which uses `click` and a custom `LazyGroup` for performance. Your task is to generate the boilerplate for a new command.
You are an expert on the `viu` CLI structure, which uses `click` and a custom `LazyGroup` for performance. Your task is to generate the boilerplate for a new command.
**First, ask the user if this is a top-level command (like `fastanime new-cmd`) or a subcommand (like `fastanime anilist new-sub-cmd`).**
**First, ask the user if this is a top-level command (like `viu new-cmd`) or a subcommand (like `viu anilist new-sub-cmd`).**
---
### If Top-Level Command:
1. **File Location:** State that the new command file should be created at: `fastanime/cli/commands/{command_name}.py`.
1. **File Location:** State that the new command file should be created at: `viu/cli/commands/{command_name}.py`.
2. **Boilerplate:** Generate the `click.command()` function.
* It **must** accept `config: AppConfig` as the first argument using `@click.pass_obj`.
* It **must not** contain business logic. Instead, show how to instantiate a service from `fastanime.cli.service` and call its methods.
3. **Registration:** Instruct the user to register the command by adding it to the `commands` dictionary in `fastanime/cli/cli.py`. Provide the exact line to add, like: `"new-cmd": "new_cmd.new_cmd_function"`.
* It **must not** contain business logic. Instead, show how to instantiate a service from `viu.cli.service` and call its methods.
3. **Registration:** Instruct the user to register the command by adding it to the `commands` dictionary in `viu/cli/cli.py`. Provide the exact line to add, like: `"new-cmd": "new_cmd.new_cmd_function"`.
---
### If Subcommand:
1. **Ask for Parent:** Ask for the parent command group (e.g., `anilist`, `registry`).
2. **File Location:** State that the new command file should be created at: `fastanime/cli/commands/{parent_name}/commands/{command_name}.py`.
2. **File Location:** State that the new command file should be created at: `viu/cli/commands/{parent_name}/commands/{command_name}.py`.
3. **Boilerplate:** Generate the `click.command()` function, similar to the top-level command.
4. **Registration:** Instruct the user to register the subcommand in the parent's `cmd.py` file (e.g., `fastanime/cli/commands/anilist/cmd.py`) by adding it to the `lazy_subcommands` dictionary within the `@click.group` decorator.
4. **Registration:** Instruct the user to register the subcommand in the parent's `cmd.py` file (e.g., `viu/cli/commands/anilist/cmd.py`) by adding it to the `lazy_subcommands` dictionary within the `@click.group` decorator.
**Final Instruction:** Remind the user that if the command introduces new logic, it should be encapsulated in a new or existing **Service** class in the `fastanime/cli/service/` directory. The CLI command function should only handle argument parsing and calling the service.
**Final Instruction:** Remind the user that if the command introduces new logic, it should be encapsulated in a new or existing **Service** class in the `viu/cli/service/` directory. The CLI command function should only handle argument parsing and calling the service.

View File

@@ -2,9 +2,9 @@
description: "Scaffold the necessary files and code for a new Player or Selector component, including configuration."
tools: ['codebase', 'search']
---
# FastAnime: New Component Generation Mode
# viu: New Component Generation Mode
You are an expert on `fastanime`'s modular architecture. Your task is to help the developer add a new **Player** or **Selector** component.
You are an expert on `viu`'s modular architecture. Your task is to help the developer add a new **Player** or **Selector** component.
**First, ask the user whether they want to create a 'Player' or a 'Selector'.** Then, follow the appropriate path below.
@@ -12,13 +12,13 @@ You are an expert on `fastanime`'s modular architecture. Your task is to help th
### If the user chooses 'Player':
1. **Scaffold Directory:** Create a directory at `fastanime/libs/player/{player_name}/`.
2. **Implement `BasePlayer`:** Create a `player.py` file with a class `NewPlayer` that inherits from `fastanime.libs.player.base.BasePlayer`. Implement the `play` and `play_with_ipc` methods. The `play` method should use `subprocess` to call the player's executable.
1. **Scaffold Directory:** Create a directory at `viu/libs/player/{player_name}/`.
2. **Implement `BasePlayer`:** Create a `player.py` file with a class `NewPlayer` that inherits from `viu.libs.player.base.BasePlayer`. Implement the `play` and `play_with_ipc` methods. The `play` method should use `subprocess` to call the player's executable.
3. **Add Configuration:**
* Instruct to create a new Pydantic model `NewPlayerConfig(OtherConfig)` in `fastanime/core/config/model.py`.
* Instruct to create a new Pydantic model `NewPlayerConfig(OtherConfig)` in `viu/core/config/model.py`.
* Add the new config model to the main `AppConfig`.
* Add defaults in `fastanime/core/config/defaults.py` and descriptions in `fastanime/core/config/descriptions.py`.
4. **Register Player:** Instruct to modify `fastanime/libs/player/player.py` by:
* Add defaults in `viu/core/config/defaults.py` and descriptions in `viu/core/config/descriptions.py`.
4. **Register Player:** Instruct to modify `viu/libs/player/player.py` by:
* Adding the player name to the `PLAYERS` list.
* Adding the instantiation logic to the `PlayerFactory.create` method.
@@ -26,9 +26,9 @@ You are an expert on `fastanime`'s modular architecture. Your task is to help th
### If the user chooses 'Selector':
1. **Scaffold Directory:** Create a directory at `fastanime/libs/selectors/{selector_name}/`.
2. **Implement `BaseSelector`:** Create a `selector.py` file with a class `NewSelector` that inherits from `fastanime.libs.selectors.base.BaseSelector`. Implement the `choose`, `confirm`, and `ask` methods.
1. **Scaffold Directory:** Create a directory at `viu/libs/selectors/{selector_name}/`.
2. **Implement `BaseSelector`:** Create a `selector.py` file with a class `NewSelector` that inherits from `viu.libs.selectors.base.BaseSelector`. Implement the `choose`, `confirm`, and `ask` methods.
3. **Add Configuration:** (Follow the same steps as for a Player).
4. **Register Selector:**
* Instruct to modify `fastanime/libs/selectors/selector.py` by adding the selector name to the `SELECTORS` list and the factory logic to `SelectorFactory.create`.
* Instruct to update the `Literal` type hint for the `selector` field in `GeneralConfig` (`fastanime/core/config/model.py`).
* Instruct to modify `viu/libs/selectors/selector.py` by adding the selector name to the `SELECTORS` list and the factory logic to `SelectorFactory.create`.
* Instruct to update the `Literal` type hint for the `selector` field in `GeneralConfig` (`viu/core/config/model.py`).

View File

@@ -1,27 +1,27 @@
---
description: "Scaffold and implement a new anime provider, following all architectural patterns of the fastanime project."
description: "Scaffold and implement a new anime provider, following all architectural patterns of the viu project."
tools: ['codebase', 'search', 'fetch']
---
# FastAnime: New Provider Generation Mode
# viu: New Provider Generation Mode
You are an expert on the `fastanime` codebase, specializing in its provider architecture. Your task is to guide the developer in creating a new anime provider. You must strictly adhere to the project's structure and coding conventions.
You are an expert on the `viu` codebase, specializing in its provider architecture. Your task is to guide the developer in creating a new anime provider. You must strictly adhere to the project's structure and coding conventions.
**Your process is as follows:**
1. **Ask for the Provider's Name:** First, ask the user for the name of the new provider (e.g., `gogoanime`, `crunchyroll`). Use this name (in lowercase) for all subsequent file and directory naming.
2. **Scaffold the Directory Structure:** Based on the name, state the required directory structure that needs to be created:
`fastanime/libs/provider/anime/{provider_name}/`
`viu/libs/provider/anime/{provider_name}/`
3. **Scaffold the Core Files:** Generate the initial code for the following files inside the new directory. Ensure all code is fully type-hinted.
* **`__init__.py`**: Can be an empty file.
* **`types.py`**: Create placeholder `TypedDict` models for the provider's specific API responses (e.g., `GogoAnimeSearchResult`, `GogoAnimeEpisode`).
* **`mappers.py`**: Create empty mapping functions that will convert the provider-specific types into the generic types from `fastanime.libs.provider.anime.types`. For example: `map_to_search_results(data: GogoAnimeSearchPage) -> SearchResults:`.
* **`provider.py`**: Generate the main provider class. It **MUST** inherit from `fastanime.libs.provider.anime.base.BaseAnimeProvider`. Include stubs for the required abstract methods: `search`, `get`, and `episode_streams`. Remind the user to use `httpx.Client` for requests and to call the mapper functions.
* **`mappers.py`**: Create empty mapping functions that will convert the provider-specific types into the generic types from `viu.libs.provider.anime.types`. For example: `map_to_search_results(data: GogoAnimeSearchPage) -> SearchResults:`.
* **`provider.py`**: Generate the main provider class. It **MUST** inherit from `viu.libs.provider.anime.base.BaseAnimeProvider`. Include stubs for the required abstract methods: `search`, `get`, and `episode_streams`. Remind the user to use `httpx.Client` for requests and to call the mapper functions.
4. **Instruct on Registration:** Clearly state the two files that **must** be modified to register the new provider:
* **`fastanime/libs/provider/anime/types.py`**: Add the new provider's name to the `ProviderName` enum.
* **`fastanime/libs/provider/anime/provider.py`**: Add an entry to the `PROVIDERS_AVAILABLE` dictionary.
* **`viu/libs/provider/anime/types.py`**: Add the new provider's name to the `ProviderName` enum.
* **`viu/libs/provider/anime/provider.py`**: Add an entry to the `PROVIDERS_AVAILABLE` dictionary.
5. **Final Guidance:** Remind the developer to add any title normalization rules to `fastanime/assets/normalizer.json` if the provider uses different anime titles than AniList.
5. **Final Guidance:** Remind the developer to add any title normalization rules to `viu/assets/normalizer.json` if the provider uses different anime titles than AniList.

View File

@@ -1,11 +1,11 @@
---
description: "Plan new features or bug fixes with architectural guidance for the fastanime project. Does not write implementation code."
description: "Plan new features or bug fixes with architectural guidance for the viu project. Does not write implementation code."
tools: ['codebase', 'search', 'githubRepo', 'fetch']
model: "gpt-4o"
---
# FastAnime: Feature & Fix Planner Mode
# viu: Feature & Fix Planner Mode
You are a senior software architect and project planner for the `fastanime` project. You are an expert in its layered architecture (`Core`, `Libs`, `Service`, `CLI`) and its commitment to modular, testable code.
You are a senior software architect and project planner for the `viu` project. You are an expert in its layered architecture (`Core`, `Libs`, `Service`, `CLI`) and its commitment to modular, testable code.
Your primary goal is to help the user break down a feature request or bug report into a clear, actionable implementation plan.
@@ -33,17 +33,17 @@ Your primary goal is to help the user break down a feature request or bug report
**2. Architectural Impact Analysis**
> This is the most important section. Detail which parts of the codebase will be touched and why.
> - **Core Layer (`fastanime/core`):**
> - **Core Layer (`viu/core`):**
> - *Config (`config/model.py`):* Will a new Pydantic model or field be needed?
> - *Utils (`utils/`):* Are any new low-level, reusable functions required?
> - *Exceptions (`exceptions.py`):* Does this introduce a new failure case that needs a custom exception?
> - **Libs Layer (`fastanime/libs`):**
> - **Libs Layer (`viu/libs`):**
> - *Media API (`media_api/`):* Does this involve a new call to the AniList API?
> - *Provider (`provider/`):* Does this affect how data is scraped?
> - *Player/Selector (`player/`, `selectors/`):* Does this change how we interact with external tools?
> - **Service Layer (`fastanime/cli/service`):**
> - **Service Layer (`viu/cli/service`):**
> - Which service will orchestrate this logic? (e.g., `DownloadService`, `PlayerService`). Will a new service be needed?
> - **CLI Layer (`fastanime/cli`):**
> - **CLI Layer (`viu/cli`):**
> - *Commands (`commands/`):* Which `click` command(s) will expose this feature?
> - *Interactive UI (`interactive/`):* Which TUI menu(s) need to be added or modified?
@@ -52,7 +52,7 @@ Your primary goal is to help the user break down a feature request or bug report
> 1. [ ] **Config:** Add `new_setting` to `GeneralConfig` in `core/config/model.py`.
> 2. [ ] **Core:** Implement `new_util()` in `core/utils/helpers.py`.
> 3. [ ] **Service:** Add method `handle_new_feature()` to `MyService`.
> 4. [ ] **CLI:** Add `--new-feature` option to the `fastanime anilist search` command.
> 4. [ ] **CLI:** Add `--new-feature` option to the `viu anilist search` command.
> 5. [ ] **Tests:** Write a unit test for `new_util()` and an integration test for the service method.
**4. Configuration Changes**

View File

@@ -1,27 +1,27 @@
# GitHub Copilot Instructions for the FastAnime Repository
# GitHub Copilot Instructions for the viu Repository
Hello, Copilot! This document provides instructions and context to help you understand the `fastanime` codebase. Following these guidelines will help you generate code that is consistent, maintainable, and aligned with the project's architecture.
Hello, Copilot! This document provides instructions and context to help you understand the `viu` codebase. Following these guidelines will help you generate code that is consistent, maintainable, and aligned with the project's architecture.
## 1. High-Level Project Goal
`fastanime` is a command-line tool that brings the anime browsing, streaming, and management experience to the terminal. It integrates with metadata providers like AniList and scrapes streaming links from various anime provider websites. The core goals are efficiency, extensibility, and providing a powerful, scriptable user experience.
`viu` is a command-line tool that brings the anime browsing, streaming, and management experience to the terminal. It integrates with metadata providers like AniList and scrapes streaming links from various anime provider websites. The core goals are efficiency, extensibility, and providing a powerful, scriptable user experience.
## 2. Core Architectural Concepts
The project follows a clean, layered architecture. When generating code, please adhere to this structure.
#### Layer 1: CLI (`fastanime/cli`)
#### Layer 1: CLI (`viu/cli`)
* **Purpose:** Handles user interaction, command parsing, and displaying output.
* **Key Libraries:** `click` for command structure, `rich` for styled output.
* **Interactive Mode:** The interactive TUI is managed by the `Session` object in `fastanime/cli/interactive/session.py`. It's a state machine where each menu is a function that returns the next `State` or an `InternalDirective` (like `BACK` or `EXIT`).
* **Interactive Mode:** The interactive TUI is managed by the `Session` object in `viu/cli/interactive/session.py`. It's a state machine where each menu is a function that returns the next `State` or an `InternalDirective` (like `BACK` or `EXIT`).
* **Guideline:** **CLI files should not contain complex business logic.** They should parse arguments and delegate tasks to the Service Layer.
#### Layer 2: Service (`fastanime/cli/service`)
#### Layer 2: Service (`viu/cli/service`)
* **Purpose:** Contains the core application logic. Services act as orchestrators, connecting the CLI layer with the various library components.
* **Examples:** `DownloadService`, `PlayerService`, `MediaRegistryService`, `WatchHistoryService`.
* **Guideline:** When adding new functionality (e.g., a new way to manage downloads), it should likely be implemented in a service or an existing service should be extended. Services are the "brains" of the application.
#### Layer 3: Libraries (`fastanime/libs`)
#### Layer 3: Libraries (`viu/libs`)
* **Purpose:** A collection of independent, reusable modules with well-defined contracts (Abstract Base Classes).
* **`media_api`:** Interfaces with metadata services like AniList. All new metadata clients **must** inherit from `BaseApiClient`.
* **`provider`:** Interfaces with anime streaming websites. All new providers **must** inherit from `BaseAnimeProvider`.
@@ -29,7 +29,7 @@ The project follows a clean, layered architecture. When generating code, please
* **`selectors`:** Wrappers for interactive UI tools like FZF or Rofi. All new selectors **must** inherit from `BaseSelector`.
* **Guideline:** Libraries should be self-contained and not depend on the CLI or Service layers. They receive configuration and perform their specific task.
#### Layer 4: Core (`fastanime/core`)
#### Layer 4: Core (`viu/core`)
* **Purpose:** Foundational code shared across the entire application.
* **`config`:** Pydantic models defining the application's configuration structure. **This is the single source of truth for all settings.**
* **`downloader`:** The underlying logic for downloading files (using `yt-dlp` or `httpx`).
@@ -39,7 +39,7 @@ The project follows a clean, layered architecture. When generating code, please
## 3. Key Technologies
* **Dependency Management:** `uv` is used for all package management and task running. Refer to `pyproject.toml` for dependencies.
* **Configuration:** **Pydantic** is used exclusively. The entire configuration is defined in `fastanime/core/config/model.py`.
* **Configuration:** **Pydantic** is used exclusively. The entire configuration is defined in `viu/core/config/model.py`.
* **CLI Framework:** `click`. We use a custom `LazyGroup` to load commands on demand for faster startup.
* **HTTP Client:** `httpx` is the standard for all network requests.
@@ -48,37 +48,37 @@ The project follows a clean, layered architecture. When generating code, please
Follow these patterns to ensure your contributions fit the existing architecture.
### How to Add a New Provider
1. **Create Directory:** Add a new folder in `fastanime/libs/provider/anime/newprovider/`.
1. **Create Directory:** Add a new folder in `viu/libs/provider/anime/newprovider/`.
2. **Implement `BaseAnimeProvider`:** In `provider.py`, create a class `NewProvider` that inherits from `BaseAnimeProvider` and implement the `search`, `get`, and `episode_streams` methods.
3. **Create Mappers:** In `mappers.py`, write functions to convert the provider's API/HTML data into the generic Pydantic models from `fastanime/libs/provider/anime/types.py` (e.g., `SearchResult`, `Anime`, `Server`).
3. **Create Mappers:** In `mappers.py`, write functions to convert the provider's API/HTML data into the generic Pydantic models from `viu/libs/provider/anime/types.py` (e.g., `SearchResult`, `Anime`, `Server`).
4. **Register Provider:**
* Add the provider's name to the `ProviderName` enum in `fastanime/libs/provider/anime/types.py`.
* Add it to the `PROVIDERS_AVAILABLE` dictionary in `fastanime/libs/provider/anime/provider.py`.
* Add the provider's name to the `ProviderName` enum in `viu/libs/provider/anime/types.py`.
* Add it to the `PROVIDERS_AVAILABLE` dictionary in `viu/libs/provider/anime/provider.py`.
### How to Add a New Player
1. **Create Directory:** Add a new folder in `fastanime/libs/player/newplayer/`.
1. **Create Directory:** Add a new folder in `viu/libs/player/newplayer/`.
2. **Implement `BasePlayer`:** In `player.py`, create a class `NewPlayer` that inherits from `BasePlayer` and implement the `play` method. It should call the player's executable via `subprocess`.
3. **Add Configuration:** If the player has settings, add a `NewPlayerConfig` Pydantic model in `fastanime/core/config/model.py`, and add it to the main `AppConfig`. Also add defaults and descriptions.
4. **Register Player:** Add the player's name to the `PLAYERS` list and the factory logic in `fastanime/libs/player/player.py`.
3. **Add Configuration:** If the player has settings, add a `NewPlayerConfig` Pydantic model in `viu/core/config/model.py`, and add it to the main `AppConfig`. Also add defaults and descriptions.
4. **Register Player:** Add the player's name to the `PLAYERS` list and the factory logic in `viu/libs/player/player.py`.
### How to Add a New Selector
1. **Create Directory:** Add a new folder in `fastanime/libs/selectors/newselector/`.
1. **Create Directory:** Add a new folder in `viu/libs/selectors/newselector/`.
2. **Implement `BaseSelector`:** In `selector.py`, create a class `NewSelector` that inherits from `BaseSelector` and implement `choose`, `confirm`, and `ask`.
3. **Add Configuration:** If needed, add a `NewSelectorConfig` to `fastanime/core/config/model.py`.
4. **Register Selector:** Add the selector's name to the `SELECTORS` list and the factory logic in `fastanime/libs/selectors/selector.py`. Update the `Literal` type hint for `selector` in `GeneralConfig`.
3. **Add Configuration:** If needed, add a `NewSelectorConfig` to `viu/core/config/model.py`.
4. **Register Selector:** Add the selector's name to the `SELECTORS` list and the factory logic in `viu/libs/selectors/selector.py`. Update the `Literal` type hint for `selector` in `GeneralConfig`.
### How to Add a New CLI Command
* **Top-Level Command (`fastanime my-command`):**
1. Create `fastanime/cli/commands/my_command.py` with your `click.command()`.
2. Register it in the `commands` dictionary in `fastanime/cli/cli.py`.
* **Subcommand (`fastanime anilist my-subcommand`):**
1. Create `fastanime/cli/commands/anilist/commands/my_subcommand.py`.
2. Register it in the `lazy_subcommands` dictionary of the parent `click.group()` (e.g., in `fastanime/cli/commands/anilist/cmd.py`).
* **Top-Level Command (`viu my-command`):**
1. Create `viu/cli/commands/my_command.py` with your `click.command()`.
2. Register it in the `commands` dictionary in `viu/cli/cli.py`.
* **Subcommand (`viu anilist my-subcommand`):**
1. Create `viu/cli/commands/anilist/commands/my_subcommand.py`.
2. Register it in the `lazy_subcommands` dictionary of the parent `click.group()` (e.g., in `viu/cli/commands/anilist/cmd.py`).
### How to Add a New Configuration Option
1. **Add to Model:** Add the field to the appropriate Pydantic model in `fastanime/core/config/model.py`.
2. **Add Default:** Add a default value in `fastanime/core/config/defaults.py`.
3. **Add Description:** Add a user-friendly description in `fastanime/core/config/descriptions.py`.
1. **Add to Model:** Add the field to the appropriate Pydantic model in `viu/core/config/model.py`.
2. **Add Default:** Add a default value in `viu/core/config/defaults.py`.
3. **Add Description:** Add a user-friendly description in `viu/core/config/descriptions.py`.
4. The config loader and CLI option generation will handle the rest automatically.
## 5. Code Style and Conventions
@@ -91,7 +91,7 @@ Follow these patterns to ensure your contributions fit the existing architecture
***DO** use the abstract base classes (`BaseProvider`, `BasePlayer`, etc.) as contracts.
***DO** place business logic in the `service` layer.
***DO** use the Pydantic models in `fastanime/core/config/model.py` as the single source of truth for configuration.
***DO** use the Pydantic models in `viu/core/config/model.py` as the single source of truth for configuration.
***DO** use the `Context` object in interactive menus to access services and configuration.
***DON'T** hardcode configuration values. Access them via the `config` object.

View File

@@ -20,13 +20,13 @@ jobs:
with:
enable-cache: true
- name: Build fastanime
- name: Build viu
run: uv build
- name: Archive production artifacts
uses: actions/upload-artifact@v4
with:
name: fastanime_debug_build
name: viu_debug_build
path: |
dist

View File

@@ -1,12 +1,3 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# GitHub recommends pinning actions to a commit SHA.
# To get a newer version, you will need to update the SHA.
# You can also reference a tag or branch, but the action may change without warning.
name: Upload Python Package
on:
@@ -32,7 +23,7 @@ jobs:
with:
enable-cache: true
- name: Build fastanime
- name: Build viu
run: uv build
- name: Upload distributions

View File

@@ -13,7 +13,7 @@ jobs:
strategy:
matrix:
python-version: ["3.10", "3.11"] # List the Python versions you want to test
python-version: ["3.11", "3.12"]
steps:
- uses: actions/checkout@v4
@@ -22,6 +22,11 @@ jobs:
with:
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
uses: astral-sh/setup-uv@v3
with:

1
.repomixignore Normal file
View File

@@ -0,0 +1 @@
**/generated/**/*

View File

@@ -1,29 +1,29 @@
# Contributing to FastAnime
# Contributing to Viu
First off, thank you for considering contributing to FastAnime! We welcome any help, whether it's reporting a bug, proposing a feature, or writing code. This document will guide you through the process.
First off, thank you for considering contributing to Viu! We welcome any help, whether it's reporting a bug, proposing a feature, or writing code. This document will guide you through the process.
## How Can I Contribute?
There are many ways to contribute to the FastAnime project:
There are many ways to contribute to the Viu project:
* **Reporting Bugs:** If you find a bug, please create an issue in our [issue tracker](https://github.com/Benexl/FastAnime/issues).
* **Reporting Bugs:** If you find a bug, please create an issue in our [issue tracker](https://github.com/Benexl/Viu/issues).
* **Suggesting Enhancements:** Have an idea for a new feature or an improvement to an existing one? We'd love to hear it.
* **Writing Code:** Help us fix bugs or implement new features.
* **Improving Documentation:** Enhance our README, add examples, or clarify our contribution guidelines.
* **Adding a Provider, Player, or Selector:** Extend FastAnime's capabilities by integrating new tools and services.
* **Adding a Provider, Player, or Selector:** Extend Viu's capabilities by integrating new tools and services.
## Contribution Workflow
We follow the standard GitHub Fork & Pull Request workflow.
1. **Create an Issue:** Before starting work on a new feature or a significant bug fix, please [create an issue](https://github.com/Benexl/FastAnime/issues/new/choose) to discuss your idea. This allows us to give feedback and prevent duplicate work. For small bugs or documentation typos, you can skip this step.
1. **Create an Issue:** Before starting work on a new feature or a significant bug fix, please [create an issue](https://github.com/Benexl/Viu/issues/new/choose) to discuss your idea. This allows us to give feedback and prevent duplicate work. For small bugs or documentation typos, you can skip this step.
2. **Fork the Repository:** Create your own fork of the FastAnime repository.
2. **Fork the Repository:** Create your own fork of the Viu repository.
3. **Clone Your Fork:**
```bash
git clone https://github.com/YOUR_USERNAME/FastAnime.git
cd FastAnime
git clone https://github.com/YOUR_USERNAME/Viu.git
cd Viu
```
4. **Create a Branch:** Create a new branch for your changes. Use a descriptive name.
@@ -64,7 +64,7 @@ We follow the standard GitHub Fork & Pull Request workflow.
git push origin feat/my-new-feature
```
9. **Submit a Pull Request:** Open a pull request from your branch to the `master` branch of the main FastAnime repository. Provide a clear title and description of your changes.
9. **Submit a Pull Request:** Open a pull request from your branch to the `master` branch of the main Viu repository. Provide a clear title and description of your changes.
## Setting Up Your Development Environment
@@ -111,7 +111,7 @@ To maintain code quality and consistency, please adhere to the following guideli
* **Modularity and Architecture:**
* **Services:** Business logic is organized into services (e.g., `PlayerService`, `DownloadService`).
* **Factories:** Use factory patterns (`create_provider`, `create_selector`) for creating instances of different implementations.
* **Configuration:** All configuration is managed through Pydantic models in `fastanime/core/config/model.py`. When adding new config options, update the model, defaults, and descriptions.
* **Configuration:** All configuration is managed through Pydantic models in `viu/core/config/model.py`. When adding new config options, update the model, defaults, and descriptions.
* **Commit Messages:** Follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) standard.
* **Testing:** New features should be accompanied by tests. Bug fixes should ideally include a regression test.
@@ -119,25 +119,25 @@ To maintain code quality and consistency, please adhere to the following guideli
Adding a new anime provider is a great way to contribute. Here are the steps:
1. **Create a New Provider Directory:** Inside `fastanime/libs/provider/anime/`, create a new directory with the provider's name (e.g., `fastanime/libs/provider/anime/newprovider/`).
1. **Create a New Provider Directory:** Inside `viu/libs/provider/anime/`, create a new directory with the provider's name (e.g., `viu/libs/provider/anime/newprovider/`).
2. **Implement the Provider:**
* Create a `provider.py` file.
* Define a class (e.g., `NewProviderApi`) that inherits from `BaseAnimeProvider`.
* Implement the abstract methods: `search`, `get`, and `episode_streams`.
* Create `mappers.py` to convert the provider's data structures into the generic types defined in `fastanime/libs/provider/anime/types.py`.
* Create `mappers.py` to convert the provider's data structures into the generic types defined in `viu/libs/provider/anime/types.py`.
* Create `types.py` for any provider-specific data structures you need.
* If the provider requires complex scraping, place extractor logic in an `extractors/` subdirectory.
3. **Register the Provider:**
* Add your new provider to the `ProviderName` enum in `fastanime/libs/provider/anime/types.py`.
* Register it in the `PROVIDERS_AVAILABLE` dictionary in `fastanime/libs/provider/anime/provider.py`.
* Add your new provider to the `ProviderName` enum in `viu/libs/provider/anime/types.py`.
* Register it in the `PROVIDERS_AVAILABLE` dictionary in `viu/libs/provider/anime/provider.py`.
4. **Add Normalization Rules (Optional):** If the provider uses different anime titles than AniList, add mappings to `fastanime/assets/normalizer.json`.
4. **Add Normalization Rules (Optional):** If the provider uses different anime titles than AniList, add mappings to `viu/assets/normalizer.json`.
## How to Add a New Player
1. **Create a New Player Directory:** Inside `fastanime/libs/player/`, create a directory for your player (e.g., `fastanime/libs/player/myplayer/`).
1. **Create a New Player Directory:** Inside `viu/libs/player/`, create a directory for your player (e.g., `viu/libs/player/myplayer/`).
2. **Implement the Player Class:**
* In `myplayer/player.py`, create a class (e.g., `MyPlayer`) that inherits from `BasePlayer`.
@@ -145,17 +145,17 @@ Adding a new anime provider is a great way to contribute. Here are the steps:
* The `play` method should handle launching the player as a subprocess and return a `PlayerResult`.
3. **Add Configuration (if needed):**
* If your player has configurable options, add a new Pydantic model (e.g., `MyPlayerConfig`) in `fastanime/core/config/model.py`. It should inherit from `OtherConfig`.
* If your player has configurable options, add a new Pydantic model (e.g., `MyPlayerConfig`) in `viu/core/config/model.py`. It should inherit from `OtherConfig`.
* Add this new config model as a field in the main `AppConfig` model.
* Add default values in `defaults.py` and descriptions in `descriptions.py`.
4. **Register the Player:**
* Add your player's name to the `PLAYERS` list in `fastanime/libs/player/player.py`.
* Add your player's name to the `PLAYERS` list in `viu/libs/player/player.py`.
* Add the logic to instantiate your player class within the `PlayerFactory.create` method.
## How to Add a New Selector
1. **Create a New Selector Directory:** Inside `fastanime/libs/selectors/`, create a new directory (e.g., `fastanime/libs/selectors/myselector/`).
1. **Create a New Selector Directory:** Inside `viu/libs/selectors/`, create a new directory (e.g., `viu/libs/selectors/myselector/`).
2. **Implement the Selector Class:**
* In `myselector/selector.py`, create a class (e.g., `MySelector`) that inherits from `BaseSelector`.
@@ -165,19 +165,19 @@ Adding a new anime provider is a great way to contribute. Here are the steps:
3. **Add Configuration (if needed):** Follow the same configuration steps as for adding a new player.
4. **Register the Selector:**
* Add your selector's name to the `SELECTORS` list in `fastanime/libs/selectors/selector.py`.
* Add your selector's name to the `SELECTORS` list in `viu/libs/selectors/selector.py`.
* Add the instantiation logic to the `SelectorFactory.create` method.
* Update the `Literal` type hint for the `selector` field in `GeneralConfig` (`fastanime/core/config/model.py`).
* Update the `Literal` type hint for the `selector` field in `GeneralConfig` (`viu/core/config/model.py`).
## How to Add a New CLI Command or Service
Our CLI uses `click` and a `LazyGroup` class to load commands on demand.
### Adding a Top-Level Command (e.g., `fastanime my-command`)
### Adding a Top-Level Command (e.g., `viu my-command`)
1. **Create the Command File:** Create a new Python file in `fastanime/cli/commands/` (e.g., `my_command.py`). This file should contain your `click.command()` function.
1. **Create the Command File:** Create a new Python file in `viu/cli/commands/` (e.g., `my_command.py`). This file should contain your `click.command()` function.
2. **Register the Command:** In `fastanime/cli/cli.py`, add your command to the `commands` dictionary.
2. **Register the Command:** In `viu/cli/cli.py`, add your command to the `commands` dictionary.
```python
commands = {
# ... existing commands
@@ -185,11 +185,11 @@ Our CLI uses `click` and a `LazyGroup` class to load commands on demand.
}
```
### Adding a Subcommand (e.g., `fastanime anilist my-subcommand`)
### Adding a Subcommand (e.g., `viu anilist my-subcommand`)
1. **Create the Command File:** Place your new command file inside the appropriate subdirectory, for example, `fastanime/cli/commands/anilist/commands/my_subcommand.py`.
1. **Create the Command File:** Place your new command file inside the appropriate subdirectory, for example, `viu/cli/commands/anilist/commands/my_subcommand.py`.
2. **Register the Subcommand:** In the parent command's entry point file (e.g., `fastanime/cli/commands/anilist/cmd.py`), add your subcommand to the `commands` dictionary within the `LazyGroup`.
2. **Register the Subcommand:** In the parent command's entry point file (e.g., `viu/cli/commands/anilist/cmd.py`), add your subcommand to the `commands` dictionary within the `LazyGroup`.
```python
@click.group(
cls=LazyGroup,
@@ -202,7 +202,7 @@ Our CLI uses `click` and a `LazyGroup` class to load commands on demand.
```
### Creating a Service
If your command involves complex logic, consider creating a service in `fastanime/cli/service/` to keep the business logic separate from the command-line interface. This service can then be instantiated and used within your `click` command function. This follows the existing pattern for services like `DownloadService` and `PlayerService`.
If your command involves complex logic, consider creating a service in `viu/cli/service/` to keep the business logic separate from the command-line interface. This service can then be instantiated and used within your `click` command function. This follows the existing pattern for services like `DownloadService` and `PlayerService`.
---
Thank you for contributing to FastAnime
Thank you for contributing to Viu

View File

@@ -2,7 +2,7 @@
<div align="center">
<h2>This project: fastanime</h2>
<h2>This project: viu</h2>
<br>

319
PLUGINS.md Normal file
View 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

135
README.md
View File

@@ -1,5 +1,5 @@
<p align="center">
<h1 align="center">FastAnime</h1>
<h1 align="center">Viu</h1>
</p>
<p align="center">
<sup>
@@ -8,12 +8,12 @@
</p>
<div align="center">
[![PyPI - Version](https://img.shields.io/pypi/v/fastanime)](https://pypi.org/project/fastanime/)
[![PyPI - Downloads](https://img.shields.io/pypi/dm/fastanime)](https://pypi.org/project/fastanime/)
[![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/Benexl/FastAnime/test.yml?label=Tests)](https://github.com/Benexl/FastAnime/actions)
[![PyPI - Version](https://img.shields.io/pypi/v/viu-media)](https://pypi.org/project/viu-media/)
[![PyPI - Downloads](https://img.shields.io/pypi/dm/viu-media)](https://pypi.org/project/viu-media/)
[![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/Benexl/Viu/test.yml?label=Tests)](https://github.com/Benexl/Viu/actions)
[![Discord](https://img.shields.io/discord/1250887070906323096?label=Discord&logo=discord)](https://discord.gg/HBEmAwvbHV)
[![GitHub Issues](https://img.shields.io/github/issues/Benexl/FastAnime)](https://github.com/Benexl/FastAnime/issues)
[![PyPI - License](https://img.shields.io/pypi/l/fastanime)](https://github.com/Benexl/FastAnime/blob/master/LICENSE)
[![GitHub Issues](https://img.shields.io/github/issues/Benexl/Viu)](https://github.com/Benexl/Viu/issues)
[![PyPI - License](https://img.shields.io/pypi/l/viu)](https://github.com/Benexl/Viu/blob/master/LICENSE)
</div>
@@ -23,37 +23,6 @@
</a>
</p>
![fastanime](https://github.com/user-attachments/assets/9ab09f26-e4a8-4b70-a315-7def998cec63)
<details>
<summary>
<b>Screenshots</b>
</summary>
<b>Media Results Menu:</b>
<img width="1346" height="710" alt="image" src="https://github.com/user-attachments/assets/c56da5d2-d55d-445c-9ad7-4e007e986d5b" />
<b>Episodes Menu with Preview:</b>
<img width="1346" height="710" alt="image" src="https://github.com/user-attachments/assets/2294f621-8549-4b1c-9e28-d851b2585037" />
</details>
<details>
<summary>
<b>Riced Preview Examples</b>
</summary>
**Anilist Results Menu (FZF):**
![image](https://github.com/user-attachments/assets/240023a7-7e4e-47dd-80ff-017d65081ee1)
**Episodes Menu with Preview (FZF):**
![image](https://github.com/user-attachments/assets/580f86ef-326f-4ab3-9bd8-c1cb312fbfa6)
**No Image Preview Mode:**
![image](https://github.com/user-attachments/assets/e1248a85-438f-4758-ae34-b0e0b224addd)
**Desktop Notifications + Episodes Menu:**
![image](https://github.com/user-attachments/assets/b7802ef1-ca0d-45f5-a13a-e39c96a5d499)
</details>
## Core Features
@@ -67,7 +36,7 @@
## Installation
FastAnime runs on any platform with Python 3.10+, including Windows, macOS, Linux, and Android (via Termux).
Viu runs on any platform with Python 3.10+, including Windows, macOS, Linux, and Android (via Termux).
### Prerequisites
@@ -84,17 +53,17 @@ For the best experience, please install these external tools:
### Recommended Installation (uv)
The best way to install FastAnime is with [**uv**](https://github.com/astral-sh/uv), a lightning-fast Python package manager.
The best way to install Viu is with [**uv**](https://github.com/astral-sh/uv), a lightning-fast Python package manager.
```bash
# Install with all optional features for the full experience
uv tool install "fastanime[standard]"
uv tool install "viu-media[standard]"
# Or, pick and choose the extras you need:
uv tool install fastanime # Core functionality only
uv tool install "fastanime[download]" # For advanced downloading with yt-dlp
uv tool install "fastanime[discord]" # For Discord Rich Presence
uv tool install "fastanime[notifications]" # For desktop notifications
uv tool install viu-media # Core functionality only
uv tool install "viu-media[download]" # For advanced downloading with yt-dlp
uv tool install "viu-media[discord]" # For Discord Rich Presence
uv tool install "viu-media[notifications]" # For desktop notifications
```
### Other Installation Methods
@@ -104,27 +73,27 @@ uv tool install "fastanime[notifications]" # For desktop notifications
#### Nix / NixOS
```bash
nix profile install github:Benexl/fastanime
nix profile install github:Benexl/viu
```
#### Arch Linux (AUR)
Use an AUR helper like `yay` or `paru`.
```bash
# Stable version (recommended)
yay -S fastanime
yay -S viu
# Git version (latest commit)
yay -S fastanime-git
yay -S viu-git
```
#### Using pipx (for isolated environments)
```bash
pipx install "fastanime[standard]"
pipx install "viu-media[standard]"
```
#### Using pip
```bash
pip install "fastanime[standard]"
pip install "viu-media[standard]"
```
</details>
@@ -133,15 +102,15 @@ uv tool install "fastanime[notifications]" # For desktop notifications
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/Benexl/FastAnime.git --depth 1
cd FastAnime
git clone https://github.com/Benexl/Viu.git --depth 1
cd Viu
uv tool install .
fastanime --version
viu --version
```
</details>
> [!TIP]
> Enable shell completions for a much better experience by running `fastanime completions` and following the on-screen instructions for your shell.
> Enable shell completions for a much better experience by running `viu completions` and following the on-screen instructions for your shell.
## Getting Started: Quick Start
@@ -149,59 +118,59 @@ Get up and running in three simple steps:
1. **Authenticate with AniList:**
```bash
fastanime anilist auth
viu anilist auth
```
This will open your browser. Authorize the app and paste the obtained token back into the terminal.
2. **Launch the Interactive TUI:**
```bash
fastanime anilist
viu anilist
```
3. **Browse & Play:** Use your arrow keys to navigate the menus, select an anime, and choose an episode to stream instantly.
## Usage Guide
### The Interactive TUI (`fastanime anilist`)
### The Interactive TUI (`viu anilist`)
This is the main, user-friendly way to use FastAnime. It provides a rich terminal experience where you can:
This is the main, user-friendly way to use Viu. It provides a rich terminal experience where you can:
* Browse trending, popular, and seasonal anime.
* Manage your personal lists (Watching, Completed, Paused, etc.).
* Search for any anime in the AniList database.
* View detailed information, characters, recommendations, reviews, and airing schedules.
* Stream or download episodes directly from the menus.
### Powerful Searching (`fastanime anilist search`)
### Powerful Searching (`viu anilist search`)
Filter the entire AniList database with powerful command-line flags.
```bash
# Search for anime from 2024, sorted by popularity, that is releasing and not on your list
fastanime anilist search -y 2024 -s POPULARITY_DESC --status RELEASING --not-on-list
viu anilist search -y 2024 -s POPULARITY_DESC --status RELEASING --not-on-list
# Find the most popular movies with the "Fantasy" genre
fastanime anilist search -g Fantasy -f MOVIE -s POPULARITY_DESC
viu anilist search -g Fantasy -f MOVIE -s POPULARITY_DESC
# Dump search results as JSON instead of launching the TUI
fastanime anilist search -t "Demon Slayer" --dump-json
viu anilist search -t "Demon Slayer" --dump-json
```
### Background Downloads (`fastanime queue` & `worker`)
### Background Downloads (`viu queue` & `worker`)
FastAnime includes a robust background downloading system.
Viu includes a robust background downloading system.
1. **Add episodes to the queue:**
```bash
# Add episodes 1-12 of Jujutsu Kaisen to the download queue
fastanime queue add -t "Jujutsu Kaisen" -r "0:12"
viu queue add -t "Jujutsu Kaisen" -r "0:12"
```
2. **Start the worker process:**
```bash
# Run the worker in the foreground (press Ctrl+C to stop)
fastanime worker
viu worker
# Or run it as a background process
fastanime worker &
viu worker &
```The worker will now process the queue, download your episodes, and check for notifications.
### Scriptable Commands (`download` & `search`)
@@ -211,24 +180,24 @@ These commands are designed for automation and quick, non-interactive tasks.
#### `download` Examples
```bash
# Download the latest 5 episodes of One Piece
fastanime download -t "One Piece" -r "-5"
viu download -t "One Piece" -r "-5"
# Download episodes 1 to 24, merge subtitles, and clean up original files
fastanime download -t "Jujutsu Kaisen" -r "0:24" --merge --clean
viu download -t "Jujutsu Kaisen" -r "0:24" --merge --clean
```
#### `search` (Binging) Examples
```bash
# Start binging an anime from the first episode
fastanime search -t "Attack on Titan" -r ":"
viu search -t "Attack on Titan" -r ":"
# Watch the latest episode directly
fastanime search -t "My Hero Academia" -r "-1"
viu search -t "My Hero Academia" -r "-1"
```
### Local Data Management (`fastanime registry`)
### Local Data Management (`viu registry`)
FastAnime maintains a local database of your anime for offline access and enhanced performance.
Viu maintains a local database of your anime for offline access and enhanced performance.
* `registry sync`: Synchronize your local data with your remote AniList account.
* `registry stats`: Show detailed statistics about your viewing habits.
@@ -239,13 +208,13 @@ FastAnime maintains a local database of your anime for offline access and enhanc
## Configuration
FastAnime is highly customizable. A default configuration file with detailed comments is created on the first run.
Viu is highly customizable. A default configuration file with detailed comments is created on the first run.
* **Find your config file:** `fastanime config --path`
* **Edit in your default editor:** `fastanime config`
* **Use the interactive wizard:** `fastanime config --interactive`
* **Find your config file:** `viu config --path`
* **Edit in your default editor:** `viu config`
* **Use the interactive wizard:** `viu config --interactive`
Most settings in the config file can be temporarily overridden with command-line flags (e.g., `fastanime --provider animepahe anilist`).
Most settings in the config file can be temporarily overridden with command-line flags (e.g., `viu --provider animepahe anilist`).
<details>
<summary><b>Default Configuration (`config.ini`) Explained</b></summary>
@@ -293,7 +262,7 @@ download_check_interval = 5 ; How often to process the download queue (minu
### MPV IPC Integration
When `use_ipc = True` is set in your config, FastAnime provides powerful in-player controls without needing to close MPV.
When `use_ipc = True` is set in your config, Viu provides powerful in-player controls without needing to close MPV.
**Key Bindings:**
* `Shift+N`: Play the next episode.
@@ -310,27 +279,27 @@ When `use_ipc = True` is set in your config, FastAnime provides powerful in-play
You can run the background worker as a systemd service for persistence.
1. Create a service file at `~/.config/systemd/user/fastanime-worker.service`:
1. Create a service file at `~/.config/systemd/user/viu-worker.service`:
```ini
[Unit]
Description=FastAnime Background Worker
Description=Viu Background Worker
After=network-online.target
[Service]
Type=simple
ExecStart=/path/to/your/fastanime worker --log
ExecStart=/path/to/your/viu worker --log
Restart=always
RestartSec=30
[Install]
WantedBy=default.target
```
*Replace `/path/to/your/fastanime` with the output of `which fastanime`.*
*Replace `/path/to/your/viu` with the output of `which viu`.*
2. Enable and start the service:
```bash
systemctl --user daemon-reload
systemctl --user enable --now fastanime-worker.service
systemctl --user enable --now viu-worker.service
```
## Contributing

View File

@@ -1,7 +1,7 @@
FROM python:3.12-slim-bookworm
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
COPY . /fastanime
COPY . /viu
ENV PATH=/root/.local/bin:$PATH
WORKDIR /fastanime
WORKDIR /viu
RUN uv tool install .
CMD ["bash"]

View File

@@ -5,7 +5,7 @@ block_cipher = None
# Collect all required data files
datas = [
('fastanime/assets/*', 'fastanime/assets'),
('viu/assets/*', 'viu/assets'),
]
# Collect all required hidden imports
@@ -16,11 +16,11 @@ hiddenimports = [
'yt_dlp',
'python_mpv',
'fuzzywuzzy',
'fastanime',
] + collect_submodules('fastanime')
'viu',
] + collect_submodules('viu')
a = Analysis(
['./fastanime/fastanime.py'], # Changed entry point
['./viu/viu.py'], # Changed entry point
pathex=[],
binaries=[],
datas=datas,
@@ -49,7 +49,7 @@ exe = EXE(
a.zipfiles,
a.datas,
[],
name='fastanime',
name='viu',
debug=False,
bootloader_ignore_signals=False,
strip=True,
@@ -61,5 +61,5 @@ exe = EXE(
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon='fastanime/assets/logo.ico'
icon='viu/assets/logo.ico'
)

View File

@@ -1,8 +1,8 @@
_fastanime_completion() {
_viu_completion() {
local IFS=$'\n'
local response
response=$(env COMP_WORDS="${COMP_WORDS[*]}" COMP_CWORD=$COMP_CWORD _FASTANIME_COMPLETE=bash_complete $1)
response=$(env COMP_WORDS="${COMP_WORDS[*]}" COMP_CWORD=$COMP_CWORD _VIU_COMPLETE=bash_complete $1)
for completion in $response; do
IFS=',' read type value <<< "$completion"
@@ -21,9 +21,9 @@ _fastanime_completion() {
return 0
}
_fastanime_completion_setup() {
complete -o nosort -F _fastanime_completion fastanime
_viu_completion_setup() {
complete -o nosort -F _viu_completion viu
}
_fastanime_completion_setup;
_viu_completion_setup;

View File

@@ -1,5 +1,5 @@
function _fastanime_completion;
set -l response (env _FASTANIME_COMPLETE=fish_complete COMP_WORDS=(commandline -cp) COMP_CWORD=(commandline -t) fastanime);
function _viu_completion;
set -l response (env _VIU_COMPLETE=fish_complete COMP_WORDS=(commandline -cp) COMP_CWORD=(commandline -t) viu);
for completion in $response;
set -l metadata (string split "," $completion);
@@ -14,5 +14,5 @@ function _fastanime_completion;
end;
end;
complete --no-files --command fastanime --arguments "(_fastanime_completion)";
complete --no-files --command viu --arguments "(_viu_completion)";

View File

@@ -1,12 +1,12 @@
#compdef fastanime
#compdef viu
_fastanime_completion() {
_viu_completion() {
local -a completions
local -a completions_with_descriptions
local -a response
(( ! $+commands[fastanime] )) && return 1
(( ! $+commands[viu] )) && return 1
response=("${(@f)$(env COMP_WORDS="${words[*]}" COMP_CWORD=$((CURRENT-1)) _FASTANIME_COMPLETE=zsh_complete fastanime)}")
response=("${(@f)$(env COMP_WORDS="${words[*]}" COMP_CWORD=$((CURRENT-1)) _VIU_COMPLETE=zsh_complete viu)}")
for type key descr in ${response}; do
if [[ "$type" == "plain" ]]; then
@@ -33,9 +33,9 @@ _fastanime_completion() {
if [[ $zsh_eval_context[-1] == loadautofunc ]]; then
# autoload from fpath, call function directly
_fastanime_completion "$@"
_viu_completion "$@"
else
# eval/source/. command, register function for later
compdef _fastanime_completion fastanime
compdef _viu_completion viu
fi

6
dev/generate_completions.sh Executable file → Normal file
View File

@@ -5,10 +5,10 @@ APP_DIR="$(
)"
# fish shell completions
_FASTANIME_COMPLETE=fish_source fastanime >"$APP_DIR/completions/fastanime.fish"
_VIU_COMPLETE=fish_source viu >"$APP_DIR/completions/viu.fish"
# zsh completions
_FASTANIME_COMPLETE=zsh_source fastanime >"$APP_DIR/completions/fastanime.zsh"
_VIU_COMPLETE=zsh_source viu >"$APP_DIR/completions/viu.zsh"
# bash completions
_FASTANIME_COMPLETE=bash_source fastanime >"$APP_DIR/completions/fastanime.bash"
_VIU_COMPLETE=bash_source viu >"$APP_DIR/completions/viu.bash"

6
dev/make_release Executable file → Normal file
View File

@@ -2,11 +2,11 @@
CLI_DIR="$(dirname "$(realpath "$0")")"
VERSION=$1
[ -z "$VERSION" ] && echo no version provided && exit 1
[ "$VERSION" = "current" ] && fastanime --version && exit 0
[ "$VERSION" = "current" ] && viu --version && exit 0
sed -i "s/^version.*/version = \"$VERSION\"/" "$CLI_DIR/pyproject.toml" &&
sed -i "s/__version__.*/__version__ = \"v$VERSION\"/" "$CLI_DIR/fastanime/__init__.py" &&
sed -i "s/__version__.*/__version__ = \"v$VERSION\"/" "$CLI_DIR/viu/__init__.py" &&
sed -i "s/version = .*/version = \"$VERSION\";/" "$CLI_DIR/flake.nix" &&
git stage "$CLI_DIR/pyproject.toml" "$CLI_DIR/fastanime/__init__.py" "$CLI_DIR/flake.nix" &&
git stage "$CLI_DIR/pyproject.toml" "$CLI_DIR/viu/__init__.py" "$CLI_DIR/flake.nix" &&
git commit -m "chore: bump version (v$VERSION)" &&
# nix flake lock &&
uv lock &&

View 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

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

View 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"

View 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

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

View 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"

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

View File

@@ -1,6 +0,0 @@
███████╗░█████╗░░██████╗████████╗░█████╗░███╗░░██╗██╗███╗░░░███╗███████╗
██╔════╝██╔══██╗██╔════╝╚══██╔══╝██╔══██╗████╗░██║██║████╗░████║██╔════╝
█████╗░░███████║╚█████╗░░░░██║░░░███████║██╔██╗██║██║██╔████╔██║█████╗░░
██╔══╝░░██╔══██║░╚═══██╗░░░██║░░░██╔══██║██║╚████║██║██║╚██╔╝██║██╔══╝░░
██║░░░░░██║░░██║██████╔╝░░░██║░░░██║░░██║██║░╚███║██║██║░╚═╝░██║███████╗
╚═╝░░░░░╚═╝░░╚═╝╚═════╝░░░░╚═╝░░░╚═╝░░╚═╝╚═╝░░╚══╝╚═╝╚═╝░░░░░╚═╝╚══════╝

View File

@@ -1,169 +0,0 @@
download = """
\b
\b\bExamples:
# Basic download by title
fastanime anilist download -t "Attack on Titan"
\b
# Download specific episodes
fastanime anilist download -t "One Piece" --episode-range "1-10"
\b
# Download single episode
fastanime anilist download -t "Death Note" --episode-range "1"
\b
# Download multiple specific episodes
fastanime anilist download -t "Naruto" --episode-range "1,5,10"
\b
# Download with quality preference
fastanime anilist download -t "Death Note" --quality 1080 --episode-range "1-5"
\b
# Download with multiple filters
fastanime anilist download -g Action -T Isekai --score-greater 80 --status RELEASING
\b
# Download with concurrent downloads
fastanime anilist download -t "Demon Slayer" --episode-range "1-5" --max-concurrent 3
\b
# Force redownload existing episodes
fastanime anilist download -t "Your Name" --episode-range "1" --force-redownload
\b
# Download from a specific season and year
fastanime anilist download --season WINTER --year 2024 -s POPULARITY_DESC
\b
# Download with genre filtering
fastanime anilist download -g Action -g Adventure --score-greater 75
\b
# Download only completed series
fastanime anilist download -g Fantasy --status FINISHED --score-greater 75
\b
# Download movies only
fastanime anilist download -F MOVIE -s SCORE_DESC --quality best
"""
search = """
\b
\b\bExamples:
# Basic search by title
fastanime anilist search -t "Attack on Titan"
\b
# Search with multiple filters
fastanime anilist search -g Action -T Isekai --score-greater 75 --status RELEASING
\b
# Get anime with the tag of isekai
fastanime anilist search -T isekai
\b
# Get anime of 2024 and sort by popularity, finished or releasing, not in your list
fastanime anilist search -y 2024 -s POPULARITY_DESC --status RELEASING --status FINISHED --not-on-list
\b
# Get anime of 2024 season WINTER
fastanime anilist search -y 2024 --season WINTER
\b
# Get anime genre action and tag isekai,magic
fastanime anilist search -g Action -T Isekai -T Magic
\b
# Get anime of 2024 thats finished airing
fastanime anilist search -y 2024 -S FINISHED
\b
# Get the most favourite anime movies
fastanime anilist search -f MOVIE -s FAVOURITES_DESC
\b
# Search with score and popularity filters
fastanime anilist search --score-greater 80 --popularity-greater 50000
\b
# Search excluding certain genres and tags
fastanime anilist search --genres-not Ecchi --tags-not "Hentai"
\b
# Search with date ranges (YYYYMMDD format)
fastanime anilist search --start-date-greater 20200101 --start-date-lesser 20241231
\b
# Get only TV series, exclude certain statuses
fastanime anilist search -f TV --status-not CANCELLED --status-not HIATUS
\b
# Paginated search with custom page size
fastanime anilist search -g Action --page 2 --per-page 25
\b
# Search for manga specifically
fastanime anilist search --media-type MANGA -g Fantasy
\b
# Complex search with multiple criteria
fastanime anilist search -t "demon" -g Action -g Supernatural --score-greater 70 --year 2020 -s SCORE_DESC
\b
# Dump search results as JSON instead of interactive mode
fastanime anilist search -g Action --dump-json
"""
main = """
\b
\b\bExamples:
# ---- search ----
\b
# Basic search by title
fastanime anilist search -t "Attack on Titan"
\b
# Search with multiple filters
fastanime anilist search -g Action -T Isekai --score-greater 75 --status RELEASING
\b
# Get anime with the tag of isekai
fastanime anilist search -T isekai
\b
# Get anime of 2024 and sort by popularity, finished or releasing, not in your list
fastanime anilist search -y 2024 -s POPULARITY_DESC --status RELEASING --status FINISHED --not-on-list
\b
# Get anime of 2024 season WINTER
fastanime anilist search -y 2024 --season WINTER
\b
# Get anime genre action and tag isekai,magic
fastanime anilist search -g Action -T Isekai -T Magic
\b
# Get anime of 2024 thats finished airing
fastanime anilist search -y 2024 -S FINISHED
\b
# Get the most favourite anime movies
fastanime anilist search -f MOVIE -s FAVOURITES_DESC
\b
# Search with score and popularity filters
fastanime anilist search --score-greater 80 --popularity-greater 50000
\b
# Search excluding certain genres and tags
fastanime anilist search --genres-not Ecchi --tags-not "Hentai"
\b
# Search with date ranges (YYYYMMDD format)
fastanime anilist search --start-date-greater 20200101 --start-date-lesser 20241231
\b
# Get only TV series, exclude certain statuses
fastanime anilist search -f TV --status-not CANCELLED --status-not HIATUS
\b
# Paginated search with custom page size
fastanime anilist search -g Action --page 2 --per-page 25
\b
# Search for manga specifically
fastanime anilist search --media-type MANGA -g Fantasy
\b
# Complex search with multiple criteria
fastanime anilist search -t "demon" -g Action -g Supernatural --score-greater 70 --year 2020 -s SCORE_DESC
\b
# Dump search results as JSON instead of interactive mode
fastanime anilist search -g Action --dump-json
\b
# ---- login ----
\b
# To sign in just run
fastanime anilist auth
\b
# To check your login status
fastanime anilist auth --status
\b
# To log out and erase credentials
fastanime anilist auth --logout
\b
# ---- notifier ----
\b
# basic form
fastanime anilist notifier
\b
# with logging to stdout
fastanime --log anilist notifier
\b
# with logging to a file. stored in the same place as your config
fastanime --log-file anilist notifier
"""

View File

@@ -1,31 +0,0 @@
"""
Example usage for the registry command
"""
main = """
Examples:
# Sync with remote AniList
fastanime registry sync --upload --download
# Show detailed registry statistics
fastanime registry stats --detailed
# Search local registry
fastanime registry search "attack on titan"
# Export registry to JSON
fastanime registry export --format json --output backup.json
# Import from backup
fastanime registry import backup.json
# Clean up orphaned entries
fastanime registry clean --dry-run
# Create full backup
fastanime registry backup --compress
# Restore from backup
fastanime registry restore backup.tar.gz
"""

View File

@@ -1,160 +0,0 @@
"""Update command for FastAnime 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 FastAnime to the latest version",
short_help="Update FastAnime",
epilog="""
\b
\b\bExamples:
# Check for updates and update if available
fastanime update
\b
# Force update even if already up to date
fastanime update --force
\b
# Only check for updates without updating
fastanime update --check-only
\b
# Show release notes for the latest version
fastanime 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 FastAnime 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]FastAnime 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 'fastanime 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)

View File

@@ -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"]

View File

@@ -1,154 +0,0 @@
import textwrap
from enum import Enum
from pathlib import Path
from typing import Any, Literal, get_args, get_origin
from pydantic.fields import FieldInfo
from ...core.config import AppConfig
from ...core.constants import APP_ASCII_ART, DISCORD_INVITE, PROJECT_NAME, REPO_HOME
# The header for the config file.
config_asci = "\n".join(
[f"# {line}" for line in APP_ASCII_ART.read_text(encoding="utf-8").split()]
)
CONFIG_HEADER = f"""
# ==============================================================================
#
{config_asci}
#
# ==============================================================================
# This file was auto-generated from the application's configuration model.
# You can modify these values to customize the behavior of FastAnime.
# For path-based options, you can use '~' for your home directory.
""".lstrip()
CONFIG_FOOTER = f"""
# ==============================================================================
#
# HOPE YOU ENJOY {PROJECT_NAME} AND BE SURE TO STAR THE PROJECT ON GITHUB
# {REPO_HOME}
#
# Also join the discord server
# where the anime tech community lives :)
# {DISCORD_INVITE}
#
# ==============================================================================
""".lstrip()
def generate_config_ini_from_app_model(app_model: AppConfig) -> str:
"""Generate a configuration file content from a Pydantic model."""
config_ini_content = [CONFIG_HEADER]
for section_name, section_model in app_model:
section_comment = section_model.model_config.get("title", "")
config_ini_content.append(f"\n#\n# {section_comment}\n#")
config_ini_content.append(f"[{section_name}]")
for field_name, field_info in section_model.model_fields.items():
description = field_info.description or ""
if description:
wrapped_comment = textwrap.fill(
description,
width=78,
initial_indent="# ",
subsequent_indent="# ",
)
config_ini_content.append(f"\n{wrapped_comment}")
field_type_comment = _get_field_type_comment(field_info)
if field_type_comment:
wrapped_comment = textwrap.fill(
field_type_comment,
width=78,
initial_indent="# ",
subsequent_indent="# ",
)
config_ini_content.append(wrapped_comment)
field_value = getattr(section_model, field_name)
if isinstance(field_value, bool):
value_str = str(field_value).lower()
elif isinstance(field_value, Path):
value_str = str(field_value)
elif field_value is None:
value_str = ""
elif isinstance(field_value, Enum):
value_str = field_value.value
else:
value_str = str(field_value)
config_ini_content.append(f"{field_name} = {value_str}")
config_ini_content.extend(["\n", CONFIG_FOOTER])
return "\n".join(config_ini_content)
def _get_field_type_comment(field_info: FieldInfo) -> str:
"""Generate a comment with type information for a field."""
field_type = field_info.annotation
# Handle Literal and Enum types
possible_values = []
if field_type is not None:
if isinstance(field_type, type) and issubclass(field_type, Enum):
possible_values = [member.value for member in field_type]
elif hasattr(field_type, "__origin__") and get_origin(field_type) is Literal:
args = get_args(field_type)
if args:
possible_values = list(args)
if possible_values:
return f"Possible values: [ {', '.join(map(str, possible_values))} ]"
# Handle basic types and numeric ranges
type_name = _get_type_name(field_type)
range_info = _get_range_info(field_info)
if range_info:
return f"Type: {type_name} ({range_info})"
elif type_name:
return f"Type: {type_name}"
return ""
def _get_type_name(field_type: Any) -> str:
"""Get a user-friendly name for a field's type."""
if field_type is str:
return "string"
if field_type is int:
return "integer"
if field_type is float:
return "float"
if field_type is bool:
return "boolean"
if field_type is Path:
return "path"
return ""
def _get_range_info(field_info: FieldInfo) -> str:
"""Get a string describing the numeric range of a field."""
constraints = {}
if hasattr(field_info, "metadata") and field_info.metadata:
for constraint in field_info.metadata:
constraint_type = type(constraint).__name__
if constraint_type == "Ge" and hasattr(constraint, "ge"):
constraints["min"] = constraint.ge
elif constraint_type == "Le" and hasattr(constraint, "le"):
constraints["max"] = constraint.le
elif constraint_type == "Gt" and hasattr(constraint, "gt"):
constraints["min"] = constraint.gt + 1
elif constraint_type == "Lt" and hasattr(constraint, "lt"):
constraints["max"] = constraint.lt - 1
if constraints:
min_val = constraints.get("min", "N/A")
max_val = constraints.get("max", "N/A")
return f"Range: {min_val}-{max_val}"
return ""

View File

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

View File

@@ -1,7 +0,0 @@
"""
Core utilities for FastAnime application.
This module provides various utility classes and functions used throughout
the FastAnime application, including concurrency management, file operations,
and other common functionality.
"""

View File

@@ -1,5 +1,5 @@
{
description = "FastAnime Project Flake";
description = "Viu Project Flake";
inputs = {
# The nixpkgs unstable latest commit breaks the plyer python package
@@ -23,7 +23,7 @@
in
{
packages.default = python3Packages.buildPythonApplication {
pname = "fastanime";
pname = "viu";
inherit version;
pyproject = true;
@@ -67,13 +67,11 @@
# Needs to be adapted for the nix derivation build
doCheck = false;
pythonImportsCheck = [ "fastanime" ];
meta = {
description = "Your browser anime experience from the terminal";
homepage = "https://github.com/Benexl/FastAnime";
changelog = "https://github.com/Benexl/FastAnime/releases/tag/v${version}";
mainProgram = "fastanime";
homepage = "https://github.com/Benexl/Viu";
changelog = "https://github.com/Benexl/Viu/releases/tag/v${version}";
mainProgram = "viu";
license = lib.licenses.unlicense;
maintainers = with lib.maintainers; [ theobori ];
};

View File

@@ -1,27 +1,30 @@
[project]
name = "fastanime"
version = "3.1.0"
name = "viu-media"
version = "3.2.7"
description = "A browser anime site experience from the terminal"
license = "UNLICENSE"
readme = "README.md"
requires-python = ">=3.10"
requires-python = ">=3.11"
dependencies = [
"click>=8.1.7",
"httpx>=0.28.1",
"inquirerpy>=0.3.4",
"pydantic>=2.11.7",
"rich>=13.9.2",
"tomli-w>=1.0.0",
]
[project.scripts]
fastanime = 'fastanime:Cli'
viu = 'viu_media:Cli'
[project.optional-dependencies]
standard = [
"thefuzz>=0.22.1",
"yt-dlp>=2025.7.21",
"pycryptodomex>=3.23.0",
"dbus-python>=1.4.0",
"pypiwin32; sys_platform == 'win32'", # For Windows-specific functionality
"pyobjc; sys_platform == 'darwin'", # For macOS-specific functionality
"dbus-python; sys_platform == 'linux'", # For Linux-specific functionality (e.g., notifications),
"plyer>=2.1.0",
"lxml>=6.0.0"
]

View File

@@ -1,5 +1,5 @@
{
"venvPath": ".",
"venv": ".venv",
"pythonVersion": "3.10"
"pythonVersion": "3.11"
}

View File

@@ -1,7 +1,7 @@
[tox]
requires =
tox>=4
env_list = lint, pyright, py{310,311}
env_list = lint, pyright, py{311,312}
[testenv]
description = run unit tests

3103
uv.lock generated

File diff suppressed because it is too large Load Diff

2
fa → viu Executable file → Normal file
View File

@@ -3,4 +3,4 @@ provider_type=$1
provider_name=$2
[ -z "$provider_type" ] && echo "Please specify provider type" && exit
[ -z "$provider_name" ] && echo "Please specify provider type" && exit
uv run python -m fastanime.libs.provider.${provider_type}.${provider_name}.provider
uv run python -m viu_media.libs.provider.${provider_type}.${provider_name}.provider

View File

@@ -1,8 +1,8 @@
import sys
if sys.version_info < (3, 10):
if sys.version_info < (3, 11):
raise ImportError(
"You are using an unsupported version of Python. Only Python versions 3.10 and above are supported by FastAnime"
"You are using an unsupported version of Python. Only Python versions 3.10 and above are supported by Viu"
) # noqa: F541

View File

@@ -0,0 +1,6 @@
██╗░░░██╗██╗██╗░░░██╗
██║░░░██║██║██║░░░██║
╚██╗░██╔╝██║██║░░░██║
░╚████╔╝░██║██║░░░██║
░░╚██╔╝░░██║╚██████╔╝
░░░╚═╝░░░╚═╝░╚═════╝░

View File

@@ -1,5 +1,5 @@
/**
* Rofi Theme: FastAnime "Tokyo Night" Confirmation
* Rofi Theme: Viu "Tokyo Night" Confirmation
* Author: Gemini ft Benexl
* Description: A compact and clear modal dialog for Yes/No confirmations that displays a prompt.
*/

View File

@@ -1,5 +1,5 @@
/**
* Rofi Theme: FastAnime "Tokyo Night" Input
* Rofi Theme: Viu "Tokyo Night" Input
* Author: Gemini ft Benexl
* Description: A compact, modern modal dialog for text input that correctly displays the prompt.
*/

View File

@@ -1,5 +1,5 @@
/**
* Rofi Theme: FastAnime "Tokyo Night" Main
* Rofi Theme: Viu "Tokyo Night" Main
* Author: Gemini ft Benexl
* Description: A sharp, modern, and ultra-compact theme with a Tokyo Night palette.
*/

View File

@@ -1,5 +1,5 @@
/**
* Rofi Theme: FastAnime "Tokyo Night" Horizontal Strip
* Rofi Theme: Viu "Tokyo Night" Horizontal Strip
* Author: Gemini ft Benexl
* Description: A fullscreen, horizontal, icon-centric theme for previews.
*/

View File

@@ -1,13 +1,13 @@
# values in {NAME} syntax are provided by python using .replace()
#
[Unit]
Description=FastAnime Background Worker
Description=Viu Background Worker
After=network-online.target
[Service]
Type=simple
# Ensure you have the full path to your fastanime executable
# Use `which fastanime` to find it
# Ensure you have the full path to your viu executable
# Use `which viu` to find it
ExecStart={EXECUTABLE} worker --log
Restart=always
RestartSec=30

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Before

Width:  |  Height:  |  Size: 276 KiB

After

Width:  |  Height:  |  Size: 276 KiB

View File

@@ -1,6 +1,6 @@
#!/bin/sh
#
# FastAnime Airing Schedule Info Script Template
# Viu Airing Schedule Info Script Template
# This script formats and displays airing schedule details in the FZF preview pane.
# Python injects the actual data values into the placeholders.

View File

@@ -1,6 +1,6 @@
#!/bin/sh
#
# FastAnime Character Info Script Template
# Viu Character Info Script Template
# This script formats and displays character details in the FZF preview pane.
# Python injects the actual data values into the placeholders.

View File

@@ -1,6 +1,6 @@
#!/bin/sh
#
# FastAnime Preview Info Script Template
# Viu Preview Info Script Template
# This script formats and displays the textual information in the FZF preview pane.
# Some values are injected by python those with '{name}' syntax using .replace()

View File

@@ -1,6 +1,6 @@
#!/bin/sh
#
# FastAnime Review Info Script Template
# Viu Review Info Script Template
# This script formats and displays review details in the FZF preview pane.
# Python injects the actual data values into the placeholders.

View File

@@ -6,7 +6,7 @@ import click
from click.core import ParameterSource
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 .options import options_from_model
from .utils.exception import setup_exceptions_handler
@@ -38,14 +38,17 @@ commands = {
"registry": "registry.registry",
"worker": "worker.worker",
"queue": "queue.queue",
"completions": "completions.completions",
"plugin": "plugin.plugin",
}
@click.group(
cls=LazyGroup,
root="fastanime.cli.commands",
root="viu_media.cli.commands",
invoke_without_command=True,
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.option("--no-config", is_flag=True, help="Don't load the user config file.")
@@ -68,7 +71,7 @@ commands = {
@click.pass_context
def cli(ctx: click.Context, **options: "Unpack[Options]"):
"""
The main entry point for the FastAnime CLI.
The main entry point for the Viu CLI.
"""
setup_logging(options["log"])
setup_exceptions_handler(
@@ -106,3 +109,50 @@ def cli(ctx: click.Context, **options: "Unpack[Options]"):
else loader.load(cli_overrides)
)
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:
from .commands.anilist import cmd
ctx.invoke(cmd.anilist)

View File

@@ -18,7 +18,7 @@ commands = {
@click.group(
cls=LazyGroup,
name="anilist",
root="fastanime.cli.commands.anilist.commands",
root="viu_media.cli.commands.anilist.commands",
invoke_without_command=True,
help="A beautiful interface that gives you access to a commplete streaming experience",
short_help="Access all streaming options",

View File

@@ -45,7 +45,9 @@ def auth(config: AppConfig, status: bool, logout: bool):
open_success = webbrowser.open(ANILIST_AUTH, new=2)
if open_success:
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:
feedback.warning(
f"Failed to open the browser. Please visit the site manually [magenta][link={ANILIST_AUTH}]here[/link][/magenta]."

View File

@@ -1,10 +1,10 @@
from typing import TYPE_CHECKING, Dict, List
import click
from fastanime.cli.utils.completion import anime_titles_shell_complete
from fastanime.core.config import AppConfig
from fastanime.core.exceptions import FastAnimeError
from fastanime.libs.media_api.types import (
from viu_media.cli.utils.completion import anime_titles_shell_complete
from viu_media.core.config import AppConfig
from viu_media.core.exceptions import ViuError
from viu_media.libs.media_api.types import (
MediaFormat,
MediaGenre,
MediaItem,
@@ -112,15 +112,15 @@ if TYPE_CHECKING:
)
@click.pass_obj
def download(config: AppConfig, **options: "Unpack[DownloadOptions]"):
from fastanime.cli.service.download.service import DownloadService
from fastanime.cli.service.feedback import FeedbackService
from fastanime.cli.service.registry import MediaRegistryService
from fastanime.cli.service.watch_history import WatchHistoryService
from fastanime.cli.utils.parser import parse_episode_range
from fastanime.libs.media_api.api import create_api_client
from fastanime.libs.media_api.params import MediaSearchParams
from fastanime.libs.provider.anime.provider import create_provider
from fastanime.libs.selectors import create_selector
from viu_media.cli.service.download.service import DownloadService
from viu_media.cli.service.feedback import FeedbackService
from viu_media.cli.service.registry import MediaRegistryService
from viu_media.cli.service.watch_history import WatchHistoryService
from viu_media.cli.utils.parser import parse_episode_range
from viu_media.libs.media_api.api import create_api_client
from viu_media.libs.media_api.params import MediaSearchParams
from viu_media.libs.provider.anime.provider import create_provider
from viu_media.libs.selectors import create_selector
from rich.progress import Progress
feedback = FeedbackService(config)
@@ -181,7 +181,7 @@ def download(config: AppConfig, **options: "Unpack[DownloadOptions]"):
search_result = media_api.search_media(search_params)
if not search_result or not search_result.media:
raise FastAnimeError("No anime found matching your search criteria.")
raise ViuError("No anime found matching your search criteria.")
anime_to_download: List[MediaItem]
if options.get("yes"):
@@ -219,7 +219,7 @@ def download(config: AppConfig, **options: "Unpack[DownloadOptions]"):
total_downloaded = 0
episode_range_str = options.get("episode_range")
if not episode_range_str:
raise FastAnimeError("--episode-range is required.")
raise ViuError("--episode-range is required.")
for media_item in anime_to_download:
watch_history.add_media_to_list_if_not_present(media_item)
@@ -259,7 +259,7 @@ def download(config: AppConfig, **options: "Unpack[DownloadOptions]"):
f"Finished. Successfully downloaded a total of {total_downloaded} episodes."
)
except FastAnimeError as e:
except ViuError as e:
feedback.error("Download command failed", str(e))
except Exception as e:
feedback.error("An unexpected error occurred", str(e))

View File

@@ -1,5 +1,4 @@
import json
from typing import TYPE_CHECKING
import click

View File

@@ -1,5 +1,5 @@
import click
from fastanime.core.config import AppConfig
from viu_media.core.config import AppConfig
from rich.console import Console
from rich.table import Table
@@ -11,8 +11,8 @@ def notifications(config: AppConfig):
Displays unread notifications from AniList.
Running this command will also mark the notifications as read on the AniList website.
"""
from fastanime.cli.service.feedback import FeedbackService
from fastanime.libs.media_api.api import create_api_client
from viu_media.cli.service.feedback import FeedbackService
from viu_media.libs.media_api.api import create_api_client
from ....service.auth import AuthService
@@ -25,7 +25,7 @@ def notifications(config: AppConfig):
if not api_client.is_authenticated():
feedback.error(
"Authentication Required", "Please log in with 'fastanime anilist auth'."
"Authentication Required", "Please log in with 'viu anilist auth'."
)
return

View File

@@ -3,7 +3,7 @@ from typing import TYPE_CHECKING
import click
from .....core.config import AppConfig
from .....core.exceptions import FastAnimeError
from .....core.exceptions import ViuError
from .....libs.media_api.types import (
MediaFormat,
MediaGenre,
@@ -235,14 +235,14 @@ def search(config: AppConfig, **options: "Unpack[SearchOptions]"):
and score_lesser is not None
and score_greater > score_lesser
):
raise FastAnimeError("Minimum score cannot be higher than maximum score")
raise ViuError("Minimum score cannot be higher than maximum score")
if (
popularity_greater is not None
and popularity_lesser is not None
and popularity_greater > popularity_lesser
):
raise FastAnimeError(
raise ViuError(
"Minimum popularity cannot be higher than maximum popularity"
)
@@ -251,18 +251,14 @@ def search(config: AppConfig, **options: "Unpack[SearchOptions]"):
and start_date_lesser is not None
and start_date_greater > start_date_lesser
):
raise FastAnimeError(
"Start date greater cannot be later than start date lesser"
)
raise ViuError("Start date greater cannot be later than start date lesser")
if (
end_date_greater is not None
and end_date_lesser is not None
and end_date_greater > end_date_lesser
):
raise FastAnimeError(
"End date greater cannot be later than end date lesser"
)
raise ViuError("End date greater cannot be later than end date lesser")
# Build search parameters
search_params = MediaSearchParams(
@@ -297,7 +293,7 @@ def search(config: AppConfig, **options: "Unpack[SearchOptions]"):
search_result = api_client.search_media(search_params)
if not search_result or not search_result.media:
raise FastAnimeError("No anime found matching your search criteria")
raise ViuError("No anime found matching your search criteria")
if dump_json:
# Use Pydantic's built-in serialization
@@ -326,7 +322,7 @@ def search(config: AppConfig, **options: "Unpack[SearchOptions]"):
session.load_menus_from_folder("media")
session.run(config, history=[initial_state])
except FastAnimeError as e:
except ViuError as e:
feedback.error("Search failed", str(e))
raise click.Abort()
except Exception as e:

View File

@@ -3,7 +3,7 @@ from typing import TYPE_CHECKING
import click
if TYPE_CHECKING:
from fastanime.core.config import AppConfig
from viu_media.core.config import AppConfig
@click.command(help="Print out your anilist stats")
@@ -38,7 +38,7 @@ def stats(config: "AppConfig"):
)
feedback.info(
"Run this command to authenticate:",
f"fastanime {config.general.media_api} auth",
f"viu {config.general.media_api} auth",
)
raise click.Abort()

View File

@@ -0,0 +1,169 @@
download = """
\b
\b\bExamples:
# Basic download by title
viu anilist download -t "Attack on Titan"
\b
# Download specific episodes
viu anilist download -t "One Piece" --episode-range "1-10"
\b
# Download single episode
viu anilist download -t "Death Note" --episode-range "1"
\b
# Download multiple specific episodes
viu anilist download -t "Naruto" --episode-range "1,5,10"
\b
# Download with quality preference
viu anilist download -t "Death Note" --quality 1080 --episode-range "1-5"
\b
# Download with multiple filters
viu anilist download -g Action -T Isekai --score-greater 80 --status RELEASING
\b
# Download with concurrent downloads
viu anilist download -t "Demon Slayer" --episode-range "1-5" --max-concurrent 3
\b
# Force redownload existing episodes
viu anilist download -t "Your Name" --episode-range "1" --force-redownload
\b
# Download from a specific season and year
viu anilist download --season WINTER --year 2024 -s POPULARITY_DESC
\b
# Download with genre filtering
viu anilist download -g Action -g Adventure --score-greater 75
\b
# Download only completed series
viu anilist download -g Fantasy --status FINISHED --score-greater 75
\b
# Download movies only
viu anilist download -F MOVIE -s SCORE_DESC --quality best
"""
search = """
\b
\b\bExamples:
# Basic search by title
viu anilist search -t "Attack on Titan"
\b
# Search with multiple filters
viu anilist search -g Action -T Isekai --score-greater 75 --status RELEASING
\b
# Get anime with the tag of isekai
viu anilist search -T isekai
\b
# Get anime of 2024 and sort by popularity, finished or releasing, not in your list
viu anilist search -y 2024 -s POPULARITY_DESC --status RELEASING --status FINISHED --not-on-list
\b
# Get anime of 2024 season WINTER
viu anilist search -y 2024 --season WINTER
\b
# Get anime genre action and tag isekai,magic
viu anilist search -g Action -T Isekai -T Magic
\b
# Get anime of 2024 thats finished airing
viu anilist search -y 2024 -S FINISHED
\b
# Get the most favourite anime movies
viu anilist search -f MOVIE -s FAVOURITES_DESC
\b
# Search with score and popularity filters
viu anilist search --score-greater 80 --popularity-greater 50000
\b
# Search excluding certain genres and tags
viu anilist search --genres-not Ecchi --tags-not "Hentai"
\b
# Search with date ranges (YYYYMMDD format)
viu anilist search --start-date-greater 20200101 --start-date-lesser 20241231
\b
# Get only TV series, exclude certain statuses
viu anilist search -f TV --status-not CANCELLED --status-not HIATUS
\b
# Paginated search with custom page size
viu anilist search -g Action --page 2 --per-page 25
\b
# Search for manga specifically
viu anilist search --media-type MANGA -g Fantasy
\b
# Complex search with multiple criteria
viu anilist search -t "demon" -g Action -g Supernatural --score-greater 70 --year 2020 -s SCORE_DESC
\b
# Dump search results as JSON instead of interactive mode
viu anilist search -g Action --dump-json
"""
main = """
\b
\b\bExamples:
# ---- search ----
\b
# Basic search by title
viu anilist search -t "Attack on Titan"
\b
# Search with multiple filters
viu anilist search -g Action -T Isekai --score-greater 75 --status RELEASING
\b
# Get anime with the tag of isekai
viu anilist search -T isekai
\b
# Get anime of 2024 and sort by popularity, finished or releasing, not in your list
viu anilist search -y 2024 -s POPULARITY_DESC --status RELEASING --status FINISHED --not-on-list
\b
# Get anime of 2024 season WINTER
viu anilist search -y 2024 --season WINTER
\b
# Get anime genre action and tag isekai,magic
viu anilist search -g Action -T Isekai -T Magic
\b
# Get anime of 2024 thats finished airing
viu anilist search -y 2024 -S FINISHED
\b
# Get the most favourite anime movies
viu anilist search -f MOVIE -s FAVOURITES_DESC
\b
# Search with score and popularity filters
viu anilist search --score-greater 80 --popularity-greater 50000
\b
# Search excluding certain genres and tags
viu anilist search --genres-not Ecchi --tags-not "Hentai"
\b
# Search with date ranges (YYYYMMDD format)
viu anilist search --start-date-greater 20200101 --start-date-lesser 20241231
\b
# Get only TV series, exclude certain statuses
viu anilist search -f TV --status-not CANCELLED --status-not HIATUS
\b
# Paginated search with custom page size
viu anilist search -g Action --page 2 --per-page 25
\b
# Search for manga specifically
viu anilist search --media-type MANGA -g Fantasy
\b
# Complex search with multiple criteria
viu anilist search -t "demon" -g Action -g Supernatural --score-greater 70 --year 2020 -s SCORE_DESC
\b
# Dump search results as JSON instead of interactive mode
viu anilist search -g Action --dump-json
\b
# ---- login ----
\b
# To sign in just run
viu anilist auth
\b
# To check your login status
viu anilist auth --status
\b
# To log out and erase credentials
viu anilist auth --logout
\b
# ---- notifier ----
\b
# basic form
viu anilist notifier
\b
# with logging to stdout
viu --log anilist notifier
\b
# with logging to a file. stored in the same place as your config
viu --log-file anilist notifier
"""

View File

@@ -7,16 +7,16 @@ import click
\b
\b\bExamples:
# try to detect your shell and print completions
fastanime completions
viu completions
\b
# print fish completions
fastanime completions --fish
viu completions --fish
\b
# print bash completions
fastanime completions --bash
viu completions --bash
\b
# print zsh completions
fastanime completions --zsh
viu completions --zsh
""",
)
@click.option("--fish", is_flag=True, help="print fish completions")
@@ -40,8 +40,8 @@ def completions(fish, zsh, bash):
if fish or (current_shell == "fish" and not zsh and not bash):
print(
"""
function _fastanime_completion;
set -l response (env _FASTANIME_COMPLETE=fish_complete COMP_WORDS=(commandline -cp) COMP_CWORD=(commandline -t) fastanime);
function _viu_completion;
set -l response (env _VIU_COMPLETE=fish_complete COMP_WORDS=(commandline -cp) COMP_CWORD=(commandline -t) viu);
for completion in $response;
set -l metadata (string split "," $completion);
@@ -56,21 +56,21 @@ function _fastanime_completion;
end;
end;
complete --no-files --command fastanime --arguments "(_fastanime_completion)";
complete --no-files --command viu --arguments "(_viu_completion)";
"""
)
elif zsh or (current_shell == "zsh" and not bash):
print(
"""
#compdef fastanime
#compdef viu
_fastanime_completion() {
_viu_completion() {
local -a completions
local -a completions_with_descriptions
local -a response
(( ! $+commands[fastanime] )) && return 1
(( ! $+commands[viu] )) && return 1
response=("${(@f)$(env COMP_WORDS="${words[*]}" COMP_CWORD=$((CURRENT-1)) _FASTANIME_COMPLETE=zsh_complete fastanime)}")
response=("${(@f)$(env COMP_WORDS="${words[*]}" COMP_CWORD=$((CURRENT-1)) _VIU_COMPLETE=zsh_complete viu)}")
for type key descr in ${response}; do
if [[ "$type" == "plain" ]]; then
@@ -97,21 +97,21 @@ _fastanime_completion() {
if [[ $zsh_eval_context[-1] == loadautofunc ]]; then
# autoload from fpath, call function directly
_fastanime_completion "$@"
_viu_completion "$@"
else
# eval/source/. command, register function for later
compdef _fastanime_completion fastanime
compdef _viu_completion viu
fi
"""
)
elif bash or current_shell == "bash":
print(
"""
_fastanime_completion() {
_viu_completion() {
local IFS=$'\n'
local response
response=$(env COMP_WORDS="${COMP_WORDS[*]}" COMP_CWORD=$COMP_CWORD _FASTANIME_COMPLETE=bash_complete $1)
response=$(env COMP_WORDS="${COMP_WORDS[*]}" COMP_CWORD=$COMP_CWORD _VIU_COMPLETE=bash_complete $1)
for completion in $response; do
IFS=',' read type value <<< "$completion"
@@ -130,11 +130,11 @@ _fastanime_completion() {
return 0
}
_fastanime_completion_setup() {
complete -o nosort -F _fastanime_completion fastanime
_viu_completion_setup() {
complete -o nosort -F _viu_completion viu
}
_fastanime_completion_setup;
_viu_completion_setup;
"""
)
else:

Some files were not shown because too many files have changed in this diff Show More