Compare commits

...

132 Commits

Author SHA1 Message Date
Benedict Xavier
8b3a57ed07 Merge pull request #163 from Oreo-Kuuki/patch-1 2025-11-03 23:41:19 +03:00
Oreo-kuuki
b2f9c8349a Fix formatting of 'Hanka x Hanka' entry in normalizer.json
So like this, right?
2025-11-03 15:37:24 -05:00
Oreo-kuuki
25fe1e5e01 Fix formatting in normalizer.json entries
Added comma, hanka x hanka without the unicode
2025-11-03 15:14:08 -05:00
Oreo-kuuki
45ff463f7a Add mapping for 'Hanka×Hanka (2011)' to 'Hunter x Hunter (2011)' 2025-11-03 15:00:41 -05:00
Benexl
2217f011af fix(core-constants): use project name over cli name 2025-11-01 20:06:53 +03:00
Benexl
5960a7c502 feat(notifications): use seconds instead of minutes 2025-11-01 19:50:46 +03:00
Benexl
bd0309ee85 feat(dev): add .venv/bin to path using direnv 2025-11-01 19:15:45 +03:00
Benexl
3724f06e33 fix(allanime-anime-provider): not giving different qualities 2025-11-01 17:26:45 +03:00
Benexl
d20af89fc8 feat(debug-anime-provider-utils): allow for quality selection 2025-11-01 16:48:51 +03:00
Benexl
3872b4c8a8 feat(search-command): allow quality selection 2025-11-01 16:48:07 +03:00
Benexl
9545b893e1 feat(search-command): if no title is provided as an option prompt it 2025-11-01 16:47:28 +03:00
Benexl
5634214fb8 chore(ci): update stale.yml to emphasize devs limited time 2025-10-27 00:33:36 +03:00
Benexl
66c0ada29d chore(ci): update days to closure of pr or issue 2025-10-27 00:24:07 +03:00
Benexl
02465b4ddb chore(ci): add stale.yml 2025-10-27 00:19:07 +03:00
Benexl
5ffd94ac24 chore(pre-commit): update pre-commit config to use only Ruff 2025-10-26 23:47:28 +03:00
Benexl
d2864df6d0 style(dev): add extra space inorder to pass ruff fmt 2025-10-26 23:37:19 +03:00
Benexl
2a28e3b9a3 chore: temporarily disable tests in workflow 2025-10-26 23:32:05 +03:00
Benexl
7b8027a8b3 fix(viu): correct import path 2025-10-26 23:28:23 +03:00
Benexl
2a36152c38 fix(provider-scraping-html-parser): pyright errors 2025-10-26 23:26:36 +03:00
Benexl
2048c7b743 fix(inquirer-selector): pyright errors 2025-10-26 23:25:55 +03:00
Benexl
133fd4c1c8 chore: run ruff check --fix 2025-10-26 23:20:30 +03:00
Benexl
e22120fe99 fix(allanime-anime-provider-utils): pyright errors 2025-10-26 23:19:36 +03:00
Benexl
44e6220662 chore: cleanup; directly implement syncplay logic in the actual players 2025-10-26 23:16:23 +03:00
Benexl
1fea1335c6 chore: move to feature branch 2025-10-26 23:10:05 +03:00
Benexl
8b664fae36 chore: move to feature branch 2025-10-26 23:09:53 +03:00
Benexl
19a85511b4 chore: move to feature branch 2025-10-26 23:09:42 +03:00
Benexl
205299108b fix(media-api-debug-utils): pyright errors 2025-10-26 23:05:31 +03:00
Benexl
7670bdd2f3 fix(jikan-media-api-mapper): pyright errors 2025-10-26 23:03:05 +03:00
Benexl
cd3f7f7fb8 fix(anilist-media-api-mapper): pyright errors 2025-10-26 22:58:12 +03:00
Benexl
5be03ed5b8 fix(core-concurrency-utils): pyright errors 2025-10-26 22:56:17 +03:00
Benexl
6581179336 fix(yt-dlp-downloader): pyright errors 2025-10-26 22:53:56 +03:00
Benexl
2bb674f4a0 fix(cli-image-utils): pyright errors 2025-10-26 22:49:32 +03:00
Benexl
642e77f601 fix(config-editor): pyright errors 2025-10-26 22:37:57 +03:00
Benexl
a5e99122f5 fix(registry-cmds): pyright errors 2025-10-26 21:30:10 +03:00
Benexl
39bd7bed61 chore: update deps 2025-10-26 20:18:08 +03:00
Benexl
869072633b chore: create .python-version 2025-10-26 20:17:47 +03:00
Benexl
cbd788a573 chore: bump python version for pyright 2025-10-26 20:13:49 +03:00
Benexl
11fe54b146 chore: update lock file 2025-10-26 19:17:48 +03:00
Benexl
a13bdb1aa0 chore: bump version 2025-10-26 19:12:56 +03:00
Benexl
627b09a723 fix(menu): runtime setting of provider 2025-10-26 19:03:51 +03:00
Benedict Xavier
aecec5c75b Add video showcase and Rofi details to README 2025-10-24 16:16:45 +03:00
Benexl
49b298ed52 chore: update lock file 2025-10-24 13:32:43 +03:00
Benexl
9a90fa196b chore: update dev deps specification to latest uv spec 2025-10-24 13:26:28 +03:00
Benexl
4ac059e873 feat(dev): automate media tag enum creation 2025-10-24 13:25:58 +03:00
Benexl
8b39a28e32 Merge pull request #157 from Abdisto/master
Adding missing media-tag
2025-10-23 01:03:02 +03:00
Abdist
066cc89b74 Update tags.json 2025-10-20 00:00:52 +02:00
Abdist
db16758d9f Fix missing closing quote in REVERSE_ISEKAI
ups
2025-10-19 23:50:41 +02:00
Abdist
78e17b2ba0 Update tags.json 2025-10-19 23:48:05 +02:00
Abdist
c5326eb8d9 Update types.py 2025-10-19 23:44:58 +02:00
Benexl
4a2d95e75e fix(animepahe-provider): update kwik.si to kwik.cx in headers 2025-10-12 12:08:05 +03:00
Benexl
3a92ba69df fix(fzf-selector): ensure consistent encoding in subprocess calls 2025-10-07 21:18:55 +03:00
Benexl
cf59f4822e feat: update repo url 2025-10-07 20:57:24 +03:00
Benexl
1cea6d0179 Merge pull request #152 from umop3plsdn/fix-category 2025-09-26 14:56:17 +03:00
David Grindle
4bc1edcc4e Fix: added the Kabuki category that was missing 2025-09-25 17:16:17 -04:00
Benexl
0c546af99c Merge pull request #149 from viu-media/minor-fixes 2025-09-21 11:53:50 +03:00
Type-Delta
1b49e186c8 change: animepahe provider domain from '.ru' to '.si' 2025-09-20 15:16:54 +07:00
Benexl
fe831f9658 Merge pull request #137 from axtrat/provider/animeunity 2025-09-07 13:57:10 +03:00
Benexl
72f0e2e5b9 Merge branch 'master' into provider/animeunity 2025-09-07 13:56:45 +03:00
Benexl
8530da23ef Merge pull request #141 from mkuritsu/master 2025-08-30 14:59:40 +03:00
mkuritsu
1e01b6e54a fix(nix): bump version and force use of python 3.12 to fix mpv gpu issues 2025-08-30 01:36:37 +01:00
axtrat
aa6ba9018d feat: limit quality selection to what's available from servers
This change affects all providers. It limits the selection if the servers don't
implement multiple qualities, ensuring that only qualities actually available
are displayed to the user.
2025-08-25 19:46:43 +02:00
axtrat
354ba6256a fix: Normalized some titles 2025-08-25 17:43:11 +02:00
axtrat
eae31420f9 fix: Error: o streaming servers 2025-08-25 15:19:25 +02:00
axtrat
01432a0fec feat: Added video quality source options 2025-08-25 15:07:38 +02:00
Benexl
c158d3fb99 Merge branch 'master' into provider/animeunity 2025-08-25 09:58:43 +03:00
axtrat
877bc043a0 fix: restoreded changes to update.py 2025-08-24 21:54:29 +02:00
axtrat
4968f8030a fix: Addes VIXCLOUD to available ProviderServer 2025-08-24 21:15:06 +02:00
axtrat
c5c7644d0d fix: Cannot fetch anime with a certain title
- added a replacing word dictionary
 - added a manual cache dictionary ID -> SearchResult to get more accurate results.
2025-08-24 18:19:39 +02:00
axtrat
ff2a5d635a feat/fix: Added special episodes to selection 2025-08-22 14:16:10 +02:00
axtrat
8626d1991c fix: Failing to get the episode list for anime that is ongoing or has more than 119 episodes. 2025-08-22 13:48:20 +02:00
Benexl
75d15a100d Merge pull request #135 from Aethar01/master 2025-08-22 13:38:29 +03:00
Aethar
25d9895c52 updated readme with correct AUR install instructions 2025-08-22 07:58:06 +09:00
axtrat
f1b796d72b feat: Initial implementation of AnimeUnity provider 2025-08-21 10:19:25 +02:00
Benexl
3f63198563 Merge pull request #132 from 0xDracula/docs/nixos-installation-instructions
docs: update installation instructions for nixos
2025-08-18 20:50:37 +03:00
Abdallah Ebrahim
8d61463156 Update README.md
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-18 20:25:10 +03:00
0xDracula
2daa51d384 docs: update installation instructions for nixos 2025-08-18 20:17:11 +03:00
Benexl
43a0d77e1b dev(envrc): isolate development files 2025-08-18 16:27:43 +03:00
Benexl
eaedf3268d feat(config): switch to toml format 2025-08-18 14:06:31 +03:00
Benexl
ade0465ea4 chore: set py version for pyright 2025-08-18 13:24:50 +03:00
Benexl
5e82db4ea8 chore: add repomixignore 2025-08-18 13:23:48 +03:00
Benexl
a10e56cb6f refactor:set min supported python version to 3.11 2025-08-18 13:19:56 +03:00
Benexl
fbd95e1966 feat(config-loader): allow env vars 2025-08-18 13:04:00 +03:00
Benexl
d37a441ccf fix(state): check for is None instead 2025-08-18 12:33:15 +03:00
Benexl
cbc1ceccbb feat(cli): auto check for updates 2025-08-18 02:14:56 +03:00
Benexl
249a207cad fix(update-command): use viu-media when updating 2025-08-18 01:28:59 +03:00
Benexl
c8a42c4920 Update README.md 2025-08-18 01:16:47 +03:00
Benexl
de8b6b7f2f chore: bump version 2025-08-18 01:15:00 +03:00
Benexl
54e0942233 chore: update uv.lock 2025-08-18 01:12:10 +03:00
Benexl
8ea0c121c2 chore: viu_media 2025-08-18 01:08:27 +03:00
Benexl
eddaad64e7 chore: viu media is better 2025-08-18 01:07:36 +03:00
Benexl
43be7a52cf chore(envrc): check if nix command is available 2025-08-18 00:31:05 +03:00
Benexl
b689760a25 Merge pull request #129 from s-weigand/fix-ci
🚇🩹 Fix test CI workflow
2025-08-17 20:25:55 +03:00
Benexl
e53246b79b feat(interactive-state): media api state should come second 2025-08-17 19:45:59 +03:00
Benexl
b0fc94cdc5 style: ruff format 2025-08-17 19:40:53 +03:00
Benexl
449f6c1e59 feat(interactive-state): create accessors that ensure values exist 2025-08-17 19:38:55 +03:00
Benexl
ab4734b79d fix(session): allow offline viewing by wrapping authenticate in try block 2025-08-17 17:49:38 +03:00
Benexl
93d0f6a1a5 refactor: fa to viu 2025-08-17 17:22:38 +03:00
Benexl
19c75c48b2 Merge pull request #128 from s-weigand/improve-title-matching
👌 Make finding best_match_title more robust
2025-08-17 16:49:32 +03:00
Benexl
5341b0a844 Update README.md 2025-08-17 16:40:26 +03:00
Benexl
24e7e6a16b Update README.md 2025-08-17 16:36:52 +03:00
s-weigand
4b310e60b8 Revert " Run on feature-branch"
This reverts commit c6b8cfc294.
2025-08-17 13:42:43 +02:00
s-weigand
4d50cffd86 🧹 Ignore blank except ruff rule 2025-08-17 13:14:32 +02:00
s-weigand
f6fedf0500 🧹 Remove unused TYPE_CHECKING import 2025-08-17 13:13:11 +02:00
s-weigand
7b431450fe 🩹 Relock uv.lock file due to changed package name 2025-08-17 13:09:52 +02:00
s-weigand
66b247330b 🚇🩹 Install libglib2.0-dev 2025-08-17 12:56:03 +02:00
s-weigand
c6b8cfc294 Run on feature-branch 2025-08-17 12:49:05 +02:00
s-weigand
6895426d67 🚇🩹 Install dbus-python build dependencies 2025-08-17 12:48:29 +02:00
s-weigand
cc69dc35f6 👌 Make finding best_match_title more robust 2025-08-17 12:34:25 +02:00
Benexl
ed81f37ae4 Merge pull request #126 from blob5/master
Build failure on nixOS. ModuleNotFoundError: No module named 'viu'
2025-08-16 23:47:43 +03:00
Senna
c6858b00c4 remove pythonImportsCheck 2025-08-16 22:08:06 +02:00
Benexl
a44034a5d4 chore: remove 2025-08-16 21:47:44 +03:00
Benexl
f768518721 Update README.md 2025-08-16 19:48:57 +03:00
Benexl
97f5bb9cb3 chore: bump 2025-08-16 19:45:25 +03:00
Benexl
b09fdbf69b chore: update deps 2025-08-16 19:44:49 +03:00
Benexl
071c46cad9 chore: bump version 2025-08-16 19:32:23 +03:00
Benexl
5d32503ff9 chore: update publish.yml 2025-08-16 19:31:28 +03:00
Benexl
e67532c496 chore: bump version 2025-08-16 19:19:32 +03:00
Benexl
819012897d Update README.md 2025-08-16 19:17:44 +03:00
Benexl
c4f78b12a4 revert 2025-08-16 19:16:11 +03:00
Benexl
8aacbcc35b Update README.md 2025-08-16 19:11:21 +03:00
Benexl
5976ab43b2 chore: correct package issues 2025-08-16 19:08:39 +03:00
Benexl
99c67a4bc0 fix: publish.yml 2025-08-16 19:00:44 +03:00
Benexl
34851fd3e4 chore: update publish.yml 2025-08-16 18:58:59 +03:00
Benexl
e74b5977bb chore: update workflow 2025-08-16 18:56:31 +03:00
Benexl
0650f45fba revert 2025-08-16 18:55:33 +03:00
Benexl
0c8f2a70ba chore: update project name 2025-08-16 18:50:42 +03:00
Benexl
ce7cd98783 feat(constants): _LOWER 2025-08-16 16:57:08 +03:00
Benexl
ec14c40c77 feat(config-generator): include computed fields + show defaults 2025-08-16 16:54:47 +03:00
Benexl
6a6e03c744 feat(cli): invoke the default media api on no subcommand 2025-08-16 16:32:07 +03:00
Benexl
26de1a0fb4 feat(cli): register completions cmd 2025-08-16 16:21:41 +03:00
Benexl
e49fb4898c refactor: rename to viu 2025-08-16 16:18:41 +03:00
Benexl
e2407d4948 Update README.md 2025-08-15 13:45:52 +03:00
292 changed files with 9397 additions and 5688 deletions

7
.envrc
View File

@@ -1 +1,6 @@
use flake
VIU_APP_NAME="viu-dev"
PATH="./.venv/bin/:$PATH"
export PATH VIU_APP_NAME
if command -v nix >/dev/null;then
use flake
fi

15
.github/FUNDING.yml vendored
View File

@@ -1,15 +0,0 @@
# These are supported funding model platforms
github: benexl # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: benexl # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
thanks_dev: # Replace with a single thanks.dev username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

57
.github/workflows/stale.yml vendored Normal file
View File

@@ -0,0 +1,57 @@
name: Mark Stale Issues and Pull Requests
on:
schedule:
# Runs every day at 6:30 UTC
- cron: "30 6 * * *"
# Allows you to run this workflow manually from the Actions tab for testing
workflow_dispatch:
jobs:
stale:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v5
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: |
Greetings @{{author}},
This bug report is like an ancient scroll detailing a legendary beast. Our small guild of developers is often on many quests at once, so our response times can be slower than a tortoise in a time-stop spell. We deeply appreciate your patience!
**Seeking Immediate Help or Discussion?**
Our **[Discord Tavern](https://discord.gg/HBEmAwvbHV)** is the best place to get a quick response from the community for general questions or setup help!
**Want to Be the Hero?**
You could try to tame this beast yourself! With modern grimoires (like AI coding assistants) and our **[Contribution Guide](https://github.com/viu-media/Viu/blob/master/CONTRIBUTIONS.md)**, you might just be the hero we're waiting for. We would be thrilled to review your solution!
---
To keep our quest board tidy, we need to know if this creature is still roaming the lands in the latest version of `viu`. If we don't get an update within **7 days**, we'll assume it has vanished and archive the scroll.
Thanks for being our trusted scout!
stale-pr-message: |
Hello @{{author}}, it looks like this powerful contribution has been left in the middle of its training arc! 💪
Our review dojo is managed by just a few senseis who are sometimes away on long missions, so thank you for your patience as we work through the queue.
We were excited to see this new technique being developed. Are you still planning to complete its training, or have you embarked on a different quest? If you need a sparring partner (reviewer) or some guidance from a senpai, just let us know!
To keep our dojo tidy, we'll be archiving unfinished techniques. If we don't hear back within **7 days**, we'll assume it's time to close this PR for now. You can always resume your training and reopen it when you're ready.
Thank you for your incredible effort!
# --- Labels and Timing ---
stale-issue-label: "stale"
stale-pr-label: "stale"
# How many days of inactivity before an issue/PR is marked as stale.
days-before-stale: 14
# How many days of inactivity to wait before closing a stale issue/PR.
days-before-close: 7

View File

@@ -13,7 +13,7 @@ jobs:
strategy:
matrix:
python-version: ["3.10", "3.11"] # List the Python versions you want to test
python-version: ["3.11", "3.12"]
steps:
- uses: actions/checkout@v4
@@ -22,6 +22,11 @@ jobs:
with:
python-version: ${{ matrix.python-version }}
- name: Install dbus-python build dependencies
run: |
sudo apt-get update
sudo apt-get -y install libdbus-1-dev libglib2.0-dev
- name: Install uv
uses: astral-sh/setup-uv@v3
with:
@@ -36,5 +41,7 @@ jobs:
- name: Run type checking
run: uv run pyright
- name: Run tests
run: uv run pytest tests
# TODO: write tests
# - name: Run tests
# run: uv run pytest tests

View File

@@ -1,33 +1,10 @@
default_language_version:
python: python3.12
repos:
- repo: https://github.com/pycqa/isort
rev: 5.12.0
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.14.2
hooks:
- id: isort
name: isort (python)
args: ["--profile", "black"]
- repo: https://github.com/PyCQA/autoflake
rev: v2.2.1
hooks:
- id: autoflake
args:
[
"--in-place",
"--remove-unused-variables",
"--remove-all-unused-imports",
]
# - repo: https://github.com/astral-sh/ruff-pre-commit
# rev: v0.4.10
# hooks:
# - id: ruff
# args: [--fix]
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 24.4.2
hooks:
- id: black
name: black
#language_version: python3.10
# Run the linter.
- id: ruff-check
args: [--fix]
# Run the formatter.
- id: ruff-format

1
.python-version Normal file
View File

@@ -0,0 +1 @@
3.11

1
.repomixignore Normal file
View File

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

View File

@@ -1,29 +1,29 @@
# Contributing to FastAnime
# Contributing to Viu
First off, thank you for considering contributing to FastAnime! We welcome any help, whether it's reporting a bug, proposing a feature, or writing code. This document will guide you through the process.
First off, thank you for considering contributing to Viu! We welcome any help, whether it's reporting a bug, proposing a feature, or writing code. This document will guide you through the process.
## How Can I Contribute?
There are many ways to contribute to the FastAnime project:
There are many ways to contribute to the Viu project:
* **Reporting Bugs:** If you find a bug, please create an issue in our [issue tracker](https://github.com/Benexl/FastAnime/issues).
* **Reporting Bugs:** If you find a bug, please create an issue in our [issue tracker](https://github.com/viu-media/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/viu-media/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>

147
README.md
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,66 @@
#!/usr/bin/env -S uv run --script
import json
from collections import defaultdict
from pathlib import Path
import httpx
from viu_media.core.utils.graphql import execute_graphql
DEV_DIR = Path(__file__).resolve().parent
media_tags_type_py = (
DEV_DIR.parent / "viu_media" / "libs" / "media_api" / "_media_tags.py"
)
media_tags_gql = DEV_DIR / "graphql" / "anilist" / "media_tags.gql"
generated_tags_json = DEV_DIR / "generated" / "anilist" / "tags.json"
media_tags_response = execute_graphql(
"https://graphql.anilist.co", httpx.Client(), media_tags_gql, {}
)
media_tags_response.raise_for_status()
template = """\
# DO NOT EDIT THIS FILE !!! ( 。 •̀ ᴖ •́ 。)
# ITS AUTOMATICALLY GENERATED BY RUNNING ./dev/generate_anilist_media_tags.py
# FROM THE PROJECT ROOT
# SO RUN THAT INSTEAD TO UPDATE THE FILE WITH THE LATEST MEDIA TAGS :)
from enum import Enum
class MediaTag(Enum):\
"""
# 4 spaces
tab = " "
tags = defaultdict(list)
for tag in media_tags_response.json()["data"]["MediaTagCollection"]:
tags[tag["category"]].append(
{
"name": tag["name"],
"description": tag["description"],
"is_adult": tag["isAdult"],
}
)
# save copy of data used to generate the class
json.dump(tags, generated_tags_json.open("w", encoding="utf-8"), indent=2)
for key, value in tags.items():
template = f"{template}\n{tab}#\n{tab}# {key.upper()}\n{tab}#\n"
for tag in value:
name = tag["name"]
_tag_name = name.replace("-", "_").replace(" ", "_").upper()
if _tag_name.startswith(("0", "1", "2", "3", "4", "5", "6", "7", "8", "9")):
_tag_name = f"_{_tag_name}"
tag_name = ""
# sanitize invalid characters for attribute names
for char in _tag_name:
if char.isidentifier() or char.isdigit():
tag_name += char
desc = tag["description"].replace("\n", "")
is_adult = tag["is_adult"]
template = f'{template}\n{tab}# {desc} (is_adult: {is_adult})\n{tab}{tag_name} = "{name}"\n'
media_tags_type_py.write_text(template, "utf-8")

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"

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,8 @@
query {
MediaTagCollection {
name
description
category
isAdult
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,160 +0,0 @@
"""Update command for FastAnime CLI."""
import sys
from typing import TYPE_CHECKING
import click
from rich import print
from rich.console import Console
from rich.markdown import Markdown
from ..utils.update import check_for_updates, update_app
if TYPE_CHECKING:
from ...core.config import AppConfig
@click.command(
help="Update FastAnime to the latest version",
short_help="Update FastAnime",
epilog="""
\b
\b\bExamples:
# Check for updates and update if available
fastanime update
\b
# Force update even if already up to date
fastanime update --force
\b
# Only check for updates without updating
fastanime update --check-only
\b
# Show release notes for the latest version
fastanime update --release-notes
""",
)
@click.option(
"--force",
"-f",
is_flag=True,
help="Force update even if already up to date",
)
@click.option(
"--check-only",
"-c",
is_flag=True,
help="Only check for updates without updating",
)
@click.option(
"--release-notes",
"-r",
is_flag=True,
help="Show release notes for the latest version",
)
@click.pass_context
@click.pass_obj
def update(
config: "AppConfig",
ctx: click.Context,
force: bool,
check_only: bool,
release_notes: bool,
) -> None:
"""
Update FastAnime to the latest version.
This command checks for available updates and optionally updates
the application to the latest version from the configured sources
(pip, uv, pipx, git, or nix depending on installation method).
Args:
config: The application configuration object
ctx: The click context containing CLI options
force: Whether to force update even if already up to date
check_only: Whether to only check for updates without updating
release_notes: Whether to show release notes for the latest version
"""
try:
if release_notes:
print("[cyan]Fetching latest release notes...[/]")
is_latest, release_json = check_for_updates()
if not release_json:
print(
"[yellow]Could not fetch release information. Please check your internet connection.[/]"
)
sys.exit(1)
version = release_json.get("tag_name", "unknown")
release_name = release_json.get("name", version)
release_body = release_json.get("body", "No release notes available.")
published_at = release_json.get("published_at", "unknown")
console = Console()
print(f"[bold cyan]Release: {release_name}[/]")
print(f"[dim]Version: {version}[/]")
print(f"[dim]Published: {published_at}[/]")
print()
# Display release notes as markdown if available
if release_body.strip():
markdown = Markdown(release_body)
console.print(markdown)
else:
print("[dim]No release notes available for this version.[/]")
return
elif check_only:
print("[cyan]Checking for updates...[/]")
is_latest, release_json = check_for_updates()
if not release_json:
print(
"[yellow]Could not check for updates. Please check your internet connection.[/]"
)
sys.exit(1)
if is_latest:
print("[green]FastAnime is up to date![/]")
print(
f"[dim]Current version: {release_json.get('tag_name', 'unknown')}[/]"
)
else:
latest_version = release_json.get("tag_name", "unknown")
print(f"[yellow]Update available: {latest_version}[/]")
print("[dim]Run 'fastanime update' to update[/]")
sys.exit(1)
else:
print("[cyan]Checking for updates and updating if necessary...[/]")
success, release_json = update_app(force=force)
if not release_json:
print(
"[red]Could not check for updates. Please check your internet connection.[/]"
)
sys.exit(1)
if success:
latest_version = release_json.get("tag_name", "unknown")
print(f"[green]Successfully updated to version {latest_version}![/]")
else:
if force:
print(
"[red]Update failed. Please check the error messages above.[/]"
)
sys.exit(1)
# If not forced and update failed, it might be because already up to date
# The update_app function already prints appropriate messages
except KeyboardInterrupt:
print("\n[yellow]Update cancelled by user.[/]")
sys.exit(1)
except Exception as e:
print(f"[red]An error occurred during update: {e}[/]")
# Get trace option from parent context
trace = ctx.parent.params.get("trace", False) if ctx.parent else False
if trace:
raise
sys.exit(1)

View File

@@ -1,4 +0,0 @@
from .generate import generate_config_ini_from_app_model
from .loader import ConfigLoader
__all__ = ["ConfigLoader", "generate_config_ini_from_app_model"]

View File

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

View File

@@ -1,85 +0,0 @@
from enum import Enum
from typing import Dict, Optional, Union
from pydantic import BaseModel, ConfigDict, Field
from ...libs.media_api.params import MediaSearchParams, UserMediaListSearchParams
from ...libs.media_api.types import MediaItem, PageInfo
from ...libs.provider.anime.types import Anime, SearchResults, Server
# TODO: is internal directive a good name
class InternalDirective(Enum):
MAIN = "MAIN"
BACK = "BACK"
BACKX2 = "BACKX2"
BACKX3 = "BACKX3"
BACKX4 = "BACKX4"
EXIT = "EXIT"
CONFIG_EDIT = "CONFIG_EDIT"
RELOAD = "RELOAD"
class MenuName(Enum):
MAIN = "MAIN"
AUTH = "AUTH"
EPISODES = "EPISODES"
RESULTS = "RESULTS"
SERVERS = "SERVERS"
WATCH_HISTORY = "WATCH_HISTORY"
PROVIDER_SEARCH = "PROVIDER_SEARCH"
PLAYER_CONTROLS = "PLAYER_CONTROLS"
USER_MEDIA_LIST = "USER_MEDIA_LIST"
SESSION_MANAGEMENT = "SESSION_MANAGEMENT"
MEDIA_ACTIONS = "MEDIA_ACTIONS"
DOWNLOADS = "DOWNLOADS"
DYNAMIC_SEARCH = "DYNAMIC_SEARCH"
MEDIA_REVIEW = "MEDIA_REVIEW"
MEDIA_CHARACTERS = "MEDIA_CHARACTERS"
MEDIA_AIRING_SCHEDULE = "MEDIA_AIRING_SCHEDULE"
PLAY_DOWNLOADS = "PLAY_DOWNLOADS"
DOWNLOADS_PLAYER_CONTROLS = "DOWNLOADS_PLAYER_CONTROLS"
DOWNLOAD_EPISODES = "DOWNLOAD_EPISODES"
class StateModel(BaseModel):
model_config = ConfigDict(frozen=True)
class MediaApiState(StateModel):
search_result: Optional[Dict[int, MediaItem]] = None
search_params: Optional[Union[MediaSearchParams, UserMediaListSearchParams]] = None
page_info: Optional[PageInfo] = None
media_id: Optional[int] = None
@property
def media_item(self) -> Optional[MediaItem]:
if self.search_result and self.media_id:
return self.search_result[self.media_id]
class ProviderState(StateModel):
search_results: Optional[SearchResults] = None
anime: Optional[Anime] = None
episode: Optional[str] = None
servers: Optional[Dict[str, Server]] = None
server_name: Optional[str] = None
start_time: Optional[str] = None
@property
def server(self) -> Optional[Server]:
if self.servers and self.server_name:
return self.servers[self.server_name]
class State(StateModel):
menu_name: MenuName
provider: ProviderState = Field(default_factory=ProviderState)
media_api: MediaApiState = Field(default_factory=MediaApiState)

View File

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

View File

@@ -1,22 +0,0 @@
from httpx import get
ANISKIP_ENDPOINT = "https://api.aniskip.com/v1/skip-times"
# TODO: Finish own implementation of aniskip script
class AniSkip:
@classmethod
def get_skip_times(
cls, mal_id: int, episode_number: float | int, types=["op", "ed"]
):
url = f"{ANISKIP_ENDPOINT}/{mal_id}/{episode_number}?types=op&types=ed"
response = get(url)
print(response.text)
return response.json()
if __name__ == "__main__":
mal_id = input("Mal id: ")
episode_number = input("episode_number: ")
skip_times = AniSkip.get_skip_times(int(mal_id), float(episode_number))
print(skip_times)

View File

@@ -1,3 +0,0 @@
from .api import connect
__all__ = ["connect"]

View File

@@ -1,13 +0,0 @@
import time
from pypresence import Presence
def connect(show, episode, switch):
presence = Presence(client_id="1292070065583165512")
presence.connect()
if not switch.is_set():
presence.update(details=show, state="Watching episode " + episode)
time.sleep(10)
else:
presence.close()

View File

@@ -1,873 +0,0 @@
from __future__ import annotations
from datetime import datetime
from enum import Enum
from typing import Dict, List, Optional
from pydantic import BaseModel, ConfigDict, Field
# ENUMS
class MediaStatus(Enum):
FINISHED = "FINISHED"
RELEASING = "RELEASING"
NOT_YET_RELEASED = "NOT_YET_RELEASED"
CANCELLED = "CANCELLED"
HIATUS = "HIATUS"
class MediaType(Enum):
ANIME = "ANIME"
MANGA = "MANGA"
class UserMediaListStatus(Enum):
PLANNING = "planning"
WATCHING = "watching"
COMPLETED = "completed"
DROPPED = "dropped"
PAUSED = "paused"
REPEATING = "repeating"
class MediaGenre(Enum):
ACTION = "Action"
ADVENTURE = "Adventure"
COMEDY = "Comedy"
DRAMA = "Drama"
ECCHI = "Ecchi"
FANTASY = "Fantasy"
HORROR = "Horror"
MAHOU_SHOUJO = "Mahou Shoujo"
MECHA = "Mecha"
MUSIC = "Music"
MYSTERY = "Mystery"
PSYCHOLOGICAL = "Psychological"
ROMANCE = "Romance"
SCI_FI = "Sci-Fi"
SLICE_OF_LIFE = "Slice of Life"
SPORTS = "Sports"
SUPERNATURAL = "Supernatural"
THRILLER = "Thriller"
HENTAI = "Hentai"
class MediaFormat(Enum):
TV = "TV"
TV_SHORT = "TV_SHORT"
MOVIE = "MOVIE"
MANGA = "MANGA"
SPECIAL = "SPECIAL"
OVA = "OVA"
ONA = "ONA"
MUSIC = "MUSIC"
NOVEL = "NOVEL"
ONE_SHOT = "ONE_SHOT"
class NotificationType(Enum):
AIRING = "AIRING"
RELATED_MEDIA_ADDITION = "RELATED_MEDIA_ADDITION"
MEDIA_DATA_CHANGE = "MEDIA_DATA_CHANGE"
# ... add other types as needed
# MODELS
class BaseMediaApiModel(BaseModel):
model_config = ConfigDict(frozen=True)
class MediaImage(BaseMediaApiModel):
"""A generic representation of media imagery URLs."""
large: str
medium: Optional[str] = None
extra_large: Optional[str] = None
class MediaTitle(BaseMediaApiModel):
"""A generic representation of media titles."""
english: str
romaji: Optional[str] = None
native: Optional[str] = None
class MediaTrailer(BaseMediaApiModel):
"""A generic representation of a media trailer."""
id: str
site: str # e.g., "youtube"
thumbnail_url: Optional[str] = None
class AiringSchedule(BaseMediaApiModel):
"""A generic representation of the next airing episode."""
episode: int
airing_at: Optional[datetime] = None
class CharacterName(BaseMediaApiModel):
"""A generic representation of a character's name."""
first: Optional[str] = None
middle: Optional[str] = None
last: Optional[str] = None
full: Optional[str] = None
native: Optional[str] = None
class CharacterImage(BaseMediaApiModel):
"""A generic representation of a character's image."""
medium: Optional[str] = None
large: Optional[str] = None
class Character(BaseMediaApiModel):
"""A generic representation of an anime character."""
id: Optional[int] = None
name: CharacterName
image: Optional[CharacterImage] = None
description: Optional[str] = None
gender: Optional[str] = None
age: Optional[str] = None
blood_type: Optional[str] = None
favourites: Optional[int] = None
date_of_birth: Optional[datetime] = None
class AiringScheduleItem(BaseMediaApiModel):
"""A generic representation of an airing schedule item."""
episode: int
airing_at: Optional[datetime] = None
time_until_airing: Optional[int] = None # In seconds
class CharacterSearchResult(BaseMediaApiModel):
"""A generic representation of character search results."""
characters: List[Character] = Field(default_factory=list)
page_info: Optional[PageInfo] = None
class AiringScheduleResult(BaseMediaApiModel):
"""A generic representation of airing schedule results."""
schedule_items: List[AiringScheduleItem] = Field(default_factory=list)
page_info: Optional[PageInfo] = None
class Studio(BaseMediaApiModel):
"""A generic representation of an animation studio."""
id: Optional[int] = None
name: Optional[str] = None
favourites: Optional[int] = None
is_animation_studio: Optional[bool] = None
class MediaTagItem(BaseMediaApiModel):
"""A generic representation of a descriptive tag."""
name: MediaTag
rank: Optional[int] = None # Percentage relevance from 0-100
class StreamingEpisode(BaseMediaApiModel):
"""A generic representation of a streaming episode."""
title: str
thumbnail: Optional[str] = None
class UserListItem(BaseMediaApiModel):
"""Generic representation of a user's list status for a media item."""
id: Optional[int] = None
status: Optional[UserMediaListStatus] = None
progress: Optional[int] = None
score: Optional[float] = None
repeat: Optional[int] = None
notes: Optional[str] = None
start_date: Optional[datetime] = None
completed_at: Optional[datetime] = None
created_at: Optional[str] = None
class MediaItem(BaseMediaApiModel):
id: int
title: MediaTitle
id_mal: Optional[int] = None
type: MediaType = MediaType.ANIME
status: MediaStatus = MediaStatus.FINISHED
format: Optional[MediaFormat] = MediaFormat.TV
cover_image: Optional[MediaImage] = None
banner_image: Optional[str] = None
trailer: Optional[MediaTrailer] = None
description: Optional[str] = None
episodes: Optional[int] = None
duration: Optional[int] = None # In minutes
genres: List[MediaGenre] = Field(default_factory=list)
tags: List[MediaTagItem] = Field(default_factory=list)
studios: List[Studio] = Field(default_factory=list)
synonymns: List[str] = Field(default_factory=list)
average_score: Optional[float] = None
popularity: Optional[int] = None
favourites: Optional[int] = None
start_date: Optional[datetime] = None
end_date: Optional[datetime] = None
next_airing: Optional[AiringSchedule] = None
# streaming episodes
streaming_episodes: Dict[str, StreamingEpisode] = Field(default_factory=dict)
# user related
user_status: Optional[UserListItem] = None
class Notification(BaseMediaApiModel):
"""A generic representation of a user notification."""
id: int
type: NotificationType
episode: Optional[int] = None
contexts: List[str] = Field(default_factory=list)
created_at: datetime
media: MediaItem
class PageInfo(BaseMediaApiModel):
"""Generic pagination information."""
total: int = 1
current_page: int = 1
has_next_page: bool = False
per_page: int = 15
class MediaSearchResult(BaseMediaApiModel):
"""A generic representation of a page of media search results."""
page_info: PageInfo
media: List[MediaItem] = Field(default_factory=list)
class UserProfile(BaseMediaApiModel):
"""A generic representation of a user's profile."""
id: int
name: str
avatar_url: Optional[str] = None
banner_url: Optional[str] = None
class Reviewer(BaseMediaApiModel):
"""A generic representation of a user who wrote a review."""
name: str
avatar_url: Optional[str] = None
class MediaReview(BaseMediaApiModel):
"""A generic representation of a media review."""
summary: Optional[str] = None
body: str
user: Reviewer
# ENUMS
class MediaTag(Enum):
# Cast
POLYAMOROUS = "Polyamorous"
# Cast Main Cast
ANTI_HERO = "Anti-Hero"
ELDERLY_PROTAGONIST = "Elderly Protagonist"
ENSEMBLE_CAST = "Ensemble Cast"
ESTRANGED_FAMILY = "Estranged Family"
FEMALE_PROTAGONIST = "Female Protagonist"
MALE_PROTAGONIST = "Male Protagonist"
PRIMARILY_ADULT_CAST = "Primarily Adult Cast"
PRIMARILY_ANIMAL_CAST = "Primarily Animal Cast"
PRIMARILY_CHILD_CAST = "Primarily Child Cast"
PRIMARILY_FEMALE_CAST = "Primarily Female Cast"
PRIMARILY_MALE_CAST = "Primarily Male Cast"
PRIMARILY_TEEN_CAST = "Primarily Teen Cast"
# Cast Traits
AGE_REGRESSION = "Age Regression"
AGENDER = "Agender"
ALIENS = "Aliens"
AMNESIA = "Amnesia"
ANGELS = "Angels"
ANTHROPOMORPHISM = "Anthropomorphism"
AROMANTIC = "Aromantic"
ARRANGED_MARRIAGE = "Arranged Marriage"
ARTIFICIAL_INTELLIGENCE = "Artificial Intelligence"
ASEXUAL = "Asexual"
BISEXUAL = "Bisexual"
BUTLER = "Butler"
CENTAUR = "Centaur"
CHIMERA = "Chimera"
CHUUNIBYOU = "Chuunibyou"
CLONE = "Clone"
COSPLAY = "Cosplay"
COWBOYS = "Cowboys"
CROSSDRESSING = "Crossdressing"
CYBORG = "Cyborg"
DELINQUENTS = "Delinquents"
DEMONS = "Demons"
DETECTIVE = "Detective"
DINOSAURS = "Dinosaurs"
DISABILITY = "Disability"
DISSOCIATIVE_IDENTITIES = "Dissociative Identities"
DRAGONS = "Dragons"
DULLAHAN = "Dullahan"
ELF = "Elf"
FAIRY = "Fairy"
FEMBOY = "Femboy"
GHOST = "Ghost"
GOBLIN = "Goblin"
GODS = "Gods"
GYARU = "Gyaru"
HIKIKOMORI = "Hikikomori"
HOMELESS = "Homeless"
IDOL = "Idol"
KEMONOMIMI = "Kemonomimi"
KUUDERE = "Kuudere"
MAIDS = "Maids"
MERMAID = "Mermaid"
MONSTER_BOY = "Monster Boy"
MONSTER_GIRL = "Monster Girl"
NEKOMIMI = "Nekomimi"
NINJA = "Ninja"
NUDITY = "Nudity"
NUN = "Nun"
OFFICE_LADY = "Office Lady"
OIRAN = "Oiran"
OJOU_SAMA = "Ojou-sama"
ORPHAN = "Orphan"
PIRATES = "Pirates"
ROBOTS = "Robots"
SAMURAI = "Samurai"
SHRINE_MAIDEN = "Shrine Maiden"
SKELETON = "Skeleton"
SUCCUBUS = "Succubus"
TANNED_SKIN = "Tanned Skin"
TEACHER = "Teacher"
TOMBOY = "Tomboy"
TRANSGENDER = "Transgender"
TSUNDERE = "Tsundere"
TWINS = "Twins"
VAMPIRE = "Vampire"
VETERINARIAN = "Veterinarian"
VIKINGS = "Vikings"
VILLAINESS = "Villainess"
VTUBER = "VTuber"
WEREWOLF = "Werewolf"
WITCH = "Witch"
YANDERE = "Yandere"
YOUKAI = "Youkai"
ZOMBIE = "Zombie"
# Demographic
JOSEI = "Josei"
KIDS = "Kids"
SEINEN = "Seinen"
SHOUJO = "Shoujo"
SHOUNEN = "Shounen"
# Setting
MATRIARCHY = "Matriarchy"
# Setting Scene
BAR = "Bar"
BOARDING_SCHOOL = "Boarding School"
CAMPING = "Camping"
CIRCUS = "Circus"
COASTAL = "Coastal"
COLLEGE = "College"
DESERT = "Desert"
DUNGEON = "Dungeon"
FOREIGN = "Foreign"
INN = "Inn"
KONBINI = "Konbini"
NATURAL_DISASTER = "Natural Disaster"
OFFICE = "Office"
OUTDOOR_ACTIVITIES = "Outdoor Activities"
PRISON = "Prison"
RESTAURANT = "Restaurant"
RURAL = "Rural"
SCHOOL = "School"
SCHOOL_CLUB = "School Club"
SNOWSCAPE = "Snowscape"
URBAN = "Urban"
WILDERNESS = "Wilderness"
WORK = "Work"
# Setting Time
ACHRONOLOGICAL_ORDER = "Achronological Order"
ANACHRONISM = "Anachronism"
ANCIENT_CHINA = "Ancient China"
DYSTOPIAN = "Dystopian"
HISTORICAL = "Historical"
MEDIEVAL = "Medieval"
TIME_SKIP = "Time Skip"
# Setting Universe
AFTERLIFE = "Afterlife"
ALTERNATE_UNIVERSE = "Alternate Universe"
AUGMENTED_REALITY = "Augmented Reality"
OMEGAVERSE = "Omegaverse"
POST_APOCALYPTIC = "Post-Apocalyptic"
SPACE = "Space"
URBAN_FANTASY = "Urban Fantasy"
VIRTUAL_WORLD = "Virtual World"
# Sexual Content
AHEGAO = "Ahegao"
AMPUTATION = "Amputation"
ANAL_SEX = "Anal Sex"
ARMPITS = "Armpits"
ASHIKOKI = "Ashikoki"
ASPHYXIATION = "Asphyxiation"
BONDAGE = "Bondage"
BOOBJOB = "Boobjob"
CERVIX_PENETRATION = "Cervix Penetration"
CHEATING = "Cheating"
CUMFLATION = "Cumflation"
CUNNILINGUS = "Cunnilingus"
DEEPTHROAT = "Deepthroat"
DEFLORATION = "Defloration"
DILF = "DILF"
DOUBLE_PENETRATION = "Double Penetration"
EROTIC_PIERCINGS = "Erotic Piercings"
EXHIBITIONISM = "Exhibitionism"
FACIAL = "Facial"
FEET = "Feet"
FELLATIO = "Fellatio"
FEMDOM = "Femdom"
FISTING = "Fisting"
FLAT_CHEST = "Flat Chest"
FUTANARI = "Futanari"
GROUP_SEX = "Group Sex"
HAIR_PULLING = "Hair Pulling"
HANDJOB = "Handjob"
HUMAN_PET = "Human Pet"
HYPERSEXUALITY = "Hypersexuality"
INCEST = "Incest"
INSEKI = "Inseki"
IRRUMATIO = "Irrumatio"
LACTATION = "Lactation"
LARGE_BREASTS = "Large Breasts"
MALE_PREGNANCY = "Male Pregnancy"
MASOCHISM = "Masochism"
MASTURBATION = "Masturbation"
MATING_PRESS = "Mating Press"
MILF = "MILF"
NAKADASHI = "Nakadashi"
NETORARE = "Netorare"
NETORASE = "Netorase"
NETORI = "Netori"
PET_PLAY = "Pet Play"
PROSTITUTION = "Prostitution"
PUBLIC_SEX = "Public Sex"
RAPE = "Rape"
RIMJOB = "Rimjob"
SADISM = "Sadism"
SCAT = "Scat"
SCISSORING = "Scissoring"
SEX_TOYS = "Sex Toys"
SHIMAIDON = "Shimaidon"
SQUIRTING = "Squirting"
SUMATA = "Sumata"
SWEAT = "Sweat"
TENTACLES = "Tentacles"
THREESOME = "Threesome"
VIRGINITY = "Virginity"
VORE = "Vore"
VOYEUR = "Voyeur"
WATERSPORTS = "Watersports"
ZOOPHILIA = "Zoophilia"
# Technical
_4_KOMA = "4-koma"
ACHROMATIC = "Achromatic"
ADVERTISEMENT = "Advertisement"
ANTHOLOGY = "Anthology"
CGI = "CGI"
EPISODIC = "Episodic"
FLASH = "Flash"
FULL_CGI = "Full CGI"
FULL_COLOR = "Full Color"
LONG_STRIP = "Long Strip"
MIXED_MEDIA = "Mixed Media"
NO_DIALOGUE = "No Dialogue"
NON_FICTION = "Non-fiction"
POV = "POV"
PUPPETRY = "Puppetry"
ROTOSCOPING = "Rotoscoping"
STOP_MOTION = "Stop Motion"
VERTICAL_VIDEO = "Vertical Video"
# Theme Action
ARCHERY = "Archery"
BATTLE_ROYALE = "Battle Royale"
ESPIONAGE = "Espionage"
FUGITIVE = "Fugitive"
GUNS = "Guns"
MARTIAL_ARTS = "Martial Arts"
SPEARPLAY = "Spearplay"
SWORDPLAY = "Swordplay"
# Theme Arts
ACTING = "Acting"
CALLIGRAPHY = "Calligraphy"
CLASSIC_LITERATURE = "Classic Literature"
DRAWING = "Drawing"
FASHION = "Fashion"
FOOD = "Food"
MAKEUP = "Makeup"
PHOTOGRAPHY = "Photography"
RAKUGO = "Rakugo"
WRITING = "Writing"
# Theme Arts-Music
BAND = "Band"
CLASSICAL_MUSIC = "Classical Music"
DANCING = "Dancing"
HIP_HOP_MUSIC = "Hip-hop Music"
JAZZ_MUSIC = "Jazz Music"
METAL_MUSIC = "Metal Music"
MUSICAL_THEATER = "Musical Theater"
ROCK_MUSIC = "Rock Music"
# Theme Comedy
PARODY = "Parody"
SATIRE = "Satire"
SLAPSTICK = "Slapstick"
SURREAL_COMEDY = "Surreal Comedy"
# Theme Drama
BULLYING = "Bullying"
CLASS_STRUGGLE = "Class Struggle"
COMING_OF_AGE = "Coming of Age"
CONSPIRACY = "Conspiracy"
ECO_HORROR = "Eco-Horror"
FAKE_RELATIONSHIP = "Fake Relationship"
KINGDOM_MANAGEMENT = "Kingdom Management"
REHABILITATION = "Rehabilitation"
REVENGE = "Revenge"
SUICIDE = "Suicide"
TRAGEDY = "Tragedy"
# Theme Fantasy
ALCHEMY = "Alchemy"
BODY_SWAPPING = "Body Swapping"
CULTIVATION = "Cultivation"
CURSES = "Curses"
EXORCISM = "Exorcism"
FAIRY_TALE = "Fairy Tale"
HENSHIN = "Henshin"
ISEKAI = "Isekai"
KAIJU = "Kaiju"
MAGIC = "Magic"
MYTHOLOGY = "Mythology"
NECROMANCY = "Necromancy"
SHAPESHIFTING = "Shapeshifting"
STEAMPUNK = "Steampunk"
SUPER_POWER = "Super Power"
SUPERHERO = "Superhero"
WUXIA = "Wuxia"
# Theme Game
BOARD_GAME = "Board Game"
E_SPORTS = "E-Sports"
VIDEO_GAMES = "Video Games"
# Theme Game-Card & Board Game
CARD_BATTLE = "Card Battle"
GO = "Go"
KARUTA = "Karuta"
MAHJONG = "Mahjong"
POKER = "Poker"
SHOGI = "Shogi"
# Theme Game-Sport
ACROBATICS = "Acrobatics"
AIRSOFT = "Airsoft"
AMERICAN_FOOTBALL = "American Football"
ATHLETICS = "Athletics"
BADMINTON = "Badminton"
BASEBALL = "Baseball"
BASKETBALL = "Basketball"
BOWLING = "Bowling"
BOXING = "Boxing"
CHEERLEADING = "Cheerleading"
CYCLING = "Cycling"
FENCING = "Fencing"
FISHING = "Fishing"
FITNESS = "Fitness"
FOOTBALL = "Football"
GOLF = "Golf"
HANDBALL = "Handball"
ICE_SKATING = "Ice Skating"
JUDO = "Judo"
LACROSSE = "Lacrosse"
PARKOUR = "Parkour"
RUGBY = "Rugby"
SCUBA_DIVING = "Scuba Diving"
SKATEBOARDING = "Skateboarding"
SUMO = "Sumo"
SURFING = "Surfing"
SWIMMING = "Swimming"
TABLE_TENNIS = "Table Tennis"
TENNIS = "Tennis"
VOLLEYBALL = "Volleyball"
WRESTLING = "Wrestling"
# Theme Other
ADOPTION = "Adoption"
ANIMALS = "Animals"
ASTRONOMY = "Astronomy"
AUTOBIOGRAPHICAL = "Autobiographical"
BIOGRAPHICAL = "Biographical"
BLACKMAIL = "Blackmail"
BODY_HORROR = "Body Horror"
BODY_IMAGE = "Body Image"
CANNIBALISM = "Cannibalism"
CHIBI = "Chibi"
COSMIC_HORROR = "Cosmic Horror"
CREATURE_TAMING = "Creature Taming"
CRIME = "Crime"
CROSSOVER = "Crossover"
DEATH_GAME = "Death Game"
DENPA = "Denpa"
DRUGS = "Drugs"
ECONOMICS = "Economics"
EDUCATIONAL = "Educational"
ENVIRONMENTAL = "Environmental"
ERO_GURO = "Ero Guro"
FILMMAKING = "Filmmaking"
FOUND_FAMILY = "Found Family"
GAMBLING = "Gambling"
GENDER_BENDING = "Gender Bending"
GORE = "Gore"
INDIGENOUS_CULTURES = "Indigenous Cultures"
LANGUAGE_BARRIER = "Language Barrier"
LGBTQ_PLUS_THEMES = "LGBTQ+ Themes"
LOST_CIVILIZATION = "Lost Civilization"
MARRIAGE = "Marriage"
MEDICINE = "Medicine"
MEMORY_MANIPULATION = "Memory Manipulation"
META = "Meta"
MOUNTAINEERING = "Mountaineering"
NOIR = "Noir"
OTAKU_CULTURE = "Otaku Culture"
PANDEMIC = "Pandemic"
PHILOSOPHY = "Philosophy"
POLITICS = "Politics"
PREGNANCY = "Pregnancy"
PROXY_BATTLE = "Proxy Battle"
PSYCHOSEXUAL = "Psychosexual"
REINCARNATION = "Reincarnation"
RELIGION = "Religion"
RESCUE = "Rescue"
ROYAL_AFFAIRS = "Royal Affairs"
SLAVERY = "Slavery"
SOFTWARE_DEVELOPMENT = "Software Development"
SURVIVAL = "Survival"
TERRORISM = "Terrorism"
TORTURE = "Torture"
TRAVEL = "Travel"
VOCAL_SYNTH = "Vocal Synth"
WAR = "War"
# Theme Other-Organisations
ASSASSINS = "Assassins"
CRIMINAL_ORGANIZATION = "Criminal Organization"
CULT = "Cult"
FIREFIGHTERS = "Firefighters"
GANGS = "Gangs"
MAFIA = "Mafia"
MILITARY = "Military"
POLICE = "Police"
TRIADS = "Triads"
YAKUZA = "Yakuza"
# Theme Other-Vehicle
AVIATION = "Aviation"
CARS = "Cars"
MOPEDS = "Mopeds"
MOTORCYCLES = "Motorcycles"
SHIPS = "Ships"
TANKS = "Tanks"
TRAINS = "Trains"
# Theme Romance
AGE_GAP = "Age Gap"
BOYS_LOVE = "Boys' Love"
COHABITATION = "Cohabitation"
FEMALE_HAREM = "Female Harem"
HETEROSEXUAL = "Heterosexual"
LOVE_TRIANGLE = "Love Triangle"
MALE_HAREM = "Male Harem"
MATCHMAKING = "Matchmaking"
MIXED_GENDER_HAREM = "Mixed Gender Harem"
TEENS_LOVE = "Teens' Love"
UNREQUITED_LOVE = "Unrequited Love"
YURI = "Yuri"
# Theme Sci-Fi
CYBERPUNK = "Cyberpunk"
SPACE_OPERA = "Space Opera"
TIME_LOOP = "Time Loop"
TIME_MANIPULATION = "Time Manipulation"
TOKUSATSU = "Tokusatsu"
# Theme Sci-Fi-Mecha
REAL_ROBOT = "Real Robot"
SUPER_ROBOT = "Super Robot"
# Theme Slice of Life
AGRICULTURE = "Agriculture"
CUTE_BOYS_DOING_CUTE_THINGS = "Cute Boys Doing Cute Things"
CUTE_GIRLS_DOING_CUTE_THINGS = "Cute Girls Doing Cute Things"
FAMILY_LIFE = "Family Life"
HORTICULTURE = "Horticulture"
IYASHIKEI = "Iyashikei"
PARENTHOOD = "Parenthood"
class MediaSort(Enum):
ID = "ID"
ID_DESC = "ID_DESC"
TITLE_ROMAJI = "TITLE_ROMAJI"
TITLE_ROMAJI_DESC = "TITLE_ROMAJI_DESC"
TITLE_ENGLISH = "TITLE_ENGLISH"
TITLE_ENGLISH_DESC = "TITLE_ENGLISH_DESC"
TITLE_NATIVE = "TITLE_NATIVE"
TITLE_NATIVE_DESC = "TITLE_NATIVE_DESC"
TYPE = "TYPE"
TYPE_DESC = "TYPE_DESC"
FORMAT = "FORMAT"
FORMAT_DESC = "FORMAT_DESC"
START_DATE = "START_DATE"
START_DATE_DESC = "START_DATE_DESC"
END_DATE = "END_DATE"
END_DATE_DESC = "END_DATE_DESC"
SCORE = "SCORE"
SCORE_DESC = "SCORE_DESC"
POPULARITY = "POPULARITY"
POPULARITY_DESC = "POPULARITY_DESC"
TRENDING = "TRENDING"
TRENDING_DESC = "TRENDING_DESC"
EPISODES = "EPISODES"
EPISODES_DESC = "EPISODES_DESC"
DURATION = "DURATION"
DURATION_DESC = "DURATION_DESC"
STATUS = "STATUS"
STATUS_DESC = "STATUS_DESC"
CHAPTERS = "CHAPTERS"
CHAPTERS_DESC = "CHAPTERS_DESC"
VOLUMES = "VOLUMES"
VOLUMES_DESC = "VOLUMES_DESC"
UPDATED_AT = "UPDATED_AT"
UPDATED_AT_DESC = "UPDATED_AT_DESC"
SEARCH_MATCH = "SEARCH_MATCH"
FAVOURITES = "FAVOURITES"
FAVOURITES_DESC = "FAVOURITES_DESC"
class UserMediaListSort(Enum):
MEDIA_ID = "MEDIA_ID"
MEDIA_ID_DESC = "MEDIA_ID_DESC"
SCORE = "SCORE"
SCORE_DESC = "SCORE_DESC"
STATUS = "STATUS"
STATUS_DESC = "STATUS_DESC"
PROGRESS = "PROGRESS"
PROGRESS_DESC = "PROGRESS_DESC"
PROGRESS_VOLUMES = "PROGRESS_VOLUMES"
PROGRESS_VOLUMES_DESC = "PROGRESS_VOLUMES_DESC"
REPEAT = "REPEAT"
REPEAT_DESC = "REPEAT_DESC"
PRIORITY = "PRIORITY"
PRIORITY_DESC = "PRIORITY_DESC"
STARTED_ON = "STARTED_ON"
STARTED_ON_DESC = "STARTED_ON_DESC"
FINISHED_ON = "FINISHED_ON"
FINISHED_ON_DESC = "FINISHED_ON_DESC"
ADDED_TIME = "ADDED_TIME"
ADDED_TIME_DESC = "ADDED_TIME_DESC"
UPDATED_TIME = "UPDATED_TIME"
UPDATED_TIME_DESC = "UPDATED_TIME_DESC"
MEDIA_TITLE_ROMAJI = "MEDIA_TITLE_ROMAJI"
MEDIA_TITLE_ROMAJI_DESC = "MEDIA_TITLE_ROMAJI_DESC"
MEDIA_TITLE_ENGLISH = "MEDIA_TITLE_ENGLISH"
MEDIA_TITLE_ENGLISH_DESC = "MEDIA_TITLE_ENGLISH_DESC"
MEDIA_TITLE_NATIVE = "MEDIA_TITLE_NATIVE"
MEDIA_TITLE_NATIVE_DESC = "MEDIA_TITLE_NATIVE_DESC"
MEDIA_POPULARITY = "MEDIA_POPULARITY"
MEDIA_POPULARITY_DESC = "MEDIA_POPULARITY_DESC"
MEDIA_SCORE = "MEDIA_SCORE"
MEDIA_SCORE_DESC = "MEDIA_SCORE_DESC"
MEDIA_START_DATE = "MEDIA_START_DATE"
MEDIA_START_DATE_DESC = "MEDIA_START_DATE_DESC"
MEDIA_RATING = "MEDIA_RATING"
MEDIA_RATING_DESC = "MEDIA_RATING_DESC"
class MediaSeason(Enum):
WINTER = "WINTER"
SPRING = "SPRING"
SUMMER = "SUMMER"
FALL = "FALL"
class MediaYear(Enum):
_1900 = "1900"
_1910 = "1910"
_1920 = "1920"
_1930 = "1930"
_1940 = "1940"
_1950 = "1950"
_1960 = "1960"
_1970 = "1970"
_1980 = "1980"
_1990 = "1990"
_2000 = "2000"
_2004 = "2004"
_2005 = "2005"
_2006 = "2006"
_2007 = "2007"
_2008 = "2008"
_2009 = "2009"
_2010 = "2010"
_2011 = "2011"
_2012 = "2012"
_2013 = "2013"
_2014 = "2014"
_2015 = "2015"
_2016 = "2016"
_2017 = "2017"
_2018 = "2018"
_2019 = "2019"
_2020 = "2020"
_2021 = "2021"
_2022 = "2022"
_2023 = "2023"
_2024 = "2024"
_2025 = "2025"

View File

@@ -1,65 +0,0 @@
"""
Syncplay integration for FastAnime.
This module provides a procedural function to launch Syncplay with the given media and options.
"""
import shutil
import subprocess
from .tools import exit_app
def SyncPlayer(url: str, anime_title=None, headers={}, subtitles=[], *args):
"""
Launch Syncplay for synchronized playback with friends.
Args:
url: The media URL to play.
anime_title: Optional title to display in the player.
headers: Optional HTTP headers to pass to the player.
subtitles: Optional list of subtitle dicts with 'url' keys.
*args: Additional arguments (unused).
Returns:
Tuple of ("0", "0") for compatibility.
"""
# TODO: handle m3u8 multi quality streams
#
# check for SyncPlay
SYNCPLAY_EXECUTABLE = shutil.which("syncplay")
if not SYNCPLAY_EXECUTABLE:
print("Syncplay not found")
exit_app(1)
return "0", "0"
# start SyncPlayer
mpv_args = []
if headers:
mpv_headers = "--http-header-fields="
for header_name, header_value in headers.items():
mpv_headers += f"{header_name}:{header_value},"
mpv_args.append(mpv_headers)
for subtitle in subtitles:
mpv_args.append(f"--sub-file={subtitle['url']}")
if not anime_title:
subprocess.run(
[
SYNCPLAY_EXECUTABLE,
url,
],
check=False,
)
else:
subprocess.run(
[
SYNCPLAY_EXECUTABLE,
url,
"--",
f"--force-media-title={anime_title}",
*mpv_args,
],
check=False,
)
# for compatability
return "0", "0"

View File

@@ -1,105 +0,0 @@
"""An abstraction over all providers offering added features with a simple and well typed api
[TODO:description]
"""
import importlib
import logging
from typing import TYPE_CHECKING
from .libs.manga_provider import manga_sources
if TYPE_CHECKING:
pass
logger = logging.getLogger(__name__)
class MangaProvider:
"""Class that manages all anime sources adding some extra functionality to them.
Attributes:
PROVIDERS: [TODO:attribute]
provider: [TODO:attribute]
provider: [TODO:attribute]
dynamic: [TODO:attribute]
retries: [TODO:attribute]
manga_provider: [TODO:attribute]
"""
PROVIDERS = list(manga_sources.keys())
provider = PROVIDERS[0]
def __init__(self, provider="mangadex", dynamic=False, retries=0) -> None:
self.provider = provider
self.dynamic = dynamic
self.retries = retries
self.lazyload_provider(self.provider)
def lazyload_provider(self, provider):
"""updates the current provider being used"""
_, anime_provider_cls_name = manga_sources[provider].split(".", 1)
package = f"fastanime.libs.manga_provider.{provider}"
provider_api = importlib.import_module(".api", package)
manga_provider = getattr(provider_api, anime_provider_cls_name)
self.manga_provider = manga_provider()
def search_for_manga(
self,
user_query,
nsfw=True,
unknown=True,
):
"""core abstraction over all providers search functionality
Args:
user_query ([TODO:parameter]): [TODO:description]
translation_type ([TODO:parameter]): [TODO:description]
nsfw ([TODO:parameter]): [TODO:description]
manga_provider ([TODO:parameter]): [TODO:description]
anilist_obj: [TODO:description]
Returns:
[TODO:return]
"""
manga_provider = self.manga_provider
try:
results = manga_provider.search_for_manga(user_query, nsfw, unknown)
except Exception as e:
logger.error(e)
results = None
return results
def get_manga(
self,
anime_id: str,
):
"""core abstraction over getting info of an anime from all providers
Args:
anime_id: [TODO:description]
anilist_obj: [TODO:description]
Returns:
[TODO:return]
"""
manga_provider = self.manga_provider
try:
results = manga_provider.get_manga(anime_id)
except Exception as e:
logger.error(e)
results = None
return results
def get_chapter_thumbnails(
self,
manga_id: str,
chapter: str,
):
manga_provider = self.manga_provider
try:
results = manga_provider.get_chapter_thumbnails(manga_id, chapter)
except Exception as e:
logger.error(e)
results = None
return results # pyright:ignore

View File

@@ -1 +0,0 @@
manga_sources = {"mangadex": "api.MangaDexApi"}

View File

@@ -1,18 +0,0 @@
from httpx import Client
from ....core.utils.networking import random_user_agent
class MangaProvider:
session: Client
USER_AGENT = random_user_agent()
HEADERS = {}
def __init__(self) -> None:
self.session = Client(
headers={
"User-Agent": self.USER_AGENT,
**self.HEADERS,
},
timeout=10,
)

View File

@@ -1,15 +0,0 @@
import logging
from httpx import get
logger = logging.getLogger(__name__)
def fetch_manga_info_from_bal(anilist_id):
try:
url = f"https://raw.githubusercontent.com/bal-mackup/mal-backup/master/anilist/manga/{anilist_id}.json"
response = get(url, timeout=11)
if response.ok:
return response.json()
except Exception as e:
logger.error(e)

View File

@@ -1,51 +0,0 @@
import logging
from ...common.mini_anilist import search_for_manga_with_anilist
from ..base_provider import MangaProvider
from ..common import fetch_manga_info_from_bal
logger = logging.getLogger(__name__)
class MangaDexApi(MangaProvider):
def search_for_manga(self, title: str, *args):
try:
search_results = search_for_manga_with_anilist(title)
return search_results
except Exception as e:
logger.error(f"[MANGADEX-ERROR]: {e}")
def get_manga(self, anilist_manga_id: str):
bal_data = fetch_manga_info_from_bal(anilist_manga_id)
if not bal_data:
return
manga_id, MangaDexManga = next(iter(bal_data["Sites"]["Mangadex"].items()))
return {
"id": manga_id,
"title": MangaDexManga["title"],
"poster": MangaDexManga["image"],
"availableChapters": [],
}
def get_chapter_thumbnails(self, manga_id, chapter):
chapter_info_url = f"https://api.mangadex.org/chapter?manga={manga_id}&translatedLanguage[]=en&chapter={chapter}&includeEmptyPages=0"
chapter_info_response = self.session.get(chapter_info_url)
if not chapter_info_response.ok:
return
chapter_info = next(iter(chapter_info_response.json()["data"]))
chapters_thumbnails_url = (
f"https://api.mangadex.org/at-home/server/{chapter_info['id']}"
)
chapter_thumbnails_response = self.session.get(chapters_thumbnails_url)
if not chapter_thumbnails_response.ok:
return
chapter_thumbnails_info = chapter_thumbnails_response.json()
base_url = chapter_thumbnails_info["baseUrl"]
hash = chapter_thumbnails_info["chapter"]["hash"]
return {
"thumbnails": [
f"{base_url}/data/{hash}/{chapter_thumbnail}"
for chapter_thumbnail in chapter_thumbnails_info["chapter"]["data"]
],
"title": chapter_info["attributes"]["title"],
}

8
flake.lock generated
View File

@@ -20,17 +20,17 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1753345091,
"narHash": "sha256-CdX2Rtvp5I8HGu9swBmYuq+ILwRxpXdJwlpg8jvN4tU=",
"lastModified": 1756386758,
"narHash": "sha256-1wxxznpW2CKvI9VdniaUnTT2Os6rdRJcRUf65ZK9OtE=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "3ff0e34b1383648053bba8ed03f201d3466f90c9",
"rev": "dfb2f12e899db4876308eba6d93455ab7da304cd",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"rev": "3ff0e34b1383648053bba8ed03f201d3466f90c9",
"type": "github"
}
},

View File

@@ -1,9 +1,8 @@
{
description = "FastAnime Project Flake";
description = "Viu Project Flake";
inputs = {
# The nixpkgs unstable latest commit breaks the plyer python package
nixpkgs.url = "github:nixos/nixpkgs/3ff0e34b1383648053bba8ed03f201d3466f90c9";
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
@@ -17,21 +16,21 @@
system:
let
pkgs = nixpkgs.legacyPackages.${system};
inherit (pkgs) lib python3Packages;
inherit (pkgs) lib python312Packages;
version = "3.1.0";
in
{
packages.default = python3Packages.buildPythonApplication {
pname = "fastanime";
packages.default = python312Packages.buildPythonApplication {
pname = "viu";
inherit version;
pyproject = true;
src = self;
build-system = with python3Packages; [ hatchling ];
build-system = with python312Packages; [ hatchling ];
dependencies = with python3Packages; [
dependencies = with python312Packages; [
click
inquirerpy
requests
@@ -67,13 +66,11 @@
# Needs to be adapted for the nix derivation build
doCheck = false;
pythonImportsCheck = [ "fastanime" ];
meta = {
description = "Your browser anime experience from the terminal";
homepage = "https://github.com/Benexl/FastAnime";
changelog = "https://github.com/Benexl/FastAnime/releases/tag/v${version}";
mainProgram = "fastanime";
homepage = "https://github.com/viu-media/Viu";
changelog = "https://github.com/viu-media/Viu/releases/tag/v${version}";
mainProgram = "viu";
license = lib.licenses.unlicense;
maintainers = with lib.maintainers; [ theobori ];
};

View File

@@ -1,10 +1,10 @@
[project]
name = "fastanime"
version = "3.1.0"
name = "viu-media"
version = "3.2.8"
description = "A browser anime site experience from the terminal"
license = "UNLICENSE"
readme = "README.md"
requires-python = ">=3.10"
requires-python = ">=3.11"
dependencies = [
"click>=8.1.7",
"httpx>=0.28.1",
@@ -14,14 +14,16 @@ dependencies = [
]
[project.scripts]
fastanime = 'fastanime:Cli'
viu = 'viu_media:Cli'
[project.optional-dependencies]
standard = [
"thefuzz>=0.22.1",
"yt-dlp>=2025.7.21",
"pycryptodomex>=3.23.0",
"dbus-python>=1.4.0",
"pypiwin32; sys_platform == 'win32'", # For Windows-specific functionality
"pyobjc; sys_platform == 'darwin'", # For macOS-specific functionality
"dbus-python; sys_platform == 'linux'", # For Linux-specific functionality (e.g., notifications),
"plyer>=2.1.0",
"lxml>=6.0.0"
]
@@ -47,8 +49,8 @@ torrents = [
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.uv]
dev-dependencies = [
[dependency-groups]
dev = [
"pre-commit>=4.0.1",
"pyinstaller>=6.11.1",
"pyright>=1.1.384",

View File

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

View File

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

3823
uv.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Before

Width:  |  Height:  |  Size: 276 KiB

After

Width:  |  Height:  |  Size: 276 KiB

View File

@@ -4,7 +4,8 @@
"Magia Record: Mahou Shoujo Madoka☆Magica Gaiden (TV)": "Mahou Shoujo Madoka☆Magica",
"Dungeon ni Deai o Motomeru no wa Machigatte Iru Darouka": "Dungeon ni Deai wo Motomeru no wa Machigatteiru Darou ka",
"Hazurewaku no \"Joutai Ijou Skill\" de Saikyou ni Natta Ore ga Subete wo Juurin suru made": "Hazure Waku no [Joutai Ijou Skill] de Saikyou ni Natta Ore ga Subete wo Juurin Suru made",
"Re:Zero kara Hajimeru Isekai Seikatsu Season 3": "Re:Zero kara Hajimeru Isekai Seikatsu 3rd Season"
"Re:Zero kara Hajimeru Isekai Seikatsu Season 3": "Re:Zero kara Hajimeru Isekai Seikatsu 3rd Season",
"Hanka×Hanka (2011)": "Hunter × Hunter (2011)"
},
"hianime": {
"My Star": "Oshi no Ko"
@@ -13,5 +14,12 @@
"Azumanga Daiou The Animation": "Azumanga Daioh",
"Mairimashita! Iruma-kun 2nd Season": "Mairimashita! Iruma-kun 2",
"Mairimashita! Iruma-kun 3rd Season": "Mairimashita! Iruma-kun 3"
},
"animeunity": {
"Kaiju No. 8": "Kaiju No.8",
"Naruto Shippuden": "Naruto: Shippuden",
"Psycho-Pass: Sinners of the System Case.1 - Crime and Punishment": "PSYCHO-PASS Sinners of the System: Case.1 Crime and Punishment",
"Psycho-Pass: Sinners of the System Case.2 - First Guardian": "PSYCHO-PASS Sinners of the System: Case.2 First Guardian",
"Psycho-Pass: Sinners of the System Case.3 - On the Other Side of Love and Hate": "PSYCHO-PASS Sinners of the System: Case.3 Beyond the Pale of Vengeance"
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,7 @@ import click
from click.core import ParameterSource
from ..core.config import AppConfig
from ..core.constants import PROJECT_NAME, USER_CONFIG, __version__
from ..core.constants import CLI_NAME, USER_CONFIG, __version__
from .config import ConfigLoader
from .options import options_from_model
from .utils.exception import setup_exceptions_handler
@@ -38,14 +38,16 @@ commands = {
"registry": "registry.registry",
"worker": "worker.worker",
"queue": "queue.queue",
"completions": "completions.completions",
}
@click.group(
cls=LazyGroup,
root="fastanime.cli.commands",
root="viu_media.cli.commands",
invoke_without_command=True,
lazy_subcommands=commands,
context_settings=dict(auto_envvar_prefix=PROJECT_NAME),
context_settings=dict(auto_envvar_prefix=CLI_NAME),
)
@click.version_option(__version__, "--version")
@click.option("--no-config", is_flag=True, help="Don't load the user config file.")
@@ -68,7 +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,50 @@ def cli(ctx: click.Context, **options: "Unpack[Options]"):
else loader.load(cli_overrides)
)
ctx.obj = config
if config.general.check_for_updates:
import time
from ..core.constants import APP_CACHE_DIR
last_updated_at_file = APP_CACHE_DIR / "last_update"
should_check_for_update = False
if last_updated_at_file.exists():
try:
last_updated_at_time = float(
last_updated_at_file.read_text(encoding="utf-8")
)
if (
time.time() - last_updated_at_time
) > config.general.update_check_interval * 3600:
should_check_for_update = True
except Exception as e:
logger.warning(f"Failed to check for update: {e}")
else:
should_check_for_update = True
if should_check_for_update:
last_updated_at_file.write_text(str(time.time()), encoding="utf-8")
from .service.feedback import FeedbackService
from .utils.update import check_for_updates, print_release_json, update_app
feedback = FeedbackService(config)
feedback.info("Checking for updates...")
is_latest, release_json = check_for_updates()
if not is_latest:
from ..libs.selectors.selector import create_selector
selector = create_selector(config)
if release_json and selector.confirm(
"Theres an update available would you like to see the release notes before deciding to update?"
):
print_release_json(release_json)
selector.ask("Enter to continue...")
if selector.confirm("Would you like to update?"):
update_app()
if ctx.invoked_subcommand is None:
from .commands.anilist import cmd
ctx.invoke(cmd.anilist)

View File

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

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