Compare commits

...

53 Commits

Author SHA1 Message Date
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
Benexl
cd16ab50e3 feat(rofi-selector): use plyer for notifications when using it 2025-08-15 13:08:33 +03:00
Benexl
b53a7d9b03 chore: bump version 2025-08-15 12:31:13 +03:00
Benexl
2b9fdb99b1 chore: priority to the enabled lol 2025-08-13 16:27:42 +03:00
Benexl
9c6e1877ed feat(rofi-selector): force exit on no input 2025-08-12 16:56:18 +03:00
Benexl
73bb77fe46 feat(rofi-theme-defaults): enhance ui and ux 2025-08-12 16:50:21 +03:00
Benexl
4cdc5bfd34 feat(config): make desktop notification duration configurable 2025-08-12 14:45:35 +03:00
Benexl
ca491d95a0 chore: update deps 2025-08-12 14:36:41 +03:00
Benexl
1003f75db3 feat(assets): update logo 2025-08-12 14:32:53 +03:00
Benexl
5821c4ca97 Update README.md 2025-08-12 13:46:05 +03:00
Benexl
de774a58d2 feat(anilist-downloads-command): create it 2025-08-12 11:40:20 +03:00
Benexl
ee25cbba10 fix(normalizer): convert media api title to lower 2025-08-12 10:57:15 +03:00
Benexl
278a771f64 Merge pull request #119 from Benexl/minor-fixes
fix: add back missing yt-dlp's "external_downloader" logic
2025-08-12 10:37:50 +03:00
Type-Delta
0d8c287e2f Merge branch 'master' of https://github.com/Benexl/FastAnime into minor-fixes 2025-08-12 14:35:52 +07:00
Type-Delta
74308dfdc5 fix: add back missing yt-dlp's "external_downloader" logic 2025-08-12 14:35:23 +07:00
Benexl
7ca1b8572e fix(rofi-previews): ensure the cover image is used if episode image unavailable 2025-08-12 02:26:19 +03:00
Benexl
54aed9e5a0 feat(config-command): use title case for the desktop entry title 2025-08-12 02:25:25 +03:00
Benexl
4511d14e8b feat(previews): implement rofi image preview logic 2025-08-12 02:11:27 +03:00
Benexl
bff684e8cb refactor(config-command): rename desktop entry option to generate-desktop-entry 2025-08-12 02:11:04 +03:00
Benexl
cfc83450c8 feat(config): include possible values for pygment style 2025-08-12 01:07:06 +03:00
Benexl
04a6a425b7 feat(config): show possible types and possible values in config comments 2025-08-12 00:55:51 +03:00
Benexl
088d232bfd feat(normalizer): add user normalizer json 2025-08-12 00:39:43 +03:00
Benexl
03fd8c0bf8 feat(selectors): complete contracts 2025-08-11 23:57:34 +03:00
Benexl
17f1744025 feat(watch-history): intelligently switch to repeating if was initially completed 2025-08-11 23:38:24 +03:00
Benexl
9a5f3d46be feat(download-service): notify on completed episode download if plyer available 2025-08-11 23:37:50 +03:00
Benexl
66eb854da5 feat: the worker command lol 2025-08-11 22:42:13 +03:00
Benexl
ae62adf233 feat: scaffhold worker command with gpt 5, it is actually pretty good lol 2025-08-11 22:42:13 +03:00
Benexl
55a7c7facf Merge pull request #117 from Benexl/minor-fixes
fix: anilist auth failed to open link on Windows
2025-08-08 11:17:49 +03:00
Type-Delta
2340c34d02 fix: anilist auth failed to open link on Windows
Switches to using the standard web browser module for opening the authentication URL, providing clearer feedback to the user about browser launch success or failure.
2025-08-08 12:28:23 +07:00
Benexl
40b29ba6e5 Merge pull request #115 from Benexl/minor-fixes
fix: anilist auth example
2025-08-07 13:14:48 +03:00
Type-Delta
5dc768f7e8 fix: anilist auth example 2025-08-07 15:19:38 +07:00
Benexl
b343bfb645 docs: enhance docstrings across player modules for clarity and completeness 2025-08-05 12:11:02 +03:00
Benexl
37773265ce Merge pull request #111 from khachbe/bugfix_formatter
Use int() cast instead of is_integer() for renumbered_val
2025-08-03 15:19:03 +03:00
khachbe
70ef1bf633 Merge branch 'master' into bugfix_formatter 2025-08-03 13:58:54 +02:00
khachbe
bee97acd35 Update formatter.py 2025-08-03 13:58:12 +02:00
Benexl
fb61fd17f1 fix(preview-script): quote $value to prevent interpretation of special characters 2025-08-02 14:01:30 +03:00
Benexl
98fff7d00f chore: bump lockfile 2025-08-02 14:00:47 +03:00
Benexl
3cc9ae50b6 Merge pull request #112 from khachbe/bugfix_typedict
Add list to AllAnimeEpisodeStreams
2025-08-02 13:46:37 +03:00
khachbe
26f7de172a add list to AllAnimeEpisodeStreams 2025-08-01 11:46:05 +02:00
khachbe
673b6280e4 Use int() cast instead of is_integer() for renumbered_val 2025-08-01 11:20:57 +02:00
Benexl
7943dcc3db Merge pull request #110 from theobori/fix/ruff-check-errors
Fixed the `ruff check` command line errors
2025-07-30 22:38:25 +03:00
Théo Bori
49ee1f9bbd Fixed the ruff check command line errors 2025-07-30 18:38:15 +02:00
Benexl
fd80149e74 Merge pull request #109 from iMithrellas/preview-scaling
Add optional --scale-up flag for icat image previews
2025-07-30 00:49:09 +03:00
iMithrellas
7c11616bea fix(fzf-preview): Make --scale-up flag opt in instead of opt out 2025-07-29 23:20:56 +02:00
iMithrellas
b9130018ca Merge branch 'master' into preview-scaling 2025-07-29 21:59:06 +02:00
iMithrellas
70ade13017 feat(fzf-preview): make --scale-up flag a config option 2025-07-29 21:52:48 +02:00
iMithrellas
071c0daf4f feat(fzf-preview): add --scale-up flag so that previews fill the available space 2025-07-29 21:52:48 +02:00
288 changed files with 3802 additions and 1933 deletions

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

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

440
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,29 +8,41 @@
</p>
<div align="center">
![PyPI - Downloads](https://img.shields.io/pypi/dm/fastanime) ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/Benexl/FastAnime/test.yml?label=Tests)
![Discord](https://img.shields.io/discord/1250887070906323096?label=Discord)
![GitHub Issues or Pull Requests](https://img.shields.io/github/issues/Benexl/FastAnime)
![PyPI - License](https://img.shields.io/pypi/l/fastanime)
![Static Badge](https://img.shields.io/badge/lines%20of%20code-13k%2B-green)
[![PyPI - Version](https://img.shields.io/pypi/v/viu)](https://pypi.org/project/viu/)
[![PyPI - Downloads](https://img.shields.io/pypi/dm/viu)](https://pypi.org/project/viu/)
[![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/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>
<p align="center">
<a href="https://discord.gg/HBEmAwvbHV">
<img src="https://invidget.switchblade.xyz/C4rhMA4mmK">
</a>
<a href="https://discord.gg/HBEmAwvbHV">
<img src="https://invidget.switchblade.xyz/C4rhMA4mmK" alt="Discord Server Invite">
</a>
</p>
![fastanime](https://github.com/user-attachments/assets/9ab09f26-e4a8-4b70-a315-7def998cec63)
![viu](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" />
<b>Fzf:</b>
<img width="1346" height="710" alt="250815_13h29m15s_screenshot" src="https://github.com/user-attachments/assets/d8fb8473-a0fe-47b1-b112-5cd8bec51937" />
<img width="1346" height="710" alt="250815_13h29m43s_screenshot" src="https://github.com/user-attachments/assets/16a2555d-f81e-4044-9e65-e61205dfe899" />
<img width="1346" height="710" alt="250815_13h30m09s_screenshot" src="https://github.com/user-attachments/assets/f521670a-c04f-4f5e-a62a-6c849fbf49bd" />
<img width="1346" height="710" alt="250815_13h30m33s_screenshot" src="https://github.com/user-attachments/assets/27fd2ef9-ec1f-4677-b816-038eaaca1391" />
<img width="1346" height="710" alt="250815_13h31m07s_screenshot" src="https://github.com/user-attachments/assets/6a64aa99-507e-449a-9e4a-9daa4fe496a3" />
<img width="1346" height="710" alt="250815_13h31m44s_screenshot" src="https://github.com/user-attachments/assets/a2896d1f-0e23-4ff3-b0c6-121d21a9f99a" />
<b>Rofi:</b>
<img width="1366" height="729" alt="250815_13h23m12s_screenshot" src="https://github.com/user-attachments/assets/6d18d950-11e5-41fc-a7fe-1f9eaa481e46" />
<img width="1366" height="765" alt="250815_13h24m09s_screenshot" src="https://github.com/user-attachments/assets/af852fee-17bf-4f24-ada9-7cf0e6f3451c" />
<img width="1366" height="768" alt="250815_13h24m57s_screenshot" src="https://github.com/user-attachments/assets/d3b4e2ab-10bd-40ae-88ed-0720b57957c1" />
<img width="1366" height="735" alt="250815_13h26m47s_screenshot" src="https://github.com/user-attachments/assets/64682b09-c88e-4d4c-ae26-a3aa34dd08a1" />
<img width="1366" height="768" alt="250815_13h28m05s_screenshot" src="https://github.com/user-attachments/assets/d6cd6931-0113-462c-86bb-abe6f3e12d68" />
</details>
@@ -53,309 +65,291 @@
</details>
## Core Features
* 📺 **Interactive TUI:** Browse, search, and manage your AniList library in a rich terminal interface powered by `fzf`, `rofi`, or a built-in selector.
***Powerful Search:** Filter the entire AniList database with over 20 different criteria, including genres, tags, year, status, and score.
* 💾 **Local Registry:** Maintain a fast, local database of your anime for offline access, detailed stats, and robust data management.
* ⚙️ **Background Downloader:** Queue episodes for download and let a persistent background worker handle the rest.
* 📜 **Scriptable CLI:** Automate streaming and downloading with powerful, non-interactive commands perfect for scripting.
* 🔧 **Highly Customizable:** Tailor every aspect—from UI colors and providers to playback behavior—via a simple, well-documented configuration file.
* 🔌 **Extensible Architecture:** Easily add new providers, media players, and UI selectors to fit your workflow.
## Installation
![Windows](https://img.shields.io/badge/-Windows_x64-blue.svg?style=for-the-badge&logo=windows)
![Linux/BSD](https://img.shields.io/badge/-Linux/BSD-red.svg?style=for-the-badge&logo=linux)
![Arch Linux](https://img.shields.io/badge/-Arch_Linux-black.svg?style=for-the-badge&logo=archlinux)
![MacOS](https://img.shields.io/badge/-MacOS-lightblue.svg?style=for-the-badge&logo=apple)
![Android](https://img.shields.io/badge/-Android-green.svg?style=for-the-badge&logo=android)
Viu runs on any platform with Python 3.10+, including Windows, macOS, Linux, and Android (via Termux).
The app runs wherever Python 3.10+ is available. On Android, you can use [Termux](https://github.com/termux/termux-app). For installation help, join our [Discord](https://discord.gg/HBEmAwvbHV).
### Prerequisites
### Installation on NixOS
For the best experience, please install these external tools:
![Static Badge](https://img.shields.io/badge/NixOs-black?style=flat&logo=nixos)
```bash
nix profile install github:Benexl/fastanime
```
### Installation on Arch Linux
![Static Badge](https://img.shields.io/badge/arch-black?style=flat&logo=archlinux)
Install from the AUR using an AUR helper like `yay` or `paru`.
```bash
# Stable version (recommended)
yay -S fastanime
# Git version (latest commit)
yay -S fastanime-git
```
* **Required for Streaming:**
* [**mpv**](https://mpv.io/installation/) - The primary and recommended media player.
* **Recommended for UI & Previews:**
* [**fzf**](https://github.com/junegunn/fzf) - For the best fuzzy-finder interface.
* [**chafa**](https://github.com/hpjansson/chafa) or [**kitty's icat**](https://sw.kovidgoyal.net/kitty/kittens/icat/) - For image previews in the terminal.
* **Recommended for Downloads & Advanced Features:**
* [**ffmpeg**](https://www.ffmpeg.org/) - Required for downloading HLS streams and merging subtitles.
* [**webtorrent-cli**](https://github.com/webtorrent/webtorrent-cli) - For streaming torrents directly.
### Recommended Installation (uv)
The recommended installation method is with [uv](https://docs.astral.sh/uv/), a 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 (recommended for the full experience)
uv tool install "fastanime[standard]"
# Install with all optional features for the full experience
uv tool install "viu[standard]"
# Stripped-down installations
uv tool install fastanime # Core functionality only
uv tool install "fastanime[download]" # For advanced downloading
uv tool install "fastanime[discord]" # For Discord Rich Presence
uv tool install "fastanime[notifications]" # For desktop notifications
# Or, pick and choose the extras you need:
uv tool install viu # Core functionality only
uv tool install "viu[download]" # For advanced downloading with yt-dlp
uv tool install "viu[discord]" # For Discord Rich Presence
uv tool install "viu[notifications]" # For desktop notifications
```
### Other Installation Methods
<details>
<summary><b>pipx or pip</b></summary>
<summary><b>Platform-Specific and Alternative Installers</b></summary>
#### Using pipx (Recommended for isolated environments)
#### Nix / NixOS
```bash
pipx install "fastanime[standard]"
nix profile install github:Benexl/viu
```
#### Arch Linux (AUR)
Use an AUR helper like `yay` or `paru`.
```bash
# Stable version (recommended)
yay -S viu
# Git version (latest commit)
yay -S viu-git
```
#### Using pipx (for isolated environments)
```bash
pipx install "viu[standard]"
```
#### Using pip
```bash
pip install "fastanime[standard]"
pip install "viu[standard]"
```
</details>
<details>
<summary><b>Bleeding Edge & Building from Source</b></summary>
<summary><b>Building from Source</b></summary>
### Installing the Bleeding Edge Version
Download the latest `fastanime_debug_build` artifact from the [GitHub Actions page](https://github.com/Benexl/FastAnime/actions), then:
Requires [Git](https://git-scm.com/), [Python 3.10+](https://www.python.org/), and [uv](https://astral.sh/blog/uv).
```bash
unzip fastanime_debug_build.zip
uv tool install fastanime-*.whl
```
### Building from Source
Requirements: [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.
### External Dependencies
## Getting Started: Quick Start
For the best experience, install these external tools:
Get up and running in three simple steps:
* **Required for Streaming:**
* [**mpv**](https://mpv.io/installation/) - The primary media player.
* **Recommended for UI & Previews:**
* [**fzf**](https://github.com/junegunn/fzf) - For a powerful fuzzy-finder interface.
* [**chafa**](https://github.com/hpjansson/chafa) or [**kitty's icat**](https://sw.kovidgoyal.net/kitty/kittens/icat/) - For image previews in the terminal.
* **Recommended for Downloads & Features:**
* [**ffmpeg**](https://www.ffmpeg.org/) - Required for downloading HLS streams.
* [**webtorrent-cli**](https://github.com/webtorrent/webtorrent-cli) - For streaming torrents.
* [**syncplay**](https://syncplay.pl/) - To watch anime together with friends.
* [**feh**](https://github.com/derf/feh) or **kitty's icat** - For the experimental manga mode.
1. **Authenticate with AniList:**
```bash
viu anilist auth
```
This will open your browser. Authorize the app and paste the obtained token back into the terminal.
## Usage
2. **Launch the Interactive TUI:**
```bash
viu anilist
```
FastAnime offers a rich interactive TUI for browsing and a powerful CLI for scripting and automation.
3. **Browse & Play:** Use your arrow keys to navigate the menus, select an anime, and choose an episode to stream instantly.
### Global Options
## Usage Guide
Most options can be passed directly to the `fastanime` command to override your config for that session.
* `--provider <allanime|animepahe>`: Choose the streaming site to use.
* `--selector <fzf|rofi|default>`: Choose the UI backend.
* `--preview`, `--no-preview`: Enable/disable image and info previews (requires `fzf`).
* `--dub`, `--sub`: Set preferred translation type.
* `--icons`, `--no-icons`: Toggle UI icons.
* `--log`, `--log-file`: Enable logging to stdout or a file for debugging.
* `--rich-traceback`: Show detailed, formatted tracebacks on error.
### Main Commands
* `fastanime anilist`: The main entry point for the interactive TUI. Browse, search, and manage your lists.
* `fastanime registry`: Manage your local database of anime. Sync, search, backup, and restore.
* `fastanime download`: Scriptable command to download specific episodes.
* `fastanime search`: Scriptable command to find and stream episodes directly.
* `fastanime config`: Manage your configuration file.
* `fastanime update`: Update FastAnime to the latest version.
* `fastanime queue`: Add episodes to the background download queue.
* `fastanime worker`: Run the background worker for downloads and notifications.
---
### Deep Dive: `fastanime anilist` (Interactive TUI)
This is the primary way to use FastAnime. Simply run `fastanime anilist` to launch a rich, interactive terminal experience. From here you can:
### The Interactive TUI (`viu anilist`)
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, etc.) after logging in with `fastanime anilist auth`.
* 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.
* Stream or download episodes directly from the menus.
#### `anilist search` Subcommand
### Powerful Searching (`viu anilist search`)
A powerful command to filter the AniList database directly from your terminal.
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
```
#### `anilist download` Subcommand
### Background Downloads (`viu queue` & `worker`)
Combines the power of `anilist search` with the `download` command, allowing you to batch-download based on filters.
Viu includes a robust background downloading system.
```bash
# Download episodes 1-12 of all fantasy anime that aired in Winter 2024
fastanime anilist download --season WINTER -y 2024 -g Fantasy -r "0:12"
```
1. **Add episodes to the queue:**
```bash
# Add episodes 1-12 of Jujutsu Kaisen to the download queue
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)
viu worker
---
# Or run it as a background process
viu worker &
```The worker will now process the queue, download your episodes, and check for notifications.
### Deep Dive: `fastanime registry` (Local Database)
### Scriptable Commands (`download` & `search`)
FastAnime maintains a local registry of your anime for offline access, enhanced performance, and powerful data management.
* `registry sync`: Synchronize your local data with your remote AniList account.
* `registry stats`: Show detailed statistics about your viewing habits.
* `registry search`: Search your locally stored anime data.
* `registry backup`: Create a compressed backup of your entire registry.
* `registry restore`: Restore your data from a backup file.
* `registry export/import`: Export your data to JSON/CSV for use in other applications.
---
### Scriptable Commands: `download` & `search`
These commands are designed for automation and quick access.
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 (`viu registry`)
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.
* `registry backup`: Create a compressed backup of your entire registry.
* `registry restore`: Restore your data from a backup file.
* `registry export/import`: Export/import your data to JSON/CSV for use in other applications.
* `registry clean`: Clean up orphaned or invalid entries from your local database.
## Configuration
Viu is highly customizable. A default configuration file with detailed comments is created on the first run.
* **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., `viu --provider animepahe anilist`).
<details>
<summary><b>Default Configuration (`config.ini`) Explained</b></summary>
```ini
# [general] Section: Controls overall application behavior.
[general]
provider = allanime ; The default anime provider (allanime, animepahe).
selector = fzf ; The interactive UI tool (fzf, rofi, default).
preview = full ; Preview type in selectors (full, text, image, none).
image_renderer = icat ; Tool for terminal image previews (icat, chafa).
icons = True ; Display emoji icons in the UI.
auto_select_anime_result = True ; Automatically select the best search match.
...
# [stream] Section: Controls playback and streaming.
[stream]
player = mpv ; The media player to use (mpv, vlc).
quality = 1080 ; Preferred stream quality (1080, 720, 480, 360).
translation_type = sub ; Preferred audio/subtitle type (sub, dub).
auto_next = False ; Automatically play the next episode.
continue_from_watch_history = True ; Resume playback from where you left off.
use_ipc = True ; Enable in-player controls via MPV's IPC.
...
# [downloads] Section: Controls the downloader.
[downloads]
downloader = auto ; Downloader to use (auto, default, yt-dlp).
downloads_dir = ... ; Directory to save downloaded anime.
max_concurrent_downloads = 3 ; Number of parallel downloads in the worker.
merge_subtitles = True ; Automatically merge subtitles into the video file.
cleanup_after_merge = True ; Delete original files after merging.
...
# [worker] Section: Controls the background worker process.
[worker]
enabled = True
notification_check_interval = 15 ; How often to check for new episodes (minutes).
download_check_interval = 5 ; How often to process the download queue (minutes).
...
```
</details>
## Advanced Features
### MPV IPC Integration
When `use_ipc` is enabled, FastAnime provides powerful in-player controls without closing MPV.
When `use_ipc = True` is set in your config, Viu provides powerful in-player controls without needing to close MPV.
#### Key Bindings
**Key Bindings:**
* `Shift+N`: Play the next episode.
* `Shift+P`: Play the previous episode.
* `Shift+R`: Reload the current episode.
* `Shift+A`: Toggle auto-play for the next episode.
* `Shift+T`: Toggle between `dub` and `sub`.
#### Script Messages (MPV Console)
**Script Messages (For MPV Console):**
* `script-message select-episode <number>`: Jump to a specific episode.
* `script-message select-server <name>`: Switch to a different streaming server.
## Configuration
### Running as a Service (Linux/systemd)
FastAnime is highly customizable via its configuration file, located at `~/.config/fastanime/config.ini` (path may vary by OS).
Run `fastanime config --path` to find the exact location on your system.
You can run the background worker as a systemd service for persistence.
A default configuration file with detailed comments is created on first run. You can edit it with `fastanime config` or use the interactive wizard with `fastanime config --interactive`.
1. Create a service file at `~/.config/systemd/user/viu-worker.service`:
```ini
[Unit]
Description=Viu Background Worker
After=network-online.target
<details>
<summary><b>Default Configuration (`config.ini`)</b></summary>
```ini
[general]
# The preferred watch history tracker (local,remote) in cases of conflicts
preferred_tracker = local
# The pygment style to use
pygment_style = github-dark
# The spinner to use
preferred_spinner = smiley
# The media database API to use (e.g., 'anilist', 'jikan').
media_api = anilist
# The default anime provider to use for scraping.
provider = allanime
# The interactive selector tool to use for menus.
selector = fzf
# Automatically select the best-matching search result from a provider.
auto_select_anime_result = True
# Display emoji icons in the user interface.
icons = True
# Type of preview to display in selectors.
preview = full
# The command-line tool to use for rendering images in the terminal.
image_renderer = icat
# The external application to use for viewing manga pages.
manga_viewer = feh
# Automatically check for new versions of FastAnime on startup.
check_for_updates = True
# Enable caching of network requests to speed up subsequent operations.
cache_requests = True
# Maximum lifetime for a cached request in DD:HH:MM format.
max_cache_lifetime = 03:00:00
# Attempt to normalize provider titles to match AniList titles.
normalize_titles = True
# Enable Discord Rich Presence to show your current activity.
discord = False
# Number of recently watched anime to keep in history.
recent = 50
[Service]
Type=simple
ExecStart=/path/to/your/viu worker --log
Restart=always
RestartSec=30
[stream]
# The media player to use for streaming.
player = mpv
# Preferred stream quality.
quality = 1080
# Preferred audio/subtitle language type.
translation_type = sub
# The default server to use from a provider. 'top' uses the first available.
server = TOP
# Automatically play the next episode when the current one finishes.
auto_next = False
# Automatically resume playback from the last known episode and position.
continue_from_watch_history = True
# Which watch history to prioritize: local file or remote AniList progress.
preferred_watch_history = local
# Automatically skip openings/endings if skip data is available.
auto_skip = False
# Percentage of an episode to watch before it's marked as complete.
episode_complete_at = 80
# The format selection string for yt-dlp.
ytdlp_format = best[height<=1080]/bestvideo[height<=1080]+bestaudio/best
# Prevent updating AniList progress to a lower episode number.
force_forward_tracking = True
# Default behavior for tracking progress on AniList.
default_media_list_tracking = prompt
# Preferred language code for subtitles (e.g., 'en', 'es').
sub_lang = eng
# Use IPC communication with the player for advanced features like episode navigation.
use_ipc = True
```
</details>
[Install]
WantedBy=default.target
```
*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 viu-worker.service
```
## Contributing
Pull requests are highly welcome! Please read our [**Contributing Guidelines**](CONTRIBUTIONS.md) to get started with setting up a development environment and understanding our coding standards.
Contributions are welcome! Whether it's reporting a bug, proposing a feature, or writing code, your help is appreciated. Please read our [**Contributing Guidelines**](CONTRIBUTIONS.md) to get started.
## Disclaimer
> [!IMPORTANT]
>
> This project scrapes public-facing websites (`allanime`, `animepahe`). The developer(s) of this application have no affiliation with these content providers. This application hosts zero content. Use at your own risk.
>
> [**Full Disclaimer**](DISCLAIMER.md)
> This project scrapes public-facing websites. The developer(s) of this application have no affiliation with these content providers. This application hosts zero content and is intended for educational and personal use only. Use at your own risk.
>
> [**Read the Full Disclaimer**](DISCLAIMER.md)

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 &&

2
fa 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.libs.provider.${provider_type}.${provider_name}.provider

View File

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

View File

@@ -1,55 +0,0 @@
configuration {
font: "Sans 12";
}
* {
background-color: rgba(0, 0, 0, 0.7);
text-color: #FFFFFF;
}
window {
fullscreen: true;
transparency: "real";
background-color: transparent;
}
mainbox {
children: [ message, listview, inputbar ];
padding: 40% 30%;
background-color: transparent;
}
message {
border: 0;
padding: 10px;
border-radius:20px;
margin: 0 0 20px 0;
font: "Sans Bold 24"; /* Increased font size and made it bold */
}
inputbar {
children: [ prompt, entry ];
background-color: rgba(255, 255, 255, 0.1);
padding: 8px;
border-radius: 4px;
}
prompt {
padding: 8px;
}
entry {
padding: 8px;
background-color: transparent;
}
listview {
lines: 0;
}
/* Style for the message text specifically */
textbox {
horizontal-align: 0.5; /* Center the text */
font: "Sans Bold 24"; /* Match message font */
background-color: transparent;
}

View File

@@ -1,55 +0,0 @@
configuration {
font: "Sans 12";
}
* {
background-color: rgba(0, 0, 0, 0.7);
text-color: #FFFFFF;
}
window {
fullscreen: true;
transparency: "real";
background-color: transparent;
}
mainbox {
children: [ message, listview, inputbar ];
padding: 40% 30%;
background-color: transparent;
}
message {
border: 0;
padding: 10px;
border-radius:20px;
margin: 0 0 20px 0;
font: "Sans Bold 24"; /* Increased font size and made it bold */
}
inputbar {
children: [ prompt, entry ];
background-color: rgba(255, 255, 255, 0.1);
padding: 8px;
border-radius: 4px;
}
prompt {
padding: 8px;
}
entry {
padding: 8px;
background-color: transparent;
}
listview {
lines: 0;
}
/* Style for the message text specifically */
textbox {
horizontal-align: 0.5; /* Center the text */
font: "Sans Bold 24"; /* Match message font */
background-color: transparent;
}

View File

@@ -1,80 +0,0 @@
configuration {
font: "Sans 12";
line-margin: 10;
display-drun: "";
}
* {
background: #000000; /* Black background for everything */
background-alt: #000000; /* Ensures no alternation */
foreground: #CCCCCC;
selected: #3584E4;
active: #2E7D32;
urgent: #C62828;
}
window {
fullscreen: false;
background-color: rgba(0, 0, 0, 0.8); /* Solid black transparent background */
border-radius: 50px;
}
mainbox {
padding: 50px 50px;
background-color: transparent; /* Ensures black background fills entire main area */
children: [inputbar, listview];
spacing: 20px;
}
inputbar {
background-color: #333333; /* Dark gray background for input bar */
padding: 8px;
border-radius: 8px;
children: [prompt, entry];
}
prompt {
enabled: true;
padding: 8px;
background-color: @selected;
text-color: #000000;
border-radius: 4px;
}
entry {
padding: 8px;
background-color: transparent; /* Slightly lighter gray for visibility */
text-color: #FFFFFF; /* White text to make typing visible */
placeholder: "Search...";
placeholder-color: rgba(255, 255, 255, 0.5);
border-radius: 6px;
}
listview {
layout: vertical;
spacing: 8px;
lines: 9;
background-color: transparent; /* Consistent black background for list items */
}
element {
padding: 12px;
border-radius: 4px;
background-color: transparent; /* Uniform color for each list item */
text-color: @foreground;
}
element normal.normal {
background-color: transparent; /* Ensures no alternating color */
}
element selected.normal {
background-color: @selected;
text-color: #FFFFFF;
}
element-text {
background-color: transparent;
text-color: inherit;
vertical-align: 0.5;
}

View File

@@ -1,120 +0,0 @@
// Colours
* {
background-color: transparent; /* Transparent background for the global UI */
background: #000000; /* Solid black background */
background-transparent: #1D2330A0; /* Semi-transparent background */
text-color: #BBBBBB; /* Default text color (light gray) */
text-color-selected: #FFFFFF; /* Text color when selected (white) */
primary: rgba(53, 132, 228, 0.75); /* Blusish primary color */
important: rgba(53, 132, 228, 0.75); /* Bluish primary color */
}
configuration {
font: "Roboto 14"; /* Sets the global font to Roboto, size 14 */
show-icons: true; /* Option to display icons in the UI */
}
window {
fullscreen: true; /* The window will open in fullscreen */
height: 100%; /* Full window height */
width: 100%; /* Full window width */
transparency: "real"; /* Real transparency effect */
background-color: @background-transparent; /* Transparent background */
border: 0px; /* No border around the window */
border-color: @primary; /* Border color set to the primary color */
}
mainbox {
children: [prompt, inputbar-box, listview]; /* Main box contains prompt, input bar, and list view */
padding: 0px; /* No padding around the main box */
}
prompt {
width: 100%; /* Prompt takes full width */
margin: 10px 0px 0px 30px; /* Margin around the prompt */
text-color: @important; /* Text color for prompt (important color) */
font: "Roboto Bold 27"; /* Bold Roboto font, size 27 */
}
listview {
layout: vertical; /* Vertical layout for list items */
padding: 10px; /* Padding inside the list view */
spacing: 20px; /* Space between items in the list */
columns: 8; /* Maximum 8 items per row */
dynamic: true; /* Allows the list to dynamically adjust */
orientation: horizontal; /* Horizontal orientation for list items */
}
inputbar-box {
children: [dummy, inputbar, dummy]; /* Input bar is centered with dummy placeholders */
orientation: horizontal; /* Horizontal layout for input bar */
expand: false; /* Does not expand to fill the space */
}
inputbar {
children: [textbox-prompt, entry]; /* Contains a prompt and an entry field */
margin: 0px; /* No margin around the input bar */
background-color: @primary; /* Background color set to the primary color */
border: 4px; /* Border thickness around the input bar */
border-color: @primary; /* Border color matches the primary color */
border-radius: 8px; /* Rounded corners for the input bar */
}
textbox-prompt {
text-color: @background; /* Text color inside prompt matches the background color */
horizontal-align: 0.5; /* Horizontally centered */
vertical-align: 0.5; /* Vertically centered */
expand: false; /* Does not expand to fill available space */
}
entry {
expand: false; /* Entry field does not expand */
padding: 8px; /* Padding inside the entry field */
margin: -6px; /* Negative margin to position entry properly */
horizontal-align: 0; /* Left-aligned text inside the entry field */
width: 300; /* Fixed width for the entry field */
background-color: @background; /* Entry background color matches the global background */
border: 6px; /* Border thickness around the entry field */
border-color: @primary; /* Border color matches the primary color */
border-radius: 8px; /* Rounded corners for the entry field */
cursor: text; /* Cursor changes to text input cursor inside the entry field */
}
element {
children: [dummy, element-box, dummy]; /* Contains an element box with dummy placeholders */
padding: 5px; /* Padding around the element */
orientation: vertical; /* Vertical layout for element content */
border: 0px; /* No border around the element */
border-radius: 16px; /* Rounded corners for the element */
background-color: transparent; /* Transparent background */
width: 100px; /* Width of each element */
height: 50px; /* Height of each element */
}
element selected {
background-color: @primary; /* Background color of the element when selected */
}
element-box {
children: [element-icon, element-text]; /* Element box contains an icon and text */
orientation: vertical; /* Vertical layout for icon and text */
expand: false; /* Does not expand to fill available space */
cursor: pointer; /* Cursor changes to a pointer when hovering over the element */
}
element-icon {
padding: 10px; /* Padding inside the icon */
cursor: inherit; /* Inherits cursor style from the parent */
size: 33%; /* Icon size is set to 33% of the parent element */
margin: 10px; /* Margin around the icon */
}
element-text {
horizontal-align: 0.5; /* Horizontally center-aligns the text */
cursor: inherit; /* Inherits cursor style from the parent */
text-color: @text-color; /* Text color for element text */
}
element-text selected {
text-color: @text-color-selected; /* Text color when the element is selected */
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 197 KiB

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 login
\b
# To view your login status
fastanime anilist login --status
\b
# To erase login data
fastanime anilist login --erase
\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,62 +0,0 @@
import click
from fastanime.core.config import AppConfig
from fastanime.core.exceptions import FastAnimeError
from fastanime.libs.media_api.params import MediaSearchParams
@click.command(help="Queue episodes for the background worker to download.")
@click.option(
"--title", "-t", required=True, multiple=True, help="Anime title to queue."
)
@click.option(
"--episode-range", "-r", required=True, help="Range of episodes (e.g., '1-10')."
)
@click.pass_obj
def queue(config: AppConfig, title: tuple, episode_range: str):
"""
Searches for an anime and adds the specified episodes to the download queue.
The background worker must be running for the downloads to start.
"""
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.utils.parser import parse_episode_range
from fastanime.libs.media_api.api import create_api_client
from fastanime.libs.provider.anime.provider import create_provider
feedback = FeedbackService(config)
media_api = create_api_client(config.general.media_api, config)
provider = create_provider(config.general.provider)
registry = MediaRegistryService(config.general.media_api, config.media_registry)
download_service = DownloadService(config, registry, media_api, provider)
for anime_title in title:
try:
feedback.info(f"Searching for '{anime_title}'...")
search_result = media_api.search_media(
MediaSearchParams(query=anime_title, per_page=1)
)
if not search_result or not search_result.media:
feedback.warning(f"Could not find '{anime_title}' on AniList.")
continue
media_item = search_result.media[0]
available_episodes = [str(i + 1) for i in range(media_item.episodes or 0)]
episodes_to_queue = list(
parse_episode_range(episode_range, available_episodes)
)
queued_count = 0
for ep in episodes_to_queue:
if download_service.add_to_queue(media_item, ep):
queued_count += 1
feedback.success(
f"Successfully queued {queued_count} episodes for '{media_item.title.english}'."
)
except FastAnimeError as e:
feedback.error(f"Failed to queue '{anime_title}'", str(e))
except Exception as e:
feedback.error("An unexpected error occurred", str(e))

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,83 +0,0 @@
import textwrap
from enum import Enum
from pathlib import Path
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."""
model_schema = AppConfig.model_json_schema(mode="serialization")
app_model_dict = app_model.model_dump()
config_ini_content = [CONFIG_HEADER]
for section_name, section_dict in app_model_dict.items():
section_ref = model_schema["properties"][section_name].get("$ref")
if not section_ref:
continue
section_class_name = section_ref.split("/")[-1]
section_schema = model_schema["$defs"][section_class_name]
section_comment = section_schema.get("description", "")
config_ini_content.append(f"\n#\n# {section_comment}\n#")
config_ini_content.append(f"[{section_name}]")
for field_name, field_value in section_dict.items():
field_properties = section_schema.get("properties", {}).get(field_name, {})
description = field_properties.get("description", "")
if description:
wrapped_comment = textwrap.fill(
description,
width=78,
initial_indent="# ",
subsequent_indent="# ",
)
config_ini_content.append(f"\n{wrapped_comment}")
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)

View File

@@ -1,103 +0,0 @@
from contextlib import contextmanager
from typing import Optional
import click
from rich.console import Console
from rich.progress import (
BarColumn,
Progress,
SpinnerColumn,
TaskProgressColumn,
TextColumn,
)
from ....core.config import AppConfig
console = Console()
class FeedbackService:
"""Centralized manager for user feedback in interactive menus."""
def __init__(self, config: AppConfig):
self.config = config
def success(self, message: str, details: Optional[str] = None) -> None:
"""Show a success message with optional details."""
icon = "" if self.config.general.icons else ""
main_msg = f"[bold green]{icon}{message}[/bold green]"
if details:
console.print(f"{main_msg}\n[dim]{details}[/dim]")
else:
console.print(main_msg)
def error(self, message: str, details: Optional[str] = None) -> None:
"""Show an error message with optional details."""
icon = "" if self.config.general.icons else ""
main_msg = f"[bold red]{icon}Error: {message}[/bold red]"
if details:
console.print(f"{main_msg}\n[dim]{details}[/dim]")
else:
console.print(main_msg)
click.pause("Enter to continue...")
def warning(self, message: str, details: Optional[str] = None) -> None:
"""Show a warning message with optional details."""
icon = "⚠️ " if self.config.general.icons else ""
main_msg = f"[bold yellow]{icon}Warning: {message}[/bold yellow]"
if details:
console.print(f"{main_msg}\n[dim]{details}[/dim]")
else:
console.print(main_msg)
def info(self, message: str, details: Optional[str] = None) -> None:
"""Show an informational message with optional details."""
icon = "" if self.config.general.icons else ""
main_msg = f"[bold blue]{icon}{message}[/bold blue]"
if details:
console.print(f"{main_msg}\n[dim]{details}[/dim]")
else:
console.print(main_msg)
# time.sleep(5)
@contextmanager
def progress(
self,
message: str,
total: Optional[float] = None,
transient: bool = False,
auto_add_task: bool = True,
success_msg: Optional[str] = None,
error_msg: Optional[str] = None,
):
"""Context manager for operations with loading indicator and result feedback."""
with Progress(
SpinnerColumn(self.config.general.preferred_spinner),
TextColumn(f"[cyan]{message}..."),
BarColumn(),
TaskProgressColumn(),
transient=transient,
console=console,
) as progress:
task_id = progress.add_task("", total=total)
try:
yield task_id, progress
if success_msg:
self.success(success_msg)
except Exception as e:
error_details = str(e) if str(e) else None
final_error_msg = error_msg or "Operation failed"
self.error(final_error_msg, error_details)
raise
def pause_for_user(self, message: str = "Press Enter to continue") -> None:
"""Pause execution and wait for user input."""
icon = "⏸️ " if self.config.general.icons else ""
click.pause(f"{icon}{message}...")
def clear_console(self):
console.clear()

View File

@@ -1,78 +0,0 @@
import json
import logging
from typing import Set
from fastanime.core.constants import APP_CACHE_DIR
from fastanime.libs.media_api.base import BaseApiClient
try:
import plyer
PLYER_AVAILABLE = True
except ImportError:
PLYER_AVAILABLE = False
logger = logging.getLogger(__name__)
SEEN_NOTIFICATIONS_CACHE = APP_CACHE_DIR / "seen_notifications.json"
class NotificationService:
def __init__(self, media_api: BaseApiClient):
self.media_api = media_api
self._seen_ids: Set[int] = self._load_seen_ids()
def _load_seen_ids(self) -> Set[int]:
if not SEEN_NOTIFICATIONS_CACHE.exists():
return set()
try:
with open(SEEN_NOTIFICATIONS_CACHE, "r") as f:
return set(json.load(f))
except (json.JSONDecodeError, IOError):
return set()
def _save_seen_ids(self):
try:
with open(SEEN_NOTIFICATIONS_CACHE, "w") as f:
json.dump(list(self._seen_ids), f)
except IOError:
logger.error("Failed to save seen notifications cache.")
def check_and_display_notifications(self):
if not PLYER_AVAILABLE:
logger.warning("plyer not installed. Cannot display desktop notifications.")
return
if not self.media_api.is_authenticated():
logger.info("Not authenticated, skipping notification check.")
return
logger.info("Checking for new notifications...")
notifications = self.media_api.get_notifications()
if not notifications:
logger.info("No new notifications found.")
return
new_notifications = [n for n in notifications if n.id not in self._seen_ids]
if not new_notifications:
logger.info("No unseen notifications found.")
return
for notif in new_notifications:
title = notif.media.title.english or notif.media.title.romaji
message = f"Episode {notif.episode} of {title} has aired!"
try:
plyer.notification.notify(
title="FastAnime: New Episode",
message=message,
app_name="FastAnime",
timeout=20,
)
logger.info(f"Displayed notification: {message}")
self._seen_ids.add(notif.id)
except Exception as e:
logger.error(f"Failed to display notification: {e}")
self._save_seen_ids()

View File

@@ -1,61 +0,0 @@
import logging
import time
from fastanime.cli.service.download.service import DownloadService
from fastanime.cli.service.notification.service import NotificationService
from fastanime.core.config.model import WorkerConfig
logger = logging.getLogger(__name__)
class BackgroundWorkerService:
def __init__(
self,
config: WorkerConfig,
notification_service: NotificationService,
download_service: DownloadService,
):
self.config = config
self.notification_service = notification_service
self.download_service = download_service
self.running = True
def run(self):
logger.info("Background worker started.")
last_notification_check = 0
last_download_check = 0
notification_interval_sec = self.config.notification_check_interval * 60
download_interval_sec = self.config.download_check_interval * 60
self.download_service.start()
try:
while self.running:
current_time = time.time()
# Check for notifications
if current_time - last_notification_check > notification_interval_sec:
try:
self.notification_service.check_and_display_notifications()
except Exception as e:
logger.error(f"Error during notification check: {e}")
last_notification_check = current_time
# Process download queue
if current_time - last_download_check > download_interval_sec:
try:
self.download_service.resume_unfinished_downloads()
except Exception as e:
logger.error(f"Error during download queue processing: {e}")
last_download_check = current_time
# Sleep for a short interval to prevent high CPU usage
time.sleep(30) # Sleep for 30 seconds before next check cycle
except KeyboardInterrupt:
logger.info("Background worker stopped by user.")
self.stop()
def stop(self):
self.running = False
logger.info("Background worker shutting down.")

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,27 +0,0 @@
import os
import shutil
import sys
def is_running_in_termux():
# Check environment variables
if os.environ.get("TERMUX_VERSION") is not None:
return True
# Check Python installation path
if sys.prefix.startswith("/data/data/com.termux/files/usr"):
return True
# Check for Termux-specific binary
if os.path.exists("/data/data/com.termux/files/usr/bin/termux-info"):
return True
return False
def is_running_kitty_terminal() -> bool:
return True if os.environ.get("KITTY_WINDOW_ID") else False
def has_fzf() -> bool:
return True if shutil.which("fzf") else False

View File

@@ -1,3 +0,0 @@
from .player import create_player
__all__ = ["create_player"]

View File

@@ -1,26 +0,0 @@
import subprocess
from abc import ABC, abstractmethod
from ...core.config import StreamConfig
from .params import PlayerParams
from .types import PlayerResult
class BasePlayer(ABC):
"""
Abstract Base Class defining the contract for all media players.
"""
def __init__(self, config: StreamConfig):
self.stream_config = config
@abstractmethod
def play(self, params: PlayerParams) -> PlayerResult:
"""
Plays the given media URL.
"""
pass
@abstractmethod
def play_with_ipc(self, params: PlayerParams, socket_path: str) -> subprocess.Popen:
"""Stream using IPC player for enhanced features."""

View File

@@ -1,17 +0,0 @@
from dataclasses import dataclass
from typing import TYPE_CHECKING
if TYPE_CHECKING:
pass
@dataclass(frozen=True)
class PlayerParams:
url: str
title: str
query: str
episode: str
syncplay: bool = False
subtitles: list[str] | None = None
headers: dict[str, str] | None = None
start_time: str | None = None

View File

@@ -1,8 +0,0 @@
from dataclasses import dataclass
@dataclass(frozen=True)
class PlayerResult:
episode: str
stop_time: str | None = None
total_time: str | None = None

View File

@@ -1,23 +0,0 @@
from InquirerPy.prompts import FuzzyPrompt
from rich.prompt import Confirm, Prompt
from ..base import BaseSelector
class InquirerSelector(BaseSelector):
def choose(self, prompt, choices, *, preview=None, header=None):
if header:
print(f"[bold cyan]{header}[/bold cyan]")
return FuzzyPrompt(
message=prompt,
choices=choices,
height="100%",
border=True,
validate=lambda result: result in choices,
).execute()
def confirm(self, prompt, *, default=False):
return Confirm.ask(prompt, default=default)
def ask(self, prompt, *, default=None):
return Prompt.ask(prompt=prompt, default=default or "")

View File

@@ -1,47 +0,0 @@
import shutil
import subprocess
from ....core.config import RofiConfig
from ..base import BaseSelector
class RofiSelector(BaseSelector):
def __init__(self, config: RofiConfig):
self.config = config
self.executable = shutil.which("rofi")
if not self.executable:
raise FileNotFoundError("rofi executable not found in PATH.")
def choose(self, prompt, choices, *, preview=None, header=None):
rofi_input = "\n".join(choices)
args = [
self.executable,
"-no-config",
"-theme",
self.config.theme_main,
"-p",
prompt,
"-i",
"-dmenu",
]
result = subprocess.run(
args,
input=rofi_input,
stdout=subprocess.PIPE,
text=True,
)
if result:
choice = result.stdout.strip()
return choice
def confirm(self, prompt, *, default=False):
# Maps directly to your existing `confirm` method
# ... (logic from your `Rofi.confirm` method) ...
pass
def ask(self, prompt, *, default=None):
# Maps directly to your existing `ask` method
# ... (logic from your `Rofi.ask` method) ...
pass

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
@@ -19,11 +19,11 @@
pkgs = nixpkgs.legacyPackages.${system};
inherit (pkgs) lib python3Packages;
version = "3.0.0";
version = "3.1.0";
in
{
packages.default = python3Packages.buildPythonApplication {
pname = "fastanime";
pname = "viu";
inherit version;
pyproject = true;
@@ -67,13 +67,13 @@
# Needs to be adapted for the nix derivation build
doCheck = false;
pythonImportsCheck = [ "fastanime" ];
pythonImportsCheck = [ "viu" ];
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,6 +1,6 @@
[project]
name = "fastanime"
version = "3.0.0"
name = "viu-cli"
version = "3.1.0"
description = "A browser anime site experience from the terminal"
license = "UNLICENSE"
readme = "README.md"
@@ -14,16 +14,21 @@ dependencies = [
]
[project.scripts]
fastanime = 'fastanime:Cli'
viu = 'viu:Cli'
[project.optional-dependencies]
standard = [
"lxml>=6.0.0",
"thefuzz>=0.22.1",
"yt-dlp>=2025.7.21",
"pycryptodomex>=3.23.0",
"dbus-python>=1.4.0",
"plyer>=2.1.0",
"lxml>=6.0.0"
]
notifications = [
"dbus-python>=1.4.0",
"plyer>=2.1.0",
]
notifications = ["plyer>=2.1.0"]
mpv = [
"mpv>=1.0.7",
]

View File

@@ -1,47 +0,0 @@
[tool.pytest.ini_options]
minversion = "6.0"
addopts = [
"-ra",
"--strict-markers",
"--strict-config",
"--cov=fastanime.cli.interactive",
"--cov-report=term-missing",
"--cov-report=html:htmlcov",
"--cov-report=xml",
"-v",
]
testpaths = [
"tests",
]
python_files = [
"test_*.py",
"*_test.py",
]
python_classes = [
"Test*",
]
python_functions = [
"test_*",
]
markers = [
"unit: Unit tests",
"integration: Integration tests",
"slow: Slow running tests",
"network: Tests requiring network access",
"auth: Tests requiring authentication",
]
filterwarnings = [
"ignore::DeprecationWarning",
"ignore::PendingDeprecationWarning",
]
# Test discovery patterns
collect_ignore = [
"setup.py",
]
# Pytest plugins
required_plugins = [
"pytest-cov",
"pytest-mock",
]

130
uv.lock generated
View File

@@ -22,7 +22,7 @@ wheels = [
[[package]]
name = "anyio"
version = "4.9.0"
version = "4.10.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
@@ -30,18 +30,18 @@ dependencies = [
{ name = "sniffio" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" }
sdist = { url = "https://files.pythonhosted.org/packages/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", size = 213252, upload-time = "2025-08-04T08:54:26.451Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" },
{ url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload-time = "2025-08-04T08:54:24.882Z" },
]
[[package]]
name = "certifi"
version = "2025.7.14"
version = "2025.8.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b3/76/52c535bcebe74590f296d6c77c86dabf761c41980e1347a2422e4aa2ae41/certifi-2025.7.14.tar.gz", hash = "sha256:8ea99dbdfaaf2ba2f9bac77b9249ef62ec5218e7c2b2e903378ed5fccf765995", size = 163981, upload-time = "2025-07-14T03:29:28.449Z" }
sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4f/52/34c6cf5bb9285074dc3531c437b3919e825d976fde097a7a73f79e726d03/certifi-2025.7.14-py3-none-any.whl", hash = "sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2", size = 162722, upload-time = "2025-07-14T03:29:26.863Z" },
{ url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" },
]
[[package]]
@@ -74,6 +74,12 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "dbus-python"
version = "1.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ff/24/63118050c7dd7be04b1ccd60eab53fef00abe844442e1b6dec92dae505d6/dbus-python-1.4.0.tar.gz", hash = "sha256:991666e498f60dbf3e49b8b7678f5559b8a65034fdf61aae62cdecdb7d89c770", size = 232490, upload-time = "2025-03-13T19:57:54.212Z" }
[[package]]
name = "distlib"
version = "0.4.0"
@@ -96,8 +102,8 @@ wheels = [
]
[[package]]
name = "fastanime"
version = "2.9.9"
name = "viu"
version = "3.1.0"
source = { editable = "." }
dependencies = [
{ name = "click" },
@@ -122,10 +128,13 @@ mpv = [
{ name = "mpv" },
]
notifications = [
{ name = "dbus-python" },
{ name = "plyer" },
]
standard = [
{ name = "dbus-python" },
{ name = "lxml" },
{ name = "plyer" },
{ name = "pycryptodomex" },
{ name = "thefuzz" },
{ name = "yt-dlp" },
@@ -150,6 +159,8 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "click", specifier = ">=8.1.7" },
{ name = "dbus-python", marker = "extra == 'notifications'", specifier = ">=1.4.0" },
{ name = "dbus-python", marker = "extra == 'standard'", specifier = ">=1.4.0" },
{ name = "httpx", specifier = ">=0.28.1" },
{ name = "inquirerpy", specifier = ">=0.3.4" },
{ name = "libtorrent", marker = "extra == 'torrent'", specifier = ">=2.0.11" },
@@ -158,6 +169,7 @@ requires-dist = [
{ name = "lxml", marker = "extra == 'standard'", specifier = ">=6.0.0" },
{ name = "mpv", marker = "extra == 'mpv'", specifier = ">=1.0.7" },
{ name = "plyer", marker = "extra == 'notifications'", specifier = ">=2.1.0" },
{ name = "plyer", marker = "extra == 'standard'", specifier = ">=2.1.0" },
{ name = "pycryptodomex", marker = "extra == 'download'", specifier = ">=3.23.0" },
{ name = "pycryptodomex", marker = "extra == 'standard'", specifier = ">=3.23.0" },
{ name = "pydantic", specifier = ">=2.11.7" },
@@ -181,11 +193,11 @@ dev = [
[[package]]
name = "filelock"
version = "3.18.0"
version = "3.19.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075, upload-time = "2025-03-14T07:11:40.47Z" }
sdist = { url = "https://files.pythonhosted.org/packages/40/bb/0ab3e58d22305b6f5440629d20683af28959bf793d98d11950e305c1c326/filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58", size = 17687, upload-time = "2025-08-14T16:56:03.016Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" },
{ url = "https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d", size = 15988, upload-time = "2025-08-14T16:56:01.633Z" },
]
[[package]]
@@ -227,11 +239,11 @@ wheels = [
[[package]]
name = "identify"
version = "2.6.12"
version = "2.6.13"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/88/d193a27416618628a5eea64e3223acd800b40749a96ffb322a9b55a49ed1/identify-2.6.12.tar.gz", hash = "sha256:d8de45749f1efb108badef65ee8386f0f7bb19a7f26185f74de6367bffbaf0e6", size = 99254, upload-time = "2025-05-23T20:37:53.3Z" }
sdist = { url = "https://files.pythonhosted.org/packages/82/ca/ffbabe3635bb839aa36b3a893c91a9b0d368cb4d8073e03a12896970af82/identify-2.6.13.tar.gz", hash = "sha256:da8d6c828e773620e13bfa86ea601c5a5310ba4bcd65edf378198b56a1f9fb32", size = 99243, upload-time = "2025-08-09T19:35:00.6Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7a/cd/18f8da995b658420625f7ef13f037be53ae04ec5ad33f9b718240dcfd48c/identify-2.6.12-py2.py3-none-any.whl", hash = "sha256:ad9672d5a72e0d2ff7c5c8809b62dfa60458626352fb0eb7b55e69bdc45334a2", size = 99145, upload-time = "2025-05-23T20:37:51.495Z" },
{ url = "https://files.pythonhosted.org/packages/e7/ce/461b60a3ee109518c055953729bf9ed089a04db895d47e95444071dcdef2/identify-2.6.13-py2.py3-none-any.whl", hash = "sha256:60381139b3ae39447482ecc406944190f690d4a2997f2584062089848361b33b", size = 99153, upload-time = "2025-08-09T19:34:59.1Z" },
]
[[package]]
@@ -396,14 +408,14 @@ wheels = [
[[package]]
name = "markdown-it-py"
version = "3.0.0"
version = "4.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mdurl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" }
sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" },
{ url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
]
[[package]]
@@ -489,7 +501,7 @@ wheels = [
[[package]]
name = "pre-commit"
version = "4.2.0"
version = "4.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cfgv" },
@@ -498,9 +510,9 @@ dependencies = [
{ name = "pyyaml" },
{ name = "virtualenv" },
]
sdist = { url = "https://files.pythonhosted.org/packages/08/39/679ca9b26c7bb2999ff122d50faa301e49af82ca9c066ec061cfbc0c6784/pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146", size = 193424, upload-time = "2025-03-18T21:35:20.987Z" }
sdist = { url = "https://files.pythonhosted.org/packages/ff/29/7cf5bbc236333876e4b41f56e06857a87937ce4bf91e117a6991a2dbb02a/pre_commit-4.3.0.tar.gz", hash = "sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16", size = 193792, upload-time = "2025-08-09T18:56:14.651Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707, upload-time = "2025-03-18T21:35:19.343Z" },
{ url = "https://files.pythonhosted.org/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8", size = 220965, upload-time = "2025-08-09T18:56:13.192Z" },
]
[[package]]
@@ -663,7 +675,7 @@ wheels = [
[[package]]
name = "pyinstaller"
version = "6.14.2"
version = "6.15.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "altgraph" },
@@ -674,19 +686,19 @@ dependencies = [
{ name = "pywin32-ctypes", marker = "sys_platform == 'win32'" },
{ name = "setuptools" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f8/25/41d6be08d65bdc5126e86d854f5767397483acf360f2c95c890e3fa96a31/pyinstaller-6.14.2.tar.gz", hash = "sha256:142cce0719e79315f0cc26400c2e5c45d9b6b17e7e0491fee444a9f8f16f4917", size = 4284885, upload-time = "2025-07-04T21:49:35.718Z" }
sdist = { url = "https://files.pythonhosted.org/packages/64/17/b2bb4de22650adbeef401fa82a1b43028976547a8728602e4d29735b455e/pyinstaller-6.15.0.tar.gz", hash = "sha256:a48fc4644ee4aa2aa2a35e7b51f496f8fbd7eecf6a2150646bbf1613ad07bc2d", size = 4331521, upload-time = "2025-08-03T18:33:35.709Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/dd/e5f4a4be80e291d2443ac7e73fa78f17003e4f2e3ec15a2ffdea0583a5c6/pyinstaller-6.14.2-py3-none-macosx_10_13_universal2.whl", hash = "sha256:d77d18bf5343a1afef2772393d7a489d4ec2282dee5bca549803fc0d74b78330", size = 1000610, upload-time = "2025-07-04T21:48:00.727Z" },
{ url = "https://files.pythonhosted.org/packages/8f/a5/0780ce0f9916012cafd65673a4cc3d59aee65af84c773f49b36aa98d0ce9/pyinstaller-6.14.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:3fa0c391e1300a9fd7752eb1ffe2950112b88fba9d2743eee2ef218a15f4705f", size = 720241, upload-time = "2025-07-04T21:48:05.669Z" },
{ url = "https://files.pythonhosted.org/packages/c8/d6/bf9e385cc20ee5dba5248716eda4d1271599c9ff2e173a0e7577d57866f0/pyinstaller-6.14.2-py3-none-manylinux2014_i686.whl", hash = "sha256:077efb2d01d16d9c8fdda3ad52788f0fead2791c5cec9ed6ce058af7e26eb74b", size = 730496, upload-time = "2025-07-04T21:48:09.954Z" },
{ url = "https://files.pythonhosted.org/packages/97/6f/358d23398cf210ba5a588e1311b6611762e353670d11838633cbb4c5ff79/pyinstaller-6.14.2-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:fdd2bd020a18736806a6bd5d3c4352f1209b427a96ad6c459d88aec1d90c4f21", size = 728609, upload-time = "2025-07-04T21:48:13.661Z" },
{ url = "https://files.pythonhosted.org/packages/9f/08/379af897977d77a4cf7d8c50dbe0135950be6d97be24c3ca4b45ccccd33b/pyinstaller-6.14.2-py3-none-manylinux2014_s390x.whl", hash = "sha256:03862c6b3cf7b16843d24b529f89cd4077cbe467883cd54ce7a81940d6da09d3", size = 725434, upload-time = "2025-07-04T21:48:27.748Z" },
{ url = "https://files.pythonhosted.org/packages/b8/98/460a32d2e325ad0ea81e4df478a8d84b5ebe0ceaca0cd3088f16afcaba5f/pyinstaller-6.14.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:78827a21ada2a848e98671852d20d74b2955b6e2aaf2359ed13a462e1a603d84", size = 725629, upload-time = "2025-07-04T21:48:32.118Z" },
{ url = "https://files.pythonhosted.org/packages/6f/bc/16eef174580bf4ca386479e48d5be8a977bf36cb6a9006814d754834c773/pyinstaller-6.14.2-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:185710ab1503dfdfa14c43237d394d96ac183422d588294be42531480dfa6c38", size = 724803, upload-time = "2025-07-04T21:48:36.053Z" },
{ url = "https://files.pythonhosted.org/packages/3c/6b/7162d59ee37e6883a5c4830cfe7dfb06c4997cc6aeb5f170d30ae76d9a39/pyinstaller-6.14.2-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:6c673a7e761bd4a2560cfd5dbe1ccdcfe2dff304b774e6e5242fc5afed953661", size = 724519, upload-time = "2025-07-04T21:48:40.358Z" },
{ url = "https://files.pythonhosted.org/packages/2a/26/d9559ac0851b1e3427a6b3ab0cd9edc8082b114f2499f78af532fdd5e14d/pyinstaller-6.14.2-py3-none-win32.whl", hash = "sha256:1697601aa788e3a52f0b5e620b4741a34b82e6f222ec6e1318b3a1349f566bb2", size = 1300415, upload-time = "2025-07-04T21:48:46.896Z" },
{ url = "https://files.pythonhosted.org/packages/79/69/111c85292ff99567a2408a6c6e9bf0b31910239f82b97d106321762d222c/pyinstaller-6.14.2-py3-none-win_amd64.whl", hash = "sha256:e10e0e67288d6dcb5898a917dd1d4272aa0ff33f197ad49a0e39618009d63ed9", size = 1358298, upload-time = "2025-07-04T21:48:55.685Z" },
{ url = "https://files.pythonhosted.org/packages/3d/e2/c267cadb3307a4979757b086674f592669c04bd960a8d2746dd2d18ad57d/pyinstaller-6.14.2-py3-none-win_arm64.whl", hash = "sha256:69fd11ca57e572387826afaa4a1b3d4cb74927d76f231f0308c0bd7872ca5ac1", size = 1299280, upload-time = "2025-07-04T21:49:07.744Z" },
{ url = "https://files.pythonhosted.org/packages/24/dd/d5c8a127446adda954f68ea7fac22772f7ab8656ad4b06df396d82574ca9/pyinstaller-6.15.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:9f00c71c40148cd1e61695b2c6f1e086693d3bcf9bfa22ab513aa4254c3b966f", size = 1016981, upload-time = "2025-08-03T18:31:52.034Z" },
{ url = "https://files.pythonhosted.org/packages/2d/2a/7b50593b419db43e48d9bdeebaac0ff92a5fe035f3c30f87ca3e1650d7e2/pyinstaller-6.15.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:cbcc8eb77320c60722030ac875883b564e00768fe3ff1721c7ba3ad0e0a277e9", size = 726337, upload-time = "2025-08-03T18:31:57.592Z" },
{ url = "https://files.pythonhosted.org/packages/77/83/7f498fba0154c57eb5fc93eb9680a2dbadb9f780a3389fb85b8d79683378/pyinstaller-6.15.0-py3-none-manylinux2014_i686.whl", hash = "sha256:c33e6302bc53db2df1104ed5566bd980b3e0ee7f18416a6e3caa908c12a54542", size = 737539, upload-time = "2025-08-03T18:32:02.221Z" },
{ url = "https://files.pythonhosted.org/packages/09/d6/e4477feab7c8379fb49e7ec95c82d0a69ad88f6ccc247f76bef3cb0e3432/pyinstaller-6.15.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:eb902d0fed3bb1f8b7190dc4df5c11f3b59505767e0d56d1ed782b853938bbf3", size = 735426, upload-time = "2025-08-03T18:32:06.485Z" },
{ url = "https://files.pythonhosted.org/packages/32/7e/ff25648276f15e2e77fc563d36d8cfcd917e077bf2a172420df3588601b4/pyinstaller-6.15.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:b4df862adae7cf1f08eff53c43ace283822447f7f528f72e4f94749062712f15", size = 732210, upload-time = "2025-08-03T18:32:21.667Z" },
{ url = "https://files.pythonhosted.org/packages/db/3d/267a7dddd0647de95d260780050ccd8228ab29d2b9edea54ed1f56800967/pyinstaller-6.15.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:b9ebf16ed0f99016ae8ae5746dee4cb244848a12941539e62ce2eea1df5a3f95", size = 732194, upload-time = "2025-08-03T18:32:29.536Z" },
{ url = "https://files.pythonhosted.org/packages/4d/61/962b2eb79ef225233e2d6e04600e998935328011dfb2fa775b1dd16b943a/pyinstaller-6.15.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:22193489e6a22435417103f61e7950363bba600ef36ec3ab1487303668c81092", size = 731256, upload-time = "2025-08-03T18:32:36.069Z" },
{ url = "https://files.pythonhosted.org/packages/67/5e/4e20e1c0e5791b09b69bef3ac921fd0cd25551b56879324ad999b92fa045/pyinstaller-6.15.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:18f743069849dbaee3e10900385f35795a5743eabab55e99dcc42f204e40a0db", size = 731148, upload-time = "2025-08-03T18:32:41.269Z" },
{ url = "https://files.pythonhosted.org/packages/88/31/28956c534991f289e2f981c715730b6241e75dc6295737a8cbd050a0cc8c/pyinstaller-6.15.0-py3-none-win32.whl", hash = "sha256:60da8f1b5071766b45c0f607d8bc3d7e59ba2c3b262d08f2e4066ba65f3544a2", size = 1312297, upload-time = "2025-08-03T18:32:50.572Z" },
{ url = "https://files.pythonhosted.org/packages/09/ab/6a45186c7f8e34c422faecd72580116a67d068158c57faa2d2f6d01faa7f/pyinstaller-6.15.0-py3-none-win_amd64.whl", hash = "sha256:cbea297e16eeda30b41c300d6ec2fd2abea4dbd8d8a32650eeec36431c94fcd9", size = 1373091, upload-time = "2025-08-03T18:32:58.133Z" },
{ url = "https://files.pythonhosted.org/packages/5b/86/72159af032b9db36f2470a3b085f79277ec1c38e7e48f8c5dc1ed16dc4e1/pyinstaller-6.15.0-py3-none-win_arm64.whl", hash = "sha256:f43c035621742cf2d19b84308c60e4e44e72c94786d176b8f6adcde351b5bd98", size = 1314305, upload-time = "2025-08-03T18:33:05.557Z" },
]
[[package]]
@@ -903,27 +915,28 @@ wheels = [
[[package]]
name = "ruff"
version = "0.12.5"
version = "0.12.9"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/30/cd/01015eb5034605fd98d829c5839ec2c6b4582b479707f7c1c2af861e8258/ruff-0.12.5.tar.gz", hash = "sha256:b209db6102b66f13625940b7f8c7d0f18e20039bb7f6101fbdac935c9612057e", size = 5170722, upload-time = "2025-07-24T13:26:37.456Z" }
sdist = { url = "https://files.pythonhosted.org/packages/4a/45/2e403fa7007816b5fbb324cb4f8ed3c7402a927a0a0cb2b6279879a8bfdc/ruff-0.12.9.tar.gz", hash = "sha256:fbd94b2e3c623f659962934e52c2bea6fc6da11f667a427a368adaf3af2c866a", size = 5254702, upload-time = "2025-08-14T16:08:55.2Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d4/de/ad2f68f0798ff15dd8c0bcc2889558970d9a685b3249565a937cd820ad34/ruff-0.12.5-py3-none-linux_armv6l.whl", hash = "sha256:1de2c887e9dec6cb31fcb9948299de5b2db38144e66403b9660c9548a67abd92", size = 11819133, upload-time = "2025-07-24T13:25:56.369Z" },
{ url = "https://files.pythonhosted.org/packages/f8/fc/c6b65cd0e7fbe60f17e7ad619dca796aa49fbca34bb9bea5f8faf1ec2643/ruff-0.12.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d1ab65e7d8152f519e7dea4de892317c9da7a108da1c56b6a3c1d5e7cf4c5e9a", size = 12501114, upload-time = "2025-07-24T13:25:59.471Z" },
{ url = "https://files.pythonhosted.org/packages/c5/de/c6bec1dce5ead9f9e6a946ea15e8d698c35f19edc508289d70a577921b30/ruff-0.12.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:962775ed5b27c7aa3fdc0d8f4d4433deae7659ef99ea20f783d666e77338b8cf", size = 11716873, upload-time = "2025-07-24T13:26:01.496Z" },
{ url = "https://files.pythonhosted.org/packages/a1/16/cf372d2ebe91e4eb5b82a2275c3acfa879e0566a7ac94d331ea37b765ac8/ruff-0.12.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73b4cae449597e7195a49eb1cdca89fd9fbb16140c7579899e87f4c85bf82f73", size = 11958829, upload-time = "2025-07-24T13:26:03.721Z" },
{ url = "https://files.pythonhosted.org/packages/25/bf/cd07e8f6a3a6ec746c62556b4c4b79eeb9b0328b362bb8431b7b8afd3856/ruff-0.12.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b13489c3dc50de5e2d40110c0cce371e00186b880842e245186ca862bf9a1ac", size = 11626619, upload-time = "2025-07-24T13:26:06.118Z" },
{ url = "https://files.pythonhosted.org/packages/d8/c9/c2ccb3b8cbb5661ffda6925f81a13edbb786e623876141b04919d1128370/ruff-0.12.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1504fea81461cf4841778b3ef0a078757602a3b3ea4b008feb1308cb3f23e08", size = 13221894, upload-time = "2025-07-24T13:26:08.292Z" },
{ url = "https://files.pythonhosted.org/packages/6b/58/68a5be2c8e5590ecdad922b2bcd5583af19ba648f7648f95c51c3c1eca81/ruff-0.12.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c7da4129016ae26c32dfcbd5b671fe652b5ab7fc40095d80dcff78175e7eddd4", size = 14163909, upload-time = "2025-07-24T13:26:10.474Z" },
{ url = "https://files.pythonhosted.org/packages/bd/d1/ef6b19622009ba8386fdb792c0743f709cf917b0b2f1400589cbe4739a33/ruff-0.12.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ca972c80f7ebcfd8af75a0f18b17c42d9f1ef203d163669150453f50ca98ab7b", size = 13583652, upload-time = "2025-07-24T13:26:13.381Z" },
{ url = "https://files.pythonhosted.org/packages/62/e3/1c98c566fe6809a0c83751d825a03727f242cdbe0d142c9e292725585521/ruff-0.12.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8dbbf9f25dfb501f4237ae7501d6364b76a01341c6f1b2cd6764fe449124bb2a", size = 12700451, upload-time = "2025-07-24T13:26:15.488Z" },
{ url = "https://files.pythonhosted.org/packages/24/ff/96058f6506aac0fbc0d0fc0d60b0d0bd746240a0594657a2d94ad28033ba/ruff-0.12.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c47dea6ae39421851685141ba9734767f960113d51e83fd7bb9958d5be8763a", size = 12937465, upload-time = "2025-07-24T13:26:17.808Z" },
{ url = "https://files.pythonhosted.org/packages/eb/d3/68bc5e7ab96c94b3589d1789f2dd6dd4b27b263310019529ac9be1e8f31b/ruff-0.12.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c5076aa0e61e30f848846f0265c873c249d4b558105b221be1828f9f79903dc5", size = 11771136, upload-time = "2025-07-24T13:26:20.422Z" },
{ url = "https://files.pythonhosted.org/packages/52/75/7356af30a14584981cabfefcf6106dea98cec9a7af4acb5daaf4b114845f/ruff-0.12.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a5a4c7830dadd3d8c39b1cc85386e2c1e62344f20766be6f173c22fb5f72f293", size = 11601644, upload-time = "2025-07-24T13:26:22.928Z" },
{ url = "https://files.pythonhosted.org/packages/c2/67/91c71d27205871737cae11025ee2b098f512104e26ffd8656fd93d0ada0a/ruff-0.12.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:46699f73c2b5b137b9dc0fc1a190b43e35b008b398c6066ea1350cce6326adcb", size = 12478068, upload-time = "2025-07-24T13:26:26.134Z" },
{ url = "https://files.pythonhosted.org/packages/34/04/b6b00383cf2f48e8e78e14eb258942fdf2a9bf0287fbf5cdd398b749193a/ruff-0.12.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5a655a0a0d396f0f072faafc18ebd59adde8ca85fb848dc1b0d9f024b9c4d3bb", size = 12991537, upload-time = "2025-07-24T13:26:28.533Z" },
{ url = "https://files.pythonhosted.org/packages/3e/b9/053d6445dc7544fb6594785056d8ece61daae7214859ada4a152ad56b6e0/ruff-0.12.5-py3-none-win32.whl", hash = "sha256:dfeb2627c459b0b78ca2bbdc38dd11cc9a0a88bf91db982058b26ce41714ffa9", size = 11751575, upload-time = "2025-07-24T13:26:30.835Z" },
{ url = "https://files.pythonhosted.org/packages/bc/0f/ab16e8259493137598b9149734fec2e06fdeda9837e6f634f5c4e35916da/ruff-0.12.5-py3-none-win_amd64.whl", hash = "sha256:ae0d90cf5f49466c954991b9d8b953bd093c32c27608e409ae3564c63c5306a5", size = 12882273, upload-time = "2025-07-24T13:26:32.929Z" },
{ url = "https://files.pythonhosted.org/packages/00/db/c376b0661c24cf770cb8815268190668ec1330eba8374a126ceef8c72d55/ruff-0.12.5-py3-none-win_arm64.whl", hash = "sha256:48cdbfc633de2c5c37d9f090ba3b352d1576b0015bfc3bc98eaf230275b7e805", size = 11951564, upload-time = "2025-07-24T13:26:34.994Z" },
{ url = "https://files.pythonhosted.org/packages/ad/20/53bf098537adb7b6a97d98fcdebf6e916fcd11b2e21d15f8c171507909cc/ruff-0.12.9-py3-none-linux_armv6l.whl", hash = "sha256:fcebc6c79fcae3f220d05585229463621f5dbf24d79fdc4936d9302e177cfa3e", size = 11759705, upload-time = "2025-08-14T16:08:12.968Z" },
{ url = "https://files.pythonhosted.org/packages/20/4d/c764ee423002aac1ec66b9d541285dd29d2c0640a8086c87de59ebbe80d5/ruff-0.12.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aed9d15f8c5755c0e74467731a007fcad41f19bcce41cd75f768bbd687f8535f", size = 12527042, upload-time = "2025-08-14T16:08:16.54Z" },
{ url = "https://files.pythonhosted.org/packages/8b/45/cfcdf6d3eb5fc78a5b419e7e616d6ccba0013dc5b180522920af2897e1be/ruff-0.12.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5b15ea354c6ff0d7423814ba6d44be2807644d0c05e9ed60caca87e963e93f70", size = 11724457, upload-time = "2025-08-14T16:08:18.686Z" },
{ url = "https://files.pythonhosted.org/packages/72/e6/44615c754b55662200c48bebb02196dbb14111b6e266ab071b7e7297b4ec/ruff-0.12.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d596c2d0393c2502eaabfef723bd74ca35348a8dac4267d18a94910087807c53", size = 11949446, upload-time = "2025-08-14T16:08:21.059Z" },
{ url = "https://files.pythonhosted.org/packages/fd/d1/9b7d46625d617c7df520d40d5ac6cdcdf20cbccb88fad4b5ecd476a6bb8d/ruff-0.12.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1b15599931a1a7a03c388b9c5df1bfa62be7ede6eb7ef753b272381f39c3d0ff", size = 11566350, upload-time = "2025-08-14T16:08:23.433Z" },
{ url = "https://files.pythonhosted.org/packages/59/20/b73132f66f2856bc29d2d263c6ca457f8476b0bbbe064dac3ac3337a270f/ruff-0.12.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3d02faa2977fb6f3f32ddb7828e212b7dd499c59eb896ae6c03ea5c303575756", size = 13270430, upload-time = "2025-08-14T16:08:25.837Z" },
{ url = "https://files.pythonhosted.org/packages/a2/21/eaf3806f0a3d4c6be0a69d435646fba775b65f3f2097d54898b0fd4bb12e/ruff-0.12.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:17d5b6b0b3a25259b69ebcba87908496e6830e03acfb929ef9fd4c58675fa2ea", size = 14264717, upload-time = "2025-08-14T16:08:27.907Z" },
{ url = "https://files.pythonhosted.org/packages/d2/82/1d0c53bd37dcb582b2c521d352fbf4876b1e28bc0d8894344198f6c9950d/ruff-0.12.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:72db7521860e246adbb43f6ef464dd2a532ef2ef1f5dd0d470455b8d9f1773e0", size = 13684331, upload-time = "2025-08-14T16:08:30.352Z" },
{ url = "https://files.pythonhosted.org/packages/3b/2f/1c5cf6d8f656306d42a686f1e207f71d7cebdcbe7b2aa18e4e8a0cb74da3/ruff-0.12.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a03242c1522b4e0885af63320ad754d53983c9599157ee33e77d748363c561ce", size = 12739151, upload-time = "2025-08-14T16:08:32.55Z" },
{ url = "https://files.pythonhosted.org/packages/47/09/25033198bff89b24d734e6479e39b1968e4c992e82262d61cdccaf11afb9/ruff-0.12.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fc83e4e9751e6c13b5046d7162f205d0a7bac5840183c5beebf824b08a27340", size = 12954992, upload-time = "2025-08-14T16:08:34.816Z" },
{ url = "https://files.pythonhosted.org/packages/52/8e/d0dbf2f9dca66c2d7131feefc386523404014968cd6d22f057763935ab32/ruff-0.12.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:881465ed56ba4dd26a691954650de6ad389a2d1fdb130fe51ff18a25639fe4bb", size = 12899569, upload-time = "2025-08-14T16:08:36.852Z" },
{ url = "https://files.pythonhosted.org/packages/a0/bd/b614d7c08515b1428ed4d3f1d4e3d687deffb2479703b90237682586fa66/ruff-0.12.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:43f07a3ccfc62cdb4d3a3348bf0588358a66da756aa113e071b8ca8c3b9826af", size = 11751983, upload-time = "2025-08-14T16:08:39.314Z" },
{ url = "https://files.pythonhosted.org/packages/58/d6/383e9f818a2441b1a0ed898d7875f11273f10882f997388b2b51cb2ae8b5/ruff-0.12.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:07adb221c54b6bba24387911e5734357f042e5669fa5718920ee728aba3cbadc", size = 11538635, upload-time = "2025-08-14T16:08:41.297Z" },
{ url = "https://files.pythonhosted.org/packages/20/9c/56f869d314edaa9fc1f491706d1d8a47747b9d714130368fbd69ce9024e9/ruff-0.12.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f5cd34fabfdea3933ab85d72359f118035882a01bff15bd1d2b15261d85d5f66", size = 12534346, upload-time = "2025-08-14T16:08:43.39Z" },
{ url = "https://files.pythonhosted.org/packages/bd/4b/d8b95c6795a6c93b439bc913ee7a94fda42bb30a79285d47b80074003ee7/ruff-0.12.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f6be1d2ca0686c54564da8e7ee9e25f93bdd6868263805f8c0b8fc6a449db6d7", size = 13017021, upload-time = "2025-08-14T16:08:45.889Z" },
{ url = "https://files.pythonhosted.org/packages/c7/c1/5f9a839a697ce1acd7af44836f7c2181cdae5accd17a5cb85fcbd694075e/ruff-0.12.9-py3-none-win32.whl", hash = "sha256:cc7a37bd2509974379d0115cc5608a1a4a6c4bff1b452ea69db83c8855d53f93", size = 11734785, upload-time = "2025-08-14T16:08:48.062Z" },
{ url = "https://files.pythonhosted.org/packages/fa/66/cdddc2d1d9a9f677520b7cfc490d234336f523d4b429c1298de359a3be08/ruff-0.12.9-py3-none-win_amd64.whl", hash = "sha256:6fb15b1977309741d7d098c8a3cb7a30bc112760a00fb6efb7abc85f00ba5908", size = 12840654, upload-time = "2025-08-14T16:08:50.158Z" },
{ url = "https://files.pythonhosted.org/packages/ac/fd/669816bc6b5b93b9586f3c1d87cd6bc05028470b3ecfebb5938252c47a35/ruff-0.12.9-py3-none-win_arm64.whl", hash = "sha256:63c8c819739d86b96d500cce885956a1a48ab056bbcbc61b747ad494b2485089", size = 11949623, upload-time = "2025-08-14T16:08:52.233Z" },
]
[[package]]
@@ -1018,16 +1031,17 @@ wheels = [
[[package]]
name = "virtualenv"
version = "20.32.0"
version = "20.34.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "distlib" },
{ name = "filelock" },
{ name = "platformdirs" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a9/96/0834f30fa08dca3738614e6a9d42752b6420ee94e58971d702118f7cfd30/virtualenv-20.32.0.tar.gz", hash = "sha256:886bf75cadfdc964674e6e33eb74d787dff31ca314ceace03ca5810620f4ecf0", size = 6076970, upload-time = "2025-07-21T04:09:50.985Z" }
sdist = { url = "https://files.pythonhosted.org/packages/1c/14/37fcdba2808a6c615681cd216fecae00413c9dab44fb2e57805ecf3eaee3/virtualenv-20.34.0.tar.gz", hash = "sha256:44815b2c9dee7ed86e387b842a84f20b93f7f417f95886ca1996a72a4138eb1a", size = 6003808, upload-time = "2025-08-13T14:24:07.464Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/c6/f8f28009920a736d0df434b52e9feebfb4d702ba942f15338cb4a83eafc1/virtualenv-20.32.0-py3-none-any.whl", hash = "sha256:2c310aecb62e5aa1b06103ed7c2977b81e042695de2697d01017ff0f1034af56", size = 6057761, upload-time = "2025-07-21T04:09:48.059Z" },
{ url = "https://files.pythonhosted.org/packages/76/06/04c8e804f813cf972e3262f3f8584c232de64f0cde9f703b46cf53a45090/virtualenv-20.34.0-py3-none-any.whl", hash = "sha256:341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026", size = 5983279, upload-time = "2025-08-13T14:24:05.111Z" },
]
[[package]]
@@ -1041,9 +1055,9 @@ wheels = [
[[package]]
name = "yt-dlp"
version = "2025.7.21"
version = "2025.8.11"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7e/3a/343f7a0024ddd4c30f150e8d8f57fd7b924846f97d99fc0dcd75ea8d2773/yt_dlp-2025.7.21.tar.gz", hash = "sha256:46fbb53eab1afbe184c45b4c17e9a6eba614be680e4c09de58b782629d0d7f43", size = 3050219, upload-time = "2025-07-21T23:59:03.826Z" }
sdist = { url = "https://files.pythonhosted.org/packages/80/72/de4a7f9bbfef886c7f0790b8246585310f155e4a6589dd38d846efa932e9/yt_dlp-2025.8.11.tar.gz", hash = "sha256:dc7c120a367fe55e0f711613dc80ea29d3a4e0ed8d66104cebfbe3d36e81fdfc", size = 3045769, upload-time = "2025-08-11T04:01:51.749Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e8/2f/abe59a3204c749fed494849ea29176bcefa186ec8898def9e43f649ddbcf/yt_dlp-2025.7.21-py3-none-any.whl", hash = "sha256:d7aa2b53f9b2f35453346360f41811a0dad1e956e70b35a4ae95039d4d815d15", size = 3288681, upload-time = "2025-07-21T23:59:01.788Z" },
{ url = "https://files.pythonhosted.org/packages/d4/dc/4400fb3e8bccdbd0f6ecf36a8a9aea3290ad98a1c9d19664a5c92d7b2e5d/yt_dlp-2025.8.11-py3-none-any.whl", hash = "sha256:f115d2246c1ab5737772bd4845be057eebb91c0d95125a7577d92288351df7d0", size = 3281677, upload-time = "2025-08-11T04:01:48.756Z" },
]

View File

@@ -2,7 +2,7 @@ import sys
if sys.version_info < (3, 10):
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,7 @@
██╗░░░██╗██╗██╗░░░██╗
██║░░░██║██║██║░░░██║
╚██╗░██╔╝██║██║░░░██║
░╚████╔╝░██║██║░░░██║
░░╚██╔╝░░██║╚██████╔╝
░░░╚═╝░░░╚═╝░╚═════╝░

View File

@@ -0,0 +1,113 @@
/**
* 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.
*/
/*****----- Configuration -----*****/
configuration {
font: "JetBrains Mono Nerd Font 12";
}
/*****----- Global Properties -----*****/
* {
/* Tokyo Night Color Palette */
bg-col: #1a1b26;
bg-alt: #24283b;
fg-col: #c0caf5;
blue: #7aa2f7;
green: #9ece6a; /* For 'Yes' */
red: #f7768e; /* For 'No' */
background-color: transparent;
text-color: @fg-col;
}
/*****----- Main Window -----*****/
window {
transparency: "real";
location: center;
anchor: center;
fullscreen: false;
width: 350px;
border: 2px;
border-color: @blue;
border-radius: 12px;
padding: 20px;
background-color: @bg-col;
}
/*****----- Main Box -----*****/
mainbox {
children: [ inputbar, message, listview ];
spacing: 15px;
background-color: transparent;
}
/*****----- Inputbar (Displays the -p 'prompt') -----*****/
inputbar {
background-color: transparent;
text-color: @blue;
children: [ prompt ];
}
prompt {
font: "JetBrains Mono Nerd Font Bold 14";
horizontal-align: 0.5; /* Center the title */
background-color: transparent;
text-color: inherit;
}
/*****----- Message (Displays the -mesg 'Are you Sure?') -----*****/
message {
padding: 10px;
margin: 5px 0px;
border-radius: 8px;
background-color: @bg-alt;
text-color: @fg-col;
}
textbox {
font: "JetBrains Mono Nerd Font 12";
horizontal-align: 0.5;
background-color: transparent;
text-color: inherit;
}
/*****----- Listview (The Buttons) -----*****/
listview {
columns: 2;
lines: 1;
spacing: 15px;
layout: vertical;
background-color: transparent;
}
/*****----- Elements (Yes/No Buttons) -----*****/
element {
padding: 12px;
border-radius: 8px;
background-color: @bg-alt;
text-color: @fg-col;
cursor: pointer;
}
element-text {
font: "JetBrains Mono Nerd Font Bold 12";
horizontal-align: 0.5;
background-color: transparent;
text-color: inherit;
}
element normal.normal {
background-color: @bg-alt;
text-color: @fg-col;
}
element selected.normal {
background-color: @blue;
text-color: @bg-col;
}

View File

@@ -0,0 +1,86 @@
/**
* Rofi Theme: Viu "Tokyo Night" Input
* Author: Gemini ft Benexl
* Description: A compact, modern modal dialog for text input that correctly displays the prompt.
*/
/*****----- Configuration -----*****/
configuration {
font: "JetBrains Mono Nerd Font 14";
}
/*****----- Global Properties -----*****/
* {
/* Tokyo Night Color Palette */
bg-col: #1a1b26ff;
bg-alt: #24283bff;
fg-col: #c0caf5ff;
fg-alt: #a9b1d6ff;
accent: #bb9af7ff;
blue: #7aa2f7ff;
background-color: transparent;
text-color: @fg-col;
}
/*****----- Main Window -----*****/
window {
transparency: "real";
location: center;
anchor: center;
fullscreen: false;
width: 500px;
border: 2px;
border-color: @blue;
border-radius: 8px;
padding: 20px;
background-color: @bg-col;
}
/*****----- Main Box -----*****/
mainbox {
children: [ message, inputbar ];
spacing: 20px;
background-color: transparent;
}
/*****----- Message (The Main Question, uses -mesg) -----*****/
message {
padding: 10px;
border-radius: 8px;
background-color: @bg-alt;
text-color: @fg-col;
}
textbox {
font: "JetBrains Mono Nerd Font Bold 14";
horizontal-align: 0.5; /* Center the prompt text */
background-color: transparent;
text-color: inherit;
}
/*****----- Inputbar (Contains the title and entry field) -----*****/
inputbar {
padding: 8px 12px;
border: 1px;
border-radius: 6px;
border-color: @accent;
background-color: @bg-alt;
spacing: 10px;
children: [ prompt, entry ];
}
/* This is the title from the -p flag */
prompt {
background-color: transparent;
text-color: @accent;
}
/* This is where the user types */
entry {
background-color: transparent;
text-color: @fg-col;
placeholder: "Type here...";
placeholder-color: @fg-alt;
}

View File

@@ -0,0 +1,104 @@
/**
* Rofi Theme: Viu "Tokyo Night" Main
* Author: Gemini ft Benexl
* Description: A sharp, modern, and ultra-compact theme with a Tokyo Night palette.
*/
/*****----- Configuration -----*****/
configuration {
font: "JetBrains Mono Nerd Font 14";
show-icons: false;
location: 0; /* 0 = center */
width: 50;
yoffset: -50;
lines: 3;
}
/*****----- Global Properties -----*****/
* {
/* Tokyo Night Color Palette */
bg-col: #1a1b26ff; /* Main Background */
bg-alt: #24283bff; /* Lighter Background for elements */
fg-col: #c0caf5ff; /* Main Foreground */
fg-alt: #a9b1d6ff; /* Dimmer Foreground for placeholders */
accent: #bb9af7ff; /* Magenta/Purple for accents */
selected: #7aa2f7ff; /* Blue for selection highlight */
background-color: transparent;
text-color: @fg-col;
}
/*****----- Main Window -----*****/
window {
transparency: "real";
background-color: @bg-col;
border: 2px;
border-color: @selected; /* Using blue for the main border */
border-radius: 8px;
padding: 12px;
}
/*****----- Main Box -----*****/
mainbox {
children: [ inputbar, listview ];
spacing: 10px;
background-color: transparent;
}
/*****----- Inputbar -----*****/
inputbar {
background-color: @bg-alt;
border: 1px;
border-color: @accent; /* Using magenta for the input border */
border-radius: 6px;
padding: 6px 12px;
spacing: 12px;
children: [ prompt, entry ];
}
prompt {
background-color: transparent;
text-color: @accent;
}
entry {
background-color: transparent;
text-color: @fg-col;
placeholder: "Search...";
placeholder-color: @fg-alt;
}
/*****----- List of items -----*****/
listview {
scrollbar: false;
spacing: 4px;
padding: 4px 0px;
layout: vertical;
background-color: transparent;
}
/*****----- Elements -----*****/
element {
padding: 6px 12px;
border-radius: 6px;
spacing: 15px;
background-color: transparent;
}
element-text {
vertical-align: 0.5;
background-color: transparent;
text-color: inherit;
}
/* Default state of elements */
element normal.normal {
background-color: transparent;
text-color: @fg-col;
}
/* Selected entry in the list */
element selected.normal {
background-color: @selected; /* Blue highlight */
text-color: @bg-col; /* Dark text for high contrast */
}

View File

@@ -0,0 +1,109 @@
/**
* Rofi Theme: Viu "Tokyo Night" Horizontal Strip
* Author: Gemini ft Benexl
* Description: A fullscreen, horizontal, icon-centric theme for previews.
*/
/*****----- Configuration -----*****/
configuration {
font: "JetBrains Mono Nerd Font 12";
show-icons: true;
}
/*****----- Global Properties -----*****/
* {
/* Tokyo Night Color Palette */
bg-col: #1a1b26;
bg-alt: #24283b; /* Slightly lighter for elements */
fg-col: #c0caf5;
fg-alt: #a9b1d6;
blue: #7aa2f7;
cyan: #7dcfff;
magenta: #bb9af7;
background-color: transparent;
text-color: @fg-col;
}
/*****----- Main Window -----*****/
window {
transparency: "real";
background-color: @bg-col;
fullscreen: true;
padding: 2%;
}
/*****----- Main Box -----*****/
mainbox {
children: [ inputbar, listview ];
spacing: 3%;
background-color: transparent;
}
/*****----- Inputbar -----*****/
inputbar {
spacing: 15px;
padding: 12px 16px;
border-radius: 10px;
background-color: @bg-alt;
text-color: @fg-col;
margin: 0% 20%; /* Center the input bar */
children: [ prompt, entry ];
}
prompt {
text-color: @magenta;
background-color: transparent;
}
entry {
background-color: transparent;
placeholder: "Select an option...";
placeholder-color: @fg-alt;
}
/*****----- List of items -----*****/
listview {
layout: horizontal;
columns: 5;
spacing: 20px;
fixed-height: true;
background-color: transparent;
}
/*****----- Elements -----*****/
element {
orientation: vertical;
padding: 30px 20px;
border-radius: 12px;
spacing: 20px;
background-color: @bg-alt;
cursor: pointer;
width: 200px; /* Width of each element */
height: 50px; /* Height of each element */
}
element-icon {
size: 33%;
horizontal-align: 0.5;
background-color: transparent;
}
element-text {
horizontal-align: 0.5;
background-color: transparent;
text-color: inherit;
}
/* Default state of elements */
element normal.normal {
background-color: @bg-alt;
text-color: @fg-col;
}
/* Selected entry in the list */
element selected.normal {
background-color: @blue;
text-color: @bg-col; /* Invert text color for contrast */
}

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

BIN
viu/assets/icons/logo.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
viu/assets/icons/logo.png Normal file

Binary file not shown.

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

@@ -44,18 +44,19 @@ fzf_preview() {
if [ "$IMAGE_RENDERER" = "icat" ] && [ -z "$GHOSTTY_BIN_DIR" ]; then
if command -v kitten >/dev/null 2>&1; then
kitten icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed "\$d" | sed "$(printf "\$s/\$/\033[m/")"
kitten icat --clear --transfer-mode=memory --unicode-placeholder{SCALE_UP} --stdin=no --place="$dim@0x0" "$file" | sed "\$d" | sed "$(printf "\$s/\$/\033[m/")"
elif command -v icat >/dev/null 2>&1; then
icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed "\$d" | sed "$(printf "\$s/\$/\033[m/")"
icat --clear --transfer-mode=memory --unicode-placeholder{SCALE_UP} --stdin=no --place="$dim@0x0" "$file" | sed "\$d" | sed "$(printf "\$s/\$/\033[m/")"
else
kitty icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed "\$d" | sed "$(printf "\$s/\$/\033[m/")"
kitty icat --clear --transfer-mode=memory --unicode-placeholder{SCALE_UP} --stdin=no --place="$dim@0x0" "$file" | sed "\$d" | sed "$(printf "\$s/\$/\033[m/")"
fi
elif [ -n "$GHOSTTY_BIN_DIR" ]; then
dim=$((FZF_PREVIEW_COLUMNS - 1))x${FZF_PREVIEW_LINES}
if command -v kitten >/dev/null 2>&1; then
kitten icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed "\$d" | sed "$(printf "\$s/\$/\033[m/")"
kitten icat --clear --transfer-mode=memory --unicode-placeholder{SCALE_UP} --stdin=no --place="$dim@0x0" "$file" | sed "\$d" | sed "$(printf "\$s/\$/\033[m/")"
elif command -v icat >/dev/null 2>&1; then
icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed "\$d" | sed "$(printf "\$s/\$/\033[m/")"
icat --clear --transfer-mode=memory --unicode-placeholder{SCALE_UP} --stdin=no --place="$dim@0x0" "$file" | sed "\$d" | sed "$(printf "\$s/\$/\033[m/")"
else
chafa -s "$dim" "$file"
fi
@@ -91,7 +92,7 @@ print_kv() {
# If the text is too long to fit, just add a single space for separation.
if [ "$padding_len" -lt 1 ]; then
padding_len=1
value=$(echo $value| fold -s -w "$((WIDTH - key_len - 3))")
value=$(echo "$value"| fold -s -w "$((WIDTH - key_len - 3))")
printf "{C_KEY}%s:{RESET}%*s%s\\n" "$key" "$padding_len" "" " $value"
else
printf "{C_KEY}%s:{RESET}%*s%s\\n" "$key" "$padding_len" "" " $value"

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

@@ -38,12 +38,14 @@ commands = {
"registry": "registry.registry",
"worker": "worker.worker",
"queue": "queue.queue",
"completions": "completions.completions",
}
@click.group(
cls=LazyGroup,
root="fastanime.cli.commands",
root="viu.cli.commands",
invoke_without_command=True,
lazy_subcommands=commands,
context_settings=dict(auto_envvar_prefix=PROJECT_NAME),
)
@@ -68,7 +70,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 +108,7 @@ def cli(ctx: click.Context, **options: "Unpack[Options]"):
else loader.load(cli_overrides)
)
ctx.obj = config
if ctx.invoked_subcommand is None:
from .commands.anilist import cmd
ctx.invoke(cmd.anilist)

View File

@@ -1 +1,3 @@
from .cmd import anilist
__all__ = ["anilist"]

View File

@@ -8,6 +8,7 @@ commands = {
# "recent": "recent.recent",
"search": "search.search",
"download": "download.download",
"downloads": "downloads.downloads",
"auth": "auth.auth",
"stats": "stats.stats",
"notifications": "notifications.notifications",
@@ -17,7 +18,7 @@ commands = {
@click.group(
cls=LazyGroup,
name="anilist",
root="fastanime.cli.commands.anilist.commands",
root="viu.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

@@ -1,4 +1,5 @@
import click
import webbrowser
from .....core.config.model import AppConfig
@@ -41,9 +42,14 @@ def auth(config: AppConfig, status: bool, logout: bool):
return
api_client = create_api_client("anilist", config)
# TODO: stop the printing of opening browser session to stderr
click.launch(ANILIST_AUTH)
feedback.info("Your browser has been opened to obtain an AniList token.")
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].")
else:
feedback.warning(
f"Failed to open the browser. Please visit the site manually [magenta][link={ANILIST_AUTH}]here[/link][/magenta]."
)
feedback.info(
"After authorizing, copy the token from the address bar and paste it below."
)

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.cli.utils.completion import anime_titles_shell_complete
from viu.core.config import AppConfig
from viu.core.exceptions import ViuError
from viu.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.cli.service.download.service import DownloadService
from viu.cli.service.feedback import FeedbackService
from viu.cli.service.registry import MediaRegistryService
from viu.cli.service.watch_history import WatchHistoryService
from viu.cli.utils.parser import parse_episode_range
from viu.libs.media_api.api import create_api_client
from viu.libs.media_api.params import MediaSearchParams
from viu.libs.provider.anime.provider import create_provider
from viu.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

@@ -0,0 +1,211 @@
import json
from typing import TYPE_CHECKING
import click
from .....core.config import AppConfig
from .....libs.media_api.params import MediaSearchParams
from .....libs.media_api.types import (
MediaFormat,
MediaGenre,
MediaSort,
UserMediaListStatus,
)
from ....service.feedback import FeedbackService
from ....service.registry.service import MediaRegistryService
@click.command(help="Search through the local media registry")
@click.argument("query", required=False)
@click.option(
"--status",
type=click.Choice(
[s.value for s in UserMediaListStatus],
case_sensitive=False,
),
help="Filter by watch status",
)
@click.option(
"--genre", multiple=True, help="Filter by genre (can be used multiple times)"
)
@click.option(
"--format",
type=click.Choice(
[
f.value
for f in MediaFormat
if f not in [MediaFormat.MANGA, MediaFormat.NOVEL, MediaFormat.ONE_SHOT]
],
case_sensitive=False,
),
help="Filter by format",
)
@click.option("--year", type=int, help="Filter by release year")
@click.option("--min-score", type=float, help="Minimum average score (0.0 - 10.0)")
@click.option("--max-score", type=float, help="Maximum average score (0.0 - 10.0)")
@click.option(
"--sort",
type=click.Choice(
["title", "score", "popularity", "year", "episodes", "updated"],
case_sensitive=False,
),
default="title",
help="Sort results by field",
)
@click.option("--limit", type=int, default=20, help="Maximum number of results to show")
@click.option(
"--json", "output_json", is_flag=True, help="Output results in JSON format"
)
@click.option(
"--api",
default="anilist",
type=click.Choice(["anilist"], case_sensitive=False),
help="Media API registry to search",
)
@click.pass_obj
def downloads(
config: AppConfig,
query: str | None,
status: str | None,
genre: tuple[str, ...],
format: str | None,
year: int | None,
min_score: float | None,
max_score: float | None,
sort: str,
limit: int,
output_json: bool,
api: str,
):
"""
Search through your local media registry.
You can search by title and filter by various criteria like status,
genre, format, year, and score range.
"""
feedback = FeedbackService(config)
if not has_user_input(click.get_current_context()):
from ....interactive.session import session
from ....interactive.state import MediaApiState, MenuName, State
# Create initial state with search results
initial_state = [State(menu_name=MenuName.DOWNLOADS)]
session.load_menus_from_folder("media")
session.run(config, history=initial_state)
registry_service = MediaRegistryService(api, config.media_registry)
search_params = _build_search_params(
query, status, genre, format, year, min_score, max_score, sort, limit
)
with feedback.progress("Searching local registry..."):
result = registry_service.search_for_media(search_params)
if not result or not result.media:
feedback.info("No Results", "No media found matching your criteria")
return
if output_json:
print(json.dumps(result.model_dump(mode="json"), indent=2))
return
from ....interactive.session import session
from ....interactive.state import MediaApiState, MenuName, State
feedback.info(
f"Found {len(result.media)} anime matching your search. Launching interactive mode..."
)
# Create initial state with search results
initial_state = [
State(menu_name=MenuName.DOWNLOADS),
State(
menu_name=MenuName.RESULTS,
media_api=MediaApiState(
search_result={
media_item.id: media_item for media_item in result.media
},
search_params=search_params,
page_info=result.page_info,
),
),
]
session.load_menus_from_folder("media")
session.run(config, history=initial_state)
def _build_search_params(
query: str | None,
status: str | None,
genre: tuple[str, ...],
format_str: str | None,
year: int | None,
min_score: float | None,
max_score: float | None,
sort: str,
limit: int,
) -> MediaSearchParams:
"""Build MediaSearchParams from command options for local filtering."""
sort_map = {
"title": MediaSort.TITLE_ROMAJI,
"score": MediaSort.SCORE_DESC,
"popularity": MediaSort.POPULARITY_DESC,
"year": MediaSort.START_DATE_DESC,
"episodes": MediaSort.EPISODES_DESC,
"updated": MediaSort.UPDATED_AT_DESC,
}
# Safely convert strings to enums
format_enum = next(
(f for f in MediaFormat if f.value.lower() == (format_str or "").lower()), None
)
genre_enums = [
g for g_str in genre for g in MediaGenre if g.value.lower() == g_str.lower()
]
# Note: Local search handles status separately as it's part of the index, not MediaItem
return MediaSearchParams(
query=query,
per_page=limit,
sort=[sort_map.get(sort.lower(), MediaSort.TITLE_ROMAJI)],
averageScore_greater=int(min_score * 10) if min_score is not None else None,
averageScore_lesser=int(max_score * 10) if max_score is not None else None,
genre_in=genre_enums or None,
format_in=[format_enum] if format_enum else None,
seasonYear=year,
)
def has_user_input(ctx: click.Context) -> bool:
"""
Checks if any command-line options or arguments were provided by the user
by comparing the given values to their default values.
This handles all parameter types including flags, multiple options,
and arguments with no default.
"""
import sys
if len(sys.argv) > 3:
return True
else:
return False
for param in ctx.command.params:
# Get the value for the parameter from the context.
# This will be the user-provided value or the default.
value = ctx.params.get(param.name)
# We need to explicitly check if a value was provided by the user.
# The simplest way to do this is to compare it to its default.
if value != param.default:
# If the value is different from the default, the user
# must have provided it.
return True
# If the loop completes without finding any non-default values,
# then no user input was given.
return False

View File

@@ -1,5 +1,5 @@
import click
from fastanime.core.config import AppConfig
from viu.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.cli.service.feedback import FeedbackService
from viu.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,7 +251,7 @@ def search(config: AppConfig, **options: "Unpack[SearchOptions]"):
and start_date_lesser is not None
and start_date_greater > start_date_lesser
):
raise FastAnimeError(
raise ViuError(
"Start date greater cannot be later than start date lesser"
)
@@ -260,7 +260,7 @@ def search(config: AppConfig, **options: "Unpack[SearchOptions]"):
and end_date_lesser is not None
and end_date_greater > end_date_lesser
):
raise FastAnimeError(
raise ViuError(
"End date greater cannot be later than end date lesser"
)
@@ -297,7 +297,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 +326,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.core.config import AppConfig
@click.command(help="Print out your anilist stats")
@@ -19,15 +19,11 @@ def stats(config: "AppConfig"):
from .....libs.media_api.api import create_api_client
from ....service.auth import AuthService
from ....service.feedback import FeedbackService
from ....service.registry import MediaRegistryService
console = Console()
feedback = FeedbackService(config)
auth = AuthService(config.general.media_api)
registry_service = MediaRegistryService(
config.general.media_api, config.media_registry
)
media_api_client = create_api_client(config.general.media_api, config)
@@ -42,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:

View File

@@ -11,22 +11,25 @@ from ...core.config import AppConfig
\b\bExamples:
# Edit your config in your default editor
# NB: If it opens vim or vi exit with `:q`
fastanime config
viu config
\b
# Start the interactive configuration wizard
fastanime config --interactive
viu config --interactive
\b
# get the path of the config file
fastanime config --path
viu config --path
\b
# print desktop entry info
fastanime config --desktop-entry
viu config --generate-desktop-entry
\b
# update your config without opening an editor
fastanime --icons --fzf --preview config --update
viu --icons --selector fzf --preview full config --update
\b
# interactively define your config
viu config --interactive
\b
# view the current contents of your config
fastanime config --view
viu config --view
""",
)
@click.option("--path", "-p", help="Print the config location and exit", is_flag=True)
@@ -40,15 +43,15 @@ from ...core.config import AppConfig
is_flag=True,
)
@click.option(
"--desktop-entry",
"--generate-desktop-entry",
"-d",
help="Configure the desktop entry of fastanime",
help="Generate the desktop entry of viu",
is_flag=True,
)
@click.option(
"--update",
"-u",
help="Persist all the config options passed to fastanime to your config file",
help="Persist all the config options passed to viu to your config file",
is_flag=True,
)
@click.option(
@@ -59,7 +62,13 @@ from ...core.config import AppConfig
)
@click.pass_obj
def config(
user_config: AppConfig, path, view, view_json, desktop_entry, update, interactive
user_config: AppConfig,
path,
view,
view_json,
generate_desktop_entry,
update,
interactive,
):
from ...core.constants import USER_CONFIG
from ..config.editor import InteractiveConfigEditor
@@ -85,7 +94,7 @@ def config(
import json
print(json.dumps(user_config.model_dump(mode="json")))
elif desktop_entry:
elif generate_desktop_entry:
_generate_desktop_entry()
elif interactive:
editor = InteractiveConfigEditor(current_config=user_config)
@@ -103,7 +112,7 @@ def config(
def _generate_desktop_entry():
"""
Generates a desktop entry for FastAnime.
Generates a desktop entry for Viu.
"""
import shutil
import sys
@@ -121,11 +130,11 @@ def _generate_desktop_entry():
__version__,
)
EXECUTABLE = shutil.which("fastanime")
EXECUTABLE = shutil.which("viu")
if EXECUTABLE:
cmds = f"{EXECUTABLE} --rofi anilist"
cmds = f"{EXECUTABLE} --selector rofi anilist"
else:
cmds = f"{sys.executable} -m fastanime --rofi anilist"
cmds = f"{sys.executable} -m viu --selector rofi anilist"
# TODO: Get funs of the other platforms to complete this lol
if PLATFORM == "win32":
@@ -140,7 +149,7 @@ def _generate_desktop_entry():
desktop_entry = dedent(
f"""
[Desktop Entry]
Name={PROJECT_NAME}
Name={PROJECT_NAME.title()}
Type=Application
version={__version__}
Path={Path().home()}

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 ..utils.completion import anime_titles_shell_complete
from . import examples
@@ -11,7 +11,7 @@ if TYPE_CHECKING:
from pathlib import Path
from typing import TypedDict
from fastanime.cli.service.feedback.service import FeedbackService
from viu.cli.service.feedback.service import FeedbackService
from typing_extensions import Unpack
from ...libs.provider.anime.base import BaseAnimeProvider
@@ -103,9 +103,9 @@ if TYPE_CHECKING:
)
@click.pass_obj
def download(config: AppConfig, **options: "Unpack[Options]"):
from fastanime.cli.service.feedback.service import FeedbackService
from viu.cli.service.feedback.service import FeedbackService
from ...core.exceptions import FastAnimeError
from ...core.exceptions import ViuError
from ...libs.provider.anime.params import (
AnimeParams,
SearchParams,
@@ -129,7 +129,7 @@ def download(config: AppConfig, **options: "Unpack[Options]"):
)
)
if not search_results:
raise FastAnimeError("No results were found matching your query")
raise ViuError("No results were found matching your query")
_search_results = {
search_result.title: search_result
@@ -140,7 +140,7 @@ def download(config: AppConfig, **options: "Unpack[Options]"):
"Select Anime", list(_search_results.keys())
)
if not selected_anime_title:
raise FastAnimeError("No title selected")
raise ViuError("No title selected")
anime_result = _search_results[selected_anime_title]
# ---- fetch selected anime ----
@@ -148,7 +148,7 @@ def download(config: AppConfig, **options: "Unpack[Options]"):
anime = provider.get(AnimeParams(id=anime_result.id, query=anime_title))
if not anime:
raise FastAnimeError(f"Failed to fetch anime {anime_result.title}")
raise ViuError(f"Failed to fetch anime {anime_result.title}")
available_episodes: list[str] = sorted(
getattr(anime.episodes, config.stream.translation_type), key=float
@@ -174,14 +174,14 @@ def download(config: AppConfig, **options: "Unpack[Options]"):
episode,
)
except (ValueError, IndexError) as e:
raise FastAnimeError(f"Invalid episode range: {e}") from e
raise ViuError(f"Invalid episode range: {e}") from e
else:
episode = selector.choose(
"Select Episode",
getattr(anime.episodes, config.stream.translation_type),
)
if not episode:
raise FastAnimeError("No episode selected")
raise ViuError("No episode selected")
download_anime(
config,
options,
@@ -204,7 +204,6 @@ def download_anime(
anime_title: str,
episode: str,
):
from ...core.downloader import DownloadParams, create_downloader
from ...libs.provider.anime.params import EpisodeStreamsParams
@@ -220,7 +219,7 @@ def download_anime(
)
)
if not streams:
raise FastAnimeError(
raise ViuError(
f"Failed to get streams for anime: {anime.title}, episode: {episode}"
)
@@ -228,7 +227,7 @@ def download_anime(
with feedback.progress("Fetching top server"):
server = next(streams, None)
if not server:
raise FastAnimeError(
raise ViuError(
f"Failed to get server for anime: {anime.title}, episode: {episode}"
)
else:
@@ -240,11 +239,11 @@ def download_anime(
else:
server_name = selector.choose("Select Server", servers_names)
if not server_name:
raise FastAnimeError("Server not selected")
raise ViuError("Server not selected")
server = servers[server_name]
stream_link = server.links[0].link
if not stream_link:
raise FastAnimeError(
raise ViuError(
f"Failed to get stream link for anime: {anime.title}, episode: {episode}"
)
feedback.info(f"[green bold]Now Downloading:[/] {anime.title} Episode: {episode}")

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