Compare commits

...

1063 Commits

Author SHA1 Message Date
Benexl
6a5d7a0116 chore: bump version and update deps 2025-12-02 18:31:43 +03:00
Benedict Xavier
91efee9065 Merge pull request #169 from viu-media/feat/welcomescreen 2025-12-02 18:03:25 +03:00
Benedict Xavier
69d3d2e032 Update viu_media/cli/cli.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-02 18:02:11 +03:00
Benedict Xavier
29ba77f795 Update viu_media/cli/cli.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-02 18:01:56 +03:00
Benexl
a4950efa02 feat: wait for feedback 2025-12-02 18:00:44 +03:00
Benedict Xavier
bbd7931790 Merge branch 'master' into feat/welcomescreen 2025-12-02 17:53:17 +03:00
Benedict Xavier
c3ae5f9053 Merge pull request #168 from viu-media/feature/preview-scripts-rewrite-to-python 2025-12-02 17:52:21 +03:00
Benedict Xavier
bf06d7ee2c Update viu_media/assets/scripts/fzf/media_info.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-02 17:46:58 +03:00
Benexl
41aaf92bae style: remove unused import 2025-12-02 17:44:46 +03:00
Benexl
d38dc3194f feat: export ansi utils to preview root dir when doing dynamic previews 2025-12-02 17:43:18 +03:00
Benexl
54233aca79 feat: remove redundancy and stick to ansi_utils 2025-12-02 17:42:53 +03:00
Benexl
6b8dfba57e fix: remove double quotes 2025-12-02 17:30:31 +03:00
Benexl
3b008696d5 style: remove unused imports 2025-12-02 17:27:30 +03:00
Benedict Xavier
ece1f77e99 Merge branch 'master' into feature/preview-scripts-rewrite-to-python 2025-12-02 17:16:50 +03:00
Benexl
7b9de8620b chore: cleanup old preview scripts 2025-12-02 17:15:18 +03:00
Benexl
725754ea1a feat: improve text display for dynamic search 2025-12-02 17:12:27 +03:00
Benexl
80771f65ea feat: dynamic search rewrite in python 2025-12-02 14:36:03 +03:00
Benexl
c8c4e1b2c0 feat: refactor terminal width handling in FZF scripts for improved consistency 2025-12-02 13:30:03 +03:00
Benexl
f4958cc0cc fix: clean up whitespace in display_width and print_table_row functions 2025-12-02 13:14:19 +03:00
Benexl
1f72e0a579 feat: enhance display width calculation for better text alignment in print_table_row 2025-12-02 13:07:55 +03:00
Benexl
803c8316a7 fix: improve value alignment in print_table_row for better formatting 2025-12-02 13:04:25 +03:00
Benexl
26bc84e2eb fix: clean up whitespace in ANSI utilities and preview script 2025-12-01 18:48:15 +03:00
Benexl
901d1e87c5 feat: rewrite FZF preview scripts to use ANSI utilities for improved formatting 2025-12-01 18:47:58 +03:00
Benexl
523766868c feat: implement other image renders 2025-12-01 17:44:55 +03:00
Benexl
bd9bf24e1c feat: add more image render options 2025-12-01 17:27:47 +03:00
Benexl
f27c0b8548 fix: order of operations 2025-12-01 17:25:21 +03:00
Benexl
76c1dcd5ac fix: specifying extension when saving file 2025-12-01 17:19:55 +03:00
Benexl
25a46bd242 feat: disable airing schedule preview 2025-12-01 17:19:33 +03:00
Benexl
a70db611f7 style: remove unnecessary comment 2025-12-01 17:19:11 +03:00
Benexl
091edb3a9b fix: remove extra bracket 2025-12-01 17:06:58 +03:00
Benexl
9050dd7787 feat: disable image for character, review, airing-schedule 2025-12-01 17:05:40 +03:00
Benexl
393b9e6ed6 feat: use actual file for preview script 2025-12-01 17:00:15 +03:00
Benexl
5193df2197 feat: airing schedule previews in python 2025-11-30 15:33:34 +03:00
Benexl
6ccd96d252 feat: review previews in python 2025-11-30 15:15:11 +03:00
Benexl
e8387f3db9 feat: character previews in python 2025-11-30 15:03:48 +03:00
Benexl
23ebff3f42 fix: add .py extension to final path 2025-11-30 14:41:17 +03:00
Benexl
8e803e8ecb feat(cli): search provider with title in lowercase 2025-11-20 22:14:17 +03:00
Benexl
61fcd39188 feat(dev): use PWD when specifying the viu venv bin path 2025-11-20 22:13:36 +03:00
Benexl
313f8369d7 feat: show release notes after upgrade 2025-11-18 16:32:30 +03:00
Benexl
bee73b3f9a feat(config): add show release option 2025-11-18 16:03:22 +03:00
Benexl
f647b7419a feat: add welcome screen message 2025-11-18 15:56:28 +03:00
Benexl
901c4422b5 feat: add welcome screen config option 2025-11-18 15:01:07 +03:00
Benexl
08ae8786c3 feat: sanitize " in key 2025-11-18 14:48:00 +03:00
Benexl
64093204ad feat: create temp episode preview script 2025-11-18 14:28:54 +03:00
Benexl
8440ffb5e5 feat: add a key for extra uniqueness 2025-11-18 14:20:07 +03:00
Benexl
6e287d320d feat: rewrite episode info script in python 2025-11-18 13:59:40 +03:00
Benexl
a7b0f21deb feat: rename info.py to media_info.py 2025-11-18 13:44:20 +03:00
Benedict Xavier
71b668894b Revise disclaimer and core features in README
Updated disclaimer section for clarity and removed redundancy.
2025-11-13 17:13:37 +03:00
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
29ce664e4c Merge remote-tracking branch 'origin/master' into feature/preview-scripts-rewrite-to-python 2025-11-03 11:16:36 +03: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
1519c8be17 feat: create the preview script in the cache/preview dir 2025-11-01 00:59:38 +03:00
Benexl
9a619b41f4 feat: use prefix in preview-script.py filename 2025-11-01 00:55:19 +03:00
Benexl
0c3a963cc4 feat: use ?? where episodes are unknown 2025-11-01 00:50:45 +03:00
Benexl
192818362b feat: next episode should come last in its grp for better ui ux 2025-11-01 00:04:05 +03:00
Benexl
2d8c1d3569 feat: remove colon for better ui 2025-10-31 23:50:12 +03:00
Benexl
e37f9213f6 feat: include romaji title in synonymns if not already there 2025-10-31 23:44:11 +03:00
Benexl
097db713bc feat: refactor ruling logic to function 2025-10-31 23:37:45 +03:00
Benexl
106278e386 feat: improve synopsis separator styling 2025-10-31 23:35:31 +03:00
Benexl
44b3663644 feat: grp studio, synonymns and tags separately for better ui / ux 2025-10-31 23:23:33 +03:00
Benexl
925c30c06e fix: typo should be text not info 2025-10-31 23:23:03 +03:00
Benexl
7401a1ad8f feat: prefer to use direct implementation of graphics protocol over external tools 2025-10-31 23:04:56 +03:00
Benexl
9a0bb65e52 feat: implement image preview 2025-10-31 22:49:41 +03:00
Benexl
1d129a5771 fix: remove extra bracket 2025-10-31 22:37:36 +03:00
Benexl
515660b0f6 feat: implement the main preview text logic in python 2025-10-31 22:32:51 +03:00
Benexl
9f5c895bf5 chore: temporarily relocate initial bash preview scripts to old folder 2025-10-31 22:32:14 +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
Benexl
cd16ab50e3 feat(rofi-selector): use plyer for notifications when using it 2025-08-15 13:08:33 +03:00
Benexl
b53a7d9b03 chore: bump version 2025-08-15 12:31:13 +03:00
Benexl
2b9fdb99b1 chore: priority to the enabled lol 2025-08-13 16:27:42 +03:00
Benexl
9c6e1877ed feat(rofi-selector): force exit on no input 2025-08-12 16:56:18 +03:00
Benexl
73bb77fe46 feat(rofi-theme-defaults): enhance ui and ux 2025-08-12 16:50:21 +03:00
Benexl
4cdc5bfd34 feat(config): make desktop notification duration configurable 2025-08-12 14:45:35 +03:00
Benexl
ca491d95a0 chore: update deps 2025-08-12 14:36:41 +03:00
Benexl
1003f75db3 feat(assets): update logo 2025-08-12 14:32:53 +03:00
Benexl
5821c4ca97 Update README.md 2025-08-12 13:46:05 +03:00
Benexl
de774a58d2 feat(anilist-downloads-command): create it 2025-08-12 11:40:20 +03:00
Benexl
ee25cbba10 fix(normalizer): convert media api title to lower 2025-08-12 10:57:15 +03:00
Benexl
278a771f64 Merge pull request #119 from Benexl/minor-fixes
fix: add back missing yt-dlp's "external_downloader" logic
2025-08-12 10:37:50 +03:00
Type-Delta
0d8c287e2f Merge branch 'master' of https://github.com/Benexl/FastAnime into minor-fixes 2025-08-12 14:35:52 +07:00
Type-Delta
74308dfdc5 fix: add back missing yt-dlp's "external_downloader" logic 2025-08-12 14:35:23 +07:00
Benexl
7ca1b8572e fix(rofi-previews): ensure the cover image is used if episode image unavailable 2025-08-12 02:26:19 +03:00
Benexl
54aed9e5a0 feat(config-command): use title case for the desktop entry title 2025-08-12 02:25:25 +03:00
Benexl
4511d14e8b feat(previews): implement rofi image preview logic 2025-08-12 02:11:27 +03:00
Benexl
bff684e8cb refactor(config-command): rename desktop entry option to generate-desktop-entry 2025-08-12 02:11:04 +03:00
Benexl
cfc83450c8 feat(config): include possible values for pygment style 2025-08-12 01:07:06 +03:00
Benexl
04a6a425b7 feat(config): show possible types and possible values in config comments 2025-08-12 00:55:51 +03:00
Benexl
088d232bfd feat(normalizer): add user normalizer json 2025-08-12 00:39:43 +03:00
Benexl
03fd8c0bf8 feat(selectors): complete contracts 2025-08-11 23:57:34 +03:00
Benexl
17f1744025 feat(watch-history): intelligently switch to repeating if was initially completed 2025-08-11 23:38:24 +03:00
Benexl
9a5f3d46be feat(download-service): notify on completed episode download if plyer available 2025-08-11 23:37:50 +03:00
Benexl
66eb854da5 feat: the worker command lol 2025-08-11 22:42:13 +03:00
Benexl
ae62adf233 feat: scaffhold worker command with gpt 5, it is actually pretty good lol 2025-08-11 22:42:13 +03:00
Benexl
55a7c7facf Merge pull request #117 from Benexl/minor-fixes
fix: anilist auth failed to open link on Windows
2025-08-08 11:17:49 +03:00
Type-Delta
2340c34d02 fix: anilist auth failed to open link on Windows
Switches to using the standard web browser module for opening the authentication URL, providing clearer feedback to the user about browser launch success or failure.
2025-08-08 12:28:23 +07:00
Benexl
40b29ba6e5 Merge pull request #115 from Benexl/minor-fixes
fix: anilist auth example
2025-08-07 13:14:48 +03:00
Type-Delta
5dc768f7e8 fix: anilist auth example 2025-08-07 15:19:38 +07:00
Benexl
b343bfb645 docs: enhance docstrings across player modules for clarity and completeness 2025-08-05 12:11:02 +03:00
Benexl
37773265ce Merge pull request #111 from khachbe/bugfix_formatter
Use int() cast instead of is_integer() for renumbered_val
2025-08-03 15:19:03 +03:00
khachbe
70ef1bf633 Merge branch 'master' into bugfix_formatter 2025-08-03 13:58:54 +02:00
khachbe
bee97acd35 Update formatter.py 2025-08-03 13:58:12 +02:00
Benexl
fb61fd17f1 fix(preview-script): quote $value to prevent interpretation of special characters 2025-08-02 14:01:30 +03:00
Benexl
98fff7d00f chore: bump lockfile 2025-08-02 14:00:47 +03:00
Benexl
3cc9ae50b6 Merge pull request #112 from khachbe/bugfix_typedict
Add list to AllAnimeEpisodeStreams
2025-08-02 13:46:37 +03:00
khachbe
26f7de172a add list to AllAnimeEpisodeStreams 2025-08-01 11:46:05 +02:00
khachbe
673b6280e4 Use int() cast instead of is_integer() for renumbered_val 2025-08-01 11:20:57 +02:00
Benexl
7943dcc3db Merge pull request #110 from theobori/fix/ruff-check-errors
Fixed the `ruff check` command line errors
2025-07-30 22:38:25 +03:00
Théo Bori
49ee1f9bbd Fixed the ruff check command line errors 2025-07-30 18:38:15 +02:00
Benexl
fd80149e74 Merge pull request #109 from iMithrellas/preview-scaling
Add optional --scale-up flag for icat image previews
2025-07-30 00:49:09 +03:00
iMithrellas
7c11616bea fix(fzf-preview): Make --scale-up flag opt in instead of opt out 2025-07-29 23:20:56 +02:00
iMithrellas
b9130018ca Merge branch 'master' into preview-scaling 2025-07-29 21:59:06 +02:00
iMithrellas
70ade13017 feat(fzf-preview): make --scale-up flag a config option 2025-07-29 21:52:48 +02:00
iMithrellas
071c0daf4f feat(fzf-preview): add --scale-up flag so that previews fill the available space 2025-07-29 21:52:48 +02:00
Benexl
ed136fc8a0 chore: bump version 2025-07-29 18:52:31 +03:00
Benexl
3a29127366 chore: update deps 2025-07-29 18:51:09 +03:00
Benexl
b7c938fec4 refactor: remove deprecated file 2025-07-29 18:14:40 +03:00
Benexl
c6aada6139 fix(play-downloads-menu): add missing imports 2025-07-29 18:14:21 +03:00
Benexl
e4c4203364 style: ruff check + fix 2025-07-29 18:13:58 +03:00
Benexl
52cd4a8d85 fix(feedback-service): just default to creating progress 2025-07-29 18:04:20 +03:00
Benexl
b9b0d49530 fix(registry-commands): make it work 2025-07-29 18:03:05 +03:00
Benexl
ba4df96dc8 fix(anilist-stats): should be user_profile 2025-07-29 18:02:28 +03:00
Benexl
335ba86367 Update README.md 2025-07-29 17:25:48 +03:00
Benexl
92c18f850f Update README.md 2025-07-29 17:25:30 +03:00
Benexl
70763807de fix(media-actions-menu): typo 2025-07-29 17:19:11 +03:00
Benexl
51438c8864 feat(dynamic-search): title is enough 2025-07-29 17:18:56 +03:00
Benexl
03426bd0da feat(watch-history-service): only update remote progress when episode is complete 2025-07-29 16:56:23 +03:00
Benexl
ccdb0346eb feat(download-service): correct search params and episode title 2025-07-29 16:32:58 +03:00
Benexl
6fba74b3ca docs: update copilot instructions 2025-07-29 15:42:58 +03:00
Benexl
b436f23f65 style: ruff check + format 2025-07-29 15:36:32 +03:00
Benexl
cd7b70dd6b feat(character-preview): attempt to display character image 2025-07-29 15:32:21 +03:00
Benexl
ac9b000ce8 chore: upgrade deps 2025-07-29 15:31:40 +03:00
Benexl
84a3b6185b fix(media-types): age should be string 2025-07-29 14:01:05 +03:00
Benexl
7f52d8cb39 feat(feedback-service): make it configurable 2025-07-29 14:00:44 +03:00
Benexl
87372e41be feat(media-actions-menu): bulk media list actions 2025-07-29 12:36:17 +03:00
Benexl
9cfbc0bdcf style: ruff check 2025-07-29 11:31:18 +03:00
Benexl
af6e64ee0c feat(anilist-download-command): add previews 2025-07-29 11:30:56 +03:00
Benexl
4a2f272e14 Merge pull request #108 from theobori/feature/ci
Added GitHub action step to build the Nix derivation
2025-07-29 10:24:26 +03:00
Benexl
a2e2ae8dd3 Merge branch 'master' into feature/ci 2025-07-29 10:23:44 +03:00
Benexl
5ce9bbaa0d fix(anilist-notifications): add large to coverImage 2025-07-29 10:20:26 +03:00
Benexl
25812b6562 feat(worker-service): draft 2025-07-29 02:02:32 +03:00
Benexl
ee52b945ea feat(media-api): notifications 2025-07-29 01:40:18 +03:00
Benexl
be14e6a135 fix: failure to update remote history 2025-07-29 01:13:53 +03:00
Benexl
9402e7a2b6 feat(mpv-ipc): basic support for media registry 2025-07-29 00:29:51 +03:00
Benexl
590d6a1851 feat(downloads-menu): improve 2025-07-28 23:29:44 +03:00
Benexl
8186fe9991 feat(menus): intergrate download service and downloads in menus 2025-07-28 22:16:46 +03:00
Benexl
4d2831eee1 feat(downloads-config): add support for no check certs 2025-07-28 22:16:14 +03:00
Benexl
ea918909b9 chore(pyproject.toml): add yt-dlp and pycryptodomex to standard optional and downwoal optional 2025-07-28 22:15:46 +03:00
Benexl
93c0f2ab83 feat(cli-service-download): basic implementation 2025-07-28 21:22:11 +03:00
Théo Bori
985e7fee18 Removed shell.nix 2025-07-28 19:37:15 +02:00
Théo Bori
7bd7ddecae Added a envrc file to use nix via direnv 2025-07-28 19:37:14 +02:00
Théo Bori
9afb9a9a32 Added GitHub action step to build the Nix derivation 2025-07-28 19:37:07 +02:00
Benexl
40065478cc feat(cli-sevice-download): prepare 2025-07-28 20:03:48 +03:00
Benexl
65aa8fcb4e chore: cleanup 2025-07-28 19:48:20 +03:00
Benexl
2717d0b012 style: ruff check + format 2025-07-28 15:37:40 +03:00
Benexl
9f0cf5f8dc fix(dynamic-search-menu): update search for media chosen logic 2025-07-28 15:36:39 +03:00
Benexl
ef4a850d75 fix(dynamic-search-menu): preview script 2025-07-28 15:20:05 +03:00
Benexl
d8804c711e fix(search): revert QUERY variable to use placeholder for dynamic input 2025-07-28 14:36:48 +03:00
Benexl
adf75f65b2 refactor: streamline authentication and search command preparation in dynamic search 2025-07-28 14:25:36 +03:00
Benexl
6e7e75b514 fix(scripts-dynamic-preview): should be {} 2025-07-28 14:13:47 +03:00
Benexl
9515559afb docs: include file structure in copilot-instructions 2025-07-28 14:13:06 +03:00
Benexl
e8849940e1 feat: Add media airing schedule and character selection features
- Implemented media airing schedule functionality in `media_airing_schedule.py` to fetch and display upcoming episodes with air dates and countdown timers.
- Created character selection feature in `media_characters.py` to fetch and display a list of characters, showing details upon selection.
- Updated state management to include new menu options for characters and airing schedules.
- Enhanced preview functionality to support character and airing schedule previews.
- Added necessary API methods and data models for handling character and airing schedule data.
2025-07-28 13:37:27 +03:00
Benexl
007954802f chore: cleanup old testing strategy 2025-07-28 13:36:55 +03:00
Benexl
b874bef2d5 docs: draft copilot chatmodes 2025-07-28 12:54:05 +03:00
Benexl
6c1bcebd99 docs: draft copilot-instructions.md 2025-07-28 12:41:30 +03:00
Benexl
fe5e8c641d docs: update CONTRIBUTIONS.md 2025-07-28 12:36:49 +03:00
Benexl
45a4913ead docs: draft CONTRIBUTIONS.md 2025-07-28 12:35:51 +03:00
Benexl
03f0f40c9a fix(cli-service-player): default to regular player instead of raising error 2025-07-28 11:02:56 +03:00
Benexl
af616e0047 refactor(cli-service-feedback): use click.pause 2025-07-28 02:28:31 +03:00
Benexl
6000ed2a84 refactor(assets-anilist-graphql): rename get-medialist-item.gql to media-list-item 2025-07-28 02:23:14 +03:00
Benexl
5a869060d8 feat(media-api): media reviews 2025-07-28 02:13:10 +03:00
Benexl
58618bd82d chore: cleanup unused files 2025-07-27 23:50:25 +03:00
Benexl
84570c5595 fix(cli-utils-preview-worker): default to unknown when format is none 2025-07-27 23:41:43 +03:00
Benexl
18b4071ad9 Merge pull request #106 from theobori/fix/nix-flake
Fixed the Nix Flake default package
2025-07-27 23:31:09 +03:00
Benexl
fd052c87de Merge branch 'master' into fix/nix-flake 2025-07-27 23:29:24 +03:00
Benexl
ee959b3428 chore: update ignore file 2025-07-27 23:27:28 +03:00
Benexl
ad6bdad594 refactor(cli): remove old log_file option 2025-07-27 23:22:14 +03:00
Benexl
316832e771 fix(mpv-ipc-player): deadlock from subprocess.PIPE being filled up causing mpv to wait resulting in the player freezing 2025-07-27 23:21:51 +03:00
Benexl
9edeeb5ca4 feat(mpv-ipc-player): intergrate it as cli level service and polish it to the max 2025-07-27 21:56:20 +03:00
Théo Bori
7eb6054d5c Fixed the Nix Flake default package
I also upgraded the flake.nix file.
- `mkShell` now use `packages` instead of `buildInputs` (See https://discourse.nixos.org/t/difference-between-buildinputs-and-packages-in-mkshell/60598/2)
- Now using `venvShellHook` to create the Python virtual environment
- Remove useless variables
- Added `meta` attrset
- Now using `python3Packages` variables instead
- Explicitly use the `build-system` field
- Now using `substituteInPlace` within `postPatch`
- Using the `pyproject` field instead of `format`
- Removed bad pratices, etc..
2025-07-27 18:39:39 +02:00
Benexl
5b06039cef chore: format with ruff 2025-07-27 12:49:44 +03:00
Benexl
abe36296c1 feat(cli): log the current command 2025-07-27 12:46:24 +03:00
Benexl
dcbf0df1a0 refactor(provider-search-menu): import only when needed and use feedback service for progress 2025-07-27 12:29:33 +03:00
Benexl
c2acbcdb68 fix(cli-utils-logging): add rich logger as one of the handlers if enabled 2025-07-27 12:28:28 +03:00
Benexl
96233b14ff refactor(cli-utils-image): rename render_image to render 2025-07-27 12:27:40 +03:00
Benexl
a8f2579f82 feat(mpv-ipc-player): cleanup implementation 2025-07-27 11:48:55 +03:00
Benexl
5ed9700c5c feat(cli): always log to a file 2025-07-27 11:48:34 +03:00
Benexl
fd74fbe2ef feat: rename config path var and add dedicated folder for logs 2025-07-27 11:48:18 +03:00
Benexl
19426019a2 fix(anilist-api): user media list pagination 2025-07-27 00:38:55 +03:00
Benexl
276c8d48d9 feat(player-controls-menu): add media actions menu option 2025-07-27 00:17:41 +03:00
Benexl
99809f3fd3 fix(player-controls-menu): remove back directive 2025-07-27 00:07:04 +03:00
Benexl
f79c8540c3 feat(mpv-ipc): scaffhold mpv ipc implementation 2025-07-26 23:38:31 +03:00
Benexl
e602a6fbc4 feat(local-watch-history): auto set progress on completed status 2025-07-26 22:57:42 +03:00
Benexl
8abfaed7bf feat(cli-previews): always reload info script 2025-07-26 22:16:14 +03:00
Benexl
15b920698a fix(anilist-media-api): media relations 2025-07-26 22:15:52 +03:00
Benexl
44cf9c3da7 feat(media-action-menu): add exit option 2025-07-26 21:32:42 +03:00
Benexl
460d3c7d94 fix(servers-menu): should be BACKX3 2025-07-26 21:21:22 +03:00
Benexl
5f030a5d9e fix(player-controls): should be BACKX2 2025-07-26 21:20:54 +03:00
Benexl
8091e23196 fix: episode option in player controls menu 2025-07-26 21:08:36 +03:00
Benexl
e641a48156 fix: page formatting 2025-07-26 19:42:26 +03:00
Benexl
c59babc30d feat: return some original menu options and functionality 2025-07-26 19:40:55 +03:00
Benexl
494104ee19 feat: make the session more performant by lazyloading the context 2025-07-26 17:06:10 +03:00
Benexl
159136cfb1 Merge pull request #104 from Aethar01/master
Fixes for auto enum increment and except clause outside a try block.
2025-07-26 13:04:28 +03:00
Elliott Ashby
7e211f109e Merge branch 'master' into master 2025-07-26 19:01:19 +09:00
Benexl
48e906e464 feat: dynamic search 2025-07-26 12:38:47 +03:00
Benexl
98c2fef8cd chore: remove copilot instructions 2025-07-26 11:55:25 +03:00
Benexl
7088b8ce18 Refactor preview management and caching system
- Introduced a new PreviewWorkerManager to handle both anime and episode preview caching.
- Implemented PreviewCacheWorker and EpisodeCacheWorker for specialized background tasks.
- Added context management for preview operations to ensure proper cleanup.
- Enhanced error handling and logging during image downloads and info text generation.
- Removed redundant caching logic and consolidated functionality into worker classes.
- Updated session management to clean up preview workers on session end.
- Removed unused utility functions and streamlined the codebase.
2025-07-26 11:54:01 +03:00
Aethar
6cfc766db3 fix for auto enum 2025-07-26 17:44:19 +09:00
Aethar
89ff453778 stats fix 2025-07-26 17:37:43 +09:00
Benexl
1c95d45be4 chore: ruff check 2025-07-26 10:57:05 +03:00
Benexl
75e67c22d2 chore: format with ruff 2025-07-26 10:56:26 +03:00
Benexl
1a1d8cc8f4 feat: enhance final title generation for media playback 2025-07-26 10:49:15 +03:00
Benexl
3ea37c4079 feat: add title normalization utilities and integrate into provider search 2025-07-26 10:37:49 +03:00
Benexl
b18e419831 chore: make some packages optional and cleanup deprecated 2025-07-26 10:15:56 +03:00
Benexl
fe06c8e0f1 hack: potential anilist api issue 2025-07-25 22:18:33 +03:00
Benexl
759276237f feat: proper auto status 2025-07-25 22:18:11 +03:00
Benexl
d475dda41c feat: icon for downloads 2025-07-25 22:17:42 +03:00
Benexl
ad499657e0 feat: reload menu instead 2025-07-25 22:17:24 +03:00
Benexl
dbf96afea7 fix: sync command 2025-07-25 03:05:22 +03:00
Benexl
001a63d3df feat: fix: registry command 2025-07-25 03:05:15 +03:00
Benexl
1207426a96 feat: correct import path 2025-07-25 03:04:39 +03:00
Benexl
82ca5f32b1 fix: logical issues with registry 2025-07-25 03:04:16 +03:00
Benexl
2924fcd077 feat: Add registry commands for restore, search, stats, sync, and examples
- Implemented `restore` command to restore the media registry from backup files, with options for verification and backup of current registry.
- Created `search` command to search through the local media registry with various filtering options.
- Added `stats` command to display detailed statistics about the local media registry, including breakdowns by genre, format, and year.
- Developed `sync` command to synchronize the local registry with a remote media API, allowing for both download and upload of media lists.
- Included example usage for the registry commands in `examples.py`.
- Fixed tag filtering logic in `MediaRegistryService` to ensure correct filtering based on tags.
2025-07-25 01:33:22 +03:00
Benexl
9fc66db248 feat: Add downloads menu and related actions for local media management 2025-07-25 01:05:51 +03:00
Benexl
f4e73c3335 Add AniList download command and download service integration
- Implemented a new command for downloading anime episodes using the AniList API.
- Created a DownloadService to manage episode downloads and track their status in the media registry.
- Added comprehensive command-line options for filtering and selecting anime based on various criteria.
- Integrated feedback mechanisms to inform users about download progress and issues.
- Established validation for command options to ensure correct input.
- Enhanced error handling and logging for better debugging and user experience.
- Included functionality for managing multiple downloads concurrently.
2025-07-25 00:38:07 +03:00
Benexl
5246a2fc4b feat: Implement TorrentDownloader class with libtorrent and webtorrent CLI support
- Added TorrentDownloader class for robust torrent downloading.
- Integrated libtorrent for torrent management when available.
- Implemented fallback to webtorrent CLI for downloading torrents.
- Added methods for downloading via libtorrent and webtorrent CLI.
- Included progress tracking and callback functionality.
- Updated pyproject.toml and uv.lock to include libtorrent as a dependency.
- Created unit tests for TorrentDownloader and legacy function for backward compatibility.
2025-07-24 23:37:00 +03:00
Benexl
4bbfe221f2 feat: refactor provider imports and enhance HTML parsing utilities 2025-07-24 23:36:22 +03:00
Benexl
6017833605 feat: implement DefaultDownloader for video downloads and subtitle management 2025-07-24 22:37:32 +03:00
Benexl
f2538f5341 feat: remove code reviews and mentoring from development practices 2025-07-24 22:09:51 +03:00
Benexl
fec09e9b74 feat: implement download command with examples and enhance downloader structure 2025-07-24 22:07:56 +03:00
Benexl
67a174158d feat: implement episode range parsing and enhance search functionality with improved filtering options 2025-07-24 21:19:18 +03:00
Benexl
ae3a59d116 feat: add update command to FastAnime CLI for version management 2025-07-24 20:41:17 +03:00
Benexl
fd59d64b76 feat: add Jikan API integration and enhance media fetching capabilities 2025-07-24 20:16:42 +03:00
Benexl
b1ac4a6558 feat: add character and airing schedule views, enhance media info display 2025-07-24 20:01:03 +03:00
Benexl
8e9aeb660f feat: enhance error handling in media recommendations and relations mapping 2025-07-24 19:49:58 +03:00
Benexl
4f401aa91c Refactor code structure for improved readability and maintainability 2025-07-24 19:29:03 +03:00
Benexl
b8733eccbd feat: implement recommendations and relations functionality in media actions 2025-07-24 19:24:15 +03:00
Benexl
3617465f64 fix: add missing format field in media relations query 2025-07-24 19:22:55 +03:00
Benexl
4c9ecafb9b fix: add missing format field in media recommendations query 2025-07-24 19:21:05 +03:00
Benexl
e87e4e5639 refactor: update GraphQL query paths for consistency and clarity 2025-07-24 19:15:04 +03:00
Benexl
16fa39397d refactor: improve variable naming and return types in API methods for clarity 2025-07-24 19:11:55 +03:00
Benexl
24ffc4ee53 refactor: consolidate and enhance GraphQL queries for media airing schedule, characters, and recommendations 2025-07-24 19:03:44 +03:00
Benexl
3e0be026eb refactor: rename Service to Services for consistency in context management 2025-07-24 18:55:50 +03:00
Benexl
a04643d36a fix: import issues 2025-07-24 18:52:18 +03:00
Benexl
5392d4f25a refactor: limit use of plural for package names and module names 2025-07-24 18:46:43 +03:00
Benexl
63e107ba53 refactor: providers 2025-07-24 18:41:35 +03:00
Benexl
9ba3f88813 refactor: converters 2025-07-24 18:38:11 +03:00
Benexl
5da5dc5dcc refactor: factory to api 2025-07-24 18:37:02 +03:00
Benexl
3c42f660ce refactor: formatters 2025-07-24 18:35:35 +03:00
Benexl
4349b9fc22 refactor: reorganise menus 2025-07-24 18:30:51 +03:00
Benexl
048d008ca1 refactore: rename api to media api 2025-07-24 18:21:49 +03:00
Benexl
17636d766a feat: improve performance 2025-07-24 18:17:06 +03:00
Benexl
367520450d fix: merge issues 2025-07-24 17:39:26 +03:00
Benexl
d6f773f41f feat: make config parseing more efficient 2025-07-24 17:27:39 +03:00
Benexl
f76350bc5b feat: improve preview logic 2025-07-24 16:46:53 +03:00
Benexl
d299355d90 Update README.md 2025-07-24 15:17:34 +03:00
Benexl
4aa9fa9253 Update README.md 2025-07-24 15:15:22 +03:00
Benexl
fcb16a574e chore: format 2025-07-24 15:04:13 +03:00
Benexl
8b52a1ef27 chore: format 2025-07-24 14:57:36 +03:00
Benexl
574a739cb6 chore: remove redundant commands 2025-07-24 14:55:46 +03:00
Benexl
f27a98aaa6 chore: update entry point 2025-07-24 14:53:43 +03:00
Benexl
a266a7100f chore: move dockerfile to bundle 2025-07-24 14:43:15 +03:00
Benexl
d1e07930f9 feat: cleanup 2025-07-24 14:40:17 +03:00
Benexl
bc7936d9cc Merge branch 'roadmap-to-3.0' 2025-07-24 14:35:00 +03:00
Benexl
19c6656cdf feat: fallback to cover image if episode thumbnail not available 2025-07-24 14:29:25 +03:00
Benexl
06506fb47f chore: update lock files 2025-07-24 14:18:01 +03:00
Benexl
29480c64cd chore: update lock files 2025-07-24 14:14:58 +03:00
Benexl
474da2f1fd chore: bump version (v2.9.9) 2025-07-24 14:14:44 +03:00
Benexl
abac604ccd Merge pull request #97 from cornservant/feat/no-check-certificate-flag
Add a --no-check-certificate flag
2025-07-24 14:11:30 +03:00
Benexl
48f46cdf3d refactor: reorganize assets 2025-07-24 13:38:31 +03:00
Benexl
9cafcde9e1 feat: performance review 2025-07-24 11:36:23 +03:00
Benexl
e908c793c6 feat: import configs toplevel 2025-07-24 11:11:30 +03:00
Benexl
0fd69d03dd feat: relations recommendation stubs 2025-07-24 02:47:49 +03:00
Benexl
3a9be3f699 feat: duration 2025-07-24 02:28:55 +03:00
Benexl
d3f08ea9c4 feat: show airing time 2025-07-24 02:09:44 +03:00
Benexl
9efe9f9949 feat: media actions 2025-07-24 01:54:59 +03:00
Benexl
83933f7a63 feat: results menu 2025-07-24 01:04:45 +03:00
Benexl
afe1cb68f6 feat: results menu 2025-07-24 00:07:26 +03:00
Benexl
a6ddb10734 feat: improve main menu 2025-07-24 00:07:13 +03:00
Benexl
f678fa13f0 feat: improve state models 2025-07-23 21:17:03 +03:00
Benexl
2067467134 feat: improve provider types 2025-07-23 20:02:25 +03:00
Benexl
d78b62fcee feat: improve api types 2025-07-23 18:48:57 +03:00
Benexl
6c30cf808b chore: remove broken config field 2025-07-23 15:49:54 +03:00
Benexl
6e9babf270 feat: animepahe provider 2025-07-23 11:29:52 +03:00
Benexl
aa50ab62b5 feat: only require specifying the package folder 2025-07-23 00:38:05 +03:00
Benexl
88975e59c0 feat: make episode previews unique by using a prefix 2025-07-23 00:32:55 +03:00
Benexl
64c427fe41 fix: per page 2025-07-22 23:46:48 +03:00
Benexl
987ae57e33 feat: media list sort 2025-07-22 22:46:42 +03:00
Benexl
ac36e24a32 feat: improve preview 2025-07-22 19:02:37 +03:00
Benexl
04d877a72e chore: upgrade deps 2025-07-22 18:59:45 +03:00
Benexl
43174db8e4 feat: improve preview 2025-07-22 18:46:15 +03:00
Benexl
3092ef0887 feat: properly normalize episodes 2025-07-22 17:25:33 +03:00
Benexl
5e45fba66d chore: remove crazy ai tests 2025-07-22 15:42:51 +03:00
Benexl
65e4726f82 feat: re-add the download cmd 2025-07-22 14:59:29 +03:00
Benexl
384d326fa8 feat: cleanup 2025-07-22 14:55:38 +03:00
Benexl
60c583d115 feat: anilist auth cmd 2025-07-22 14:39:16 +03:00
Benexl
f716f9687a chore: add todo 2025-07-22 01:23:58 +03:00
Benexl
db1006a6b2 fix: date error 2025-07-22 01:23:49 +03:00
Benexl
9163b1394d feat: improve previews 2025-07-22 01:23:21 +03:00
Benexl
0ce27f8e50 feat: menus 2025-07-22 00:47:42 +03:00
Benexl
0e6aeeea18 feat: update interactive session logic 2025-07-21 22:28:09 +03:00
Benexl
452c2cf764 feat: session service 2025-07-21 21:43:16 +03:00
Benexl
a1de0548f4 feat: auth service 2025-07-21 20:35:28 +03:00
Benexl
f60cdea2e1 feat: watch history service 2025-07-21 19:24:32 +03:00
Benexl
b67284cfeb refactor: service import paths 2025-07-21 17:47:53 +03:00
Benexl
17161f5f78 feat: feedback service 2025-07-21 17:36:29 +03:00
Benexl
c0d87c4351 feat: registry service 2025-07-21 17:27:51 +03:00
Benexl
725fe4875d feat: cleanup 2025-07-20 19:34:19 +03:00
Benexl
ac3c6801d7 feat: implement unified media registry and tracking system for anime 2025-07-16 01:16:38 +03:00
Benexl
27b1f3f792 feat: god help me 2025-07-16 00:54:55 +03:00
Benexl
49cdd440df feat: working with ai is a mess lol 2025-07-16 00:46:02 +03:00
Benexl
490f8b0e8b feat: stuff happened 2025-07-15 23:37:15 +03:00
Benexl
5dde02570a chore: leave testing for later 2025-07-15 22:53:10 +03:00
Benexl
e3deb28d26 chore:cleanup 2025-07-15 22:36:08 +03:00
Benexl
1a85b2f216 refactor: improve media actions tests with enhanced mocking and assertions 2025-07-15 01:23:38 +03:00
Benexl
0639a3c949 test: enhance authentication and main menu tests with detailed user profile and pagination handling 2025-07-15 01:12:25 +03:00
Benexl
bdbf0821c5 fix: tests 2025-07-15 00:44:49 +03:00
Benexl
5e81c44312 feat: copilot instructions 2025-07-15 00:16:58 +03:00
Benexl
41ed56f395 feat: tests 2025-07-15 00:02:55 +03:00
Benexl
26f9c3b8de chore: clean up 2025-07-14 23:53:25 +03:00
Benexl
ecdd1b5f20 refactor: anilist subcommands 2025-07-14 23:31:55 +03:00
Benexl
b6dd965e49 feat: switch to pydantic types for api 2025-07-14 23:00:20 +03:00
Benexl
273dd56680 fix: next 2025-07-14 22:46:54 +03:00
Benexl
be4cc58e47 feat: pagination 2025-07-14 22:44:27 +03:00
Benexl
c882691412 feat: episode preview 2025-07-14 22:34:26 +03:00
Benexl
f4c4c874df feat:auth 2025-07-14 22:14:07 +03:00
Benexl
f8992d46dd Implement watch history management system with tracking and data models
- Added WatchHistoryManager for managing local watch history storage, including methods for adding, updating, removing, and retrieving entries.
- Introduced WatchHistoryTracker to automatically track episode viewing and progress updates.
- Created data models for watch history entries and overall history management, including serialization to and from JSON.
- Implemented comprehensive error handling and logging throughout the system.
- Developed a test script to validate the functionality of the watch history management system, covering basic operations and statistics.
2025-07-14 22:00:44 +03:00
Benexl
222c50b4b2 feat: implement session management functionality with save/load capabilities and error handling 2025-07-14 21:23:31 +03:00
Benexl
064401f8e8 feat: implement authentication utilities and integrate with menus 2025-07-14 21:07:47 +03:00
Benexl
a079f9919c feat: implement enhanced feedback system for user interactions 2025-07-14 20:58:52 +03:00
Benexl
a88df7f3ef chore: remove comment 2025-07-14 20:11:42 +03:00
Benexl
d1dfddf290 feat: stabilize the interactive workflow 2025-07-14 20:09:57 +03:00
Benexl
e8491e3723 feat: anilist auth 2025-07-14 02:27:05 +03:00
Benexl
2f21e7139b feat: episode number 2025-07-14 02:26:53 +03:00
Benexl
f5c831077d feat: previews 2025-07-14 02:26:25 +03:00
Benexl
badd10bf97 feat: interactive 2025-07-14 02:24:44 +03:00
Benexl
42bd4963b8 feat: interactive search 2025-07-14 02:24:04 +03:00
Benexl
f08ff7155c feat: use auth manager in login 2025-07-14 02:23:34 +03:00
Benexl
e487435d7e feat: auth manager 2025-07-14 02:23:15 +03:00
Benexl
54f7327ed7 feat(player): pass only list of sub urls 2025-07-13 17:55:57 +03:00
Benexl
ba620bae96 feat: cli download 2025-07-13 17:46:30 +03:00
Benexl
48eac48738 feat: single source of app level constants 2025-07-13 14:52:40 +03:00
Benexl
194b8ca2df feat: update fzf selector 2025-07-13 13:46:29 +03:00
Benexl
96c2d4976c fix: inquirerpy 2025-07-13 13:41:22 +03:00
Benexl
c5034a5829 feat: update fzf opts 2025-07-13 13:41:13 +03:00
Benexl
7c91288e6e feat: improve desktop entry generation 2025-07-13 13:19:50 +03:00
Benexl
de2ba342ad feat: improve config logic 2025-07-13 13:10:49 +03:00
Benexl
b847e02fe0 feat: pass fzf opts 2025-07-13 12:30:09 +03:00
Benexl
f02f92b80b feat: custom exception handling 2025-07-13 12:30:09 +03:00
Benexl
a2da6974fa feat: cli search 2025-07-13 01:39:52 +03:00
Benexl
be1babbedc feat: mpv player syncplay 2025-07-12 23:52:04 +03:00
Benexl
5c804f7aa6 feat: add vlc player 2025-07-12 23:25:03 +03:00
Benexl
723a7ab24f feat: player mpv 2025-07-12 22:55:13 +03:00
Benexl
18a9b07144 feat: dont import any networking lib unless used 2025-07-12 19:05:25 +03:00
Benexl
d279cc70b9 feat: interactively edit config 2025-07-12 18:57:02 +03:00
Long Huynh Huu
27d71cbb23 feat: add --no-check-certificate flag 2025-07-09 03:52:19 +02:00
Long Huynh Huu
615b420c74 feat: reduce inefficient double copy (for determinate nix) 2025-07-09 03:45:43 +02:00
Long Huynh Huu
f9c2b6e939 fix: dev shell 2025-07-09 03:45:27 +02:00
Benexl
85368393fc feat: begin animepahe refactor 2025-07-07 22:34:34 +03:00
Benexl
b9636c94d3 feat: write all anilist graphls to files 2025-07-07 22:09:53 +03:00
Benexl
4920ee508a feat: make anilist api functional 2025-07-07 22:01:01 +03:00
Benexl
fd448ad701 Update README.md 2025-07-07 19:12:17 +03:00
Benexl
b223a34879 Update FUNDING.yml 2025-07-07 19:09:38 +03:00
Benexl
d5e1e60266 feat: abstract provider testing 2025-07-07 19:02:12 +03:00
Benexl
783b63219f feat: make allanime provider functional 2025-07-07 17:35:19 +03:00
Benexl
317fee916b chore: remove api from project 2025-07-07 13:49:25 +03:00
Benexl
870bb24e1b feat: recreate all allanime extractors 2025-07-07 13:48:19 +03:00
Benexl
f51ceaacd7 test: placeholder tests 2025-07-07 00:41:24 +03:00
Benexl
cdad70e40d feat: add jikan api 2025-07-07 00:23:33 +03:00
Benexl
0737c5c14b feat: mass refactor 2025-07-06 23:59:18 +03:00
Benexl
32f4d9271f feat: phase 1 of anilist_interfaces refactor 2025-07-06 22:23:14 +03:00
Benexl
355f10dd9e feat: mass refactor 2025-07-06 18:52:14 +03:00
Benexl
2f2ffc0a84 feat: mass refactor 2025-07-06 18:51:25 +03:00
Benexl
e35683e90a fix: config update logic 2025-07-06 17:40:20 +03:00
Benexl
2bd02c7e99 feat: mass refactor 2025-07-06 14:15:13 +03:00
Benexl
5a50e79216 feat: anilist to stay in libs 2025-07-06 12:34:29 +03:00
Benexl
ec78c81381 feat: mass refactor 2025-07-06 12:31:40 +03:00
Benexl
f042e5042b fix: minor error 2025-07-05 17:14:33 +03:00
Benexl
428bbb20bd feat: new config logic 2025-07-05 17:13:49 +03:00
Benexl
3af31a2dfd feat: update config logic with new philosophy 2025-07-05 17:13:21 +03:00
Benexl
759889acd4 feat: new config logic 2025-07-05 03:06:49 +03:00
Benexl
d106bf7c5d chore: update deps 2025-07-04 20:30:33 +03:00
Benexl
00ff89d14f chore: update deps 2025-07-04 20:29:36 +03:00
Benexl
76460b6c54 chore: remove deprected attr 2025-07-04 16:53:20 +03:00
Benexl
e58fd33fe0 feat: allow going back on empty search term when using fzf anilist search 2025-07-04 16:44:58 +03:00
Benexl
931f9f10f8 chore: update deps 2025-07-04 16:39:01 +03:00
Benexl
0e9dbd2c6b feat: make experimental fzf anilist search disablable lol 2025-07-04 16:33:18 +03:00
Benexl
3bdfa27e1c feat: experimental search using fzf reload 2025-07-04 16:20:48 +03:00
Benedict Xavier
f46f09ffdf Merge pull request #91 from DerDestroyer/episode-number-animepahe
fix: fixed episode number for animepahe with multiple seasons
2025-05-12 13:09:39 +03:00
Benedict Xavier
d309c04214 Merge pull request #92 from DerDestroyer/anime-relations
fix: fixed relations menu and only show ANIME
2025-05-12 13:07:37 +03:00
Benedict Xavier
b19a323d15 Merge pull request #93 from iMithrellas/manga-icat
This pull request introduces a new manga viewer option, icat.
2025-05-12 13:04:55 +03:00
iMithrellas
ff94edfd05 fix: unbound test error 2025-05-07 01:01:13 +02:00
iMithrellas
59e1a82646 feat: config option for selecting manga viewer 2025-05-07 00:44:39 +02:00
iMithrellas
2e902fa4e7 feat: PoC icat for viewing manga 2025-05-07 00:13:00 +02:00
DerDestroyer
d2e17af7a9 fix .5 episodes being numbered as whole episodes 2025-05-04 20:13:50 +02:00
DerDestroyer
f1fa40c419 fixed relations menu and only show ANIME 2025-05-02 01:51:18 +02:00
DerDestroyer
8bbde97403 fixed episode number for animepahe with multiple seasons 2025-05-01 02:30:56 +02:00
Benexl
a2b7d71eb2 Merge branch 'sudoAlphaX-main-menu-on-blacnk-search' 2025-03-30 21:48:33 +03:00
Alpha
67b4f0ea38 Merge branch 'master' into main-menu-on-blacnk-search 2025-03-27 08:22:49 +00:00
Benedict Xavier
a6d5d5f37c Merge pull request #82 from sudoAlphaX/runtime-auto-next
feat: toggle auto-next during runtime from media_player_controls
2025-03-18 04:59:57 +03:00
Alpha
67a066f16e feat: toggle auto-next during runtime from media_player_controls
Allows user to set or unset auto-next episode from media_player_controls
during runtime. This feature was only available in media_actions_menu or
config file.
2025-03-18 06:49:20 +05:30
Alpha
e6297619d4 feat: return to main menu on blank search term
Return to fastanime_main_menu on blank search term. Can be used to
cancel the search.
2025-03-16 19:04:29 +05:30
Benedict Xavier
8f514858f2 Merge pull request #80 from sudoAlphaX/hide-next-episode-button-on-last
feat: hide next episode button on reaching last episode
2025-03-16 14:42:09 +03:00
Alpha
eb9cffbd7a feat: hide next episode button on reaching last episode
Hides the next episode button if the currently completed episode is the
last available episode on the server. Also affects auto-next feature,
where it returns to media actions menu on completion of last episode.
2025-03-16 16:06:46 +05:30
Benexl
c5f9c37d3a fix: preview images not showing in rofi menu 2025-03-16 09:46:23 +03:00
Benedict Xavier
44fd65ebab Merge pull request #74 from crispy-caesus/patch-1
remove yugen from description
2025-03-03 06:22:48 +03:00
crispy-caesus
e919980ff7 remove yugen from description
yugen and gogoanime shut down
2025-03-02 12:37:01 +01:00
Benedict Xavier
6887c6ff10 Merge pull request #72 from sudoAlphaX/multiple-download-ranges
feat: multiple download ranges in download in menu
2025-03-02 12:12:59 +03:00
Benedict Xavier
b394de0b23 Merge pull request #71 from crasband1/use_preffered_history_config_option
fix: config preferred_history option was unused
2025-02-24 08:45:59 +03:00
Benedict Xavier
71003049d6 Merge branch 'master' into use_preffered_history_config_option 2025-02-24 08:45:41 +03:00
Benexl
6f69b785d8 feat(config): mpv pre args 2025-02-23 20:52:52 +03:00
Alpha
6756540fd1 feat: multiple download ranges in download in menu
Improvement to 98e41e1e which allows selection of multiple download
ranges.

Example: :3 5:7 10 13 15:16 19:
will download 1,2,3,5,7,10,13,15,16,19 till the end

Variables can be named better.
2025-02-23 23:07:09 +05:30
Benexl
e6b9df25dd feat: ensure the environs externally provided by user are preferred 2025-02-23 20:28:41 +03:00
Benexl
f40dd2363a feat(updater): add instructions for post update 2025-02-23 20:24:26 +03:00
Benexl
61a525ff94 feat(config): pass custom mpv args 2025-02-23 20:18:23 +03:00
relive010
27d671f89d fix: config preferred_history option was unused due to switched if statements 2025-02-23 10:14:02 -07:00
Benedict Xavier
58edb0427f Merge pull request #70 from crasband1/not_history_continue_off_by_one_fix
fix: fixed off by one error in condition of continuing anime from Wat…
2025-02-23 19:39:09 +03:00
Benedict Xavier
7abcdc8f7c Merge pull request #69 from sudoAlphaX/move-next-eps-button-position
refactor: just move next episode to the top
2025-02-23 19:35:51 +03:00
Benedict Xavier
ca4ca0d476 Merge pull request #68 from sudoAlphaX/download-menu-range-fix
fix: download in menu range fix
2025-02-23 19:33:51 +03:00
relive010
5a337b1c97 fix: fixed off by one error in condition of continuing anime from Watching tab when logged into anilist when the anime is not in watch_history.json 2025-02-23 09:00:30 -07:00
Alpha
6c94dd22fc refactor: just move next episode to the top
When watching episodes, it makes sense to go to the next episode after
completing the current one. Currently, when an episode is completed,
replay button appears first in media_player_controls menu.

This patch just moves replay button below such that the next episode
button takes priority, and the user can watch the next episode with a
single key press (<return>) which is less immersion breaking that
(<down> <return>).
2025-02-23 16:48:53 +05:30
Alpha
8c7e1e201f fix: download in menu range fix
When using the download in menu feature introduced by 98e41e1,
downloading using range (3:5) doesn't work as expected. It starts
incremented by 1. Example: 3:5 selects episodes 4 and 5.

This patch addresses this issue by simply decrementing the start_episode
variable by 1 before adding adding to range list.
2025-02-23 16:44:19 +05:30
Benexl
98e41e1eb5 feat: download anime menu option 2025-02-23 09:53:31 +03:00
Benexl
bd0e7db73c chore: update lock files 2025-02-22 13:02:45 +03:00
Benexl
fb705b4ac2 chore: bump version (v2.8.8) 2025-02-22 13:02:36 +03:00
Benexl
93654be74f feat: enhance login experience 2025-02-22 12:58:45 +03:00
Benedict Xavier
9b14a4c723 Merge pull request #62 from crasband1/master
added support for macOS login
2025-02-22 11:34:38 +03:00
relive010
bce5acf7b5 added support for macOS login via key pasted into anilist_key.txt in Downloads 2025-02-19 02:16:38 -07:00
Benexl
228be7e1f7 feat: add support for the year 2025 in available years list 2025-02-18 11:23:40 +03:00
Benedict Xavier
2eb434e42a Update README.md 2025-02-04 09:16:34 +03:00
Benedict Xavier
fbb3a00ab0 Create FUNDING.yml 2025-01-30 07:51:46 +03:00
Benedict Xavier
8341ffe8fd Update README.md 2025-01-29 22:13:03 +03:00
benexl
0822e2e92c feat: improve preview logic 2025-01-29 19:47:56 +03:00
benexl
3b9fbd0665 chore: upgrade deps 2025-01-29 19:47:42 +03:00
benexl
60b74bee18 feat: update the preview script 2025-01-29 18:47:12 +03:00
Benedict Xavier
d7dc63e003 Merge pull request #59 from Benexl/minor-fixes
Minor fixes
2025-01-29 04:38:53 +03:00
Type-Delta
d40edb6ff6 🧹 cleanup: lint error E402, ehe ;p 2025-01-29 00:52:07 +07:00
Benedict Xavier
803712649f Update README.md 2025-01-27 17:26:07 +03:00
Benedict Xavier
7bc0d33f69 Update README.md 2025-01-27 17:25:15 +03:00
Benedict Xavier
5885d134df Update README.md 2025-01-26 10:33:55 +03:00
Type-Delta
5500ec49c8 💫 update: which_win32_gitbash() to handle git.exe in bin dir 2025-01-24 12:10:52 +07:00
Type-Delta
8c94380050 🛠️ fix: fzf preview use the wrong bash.exe on Windows 2025-01-23 19:40:45 +07:00
Type-Delta
87a97dd0c6 🧹 cleanup: fix typos 2025-01-23 07:40:42 +07:00
Type-Delta
80d9f732b1 🛠️ fix: handle ctrl+C termination of fzf 2025-01-23 07:03:21 +07:00
Benedict Xavier
051273dac9 Merge pull request #57 from Abdisto/master
Indent Fix for Anime Description in fzf preview
2025-01-21 09:25:47 +03:00
Abdist
036f448906 Update utils.py 2025-01-18 21:25:32 +01:00
Abdist
b5aeed9268 Update utils.py
strip() was not neccessary
2025-01-18 15:47:19 +01:00
Abdist
4257502b85 Update utils.py 2025-01-16 14:28:43 +01:00
Abdist
28a857520f Update utils.py 2025-01-16 14:26:12 +01:00
Abdist
4f9fff375c Update utils.py 2025-01-16 14:21:43 +01:00
Abdist
ce31f63788 Update utils.py 2025-01-16 14:01:53 +01:00
Abdist
9412c2491e Update utils.py 2025-01-16 13:56:20 +01:00
Abdist
8209adec62 Update utils.py 2025-01-16 13:54:07 +01:00
Abdist
39703d9eca Update utils.py 2025-01-16 13:44:01 +01:00
Abdist
57d16b3e18 Update utils.py 2025-01-16 13:10:06 +01:00
Abdist
73a99f8b96 Update utils.py 2025-01-16 13:07:54 +01:00
Abdist
309d7d5858 Update utils.py 2025-01-16 13:04:49 +01:00
Abdist
8d20e490ca Update utils.py 2025-01-16 13:00:26 +01:00
Abdist
3a6e005f3a Update utils.py 2025-01-16 12:55:04 +01:00
Benedict Xavier
bdf49bd7ce Merge pull request #52 from Benexl/fix-missing-meta
Fix(downloader):  handle hls stream's properly
2025-01-06 22:43:56 +03:00
Type-Delta
c4df2587d0 Merge branch 'master' into fix-missing-meta 2025-01-06 21:28:18 +07:00
Benedict Xavier
b38f66767f Merge pull request #50 from sudoAlphaX/notify-timeout
refactor!: change notifier timeout config to seconds
2025-01-05 23:31:39 +03:00
Benedict Xavier
6c0e0ccf72 Merge pull request #49 from sudoAlphaX/anilist-login-typo
docs: fix typo for anilist login command
2025-01-05 23:29:53 +03:00
Alpha
e39c992883 refactor!: change notifier timeout config to seconds
BREAKING CHANGE: change notification timeout from minutes to seconds in
config.
2025-01-05 22:04:09 +05:30
Alpha
a1744fc9b3 docs: fix typo for anilist login command 2025-01-05 21:59:32 +05:30
Benedict Xavier
3c5106c32c Merge pull request #48 from Aethar01/master
Add arch to installation instructions in README.md
2025-01-05 00:45:30 +03:00
Elliott Ashby
fd0d899f72 grammar 2025-01-04 21:31:32 +00:00
Elliott Ashby
c753873f61 Update README.md to include arch installation 2025-01-04 21:30:39 +00:00
benexl
4c8ff2ae9b chore: update lock files 2025-01-04 23:47:48 +03:00
benexl
23274de367 chore: bump version (v2.8.7) 2025-01-04 23:47:45 +03:00
benexl
2aec40ead0 refactor: temporarily remove nix from make - release 2025-01-04 23:47:42 +03:00
benexl
172f2bb1de chore: bump version in uv.lock 2025-01-04 23:37:55 +03:00
benexl
2f5684a93a feat(cli): add option to disable user config 2025-01-04 23:37:30 +03:00
benexl
1d40160abf chore: update rofi configs 2025-01-04 23:37:11 +03:00
Benedict Xavier
af84d80137 Merge pull request #46 from piradata/patch-2
docs(config): correct some typos
2025-01-01 01:31:58 +03:00
Piradata
e6412631ae correct some typos 2024-12-31 19:06:53 -03:00
Type-Delta
978d8d45ba 🛠️ fix: prevent clipping from HE-AAC to AAC convertion 2024-12-28 12:06:21 +07:00
Type-Delta
06575120d6 Merge branch 'master' into fix-missing-meta 2024-12-28 09:39:18 +07:00
Type-Delta
72cec28613 add: --hls-use-h264 to convert ts streams to mp4 2024-12-28 09:35:49 +07:00
Benedict Xavier
8023edcf3a Update README.md 2024-12-23 20:45:07 +03:00
Benedict Xavier
0cb50cd506 Update README.md 2024-12-20 13:55:39 +03:00
Type-Delta
9981b3dec8 🛠️ fix: missing metadata when --force-ffmpeg is used 2024-12-19 07:11:48 +07:00
Benedict Xavier
50c048e158 Merge pull request #40 from serialjaywalker/master
feat: Check if in venv before attempting user install/update.
2024-12-17 20:34:51 +03:00
Serial_Jaywalker
c0a57c7814 Check if in venv before attempting user install/update. 2024-12-16 01:52:03 -08:00
benexl
bcdd88c725 chore: bump version 2024-12-13 08:34:24 +03:00
benexl
d45d438663 chore: bump version (vv2.8.6) 2024-12-13 08:18:54 +03:00
Benexl
3d12059e27 Update README.md 2024-12-12 20:04:31 +03:00
benexl
677f4690fa feat(anilist): make perPage configurable 2024-12-07 21:49:37 +03:00
Benexl
a79b59f727 Merge pull request #38 from she11sh0cked/master
fix(nix): add pypresence to flake.nix
2024-12-03 15:54:12 +03:00
she11sh0cked
5641c245e7 fix(nix): add pypresence to flake.nix 2024-12-03 13:49:52 +01:00
Benexl
058fc285cd Merge pull request #36 from gand0rf/master
Added pypresence discord function
2024-12-03 12:47:46 +03:00
Benexl
71cfe667c9 Merge branch 'master' into master 2024-12-03 12:39:56 +03:00
benex
d9692201aa fix: type errors 2024-12-03 12:38:29 +03:00
benex
1fd4087b41 fix: type errors 2024-12-03 12:32:08 +03:00
benex
787eb0c9ca refactor: conform all provider functions 2024-12-03 12:29:47 +03:00
benex
acd937f8ab fix(allanime): argument parsing 2024-12-03 12:07:53 +03:00
Gand0rf
52af68d13f minor chagne to discord.py 2024-12-02 20:26:49 -05:00
Gand0rf
1ff3074fad updated discord.py 2024-12-02 20:11:40 -05:00
Gand0rf
debaa2ffa6 Moved where threading switch is created to remove error 2024-12-02 19:37:49 -05:00
Gand0rf
5b6ccbe748 Fixing erros per github python test 2024-12-02 19:32:07 -05:00
Gand0rf
d6ca923951 Delete line from config.py 2024-12-02 19:25:50 -05:00
Gand0rf
0e9bf7f2de added pypresence with uv command 2024-12-02 19:16:25 -05:00
Gand0rf
ccad2435b0 Added pypresence discord function 2024-12-02 19:08:51 -05:00
benex
30fa9851dd feat(animepahe): refactor API calls to use query parameters and improve stream retrieval logic 2024-12-03 00:25:18 +03:00
benex
000bae9bb7 refactor(animepahe): update import path for process_animepahe_embed_page 2024-12-02 23:57:18 +03:00
benex
8c2bb71e08 feat(animepahe): enhance search functionality and improve error handling in episode retrieval 2024-12-02 23:54:35 +03:00
benex
57393b085a feat(animepahe): refactor episode stream retrieval and enhance error handling 2024-12-02 23:46:28 +03:00
benex
5f721847d7 feat(allanime): improve error handling for stream source retrieval 2024-12-02 23:19:18 +03:00
benex
383cb62ede style: format files 2024-12-02 22:26:03 +03:00
benex
434ac947dd chore: add pre-commit as dev dep 2024-12-02 21:06:48 +03:00
benex
d0fb39cede style: format files with ruff 2024-12-02 18:01:20 +03:00
benex
f98ae77587 feat(allanime): enhance stream source handling with new cases and regex for MP4 server (mp4-upload: success) 2024-12-02 16:27:58 +03:00
benex
33e1b0fb6f refactor(allanime): introduce default constants for search parameters and improve code readability 2024-12-02 12:19:03 +03:00
benex
7134702eb9 feat(allanime): add command-line interface for anime search and streaming 2024-12-02 12:08:38 +03:00
benex
cac7586a86 refactor(constants): update API_ENDPOINT to use f-string for improved readability 2024-12-02 12:08:37 +03:00
benex
0b9da27def refactor(allanime): enhance error handling in API response processing 2024-12-02 12:08:37 +03:00
benex
ddbb4ca451 refactor(anime_provider): simplify URL processing in AllAnime class 2024-12-02 12:08:37 +03:00
benex
757393aa36 refactor(allanime): update constants and improve naming for clarity 2024-12-02 12:08:37 +03:00
benex
eb54d5e995 chore: add VSCode settings for Python auto import completions 2024-12-02 12:08:37 +03:00
benex
0d95a38321 refactor(anime_provider): replace PROVIDER attribute with class name for improved clarity 2024-12-02 12:08:37 +03:00
benex
8d2734db74 refactor(allanime): streamline API methods and improve naming conventions 2024-12-02 12:08:37 +03:00
benex
b3abcb958b refactor: simplify debug_provider decorator and remove redundant provider name usage 2024-12-02 12:08:37 +03:00
benex
0667749e4c refactor: rename API classes for consistency and clarity 2024-12-02 12:08:37 +03:00
Benexl
57e73e6799 Update README.md 2024-11-28 20:00:45 +03:00
Benexl
7d890b9719 Update README.md 2024-11-28 19:51:28 +03:00
benex
8cbbcf458d chore: update lock files 2024-11-28 09:16:32 +03:00
benex
67bc25a527 chore: bump version (v2.8.4) 2024-11-28 09:16:23 +03:00
benex
e668f9326a feat(anilist): add support for relations and recommendations 2024-11-25 14:16:07 +03:00
benex
a02db6471f fix(hianim) always use fresh requests in extractors 2024-11-25 12:53:16 +03:00
Benexl
08b1f0c90c Merge pull request #31 from iMithrellas/feature-menu-order
feat: add  menu order feature
2024-11-24 22:56:31 +03:00
Benexl
3ec8dbee8c Merge branch 'master' into feature-menu-order 2024-11-24 22:55:19 +03:00
iMithrellas
473c11faca Added 'menu_order' into the default object and default config. 2024-11-24 20:45:23 +01:00
benex
320e3799d3 chore: update username 2024-11-24 15:05:33 +03:00
benex
a0f28ddf6d feat(cli): don't check for update if running notifier 2024-11-24 14:55:38 +03:00
benex
9512c3530a feat: check for updates after every 12hrs 2024-11-24 14:43:43 +03:00
iMithrellas
72602a0ec1 Removed an accidentaly added import by my LSP. 2024-11-24 12:02:23 +01:00
iMithrellas
4daf6a2b07 Merge branch 'Benexl:master' into feature-menu-order 2024-11-24 11:49:24 +01:00
benex
8b37927f6a fix(anilist-download): prefer romaji title over english when searching provider 2024-11-24 13:44:02 +03:00
benex
9d6f785a7f fix(anilist-interface): check if both total and stop time are defined 2024-11-24 13:26:57 +03:00
benex
897c34d98c chore(hianime): include source to solution 2024-11-24 13:26:28 +03:00
benex
28c75215bd chore: update flake.nix 2024-11-24 13:26:08 +03:00
benex
8697b27fe0 Merge branch 'hianime'
Recovers HD2 server of hianime
2024-11-24 13:14:22 +03:00
benex
b6e05c877b feat(hianime): finish HD2 server recovery 2024-11-24 13:10:52 +03:00
benex
d8c3ba6181 feat(hianime): finish megacloud extractor 2024-11-24 13:10:08 +03:00
benex
8b5c917038 Merge branch 'master' into hianime 2024-11-24 12:44:02 +03:00
Benexl
856f62c245 Update README.md 2024-11-24 11:48:30 +03:00
Vlastimil Urban
02dfc9d71c feat: configurable main_menu 2024-11-24 02:17:30 +01:00
benex
cef0bae528 feat(anilist-interface): finish next and previous page implementation 2024-11-23 17:44:53 +03:00
benex
4867720ad2 feat: implement experimental next and previous page. 2024-11-23 17:44:53 +03:00
benex
8d85e30150 feat(runtime): add current-page and current-data-loader to runtime 2024-11-23 17:44:53 +03:00
benex
eb99b7e6ba feat(anilist_api): also make the page configurable 2024-11-23 17:44:53 +03:00
Benexl
089c049f26 Merge pull request #28 from Type-Delta/psflag-fix
Fix(downloader): corrupted Parametric Stereo (PS) flag in downloaded .m3u8 videos
2024-11-23 14:15:30 +03:00
benex
a33e47d205 fix(config): use separate var for the config file val 2024-11-23 14:08:48 +03:00
benex
25dc35eaaf feat: print update message to stderr + disable auto check for updates (needs better implementation) 2024-11-23 14:08:22 +03:00
Type-Delta
525586e955 Merge branch 'Benexl:master' into psflag-fix 2024-11-23 17:11:58 +07:00
Type-Delta
5129219e23 fix: added --force-ffmpeg & --hls-use-mpegts options to properly handle some m3u8 streams 2024-11-23 17:10:31 +07:00
benex
7cd97c78b1 feat(config): make the max_cache_lifetime configurable 2024-11-22 23:21:58 +03:00
benex
27b4422ef3 feat(requests_cacher): make more reliable by ordering the results by created_at 2024-11-22 23:21:32 +03:00
benex
1c367c8aa1 feat(anilist-interface): add resume flag to auto continue from the most recent anime 2024-11-22 22:28:12 +03:00
Benex
7b6cc48b90 Update README.md 2024-11-22 09:56:26 +03:00
Benex
812d0110a7 Update README.md 2024-11-22 08:41:04 +03:00
Benex
60b05bf0ac Merge pull request #29 from Pixelizer09/master
feat: Update data.py
2024-11-22 08:22:12 +03:00
benex
d830cca3bc feat: init resurrection hianime 2024-11-22 08:14:45 +03:00
Pixelizer09
209e93b6d9 Merge branch 'master' into master 2024-11-22 11:28:44 +08:00
Pixelizer09
b10d9dc39a Update data.py 2024-11-22 11:27:29 +08:00
Benex
fe8cda094c Update README.md 2024-11-21 18:55:55 +03:00
Benex
33c06eab0a Update README.md 2024-11-21 18:07:13 +03:00
benex
f3f4be7410 chore: update lock files 2024-11-21 16:52:30 +03:00
benex
3915ef0fb6 chore: bump version (v2.8.3) 2024-11-21 16:52:23 +03:00
benex
20d26166dd docs: update readme 2024-11-21 16:50:07 +03:00
benex
ddca724bd8 chore: update deps 2024-11-21 16:40:21 +03:00
benex
b86c1a0479 feat: add fastanime anilist download beta 2024-11-21 16:40:09 +03:00
benex
1fa7830ddf Merge branch 'master' into fastanime-anilist-download 2024-11-21 15:35:36 +03:00
Benex
59abafbe16 Update README.md 2024-11-21 13:16:56 +03:00
Benex
b6eebb9736 Update README.md 2024-11-21 10:43:19 +03:00
benex
7797053102 chore: update lock files 2024-11-21 10:30:06 +03:00
benex
d763445f72 chore: bump version (v2.8.2) 2024-11-21 10:30:03 +03:00
benex
7bc6b14b5f docs: update config file docs 2024-11-21 10:29:39 +03:00
benex
f70d2ac8af feat(make_release): update to include nix files 2024-11-21 10:29:39 +03:00
benex
defdfc5a47 refactor: update todo 2024-11-21 10:29:39 +03:00
benex
e67eeda492 feat(updater): add ability to update on nix 2024-11-21 10:29:39 +03:00
benex
a17588d02c fix(interfaces): episode previews 2024-11-21 10:29:39 +03:00
Benex
67b59305c4 Update README.md 2024-11-21 07:43:53 +03:00
Pixelizer09
61db9aeea6 Update data.py 2024-11-21 10:57:02 +08:00
Benex
4f0768a060 Merge pull request #24 from gand0rf/chafa_call_update
fix(anilist_interface):  Updated chafa call
2024-11-19 22:14:29 +03:00
Gand0rf
21704cbbea Updated chafa call 2024-11-19 14:00:06 -05:00
Benex
886bc4d011 Update README.md 2024-11-19 19:23:51 +03:00
Benex
e3437e066a Update README.md 2024-11-19 19:18:49 +03:00
Benex
8f2795843a Update README.md 2024-11-19 19:03:53 +03:00
Benex
c6290592e8 Merge pull request #23 from she11sh0cked/master
fix(cli): prevent update check during completions
2024-11-19 19:01:57 +03:00
she11sh0cked
050ba740b8 fix(cli): prevent update check during completions 2024-11-19 15:24:55 +01:00
benex
0b1a27b223 chore: bump version (v2.8.1) 2024-11-19 14:08:42 +03:00
benex
bafd04b788 chore: update lock file 2024-11-19 14:08:08 +03:00
benex
fb5f51eea5 feat(config): add customization options 2024-11-19 14:08:08 +03:00
benex
799e1f0681 fix(updater): handle no internet 2024-11-19 14:08:08 +03:00
benex
53a2d953f8 feat: enable customization of the preview window 2024-11-19 14:08:08 +03:00
Benex
9ce5bc3c76 Update README.md 2024-11-19 11:38:33 +03:00
benex
dc58fc8536 chore: bump version (v2.8.0) 2024-11-19 10:06:57 +03:00
benex
1d5c3016fc chore: update lock file 2024-11-19 10:06:38 +03:00
benex
8737aea746 fix(player): set character encoding for compatibility with windows 2024-11-19 10:06:38 +03:00
Benedict Xavier
bd03866f5e Update README.md 2024-11-19 08:40:09 +03:00
Benedict Xavier
81690a8015 Update README.md 2024-11-19 08:37:26 +03:00
Benedict Xavier
933112a52b Update README.md 2024-11-19 08:34:23 +03:00
Benedict Xavier
eb513dfe0e Update README.md 2024-11-19 08:23:45 +03:00
Benedict Xavier
3928b77506 Update README.md 2024-11-19 08:17:44 +03:00
Benedict Xavier
95cb2bd78c Update README.md 2024-11-19 08:03:33 +03:00
Benedict Xavier
4fa1c45eb2 Update DISCLAIMER.md 2024-11-19 08:02:41 +03:00
Benedict Xavier
b9051bc792 Create DISCLAIMER.md 2024-11-19 08:01:23 +03:00
Benedict Xavier
a590024f1c Merge pull request #15 from piradata/patch-1
Update README.md typo
2024-11-19 07:42:10 +03:00
Benedict Xavier
2f51936679 Merge pull request #16 from AtticusHelvig/uvREADME
Update README.md for uv Instructions
2024-11-19 07:37:26 +03:00
Atticus Helvig
327c50d290 Update README.md 2024-11-18 20:32:38 -07:00
Piradata
031dfbb9b5 Update README.md typo 2024-11-19 00:15:47 -03:00
benex
050365302a chore: bump flake.nix 2024-11-18 20:10:21 +03:00
benex
0f248b1119 chore: update flake.nix to include external deps and all python deps 2024-11-18 20:04:57 +03:00
benex
871d5cf758 chore: add result to .gitignore 2024-11-18 20:04:22 +03:00
benex
320376d2e8 chore: bump version (v2.7.9) 2024-11-18 18:19:21 +03:00
benex
02e7fdff6f chore: update lock file 2024-11-18 18:17:03 +03:00
benex
2c5c28f295 refactor(config): remove useless functions 2024-11-18 18:17:03 +03:00
benex
2d3509ccc1 chore: bump version (v2.7.8) 2024-11-18 17:10:26 +03:00
benex
30babf2d69 feat(cli): alert the user that the cli is checking for updates 2024-11-18 17:10:10 +03:00
benex
cfbbabf898 chore: update uv.lock 2024-11-18 17:10:10 +03:00
benex
5ac6c45fdf fix(allanime_provider): give all the servers the referer header 2024-11-18 17:10:10 +03:00
Benedict Xavier
a14645b563 Update README.md 2024-11-18 15:17:32 +03:00
Benedict Xavier
90dbc26c46 Merge pull request #9 from KaylorBen/master
fix: for nix flake
2024-11-18 13:31:31 +03:00
Benjamin Kaylor
54cc830c35 sed flake preBuild 2024-11-18 03:23:32 -07:00
Benjamin Kaylor
4928ff5b74 undo pyproject.toml edit 2024-11-18 03:20:55 -07:00
Benjamin Kaylor
bb481fe21a fix for nix flake 2024-11-17 16:21:56 -07:00
benex
0d27b8f652 chore: bump version (v2.7.7) 2024-11-17 20:08:37 +03:00
benex
bdd3aae399 chore: update lockfile 2024-11-17 20:08:08 +03:00
benex
af94cd7eb5 fix: recent anime 2024-11-17 20:07:46 +03:00
benex
54044f9527 chore: bump version (v2.7.6) 2024-11-17 18:46:42 +03:00
benex
1e5c039ece chore: update flake.nix 2024-11-17 18:45:32 +03:00
benex
15555759dc feat: ask user if they want to update on new release 2024-11-17 18:45:17 +03:00
benex
0ed51e05cc chore: fix error is in flake.nix 2024-11-17 17:23:41 +03:00
benex
634ef6febf chore: update lock file 2024-11-17 14:28:02 +03:00
benex
bda4b2dbe1 chore: add flake.nix 2024-11-17 14:27:41 +03:00
benex
f015305e7c chore: add shell.nix 2024-11-17 14:27:32 +03:00
benex
d32b7e917f chore: bump version (v2.7.5) 2024-11-16 22:47:31 +03:00
benex
3b35e80199 chore: update lock file 2024-11-16 22:47:19 +03:00
benex
c65a1a2815 feat: by default check for updates when any command is ran 2024-11-16 22:47:04 +03:00
benex
0b3615c9f5 feat: add default rofi themes 2024-11-16 22:46:19 +03:00
benex
3ac4e1ac71 chore: bump version (v2.7.4) 2024-11-16 22:10:15 +03:00
benex
d62f580d7a docs: update readme 2024-11-16 22:09:41 +03:00
benex
02e35b66cb fix: implement another way to get timestamps from mpv due to issues on nixos 2024-11-16 22:09:41 +03:00
benex
7b11e0a301 feat: add rofi-theme-preview option 2024-11-16 22:09:41 +03:00
benex
aa8b91aed3 chore: remove unnecessary yt-dlp extras 2024-11-16 22:09:41 +03:00
benex
fe0fa97576 fix(player): workaround weird problem with mpv in nixos 2024-11-16 22:09:41 +03:00
Benedict Xavier
92059cd5ed Update README.md 2024-11-16 12:01:55 +03:00
benex
ed3064e3b1 feat(anime_provider): add yugen provider 2024-11-13 20:53:18 +03:00
Benex254
441d1e5e6c chore: bump version (v2.7.3) 2024-11-11 12:57:50 +03:00
Benex254
653b2cf4eb chore: add pyinstaller as dev dep 2024-11-11 12:57:29 +03:00
Benex254
8d4b71e0c8 feat: add entry point for pyinstaller executable 2024-11-11 12:57:29 +03:00
Benex254
29cc6cad09 build: pyinstaller spec 2024-11-11 12:57:28 +03:00
Benex254
8119eef263 docs(readme): update table of contents 2024-11-11 12:57:28 +03:00
Benex254
912c8674cf refactor(player): remove unnecessary orint statement 2024-11-11 12:57:28 +03:00
Benex254
6b3ca236dd fix: auto next episode 2024-11-11 12:57:28 +03:00
benex
f1c352d4ff chore: bump version (v2.7.2) 2024-11-10 12:29:46 +03:00
benex
714533d845 refactor(cli): update config options and set fastanime config environs 2024-11-10 12:06:44 +03:00
benex
56dd25df8d refactor(cli): update config options
This commit updates the configuration options in the CLI module. Specifically, it modifies the "image_previews" option to be platform-dependent, setting it to "True" for non-Windows platforms and "False" for Windows. Additionally, it sets the "normalize_titles" option to "True". These changes improve the behavior and user experience of the CLI.
2024-11-10 12:06:17 +03:00
benex
8248dc53df fix: text preview not showing on windows 2024-11-10 12:05:32 +03:00
Benex254
1a8a187de6 docs: update readme 2024-11-09 00:27:49 +03:00
Benex254
bc86be8c93 chore: bump version 2024-11-09 00:11:20 +03:00
Benex254
75026d4fc5 feat(api): add watch endpoint 2024-11-09 00:11:20 +03:00
Benedict Xavier
f8a5ccb8d2 Update README.md 2024-11-08 22:33:16 +03:00
Benex254
719d1bd187 chore: update deps 2024-11-08 16:26:43 +03:00
Benex254
0dd83463c6 docs: update readme 2024-10-20 11:28:39 +03:00
Benex254
966301bce8 feat: register fastanime anilist download(s) 2024-10-20 10:39:53 +03:00
Benex254
d776880306 feat: init fastanime anilist download(s) 2024-10-20 10:14:03 +03:00
Benex254
1ee50e8a55 chore: bump version (v2.6.9) 2024-10-20 10:06:02 +03:00
Benex254
ae95c5ea3d docs: update readme 2024-10-20 10:04:58 +03:00
Benex254
d64ad5e11d fix: move quality to stream section in config 2024-10-20 10:03:49 +03:00
Benex254
d1a47c6d44 chore: bump version (v2.6.8) 2024-10-18 22:59:18 +03:00
Benex254
51a834a62f chore: update deps 2024-10-18 22:53:39 +03:00
Benex254
3a030bf6f7 feat: add ability to update fastanime uv installations 2024-10-18 22:53:26 +03:00
Benex254
eb6a6fc82c chore: use uv in fa script 2024-10-18 22:46:44 +03:00
Benex254
437ccd94e4 ci: update to use uv 2024-10-18 22:37:14 +03:00
Benex254
d65868cc30 chore: update workflows to work with uv 2024-10-18 21:50:20 +03:00
Benex254
8678aa6544 Merge branch 'master' into uv 2024-10-18 20:26:55 +03:00
Benex254
00e5141152 chore: bump version (v2.6.7) 2024-10-12 01:08:14 +03:00
Benex254
90e757dfe1 feat: init switch to uv 2024-10-11 11:57:29 +03:00
Benex254
8b471b08e8 chore: init switch to uv 2024-10-11 10:52:18 +03:00
Benex254
158bc5710f docs: update readme 2024-10-11 10:49:53 +03:00
Benex254
a0b946a13d feat: add recent menu 2024-10-11 10:22:23 +03:00
Benex254
b547b75f03 feat: add environment variable that force updating of the cache db 2024-10-11 09:34:40 +03:00
Benex254
58c7427a47 feat(cli:serve): use the full executable path to python 2024-10-06 01:25:22 +03:00
Benex254
6220b9c55d chore: bump version (v2.6.6) 2024-10-06 01:15:15 +03:00
Benex254
6b9b5c131c fix(cli): use str instead of ints in serve 2024-10-06 01:15:05 +03:00
Benex254
212f2af39c chore: bump version (v2.6.5) 2024-10-06 01:05:28 +03:00
Benex254
f7b2b4e0c9 feat: add serve command 2024-10-06 01:04:20 +03:00
Benedict Xavier
a747529279 Update README.md 2024-10-05 19:37:19 +03:00
Benex254
1dfdcc27ce chore: bump version (v2.6.4) 2024-10-05 12:33:32 +03:00
Benex254
3c03289453 fix: add git push to make_release 2024-10-05 12:33:23 +03:00
Benex254
06fd446a72 chore: bump version (v2.6.3) 2024-10-05 12:29:29 +03:00
Benex254
172d912d8b chore(release): improve the make release script to also stage changes after bumping version 2024-10-05 12:29:15 +03:00
Benex254
2396018607 feat: make script to automate releases 2024-10-05 12:19:03 +03:00
Benex254
a9be9779c5 feat(fa): improve fa script 2024-10-05 12:14:45 +03:00
Benex254
2f76b26a99 feat(fzf): add some bindings 2024-10-05 11:54:22 +03:00
Benex254
2fe5edf810 feat(cli): make all threads daemon threads 2024-10-05 11:47:52 +03:00
Benex254
d67ee6a779 feat(downloader): add progress hook option to be passed to yt-dlp 2024-10-05 11:47:30 +03:00
Benex254
e06ec5dbd4 feat(cli): make the image previews optional 2024-10-05 11:31:13 +03:00
Benex254
c1b24ba2aa feat(cli): save images with .png extenstion to enable easier viewing by external apps 2024-10-05 11:05:07 +03:00
Benex254
59e9cf9fd0 feat: improve previews 2024-10-05 10:12:14 +03:00
Benex254
58761f5b96 chore: bump version 2024-10-04 19:44:54 +03:00
Benex254
ac959da229 feat: renable bg downloading function 2024-10-04 19:42:53 +03:00
benex
bacc8c48ec fix: image previews not showing up on windows 2024-10-04 11:03:54 +03:00
Benex254
905a159428 chore: add a mapping for re:zero s3 in normalizer 2024-10-03 15:09:51 +03:00
Benex254
20f734cab2 feat: also compare synonymns 2024-10-03 15:09:14 +03:00
Benex254
7c2c644aef chore: bump version 2024-10-03 14:18:22 +03:00
Benex254
0efc92081a feat: use .get in normlizer 2024-10-03 14:18:05 +03:00
Benex254
fafeee2367 chore: bump version 2024-10-03 12:48:41 +03:00
Benex254
e03063cd76 feat: let configuration of providers be managed by AnimeProvider wrapper 2024-10-03 12:34:34 +03:00
Benex254
93b38b055f docs: update readme 2024-10-03 12:33:52 +03:00
Benex254
045635fb55 feat: update config.py 2024-10-03 12:33:40 +03:00
Benex254
de7f773e9e feat: make the threads non-daemon 2024-10-03 11:47:27 +03:00
Benex254
ef6a465bd2 fix: typing issue 2024-10-02 22:19:02 +03:00
Benex254
0c623af8a4 chore: update poetry lockfile 2024-10-02 21:57:17 +03:00
Benex254
0589f83998 chore: bump version 2024-10-02 21:56:50 +03:00
Benex254
e17608afd5 feat: add provider store 2024-10-02 21:33:41 +03:00
Benex254
b915654685 feat: make the requests cache allow multiple connections by switching to wal 2024-10-02 16:49:36 +03:00
Benex254
2ce9bf6c47 feat: use a more meaningful name for the request caching files 2024-09-30 13:20:14 +03:00
Benex254
3c22232432 feat: add option to delete the db file 2024-09-30 13:19:33 +03:00
Benex254
3474e9520c tests: pass custom env 2024-09-29 22:09:57 +03:00
Benex254
e9bacf4f9c fix: extra atexit callbacks 2024-09-29 21:31:58 +03:00
Benex254
ef422ed6fd fix: inability to reload provider dynamically when using cached sessions 2024-09-29 21:19:15 +03:00
Benex254
d0f5366908 feat: allow access of fastanime config from environment variables 2024-09-29 21:00:41 +03:00
Benex254
3557205feb chore: cleanup requests cacher 2024-09-29 20:41:55 +03:00
Benex254
ba4c41d888 feat: implement usage of the requests cacher 2024-09-29 20:40:14 +03:00
Benex254
1427a3193c feat: implement requests cacher for fastanime 2024-09-29 20:39:27 +03:00
Benex254
b5cee20e56 fix: episodes range generated by mini anilist 2024-09-28 10:31:33 +03:00
Benex254
be7f464073 chore: update deps 2024-09-24 15:56:09 +03:00
Benex254
c7f8f168f5 docs: update readme 2024-09-24 15:55:59 +03:00
Benex254
ba59fbdcb0 chore: bump version 2024-09-24 15:55:47 +03:00
Benex254
9f54fa4998 feat: handle abscence of webtorrent-cli 2024-09-24 15:55:24 +03:00
Benex254
3c9688b32c feat: add nyaa as provider 2024-09-24 15:45:34 +03:00
Benex254
1f046447bb chore: update all instances of aniwatch to hianime 2024-09-24 10:02:49 +03:00
Benex254
87e3a275bb chore: bump version 2024-09-23 11:29:22 +03:00
Benex254
037b5c36a4 fix: normalize unknown_video to mp4 2024-09-23 11:04:31 +03:00
Benex254
7d8b60fb14 fix: change aniwatch to hianime in data.py 2024-09-23 11:04:06 +03:00
Benex254
0ad16fee53 fix: typing issue in player 2024-09-22 22:34:07 +03:00
Benex254
249243aeb4 chore: use --all-extras flag in poetry install 2024-09-22 22:31:35 +03:00
Benex254
c208dc3579 chore: bump version 2024-09-22 22:27:04 +03:00
Benex254
ea93f2ba23 chore: make some dependencies optional 2024-09-22 22:26:37 +03:00
Benex254
d910a0bb6a chore: update depenedencies 2024-09-22 22:25:52 +03:00
Benex254
550fcfeddc feat: make plyer an optional dependency 2024-09-22 22:13:12 +03:00
Benex254
c6910e5a1c feat: improve prompt text 2024-09-22 22:13:12 +03:00
Benex254
8555edb521 feat: dont pass obj to providers 2024-09-22 22:13:12 +03:00
Benex254
139193ce29 chore: remove aniwave as a provider; you shall forever live in our hearts 2024-09-22 22:13:12 +03:00
Benex254
1a87375ccd feat: add debug mode for providers 2024-09-22 22:13:12 +03:00
BeneX254
83cbef40f6 Update README.md 2024-09-21 18:06:09 +03:00
Benex254
85b4fc75a1 docs: update the readme 2024-09-20 17:58:29 +03:00
Benex254
f2e2da378f feat: improved medi list tracking 2024-09-20 17:58:06 +03:00
Benex254
7c34bc9120 feat: restrict some genres in mini_anilist 2024-09-19 19:12:10 +03:00
Benex254
6f153f2acb feat: immprove help messages for all cli commands 2024-09-19 19:11:15 +03:00
Benex254
8171083978 chore: update deps 2024-09-18 20:10:35 +03:00
Benex254
db5b9a59b4 fix: fastanime update not working with pip installs 2024-09-18 20:09:34 +03:00
Benex254
6fa656ba11 chore: bump version 2024-09-18 19:59:54 +03:00
Benex254
de0682c1bb fix: invalid cmd 2024-09-18 19:59:40 +03:00
Benex254
a6a32d8de4 chore: bump version 2024-09-18 19:44:10 +03:00
Benex254
bb14b269de feat: add --player option 2024-09-18 19:42:47 +03:00
Benex254
14331d8bc2 feat: workaround image previews on android 2024-09-18 19:42:47 +03:00
BeneX254
1729464844 Update README.md 2024-09-17 21:44:35 +03:00
benex
5fb9747285 fix: default ui not persisting when using config --update 2024-09-15 14:59:01 +03:00
benex
394228d391 chore: bump version 2024-09-15 14:42:54 +03:00
benex
5d3c0cc6ec fix: unicode error on windows when writing the config file 2024-09-15 14:38:50 +03:00
BeneX254
3ef7c5248c Update README.md 2024-09-15 13:46:50 +03:00
Benex254
8bebc401fd fix: rename use_mpv_mod to use_python_mpv in config 2024-09-15 13:40:20 +03:00
Benex254
215b28457b docs: update readme 2024-09-15 13:39:58 +03:00
Benex254
dfd2bfc857 docs: update readme 2024-09-15 13:29:32 +03:00
Benex254
f991292e94 chore: bump version 2024-09-15 13:29:20 +03:00
Benex254
d837457f80 feat: improve config file docs 2024-09-15 13:22:14 +03:00
Benex254
343bdba31b feat: add the --update option to the config command which causes all config options passed to fastanime to be persisted to your config file 2024-09-15 13:22:14 +03:00
benex
1c1c2457e8 feat: improve the preview with a workaround 2024-09-15 10:05:20 +03:00
benex
b083bfb074 fix: previews not working on windows 2024-09-15 09:36:15 +03:00
benex
ea1abcb2ae feat: dont use roaming folder for the config file 2024-09-15 08:54:02 +03:00
benex
001030ba2b fix: unicode error when running fzf on wndows 2024-09-15 08:53:22 +03:00
BeneX254
eda8984781 Update README.md 2024-09-13 21:57:15 +03:00
Benex254
d8dc6f0a34 chore: bump version 2024-09-10 19:15:43 +03:00
Benex254
2d711a7a7f docs: update readme 2024-09-10 19:15:25 +03:00
Benex254
30ca25626a feat: add --titles option to downloads 2024-09-10 19:11:52 +03:00
Benex254
b1f5a558c8 feat: improve animepahe utils 2024-09-10 19:11:19 +03:00
Benex254
8062c8dc83 feat: stat command ?? 2024-08-23 20:51:53 +03:00
Benex254
cb7eed46bc docs: update readme 2024-08-23 17:44:23 +03:00
Benex254
4626eca89e feat: improvements on media list intergration 2024-08-23 17:44:10 +03:00
Benex254
0d549c5915 docs: update readme 2024-08-23 17:18:49 +03:00
Benex254
33c518ed4c chore: cleanup codebase 2024-08-23 17:18:36 +03:00
Benex254
8e155dcc74 chore: bump version 2024-08-23 16:05:45 +03:00
Benex254
7743b0423e chore: clean up codebase 2024-08-23 16:05:26 +03:00
Benex254
6346ea7343 docs: update readme 2024-08-23 11:40:08 +03:00
Benex254
32de01047f chore:bump version 2024-08-23 11:39:57 +03:00
Benex254
35c7f81afb fix: no chapter title 2024-08-23 11:39:45 +03:00
Benex254
2dbbb1c4df feat: add experimental manga support 2024-08-23 11:19:25 +03:00
Benex254
6a6efa9d56 chore: bump version 2024-08-22 20:39:44 +03:00
Benex254
e510dc3a11 docs: update readme 2024-08-22 20:39:15 +03:00
Benex254
9639fd8c05 feat: improve normalizing of titles 2024-08-22 20:35:43 +03:00
Benex254
add35ce682 chore: bump version 2024-08-22 19:09:31 +03:00
Benex254
6bcc77ea44 fix: incorrect episode regex 2024-08-22 19:09:00 +03:00
Benex254
1a72f88be3 docs: updaate readme 2024-08-22 18:31:23 +03:00
Benex254
1a9f1120b8 chore: bump version 2024-08-22 18:31:11 +03:00
Benex254
c2fc807688 feat: episode preview 2024-08-22 18:25:41 +03:00
Benex254
2b0ade093c feat: normalize anime titles 2024-08-22 17:32:53 +03:00
BeneX254
a26193706e Update README.md 2024-08-22 13:37:18 +03:00
BeneX254
ff3c57ef9b Update README.md 2024-08-22 13:31:53 +03:00
BeneX254
3b987bd07a Update README.md 2024-08-22 12:43:58 +03:00
BeneX254
e8474c0428 Update README.md 2024-08-22 12:37:43 +03:00
BeneX254
c78a759aa1 Update README.md 2024-08-22 00:38:46 +03:00
Benex254
d1aad70c48 feat: add awesome completions to search command 2024-08-21 23:49:39 +03:00
Benex254
62b36f3e58 fix: workaround over typing issue 2024-08-21 23:20:45 +03:00
Benex254
c5b905fb0d chore: update deps 2024-08-21 23:18:12 +03:00
Benex254
7d3dc671ed fix: workaround typing issue 2024-08-21 23:07:01 +03:00
Benex254
0ec3c7a5bb docs: update docs 2024-08-21 22:53:30 +03:00
Benex254
8e0619863a feat: search command 2024-08-21 22:53:18 +03:00
Benex254
e8a05ec4b8 feat: add dump json to anilist commands 2024-08-21 20:48:01 +03:00
Benex254
34e8b2abd1 feat: update download command 2024-08-21 19:45:57 +03:00
Benex254
161b6eb961 chore: bump version 2024-08-21 19:41:35 +03:00
Benex254
dd2090f85d docs: update 2024-08-21 19:41:01 +03:00
Benex254
8b1595a5da feat:update 2024-08-21 19:40:45 +03:00
Benex254
77ffa27ed8 chore: bump version 2024-08-21 17:37:09 +03:00
Benex254
15f79b65c9 feat: aniwave?? 2024-08-21 17:18:30 +03:00
Benex254
33c3af0241 chore: remove print and input statements 2024-08-21 16:00:52 +03:00
Benex254
9badde62fb feat: improve providers 2024-08-21 15:58:01 +03:00
Benex254
4e401dca40 fix: logging issue 2024-08-21 14:53:30 +03:00
Benex254
25422b1b7d feat: improve aniwatch provider api 2024-08-21 14:52:56 +03:00
Benex254
e8463f13b4 chore: reconfigure pyright 2024-08-21 11:42:48 +03:00
Benex254
556f42e41f fix: clean option of download command 2024-08-21 11:41:55 +03:00
Benex254
b99a4f7efc chore: bump version 2024-08-19 23:44:05 +03:00
Benex254
f6f45cf322 docs: update readme 2024-08-19 23:43:50 +03:00
Benex254
ae6db1847a feat: improve download functionality 2024-08-19 23:43:34 +03:00
Benex254
20d04ea07b feat(utils): add m3u8 quality selector 2024-08-19 17:27:52 +03:00
Benex254
8f3834453c chore: bump version 2024-08-19 15:28:04 +03:00
Benex254
7ad8b8a0e3 fix: return values 2024-08-19 15:25:36 +03:00
Benex254
80b41f06da feat:add new ui command 2024-08-19 15:25:05 +03:00
Benex254
e79321ed50 chore: bump version 2024-08-19 13:05:03 +03:00
Benex254
f7b5898dfa fix: some stuff 2024-08-19 13:04:30 +03:00
Benex254
144bf53081 chore: bump version 2024-08-19 11:01:13 +03:00
Benex254
16dded9724 fix: inability to properly detect terminal 2024-08-19 10:51:39 +03:00
Benex254
c47b158bff fix: logging issue 2024-08-19 10:51:11 +03:00
Benex254
9a36e15d9d feat: intergrate subs to python-mpv based player 2024-08-19 10:37:04 +03:00
Benex254
d6b2bd7761 fix: ep title 2024-08-19 10:36:20 +03:00
Benex254
2346552dc4 fix: logging issue 2024-08-19 00:38:51 +03:00
Benex254
ba275055db fix: logging issue 2024-08-19 00:38:29 +03:00
Benex254
de4ddf2f3a chore: bump version 2024-08-19 00:21:48 +03:00
Benex254
9c94d824d1 fix: rearrange servers available 2024-08-19 00:21:16 +03:00
Benex254
495f3cfbf6 chore: bump version 2024-08-18 23:59:30 +03:00
Benex254
b56c9ae3dd docs: update reamde 2024-08-18 23:59:16 +03:00
Benex254
5e9ef87526 feat: improve provider api 2024-08-18 23:55:29 +03:00
Benex254
b68d6d6fe9 feat: accomodate subtitle streams 2024-08-18 23:54:59 +03:00
Benex254
5870cc6640 feat: accomodate subtitle streams 2024-08-18 23:54:36 +03:00
Benex254
7a43d58d82 fix: command order 2024-08-18 23:54:16 +03:00
Benex254
fc7efebc8d feat: accomodate subtitle streams 2024-08-18 23:53:36 +03:00
Benex254
528be74194 feat(aniwatch): init 2024-08-18 23:52:18 +03:00
Benex254
ab782acf2f chore: bump version 2024-08-18 15:47:44 +03:00
Benex254
45836d1ebc fix: handle no matches for search results 2024-08-18 15:47:29 +03:00
Benex254
dff059d8eb fix: workaround over typing issue 2024-08-18 15:32:13 +03:00
Benex254
4010cfc9c8 fix: correct update command 2024-08-18 15:29:54 +03:00
Benex254
6329730820 chore: bump version 2024-08-18 15:23:39 +03:00
Benex254
006592ae7d test: add grab command tests 2024-08-18 15:23:27 +03:00
Benex254
831dcf4e88 feat: fix python 3.10 incompatibility issue 2024-08-18 15:20:58 +03:00
Benex254
0d2cf7ed66 chore: bump version 2024-08-18 15:18:28 +03:00
Benex254
aa6dc2b98e docs: update readme 2024-08-18 15:18:12 +03:00
Benex254
2e5cde3365 feat(grab command): include more options for finer control 2024-08-18 15:09:56 +03:00
Benex254
d75a03e594 feat(animepahe): fix episode title 2024-08-18 15:09:24 +03:00
Benex254
9268c02683 docs: update readme 2024-08-18 13:49:20 +03:00
Benex254
89913036c9 chore: bump version 2024-08-18 13:49:05 +03:00
Benex254
2244026c67 feat(update command): improve update command 2024-08-18 13:05:27 +03:00
Benex254
c70564474b feat(download command): make the download never promt for user action while downloading episodes instead warn and sleep 2024-08-18 12:47:32 +03:00
Benex254
74514c9fbc feat(animepahe): fix title order 2024-08-18 12:46:46 +03:00
Benex254
077e9ab8c4 feat(grab command): include translation type in data 2024-08-18 12:37:39 +03:00
Benex254
b05f7f1640 feat(cli): add help for download and search command 2024-08-18 12:34:18 +03:00
Benex254
3382b720e3 feat(cli): add grab command 2024-08-18 12:33:51 +03:00
Benex254
f72c2d4b17 chore: bump version 2024-08-17 22:52:25 +03:00
Benex254
ff027991e0 chore: rename logfile 2024-08-17 22:52:11 +03:00
Benex254
21cdc6b015 docs: update readme 2024-08-17 22:50:04 +03:00
Benex254
29a2e3e6d1 feat(utils): include boundary in quality selector function 2024-08-17 22:46:44 +03:00
Benex254
5b3b9f740b feat(animepahe): remove use of node and implement custom logic to decode the string 2024-08-17 22:46:10 +03:00
Benex254
5bc0e52179 feat(download command): add headers functionality 2024-08-17 15:31:53 +03:00
Benex254
40f1c4fba5 chore: bump version 2024-08-17 15:25:26 +03:00
Benex254
454341eaf5 feat: enable use of http headers for providers 2024-08-17 15:17:53 +03:00
Benex254
abab2540a3 chore: update packages 2024-08-17 11:01:56 +03:00
Benex254
b2bc8cbace feat(download command): add more download options 2024-08-17 11:01:37 +03:00
Benex254
90bbf3c033 chore: bump version 2024-08-17 00:29:39 +03:00
Benex254
ac91b1770a feat(downloads command): use random episode for anime preview 2024-08-17 00:28:59 +03:00
Benex254
19d42b7924 feat(downloads command): add syncplay intergration 2024-08-16 23:37:37 +03:00
Benex254
9ec3136734 chore bump version 2024-08-16 23:02:24 +03:00
Benex254
943fca43cf docs: update readme 2024-08-16 23:02:24 +03:00
Benex254
b2e00feb94 feat(downloads command): sort by episode number 2024-08-16 23:02:24 +03:00
BeneX254
f726c8d55c Update README.md 2024-08-16 22:17:33 +03:00
Benex254
57db2e0626 chore: bump version 2024-08-16 22:09:56 +03:00
Benex254
40f66b5fde docs: update readme 2024-08-16 22:08:04 +03:00
Benex254
c87417e5e7 feat: add syncplay intergration 2024-08-16 22:03:22 +03:00
Benex254
a841dd6f66 chore: bump version 2024-08-16 20:04:57 +03:00
Benex254
d6e85bad5c docs: update readme 2024-08-16 20:04:45 +03:00
Benex254
b590ac1e91 feat(cli): improve download and search command 2024-08-16 19:49:40 +03:00
Benex254
9cfa3aeea5 feat(cli): use an option for providing anime title for search and download command 2024-08-16 19:45:00 +03:00
Benex254
18c60691ca feat(search command): improve binge power 2024-08-16 19:37:10 +03:00
Benex254
2e9fadf3b2 feat(download command): improve download command 2024-08-16 19:02:22 +03:00
Benex254
510b47b187 feat(downloads command): improve output by sorting the titles and episodes 2024-08-16 15:01:55 +03:00
Benex254
49c4d0eec0 docs: update readme 2024-08-16 14:57:15 +03:00
Benex254
8367f7bbed chore: bump version 2024-08-16 14:55:29 +03:00
Benex254
0182f674e0 feat: add status to graphql medialist query 2024-08-16 14:55:02 +03:00
Benex254
2b50fb4c97 fix(interface): improve error handling for non logged in user 2024-08-16 14:54:36 +03:00
Benex254
2602a20aa7 feat(login command): add option to erase login data 2024-08-16 14:53:57 +03:00
Benex254
13200e2d1f chore: bump version 2024-08-16 14:24:59 +03:00
Benex254
22f6e89400 fix:preferred server not reflecting in command 2024-08-16 14:24:42 +03:00
Benex254
8409fa7d43 chore: bump version 2024-08-16 13:51:57 +03:00
Benex254
c81da78190 chore: bump version 2024-08-16 13:51:36 +03:00
Benex254
e17ea4bb89 fix(interface): incorrect loading of episode during replat 2024-08-16 13:50:58 +03:00
Benex254
0087728aa8 docs: update readme 2024-08-16 13:22:35 +03:00
Benex254
9e48e02f7a feat(downloads command): improve local downloads experience 2024-08-16 13:12:10 +03:00
Benex254
1291d55ab0 feat(downloads command): add previews 2024-08-16 11:38:18 +03:00
Benex254
b5c6a1e39e feat: improve path handling on windows 2024-08-16 10:54:13 +03:00
Benex254
d6adb30802 feat(download command): remove unused option and improve help message 2024-08-16 10:47:25 +03:00
Benex254
1d08a69a85 feat(search command): improve help message 2024-08-16 10:46:37 +03:00
Benex254
1087ab3408 chore: add error checking todo 2024-08-16 10:46:05 +03:00
Benex254
51afd504df chore: update config obj 2024-08-16 10:45:40 +03:00
Benex254
75efc9d73a docs: update readme 2024-08-16 10:45:18 +03:00
Benex254
6b68086cff feat(interface): improve watch history experience 2024-08-16 10:10:47 +03:00
Benex254
3686cdfdb3 feat(completions): enhance speed of loading completion functions 2024-08-15 12:21:29 +03:00
Benex254
83c98936d1 chore: bump version 2024-08-15 11:33:22 +03:00
Benex254
0891cb279a docs: update readme 2024-08-15 11:32:21 +03:00
Benex254
95ba96f537 chore: remove plyer 2024-08-15 11:25:51 +03:00
Benex254
586790173b docs: update readme 2024-08-15 11:25:22 +03:00
Benex254
1d19449ab7 chore: bump version 2024-08-15 10:52:59 +03:00
Benex254
e1f73334ef feat(cli): remove unknown as possible quality 2024-08-15 10:50:32 +03:00
Benex254
4faac017b5 feat(utils): add 480 as possible quality 2024-08-15 10:49:29 +03:00
Benex254
bfbd2a57a0 feat(cli): add 480 as a possible quality 2024-08-15 10:48:39 +03:00
Benex254
9519472f83 feat(utils): pretty colors when defaulting to quality 2024-08-15 10:48:15 +03:00
Benex254
5c0c119cbc feat(download command): use actual episodes if downloading all 2024-08-15 10:47:35 +03:00
Benex254
87eb257a10 chore: use plyer.sttoragepath if possible 2024-08-15 10:29:59 +03:00
Benex254
4a08076c3b feat: use actual episodes list than inference 2024-08-15 00:20:06 +03:00
Benex254
0d239e6793 chore: bump version 2024-08-14 22:43:05 +03:00
Benex254
0a0d47ae88 chore: raise search results for anilist 2024-08-14 22:42:35 +03:00
Benex254
2ba07d47b3 feat(cli): add anime title completions 2024-08-14 22:32:47 +03:00
Benex254
f1b520fe3c chore: bump version 2024-08-14 21:22:40 +03:00
Benex254
8cfcc26468 feat(animepahe): use true episodes 2024-08-14 21:20:49 +03:00
Benex254
cd51edf0b8 feat(interface): better post error response 2024-08-14 21:10:32 +03:00
337 changed files with 36547 additions and 9607 deletions

6
.envrc Normal file
View File

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

View File

@@ -0,0 +1,30 @@
---
description: "Generate a new 'click' command following the project's lazy-loading pattern and service architecture."
tools: ['codebase']
---
# viu: CLI Command Generation Mode
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 `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: `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 `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: `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., `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 `viu/cli/service/` directory. The CLI command function should only handle argument parsing and calling the service.

View File

@@ -0,0 +1,34 @@
---
description: "Scaffold the necessary files and code for a new Player or Selector component, including configuration."
tools: ['codebase', 'search']
---
# viu: New Component Generation Mode
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.
---
### If the user chooses 'Player':
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 `viu/core/config/model.py`.
* Add the new config model to the main `AppConfig`.
* 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.
---
### If the user chooses 'Selector':
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 `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

@@ -0,0 +1,27 @@
---
description: "Scaffold and implement a new anime provider, following all architectural patterns of the viu project."
tools: ['codebase', 'search', 'fetch']
---
# viu: New Provider Generation Mode
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:
`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 `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:
* **`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 `viu/assets/normalizer.json` if the provider uses different anime titles than AniList.

73
.github/chatmodes/plan.chatmode.md vendored Normal file
View File

@@ -0,0 +1,73 @@
---
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"
---
# viu: Feature & Fix Planner Mode
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.
**Crucially, you MUST NOT write the full implementation code.** Your output is the plan itself, which will then guide the developer (or another AI agent in "Edit" mode) to write the code.
### Your Process:
1. **Understand the Goal:** Start by asking the user to describe the feature they want to build or the bug they want to fix. If they reference a GitHub issue, use the `githubRepo` tool to get the context.
2. **Analyze the Codebase:** Use the `codebase` and `search` tools to understand how the request fits into the existing architecture. Identify all potentially affected modules, classes, and layers.
3. **Ask Clarifying Questions:** Ask questions to refine the requirements. For example:
* "Will this feature need a new configuration option? If so, what should the default be?"
* "How should this behave in the interactive TUI versus the direct CLI command?"
* "Which architectural layer does the core logic for this fix belong in?"
4. **Generate the Implementation Plan:** Once you have enough information, produce a comprehensive plan in the following Markdown format:
---
### Implementation Plan: [Feature/Fix Name]
**1. Overview**
> A brief, one-sentence summary of the goal.
**2. Architectural Impact Analysis**
> This is the most important section. Detail which parts of the codebase will be touched and why.
> - **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 (`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 (`viu/cli/service`):**
> - Which service will orchestrate this logic? (e.g., `DownloadService`, `PlayerService`). Will a new service be needed?
> - **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?
**3. Implementation Steps**
> A step-by-step checklist for the developer.
> 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 `viu anilist search` command.
> 5. [ ] **Tests:** Write a unit test for `new_util()` and an integration test for the service method.
**4. Configuration Changes**
> If new settings are needed, list them here and specify which files to update.
> - **`core/config/model.py`:** Add field `new_setting: bool`.
> - **`core/config/defaults.py`:** Add `GENERAL_NEW_SETTING = False`.
> - **`core/config/descriptions.py`:** Add `GENERAL_NEW_SETTING = "Description of the new setting."`
**5. Testing Strategy**
> Briefly describe how to test this change.
> - A unit test for the pure logic in the `Core` or `Libs` layer.
> - An integration test for the `Service` layer.
> - Manual verification steps for the CLI and interactive UI.
**6. Potential Risks & Open Questions**
> - Will this change impact the performance of the provider scraping?
> - Do we need to handle a case where the external API does not support this feature?
---

101
.github/copilot-instructions.md vendored Normal file
View File

@@ -0,0 +1,101 @@
# GitHub Copilot Instructions for the viu Repository
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
`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 (`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 `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 (`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 (`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`.
* **`player`:** Wrappers around external media players like MPV. All new players **must** inherit from `BasePlayer`.
* **`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 (`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`).
* **`exceptions`:** Custom exception classes used throughout the project.
* **`utils`:** Common, low-level utility functions.
* **Guideline:** Code in `core` should be generic and have no dependencies on other layers except for other `core` modules.
## 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 `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.
## 4. How to Add New Features
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 `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 `viu/libs/provider/anime/types.py` (e.g., `SearchResult`, `Anime`, `Server`).
4. **Register Provider:**
* 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 `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 `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 `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 `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 (`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 `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
* **Style:** `ruff` for formatting, `ruff` for linting. The `pre-commit` hooks handle this.
* **Types:** Full type hinting is mandatory. All code must pass `pyright`.
* **Commits:** Adhere to the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) standard.
* **Logging:** Use Python's `logging` module. Do not use `print()` for debugging or informational messages in library or service code.
## 6. Do's and Don'ts
***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 `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.
***DON'T** put complex logic directly into `click` command functions. Delegate to a service.
***DON'T** make direct `httpx` calls outside of a `provider` or `media_api` library.
***DON'T** introduce new dependencies without updating `pyproject.toml` and discussing it first.

View File

@@ -1,38 +1,43 @@
name: debug_build
name: build
on:
workflow_run:
workflows: ["Test Workflow"]
types:
- completed
jobs:
debug_build:
build:
if: ${{ github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Python
- name: "Set up Python"
uses: actions/setup-python@v5
- name: Install poetry
uses: abatilo/actions-poetry@v2
- name: Setup a local virtual environment (if no poetry.toml file)
run: |
poetry config virtualenvs.create true --local
poetry config virtualenvs.in-project true --local
- uses: actions/cache@v3
name: Define a cache for the virtual environment based on the dependencies lock file
- name: Install uv
uses: astral-sh/setup-uv@v3
with:
path: ./.venv
key: venv-${{ hashFiles('poetry.lock') }}
- name: Install the project dependencies
run: poetry install
- name: build app
run: poetry build
enable-cache: true
- 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
!dist/*.whl
# - name: Run the automated tests (for example)
# run: poetry run pytest -v
- name: Install nix
uses: DeterminateSystems/nix-installer-action@main
- name: Use GitHub Action built-in cache
uses: DeterminateSystems/magic-nix-cache-action@main
- name: Nix Flake check (evaluation + tests)
run: nix flake check
- name: Build the nix derivation
run: nix build

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:
@@ -27,11 +18,13 @@ jobs:
with:
python-version: "3.10"
- name: Build release distributions
run: |
# NOTE: put your own distribution build steps here.
python -m pip install build
python -m build
- name: Install uv
uses: astral-sh/setup-uv@v3
with:
enable-cache: true
- name: Build viu
run: uv build
- name: Upload distributions
uses: actions/upload-artifact@v4

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

@@ -6,37 +6,42 @@ on:
pull_request:
branches:
- master
jobs:
test:
runs-on: ubuntu-latest
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
- name: Install Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install poetry
uses: abatilo/actions-poetry@v2
- name: Setup a local virtual environment (if no poetry.toml file)
- name: Install dbus-python build dependencies
run: |
poetry config virtualenvs.create true --local
poetry config virtualenvs.in-project true --local
- uses: actions/cache@v3
name: Define a cache for the virtual environment based on the dependencies lock file
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:
path: ./.venv
key: venv-${{ hashFiles('poetry.lock') }}
- name: Install the project dependencies
run: poetry install
- name: run linter, formatters and sort imports
run: |
poetry run black .
poetry run ruff check --output-format=github . --fix
poetry run isort . --profile black
- name: run type checking
run: poetry run pyright
- name: run tests
run: poetry run pytest
enable-cache: true
- name: Install the project
run: uv sync --all-extras --dev
- name: Run linter and formater
run: uv run ruff check --output-format=github
- name: Run type checking
run: uv run pyright
# TODO: write tests
# - name: Run tests
# run: uv run pytest tests

86
.gitignore vendored
View File

@@ -1,25 +1,16 @@
# mine
*.mp4
*.mp3
*.ass
vids
data/
.project/
fastanime.ini
crashdump.txt
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*.py[codz]
*$py.class
anixstream.ini
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
bin/
downloads/
eggs/
.eggs/
@@ -39,7 +30,7 @@ MANIFEST
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
# *.spec
*.spec
# Installer logs
pip-log.txt
@@ -55,7 +46,7 @@ htmlcov/
nosetests.xml
coverage.xml
*.cover
*.py,cover
*.py.cover
.hypothesis/
.pytest_cache/
cover/
@@ -103,23 +94,36 @@ ipython_config.py
# install all needed dependencies.
#Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
#poetry.toml
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
#pdm.toml
.pdm-python
.pdm-build/
# pixi
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
#pixi.lock
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
# in the .venv directory. It is recommended not to include this directory in version control.
.pixi
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
@@ -168,11 +172,41 @@ cython_debug/
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/
app/anixstream.ini
app/settings.json
app/user_data.json
app/View/SearchScreen/.search_screen.py.un~
app/View/SearchScreen/search_screen.py~
app/user_data.json
.buildozer
#.idea/
# Abstra
# Abstra is an AI-powered process automation framework.
# Ignore directories containing user credentials, local state, and settings.
# Learn more at https://abstra.io/docs
.abstra/
# Visual Studio Code
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
# and can be added to the global gitignore or merged into this file. However, if you prefer,
# you could uncomment the following to ignore the entire vscode folder
# .vscode/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc
# Cursor
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
# refer to https://docs.cursor.com/context/ignore-files
.cursorignore
.cursorindexingignore
# Marimo
marimo/_static/
marimo/_lsp/
__marimo__/
# custom
repomix-output.xml
.project/
result
.direnv

View File

@@ -1,35 +1,10 @@
default_language_version:
python: python3.10
repos:
- repo: https://github.com/pycqa/isort
rev: 5.12.0
hooks:
- id: isort
name: isort (python)
args: ["--profile", "black"] # Ensure compatibility with 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
# Ruff version.
rev: v0.4.10
rev: v0.14.2
hooks:
# Run the linter.
- id: ruff
- id: ruff-check
args: [--fix]
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 24.4.2
hooks:
- id: black
name: black
language_version: python3.10 # to ensure compatibilty
# 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/**/*

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"python.analysis.autoImportCompletions": true
}

208
CONTRIBUTIONS.md Normal file
View File

@@ -0,0 +1,208 @@
# Contributing to Viu
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 Viu project:
* **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 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/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 Viu repository.
3. **Clone Your Fork:**
```bash
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.
```bash
# For a new feature
git checkout -b feat/my-new-feature
# For a bug fix
git checkout -b fix/bug-description
```
5. **Make Your Changes:** Write your code, following the guidelines below.
6. **Run Quality Checks:** Before committing, ensure your code passes all quality checks.
```bash
# Format, lint, and sort imports
uv run ruff check --fix .
uv run ruff format .
# Run type checking
uv run pyright
# Run tests
uv run pytest
```
7. **Commit Your Changes:** We follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification. This helps us automate releases and makes the commit history more readable.
```bash
# Example commit messages
git commit -m "feat: add support for XYZ provider"
git commit -m "fix(anilist): correctly parse episode numbers with decimals"
git commit -m "docs: update installation instructions in README"
git commit -m "chore: upgrade httpx to version 0.28.1"
```
8. **Push to Your Fork:**
```bash
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 Viu repository. Provide a clear title and description of your changes.
## Setting Up Your Development Environment
### Prerequisites
* Git
* Python 3.10+
* [uv](https://github.com/astral-sh/uv) (recommended)
* **External Tools (for full functionality):** `mpv`, `fzf`, `rofi`, `webtorrent-cli`, `ffmpeg`.
### Nix / NixOS Users
The easiest way to get a development environment with all dependencies is to use our Nix flake.
```bash
nix develop
```
This command will drop you into a shell with all the necessary tools and a Python environment ready to go.
### Standard Setup (uv + venv)
1. **Clone your fork** (as described above).
2. **Create and activate a virtual environment:**
```bash
uv venv
source .venv/bin/activate
```
3. **Install all dependencies:** This command installs both runtime and development dependencies, including all optional extras.
```bash
uv sync --all-extras --dev
```
4. **Set up pre-commit hooks:** This will automatically run linters and formatters before each commit, ensuring your code meets our quality standards.
```bash
pre-commit install
```
## Coding Guidelines
To maintain code quality and consistency, please adhere to the following guidelines.
* **Formatting:** We use **Black** for code formatting and **isort** (via Ruff) for import sorting. The pre-commit hooks will handle this for you.
* **Linting:** We use **Ruff** for linting. Please ensure your code has no linting errors before submitting a PR.
* **Type Hinting:** All new code should be fully type-hinted and pass `pyright` checks. We rely on Pydantic for data validation and configuration, so leverage it where possible.
* **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 `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.
## How to Add a New Provider
Adding a new anime provider is a great way to contribute. Here are the steps:
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 `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 `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 `viu/assets/normalizer.json`.
## How to Add a New Player
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`.
* Implement the required abstract methods: `play(self, params: PlayerParams)` and `play_with_ipc(self, params: PlayerParams, socket_path: str)`. The IPC method is optional but recommended for advanced features.
* 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 `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 `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 `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`.
* Implement the abstract methods: `choose`, `confirm`, and `ask`.
* Optionally, you can override `choose_multiple` and `search` for more advanced functionality.
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 `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` (`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., `viu my-command`)
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 `viu/cli/cli.py`, add your command to the `commands` dictionary.
```python
commands = {
# ... existing commands
"my-command": "my_command.my_command_function",
}
```
### 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, `viu/cli/commands/anilist/commands/my_subcommand.py`.
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,
# ... other options
lazy_subcommands={
# ... existing subcommands
"my-subcommand": "my_subcommand.my_subcommand_function",
}
)
```
### Creating a Service
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 Viu

38
DISCLAIMER.md Normal file
View File

@@ -0,0 +1,38 @@
<h1 align="center">Disclaimer</h1>
<div align="center">
<h2>This project: viu</h2>
<br>
The core aim of this project is to co-relate automation and efficiency to extract what is provided to a user on the internet. All content available through the project is hosted by external non-affiliated sources.
<br>
<b>All content served through this project is publicly accessible. If your site is listed in this project, the code is pretty much public. Take necessary measures to counter the exploits used to extract content in your site.</b>
Think of this project as your normal browser, but a bit more straight-forward and specific. While an average browser makes hundreds of requests to get everything from a site, this project goes on to only make requests associated with getting the content served by the sites.
<b>
This project is to be used at the user's own risk, based on their government and laws.
This project has no control on the content it is serving, using copyrighted content from the providers is not going to be accounted for by the developer. It is the user's own risk.
</b>
<br>
<h2>DMCA and Copyright Infrigements</h3>
<br>
<b>
A browser is a tool, and the maliciousness of the tool is directly based on the user.
</b>
This project uses client-side content access mechanisms. Hence, the copyright infrigements or DMCA in this project's regards are to be forwarded to the associated site by the associated notifier of any such claims. This is one of the main reasons the sites are listed in this project.
<b>Do not harass the developer. Any personal information about the developer is intentionally not made public. Exploiting such information without consent in regards to this topic will lead to legal actions by the developer themselves.</b>

View File

@@ -1,10 +0,0 @@
FROM ubuntu
RUN apt-get update
RUN apt-get -y install python3
RUN apt-get update
RUN apt-get -y install pipx
RUN pipx ensurepath
COPY . /fastanime
WORKDIR /fastanime
RUN pipx install .
CMD ["bash"]

745
README.md
View File

@@ -1,515 +1,334 @@
# FastAnime
<p align="center">
<h1 align="center">Viu</h1>
</p>
<p align="center">
<sup>
Your browser anime experience, from the terminal.
</sup>
</p>
<div align="center">
Welcome to **FastAnime**, anime site experience from the terminal.
[![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/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)
**fzf mode**
</div>
[fa_fzf_demo.webm](https://github.com/user-attachments/assets/b1fecf25-e358-4e8b-a144-bcb7947210cf)
<p align="center">
<a href="https://discord.gg/HBEmAwvbHV">
<img src="https://invidget.switchblade.xyz/C4rhMA4mmK" alt="Discord Server Invite">
</a>
</p>
**other modes:**
[viu-showcase.webm](https://github.com/user-attachments/assets/5da0ec87-7780-4310-9ca2-33fae7cadd5f)
<details>
<summary><b>rofi mode</b></summary>
[fa_rofi_mode.webm](https://github.com/user-attachments/assets/2ce669bf-b62f-4c44-bd79-cf0dcaddf37a)
</details>
<details>
<summary><b>Default mode</b></summary>
<summary>Rofi</summary>
[fa_default_mode.webm](https://github.com/user-attachments/assets/1ce3a23d-f4a0-4bc1-8518-426ec7b3b69e)
[viu-showcase-rofi.webm](https://github.com/user-attachments/assets/01f197d9-5ac9-45e6-a00b-8e8cd5ab459c)
</details>
Heavily inspired by [animdl](https://github.com/justfoolingaround/animdl), [magic-tape](https://gitlab.com/christosangel/magic-tape/-/tree/main?ref_type=heads) and [ani-cli](https://github.com/pystardust/ani-cli).
<!--toc:start-->
- [FastAnime](#fastanime)
- [Installation](#installation)
- [Installation using your favourite package manager](#installation-using-your-favourite-package-manager)
- [Using pipx](#using-pipx)
- [Using pip](#using-pip)
- [Installing the bleeding edge version](#installing-the-bleeding-edge-version)
- [Building from the source](#building-from-the-source)
- [External Dependencies](#external-dependencies)
- [Usage](#usage)
- [The Commandline interface :fire:](#the-commandline-interface-fire)
- [The anilist command :fire: :fire: :fire:](#the-anilist-command-fire-fire-fire)
- [Running without any subcommand](#running-without-any-subcommand)
- [Subcommands](#subcommands)
- [download subcommand](#download-subcommand)
- [search subcommand](#search-subcommand)
- [downloads subcommand](#downloads-subcommand)
- [config subcommand](#config-subcommand)
- [cache subcommand](#cache-subcommand)
- [update subcommand](#update-subcommand)
- [completions subcommand](#completions-subcommand)
- [MPV specific commands](#mpv-specific-commands)
- [Added keybindings](#added-keybindings)
- [Added script messages](#added-script-messages)
- [Configuration](#configuration)
- [Contributing](#contributing)
- [Receiving Support](#receiving-support)
- [Supporting the Project](#supporting-the-project)
<!--toc:end-->
> [!IMPORTANT]
>
> This project currently scrapes allanime and animepahe and is in no way related to them nor does the project own any content servers. The site is in the public domain and can be access by any one with a browser.
> This project scrapes public-facing websites for its streaming / downloading capabilities and primarily acts as an anilist, jikan and many other media apis tui client. The developer(s) of this application have no affiliation with these content providers. This application hosts zero content and is intended for educational and personal use only. Use at your own risk.
>
> [**Read the Full Disclaimer**](DISCLAIMER.md)
## Core Features
* 📺 **Interactive TUI:** Browse, search, and manage your AniList library in a rich terminal interface powered by `fzf`, `rofi`, or a built-in selector.
***Powerful Search:** Filter the entire AniList database with over 20 different criteria, including genres, tags, year, status, and score.
* 💾 **Local Registry:** Maintain a fast, local database of your anime for offline access, detailed stats, and robust data management.
* ⚙️ **Background Downloader:** Queue episodes for download and let a persistent background worker handle the rest.
* 📜 **Scriptable CLI:** Automate streaming and downloading with powerful, non-interactive commands perfect for scripting.
* 🔧 **Highly Customizable:** Tailor every aspect—from UI colors and providers to playback behavior—via a simple, well-documented configuration file.
* 🔌 **Extensible Architecture:** Easily add new providers, media players, and UI selectors to fit your workflow.
## Installation
The app can run wherever python can run. So all you need to have is python installed on your device.
On android you can use [termux](https://github.com/termux/termux-app).
If you have any difficulty consult for help on the [discord channel](https://discord.gg/HRjySFjQ)
Viu runs on any platform with Python 3.10+, including Windows, macOS, Linux, and Android (via Termux).
### Installation using your favourite package manager
### Prerequisites
Currently the app is only published on [pypi](https://pypi.org/project/fastanime/).
For the best experience, please install these external tools:
#### Using pipx
* **Required for Streaming:**
* [**mpv**](https://mpv.io/installation/) - The primary and recommended media player.
* **Recommended for UI & Previews:**
* [**fzf**](https://github.com/junegunn/fzf) - For the best fuzzy-finder interface.
* [**chafa**](https://github.com/hpjansson/chafa) or [**kitty's icat**](https://sw.kovidgoyal.net/kitty/kittens/icat/) - For image previews in the terminal.
* **Recommended for Downloads & Advanced Features:**
* [**ffmpeg**](https://www.ffmpeg.org/) - Required for downloading HLS streams and merging subtitles.
* [**webtorrent-cli**](https://github.com/webtorrent/webtorrent-cli) - For streaming torrents directly.
Preferred method of installation since [Pipx](https://github.com/pypa/pipx) creates an isolated environment for each app it installs.
### Recommended Installation (uv)
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 "viu-media[standard]"
pipx install fastanime
# -- or for the development version --
pipx install 'fastanime==<latest-pre-release-tag>.dev1'
# example
# pipx install 'fastanime==0.60.1.dev1'
# Or, pick and choose the extras you need:
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
```
#### Using pip
### Other Installation Methods
<details>
<summary><b>Platform-Specific and Alternative Installers</b></summary>
#### Nix / NixOS
##### Ephemeral / One-Off Run (No Installation)
```bash
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 viu-media
# Git version (latest commit)
yay -S viu-media-git
```
#### Using pipx (for isolated environments)
```bash
pipx install "viu-media[standard]"
```
#### Using pip
```bash
pip install "viu-media[standard]"
```
</details>
<details>
<summary><b>Building from Source</b></summary>
Requires [Git](https://git-scm.com/), [Python 3.10+](https://www.python.org/), and [uv](https://astral.sh/blog/uv).
```bash
git clone https://github.com/viu-media/Viu.git --depth 1
cd Viu
uv tool install .
viu --version
```
</details>
> [!TIP]
> 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
Get up and running in three simple steps:
1. **Authenticate with AniList:**
```bash
viu anilist auth
```
This will open your browser. Authorize the app and paste the obtained token back into the terminal.
2. **Launch the Interactive TUI:**
```bash
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 (`viu anilist`)
This is the main, user-friendly way to use Viu. It provides a rich terminal experience where you can:
* Browse trending, popular, and seasonal anime.
* Manage your personal lists (Watching, Completed, 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 (`viu anilist search`)
Filter the entire AniList database with powerful command-line flags.
```bash
pip install fastanime
# -- or for the development version --
pip install 'fastanime==<latest-pre-release-tag>.dev1'
# example
# pip install 'fastanime==0.60.1.dev1'
# Search for anime from 2024, sorted by popularity, that is releasing and not on your list
viu anilist search -y 2024 -s POPULARITY_DESC --status RELEASING --not-on-list
# Find the most popular movies with the "Fantasy" genre
viu anilist search -g Fantasy -f MOVIE -s POPULARITY_DESC
# Dump search results as JSON instead of launching the TUI
viu anilist search -t "Demon Slayer" --dump-json
```
### Installing the bleeding edge version
### Background Downloads (`viu queue` & `worker`)
To install the latest build which are created on every push by GitHub actions, download the [fastanime_debug_build](https://github.com/Benex254/FastAnime/actions) of your choosing from the GitHub actions page.
Then:
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
viu queue add -t "Jujutsu Kaisen" -r "0:12"
```
2. **Start the worker process:**
```bash
# Run the worker in the foreground (press Ctrl+C to stop)
viu worker
# Or run it as a background process
viu worker &
```The worker will now process the queue, download your episodes, and check for notifications.
### Scriptable Commands (`download` & `search`)
These commands are designed for automation and quick, non-interactive tasks.
#### `download` Examples
```bash
unzip fastanime_debug_build
# Download the latest 5 episodes of One Piece
viu download -t "One Piece" -r "-5"
# outputs fastanime<version>.tar.gz
pipx install fastanime<version>.tar.gz
# --- or ---
pip install fastanime<version>.tar.gz
# Download episodes 1 to 24, merge subtitles, and clean up original files
viu download -t "Jujutsu Kaisen" -r "0:24" --merge --clean
```
### Building from the source
Requirements:
- [git](https://git-scm.com/)
- [python 3.10 and above](https://www.python.org/)
- [poetry](https://python-poetry.org/docs/#installation)
To build from the source, follow these steps:
1. Clone the repository: `git clone https://github.com/Benex254/FastAnime.git`
2. Navigate into the folder: `cd FastAnime`
3. Then build and Install the app:
#### `search` (Binging) Examples
```bash
# Normal Installation
poetry build
cd dist
pip install fastanime<version>.whl
# Start binging an anime from the first episode
viu search -t "Attack on Titan" -r ":"
# Editable installation (easiest for updates)
# just do a git pull in the Project dir
# the latter will require rebuilding the app
pip install -e .
# Watch the latest episode directly
viu search -t "My Hero Academia" -r "-1"
```
4. Enjoy! Verify installation with:
### Local Data Management (`viu registry`)
```bash
fastanime --version
```
Viu maintains a local database of your anime for offline access and enhanced performance.
> [!Tip]
>
> Download the completions from [here](https://github.com/Benex254/FastAnime/tree/master/completions) for your shell.
> To add completions:
>
> - Fish Users: `cp $FASTANIME_PATH/completions/fastanime.fish ~/.config/fish/completions/`
> - Bash Users: Add `source $FASTANIME_PATH/completions/fastanime.bash` to your `.bashrc`
> - Zsh Users: Add `source $FASTANIME_PATH/completions/fastanime.zsh` to your `.zshrc`
### External Dependencies
The only required external dependency, unless you won't be streaming, is [MPV](https://mpv.io/installation/), which i recommend installing with [uosc](https://github.com/tomasklaen/uosc) :fire: and [thumbfast](https://github.com/po5/thumbfast) for the best experience since they add a better interface to it.
> [!NOTE]
>
> The project currently sees no reason to support any other video
> player because we believe nothing beats **MPV** and it provides
> everything you could ever need with a small footprint.
> But if you have a reason feel free to encourage as to do so.
**Other external dependencies that will just make your experience better:**
- [fzf](https://github.com/junegunn/fzf) 🔥 which is used as a better alternative to the ui.
- [rofi](https://github.com/davatorium/rofi) 🔥 which is used as another alternative ui + the the desktop entry ui
- [chafa](https://github.com/hpjansson/chafa) currently the best cross platform and cross terminal image viewer for the terminal.
- [icat](https://sw.kovidgoyal.net/kitty/kittens/icat/) an image viewer that only works in [kitty terminal](https://sw.kovidgoyal.net/kitty/), which is currently the best terminal in my opinion, and by far the best image renderer for the terminal thanks to kitty's terminal graphics protocol. Its terminal graphics is so op that you can [run a browser on it](https://github.com/chase/awrit?tab=readme-ov-file)!!
- [bash](https://www.gnu.org/software/bash/) is used as the preview script language.
- [ani-skip](https://github.com/synacktraa/ani-skip) used for skipping the opening and ending theme songs
## Usage
The app offers both a graphical interface (under development) and a robust command-line interface.
> [!NOTE]
>
> The GUI is mostly in hiatus; use the CLI for now.
> However, you can try it out before i decided to change my objective by checking out this [release](https://github.com/Benex254/FastAnime/tree/v0.20.0).
> But be reassured for those who aren't terminal chads, i will still complete the GUI for the fun of it
### The Commandline interface :fire:
Designed for power users who prefer efficiency over browser-based streaming and still want the experience in their terminal.
Overview of main commands:
- `fastanime anilist`: Powerful command for browsing and exploring anime due to AniList integration.
- `fastanime download`: Download anime.
- `fastanime search`: Powerful command meant for binging since it doesn't require the interfaces
- `fastanime downloads`: View downloaded anime and watch with MPV.
- `fastanime config`: Quickly edit configuration settings.
- `fastanime cache`: Quickly manage the cache fastanime uses
Configuration is directly passed into this command at run time to override your config.
Available options include:
- `--server;-s <server>` set the default server to auto select
- `--continue;-c/--no-continue;-no-c` whether to continue from the last episode you were watching
- `--quality;-q <0|1|2|3>` the link to choose from server
- `--translation-type;- <dub|sub` what language for anime
- `--auto-select;-a/--no-auto-select;-no-a` auto select title from provider results
- `--auto-next;-A;/--no-auto-next;-no-A` auto select next episode
- `-downloads-dir;-d <path>` set the folder to download anime into
- `--fzf` use fzf for the ui
- `--default` use the default ui
- `--preview` show a preview when using fzf
- `--no-preview` dont show a preview when using fzf
- `--format <yt-dlp format string>` set the format of anime downloaded and streamed based on yt-dlp format. Works when `--server gogoanime`
- `--icons/--no-icons` toggle the visibility of the icons
- `--skip/--no-skip` whether to skip the opening and ending theme songs.
- `--rofi` use rofi for the ui
- `--rofi-theme <path>` theme to use with rofi
- `--rofi-theme-input <path>` theme to use with rofi input
- `--rofi-theme-confirm <path>` theme to use with rofi confirm
- `--log` allow logging to stdout
- `--log-file` allow logging to a file
- `--rich-traceback` allow rich traceback
- `--use-mpv-mod/--use-default-player` whether to use python-mpv
#### The anilist command :fire: :fire: :fire:
Stream, browse, and discover anime efficiently from the terminal using the [AniList API](https://github.com/AniList/ApiV2-GraphQL-Docs).
##### Running without any subcommand
Run `fastanime anilist` to access the main interface.
##### Subcommands
The subcommands are mainly their as convenience. Since all the features already exist in the main interface.
- `fastanime anilist trending`: Top 15 trending anime.
- `fastanime anilist recent`: Top 15 recently updated anime.
- `fastanime anilist search`: Search for anime (top 50 results).
- `fastanime anilist upcoming`: Top 15 upcoming anime.
- `fastanime anilist popular`: Top 15 popular anime.
- `fastanime anilist favourites`: Top 15 favorite anime.
- `fastanime anilist random`: get random anime
The following are commands you can only run if you are signed in to your AniList account:
- `fastanime anilist watching`
- `fastanime anilist planning`
- `fastanime anilist rewatching`
- `fastanime anilist dropped`
- `fastanime anilist paused`
- `fastanime anilist completed`
Plus: `fastanime anilist notifier` :fire:
```bash
# basic form
fastanime anilist notifier
# with logging to stdout
fastanime --log anilist notifier
# with logging to a file. stored in the same place as your config
fastanime --log-file anilist notifier
```
The above commands will start a loop that checks every 2 minutes if any of the anime in your watch list that are aireing has just released a new episode.
The notification will consist of a cover image of the anime in none windows systems.
You can place the command among your machines startup scripts.
For fish users for example you can decide to put this in your `~/.config/fish/config.fish`:
```fish
if ! ps aux | grep -q '[f]astanime .* notifier'
echo initializing fastanime anilist notifier
nohup fastanime --log-file anilist notifier>/dev/null &
end
```
> [!NOTE]
> To sign in just run `fastanime anilist login` and follow the instructions.
> To view your login status `fastanime anilist login --status`
#### download subcommand
Download anime to watch later dub or sub with this one command.
Its optimized for scripting due to fuzzy matching.
So every step of the way has been and can be automated.
> [!NOTE]
>
> The download feature is powered by [yt-dlp](https://github.com/yt-dlp/yt-dlp) so all the bells and whistles that it provides are readily available in the project.
> Like continuing from where you left of while downloading, after lets say you lost your internet connection.
**Syntax:**
```bash
# Download all available episodes
fastanime download <anime-title>
# Download specific episode range
# be sure to observe the range Syntax
fastanime download <anime-title> -r <episodes-start>-<episodes-end>
```
#### search subcommand
Powerful command mainly aimed at binging anime. Since it doesn't require interaction with the interfaces.
**Syntax:**
```bash
# basic form where you will still be prompted for the episode number
fastanime search <anime-title>
# binge all episodes with this command
fastanime search <anime-title> -r -
# binge a specific episode range with this command
# be sure to observe the range Syntax
fastanime search <anime-title> -r <episodes-start>-<episodes-end>
```
#### downloads subcommand
View and stream the anime you downloaded using MPV.
**Syntax:**
```bash
fastanime downloads
# to get the path to the downloads folder set
fastanime downloads --path
# useful when you want to use the value for other programs
```
#### config subcommand
Edit FastAnime configuration settings using your preferred editor (based on `$EDITOR` environment variable so be sure to set it).
**Syntax:**
```bash
fastanime config
# to get config path which is useful if you want to use it for another program.
fastanime config --path
# add a desktop entry
fastanime config --desktop-entry
# view current contents of your configuration or can be used to get an example config
fastanime config --view
```
> [!Note]
>
> If it opens [vim](https://www.vim.org/download.php) you can exit by typing `:q` .
#### cache subcommand
Easily manage the data fastanime has cached; for the previews.
**Syntax:**
```bash
# delete everything in the cache dir
fastanime cache --clean
# print the path to the cache dir and exit
fastanime cache --path
# print the current size of the cache dir and exit
fastanime cache --size
# open the cache dir and exit
fastanime cache
```
#### update subcommand
Easily update fastanime to latest
**Syntax:**
```bash
# update fastanime to latest
fastanime update
# check for latest release
fastanime update --check
```
#### completions subcommand
Helper command to setup shell completions
**Syntax:**
```bash
# try to detect your shell and print completions
fastanime completions
# print fish completions
fastanime completions --fish
# print bash completions
fastanime completions --bash
# print zsh completions
fastanime completions --zsh
```
## MPV specific commands
The project now allows on the fly media controls directly from mpv. This means you can go to the next or previous episode without the window ever closing thus offering a seamless experience.
This is all powered with [python-mpv]() which enables writing mpv scripts with python just like how it would be done in lua.
### Added keybindings
`<shift>+n` fetch the next episode
`<shift>+p` fetch the previous episode
`<shift>+t` toggle the translation type from dub to sub
`<shift>+a` toggle auto next episode
`<shit>+r` reload episode
### Added script messages
Examples:
```bash
# to select episode from mpv without window closing
script-message select-episode <episode-number>
# to select server from mpv without window closing
script-message select-server <server-name>
```
* `registry sync`: Synchronize your local data with your remote AniList account.
* `registry stats`: Show detailed statistics about your viewing habits.
* `registry backup`: Create a compressed backup of your entire registry.
* `registry restore`: Restore your data from a backup file.
* `registry export/import`: Export/import your data to JSON/CSV for use in other applications.
* `registry clean`: Clean up orphaned or invalid entries from your local database.
## Configuration
The app includes sensible defaults but can be customized extensively. Configuration is stored in `.ini` format at `~/.config/FastAnime/config.ini` on Linux and mac or somewhere on windows; you can check by running `fastanime config --path`.
Viu is highly customizable. A default configuration file with detailed comments is created on the first run.
* **Find your config file:** `viu config --path`
* **Edit in your default editor:** `viu config`
* **Use the interactive wizard:** `viu config --interactive`
Most settings in the config file can be temporarily overridden with command-line flags (e.g., `viu --provider animepahe anilist`).
<details>
<summary><b>Default Configuration (`config.ini`) Explained</b></summary>
```ini
[stream]
continue_from_history = True # Auto continue from watch history
translation_type = sub # Preferred language for anime (options: dub, sub)
server = top # Default server (options: dropbox, sharepoint, wetransfer.gogoanime, top, wixmp)
auto_next = False # Auto-select next episode
# Auto select the anime provider results with fuzzy find.
# Note this wont always be correct.But 99% of the time will be.
auto_select=True
# whether to skip the opening and ending theme songs
# note requires ani-skip to be in path
skip=false
# the maximum delta time in minutes after which the episode should be considered as completed
# used in the continue from time stamp
error=3
use_mpv_mod=False
# the format of downloaded anime and trailer
# based on yt-dlp format and passed directly to it
# learn more by looking it up on their site
# only works for downloaded anime if server=gogoanime
# since its the only one that offers different formats
# the others tend not to
format=best[height<=1080]/bestvideo[height<=1080]+bestaudio/best # default
# [general] Section: Controls overall application behavior.
[general]
# can be [allanime,animepahe]
provider = allanime
provider = allanime ; The default anime provider (allanime, animepahe).
selector = fzf ; The interactive UI tool (fzf, rofi, default).
preview = full ; Preview type in selectors (full, text, image, none).
image_renderer = icat ; Tool for terminal image previews (icat, chafa).
icons = True ; Display emoji icons in the UI.
auto_select_anime_result = True ; Automatically select the best search match.
...
preferred_language = romaji # Display language (options: english, romaji)
downloads_dir = <Default-videos-dir>/FastAnime # Download directory
preview=false # whether to show a preview window when using fzf or rofi
# [stream] Section: Controls playback and streaming.
[stream]
player = mpv ; The media player to use (mpv, vlc).
quality = 1080 ; Preferred stream quality (1080, 720, 480, 360).
translation_type = sub ; Preferred audio/subtitle type (sub, dub).
auto_next = False ; Automatically play the next episode.
continue_from_watch_history = True ; Resume playback from where you left off.
use_ipc = True ; Enable in-player controls via MPV's IPC.
...
use_fzf=False # whether to use fzf as the interface for the anilist command and others.
# [downloads] Section: Controls the downloader.
[downloads]
downloader = auto ; Downloader to use (auto, default, yt-dlp).
downloads_dir = ... ; Directory to save downloaded anime.
max_concurrent_downloads = 3 ; Number of parallel downloads in the worker.
merge_subtitles = True ; Automatically merge subtitles into the video file.
cleanup_after_merge = True ; Delete original files after merging.
...
use_rofi=false # whether to use rofi for the ui
rofi_theme=<path-to-rofi-theme-file>
rofi_theme_input=<path-to-rofi-theme-file>
rofi_theme_confirm=<path-to-rofi-theme-file>
# whether to show the icons
icons=false
# the duration in minutes a notification will stay in the screen
# used by notifier command
notification_duration=2
[anilist]
# Not implemented yet
# [worker] Section: Controls the background worker process.
[worker]
enabled = True
notification_check_interval = 15 ; How often to check for new episodes (minutes).
download_check_interval = 5 ; How often to process the download queue (minutes).
...
```
</details>
## Advanced Features
### MPV IPC Integration
When `use_ipc = 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.
* `Shift+P`: Play the previous episode.
* `Shift+R`: Reload the current episode.
* `Shift+A`: Toggle auto-play for the next episode.
* `Shift+T`: Toggle between `dub` and `sub`.
**Script Messages (For MPV Console):**
* `script-message select-episode <number>`: Jump to a specific episode.
* `script-message select-server <name>`: Switch to a different streaming server.
### Running as a Service (Linux/systemd)
You can run the background worker as a systemd service for persistence.
1. Create a service file at `~/.config/systemd/user/viu-worker.service`:
```ini
[Unit]
Description=Viu Background Worker
After=network-online.target
[Service]
Type=simple
ExecStart=/path/to/your/viu worker --log
Restart=always
RestartSec=30
[Install]
WantedBy=default.target
```
*Replace `/path/to/your/viu` with the output of `which viu`.*
2. Enable and start the service:
```bash
systemctl --user daemon-reload
systemctl --user enable --now viu-worker.service
```
## Contributing
We welcome your issues and feature requests. However, due to time constraints, we currently do not plan to add another provider.
If you wish to contribute directly, please first open an issue describing your proposed changes so it can be discussed or if you are in a rush for the feature to be merged just open a pr.
## Receiving Support
For inquiries, join our [Discord Server](https://discord.gg/4NUTj5Pt).
<p align="center">
<a href="https://discord.gg/C4rhMA4mmK">
<img src="https://invidget.switchblade.xyz/C4rhMA4mmK">
</a>
</p>
## Supporting the Project
Show your support by starring our GitHub repository or [buying us a coffee](https://ko-fi.com/benex254).
Contributions are welcome! Whether it's reporting a bug, proposing a feature, or writing code, your help is appreciated. Please read our [**Contributing Guidelines**](CONTRIBUTIONS.md) to get started.

7
bundle/Dockerfile Normal file
View File

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

65
bundle/pyinstaller.spec Normal file
View File

@@ -0,0 +1,65 @@
# -*- mode: python ; coding: utf-8 -*-
from PyInstaller.utils.hooks import collect_data_files, collect_submodules
block_cipher = None
# Collect all required data files
datas = [
('viu/assets/*', 'viu/assets'),
]
# Collect all required hidden imports
hiddenimports = [
'click',
'rich',
'requests',
'yt_dlp',
'python_mpv',
'fuzzywuzzy',
'viu',
] + collect_submodules('viu')
a = Analysis(
['./viu/viu.py'], # Changed entry point
pathex=[],
binaries=[],
datas=datas,
hiddenimports=hiddenimports,
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
strip=True, # Strip debug information
optimize=2 # Optimize bytecode noarchive=False
)
pyz = PYZ(
a.pure,
a.zipped_data,
optimize=2 # Optimize bytecode cipher=block_cipher
)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='viu',
debug=False,
bootloader_ignore_signals=False,
strip=True,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True,
disable_windowed_traceback=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
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")

View File

@@ -0,0 +1,14 @@
#!/usr/bin/env bash
APP_DIR="$(
cd -- "$(dirname "$0")" >/dev/null 2>&1
pwd -P
)"
# fish shell completions
_VIU_COMPLETE=fish_source viu >"$APP_DIR/completions/viu.fish"
# zsh completions
_VIU_COMPLETE=zsh_source viu >"$APP_DIR/completions/viu.zsh"
# bash completions
_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
}
}

16
dev/make_release Executable file
View File

@@ -0,0 +1,16 @@
#! /usr/bin/env sh
CLI_DIR="$(dirname "$(realpath "$0")")"
VERSION=$1
[ -z "$VERSION" ] && echo no version provided && exit 1
[ "$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/viu/__init__.py" &&
sed -i "s/version = .*/version = \"$VERSION\";/" "$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 &&
git stage "$CLI_DIR/flake.lock" "$CLI_DIR/uv.lock" &&
git commit -m "chore: update lock files" &&
git push &&
gh release create "v$VERSION"

4
fa
View File

@@ -1,4 +0,0 @@
#!/usr/bin/env sh
# exec "${PYTHON:-python3}" -Werror -Xdev -m "$(dirname "$(realpath "$0")")/fastanime" "$@"
cd "$(dirname "$(realpath "$0")")" || exit 1
exec python -m fastanime "$@"

View File

@@ -1,128 +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.anime_provider import anime_sources
if TYPE_CHECKING:
from typing import Iterator
from .libs.anilist.types import AnilistBaseMediaDataSchema
from .libs.anime_provider.types import Anime, SearchResults, Server
logger = logging.getLogger(__name__)
# TODO: improve performance of this class and add cool features like auto retry
class AnimeProvider:
"""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]
anime_provider: [TODO:attribute]
"""
PROVIDERS = list(anime_sources.keys())
provider = PROVIDERS[0]
def __init__(self, provider, dynamic=False, retries=0) -> None:
self.provider = provider
self.dynamic = dynamic
self.retries = retries
self.lazyload_provider()
def lazyload_provider(self):
"""updates the current provider being used"""
_, anime_provider_cls_name = anime_sources[self.provider].split(".", 1)
package = f"fastanime.libs.anime_provider.{self.provider}"
provider_api = importlib.import_module(".api", package)
anime_provider = getattr(provider_api, anime_provider_cls_name)
self.anime_provider = anime_provider()
def search_for_anime(
self,
user_query,
translation_type,
anilist_obj: "AnilistBaseMediaDataSchema | None" = None,
nsfw=True,
unknown=True,
) -> "SearchResults | None":
"""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]
unknown ([TODO:parameter]): [TODO:description]
anilist_obj: [TODO:description]
Returns:
[TODO:return]
"""
anime_provider = self.anime_provider
try:
results = anime_provider.search_for_anime(
user_query, translation_type, nsfw, unknown
)
except Exception as e:
logging.error(e)
results = None
return results
def get_anime(
self,
anime_id: str,
anilist_obj: "AnilistBaseMediaDataSchema | None" = None,
) -> "Anime | None":
"""core abstraction over getting info of an anime from all providers
Args:
anime_id: [TODO:description]
anilist_obj: [TODO:description]
Returns:
[TODO:return]
"""
anime_provider = self.anime_provider
try:
results = anime_provider.get_anime(anime_id)
except Exception as e:
logging.error(e)
results = None
return results
def get_episode_streams(
self,
anime,
episode: str,
translation_type: str,
anilist_obj: "AnilistBaseMediaDataSchema|None" = None,
) -> "Iterator[Server] | None":
"""core abstractions for getting juicy streams from all providers
Args:
anime ([TODO:parameter]): [TODO:description]
episode: [TODO:description]
translation_type: [TODO:description]
anilist_obj: [TODO:description]
Returns:
[TODO:return]
"""
anime_provider = self.anime_provider
try:
results = anime_provider.get_episode_streams(
anime, episode, translation_type
)
except Exception as e:
logging.error(e)
results = None
return results # pyright:ignore

View File

@@ -1,4 +0,0 @@
"""This package exist as away to expose functions and classes that my be useful to a developer using the fastanime library
[TODO:description]
"""

View File

@@ -1,34 +0,0 @@
from datetime import datetime
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from ..libs.anilist.types import AnilistDateObject, AnilistMediaNextAiringEpisode
# TODO: Add formating options for the final date
def format_anilist_date_object(anilist_date_object: "AnilistDateObject"):
if anilist_date_object:
return f"{anilist_date_object['day']}/{anilist_date_object['month']}/{anilist_date_object['year']}"
else:
return "Unknown"
def format_anilist_timestamp(anilist_timestamp: int | None):
if anilist_timestamp:
return datetime.fromtimestamp(anilist_timestamp).strftime("%d/%m/%Y %H:%M:%S")
else:
return "Unknown"
def format_list_data_with_comma(data: list | None):
if data:
return ", ".join(data)
else:
return "None"
def extract_next_airing_episode(airing_episode: "AnilistMediaNextAiringEpisode"):
if airing_episode:
return f"{airing_episode['episode']} on {format_anilist_timestamp(airing_episode['airingAt'])}"
else:
return "Completed"

View File

@@ -1,14 +0,0 @@
"""
Just contains some useful data used across the codebase
"""
# useful incases where the anilist title is too different from the provider title
anime_normalizer = {
"1P": "one piece",
"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",
}
anilist_sort_normalizer = {"search match": "SEARCH_MATCH"}

View File

@@ -1,74 +0,0 @@
import logging
from queue import Queue
from threading import Thread
import yt_dlp
from yt_dlp.utils import sanitize_filename
logger = logging.getLogger(__name__)
class YtDLPDownloader:
downloads_queue = Queue()
def _worker(self):
while True:
task, args = self.downloads_queue.get()
try:
task(*args)
except Exception as e:
logger.error(f"Something went wrong {e}")
self.downloads_queue.task_done()
def __init__(self):
self._thread = Thread(target=self._worker)
self._thread.daemon = True
self._thread.start()
# Function to download the file
# TODO: untpack the title to its actual values episode_title and anime_title
def _download_file(
self,
url: str,
anime_title: str,
episode_title: str,
download_dir: str,
silent: bool,
vid_format: str = "best",
):
"""Helper function that downloads anime given url and path details
Args:
url: [TODO:description]
anime_title: [TODO:description]
episode_title: [TODO:description]
download_dir: [TODO:description]
silent: [TODO:description]
vid_format: [TODO:description]
"""
anime_title = sanitize_filename(anime_title)
episode_title = sanitize_filename(episode_title)
ydl_opts = {
# Specify the output path and template
"outtmpl": f"{download_dir}/{anime_title}/{episode_title}.%(ext)s",
"silent": silent,
"verbose": False,
"format": vid_format,
}
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
ydl.download([url])
# WARN: May remove this legacy functionality
def download_file(self, url: str, title, silent=True):
"""A helper that just does things in the background
Args:
title ([TODO:parameter]): [TODO:description]
silent ([TODO:parameter]): [TODO:description]
url: [TODO:description]
"""
self.downloads_queue.put((self._download_file, (url, title, silent)))
downloader = YtDLPDownloader()

View File

@@ -1,38 +0,0 @@
import logging
from typing import TYPE_CHECKING
from thefuzz import fuzz
from .data import anime_normalizer
if TYPE_CHECKING:
from ..libs.anilist.types import AnilistBaseMediaDataSchema
logger = logging.getLogger(__name__)
def anime_title_percentage_match(
possible_user_requested_anime_title: str, anime: "AnilistBaseMediaDataSchema"
) -> float:
"""Returns the percentage match between the possible title and user title
Args:
possible_user_requested_anime_title (str): an Animdl search result title
title (str): the anime title the user wants
Returns:
int: the percentage match
"""
if normalized_anime_title := anime_normalizer.get(
possible_user_requested_anime_title
):
possible_user_requested_anime_title = normalized_anime_title
# compares both the romaji and english names and gets highest Score
title_a = str(anime["title"]["romaji"])
title_b = str(anime["title"]["english"])
percentage_ratio = max(
fuzz.ratio(title_a.lower(), possible_user_requested_anime_title.lower()),
fuzz.ratio(title_b.lower(), possible_user_requested_anime_title.lower()),
)
logger.info(f"{locals()}")
return percentage_ratio

View File

@@ -1,20 +0,0 @@
import sys
if sys.version_info < (3, 10):
raise ImportError(
"You are using an unsupported version of Python. Only Python versions 3.8 and above are supported by yt-dlp"
) # noqa: F541
__version__ = "v1.1.6"
APP_NAME = "FastAnime"
AUTHOR = "Benex254"
GIT_REPO = "github.com"
REPO = f"{GIT_REPO}/{AUTHOR}/{APP_NAME}"
def FastAnime():
from .cli import run_cli
run_cli()

View File

@@ -1,3 +0,0 @@
from .libs.anilist.api import AniListApi
AniList = AniListApi()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 197 KiB

View File

@@ -1,256 +0,0 @@
import signal
import click
from .. import __version__
from ..libs.anime_provider import SERVERS_AVAILABLE, anime_sources
from ..Utility.data import anilist_sort_normalizer
from .commands import LazyGroup
commands = {
"search": "search.search",
"download": "download.download",
"anilist": "anilist.anilist",
"config": "config.config",
"downloads": "downloads.downloads",
"cache": "cache.cache",
"completions": "completions.completions",
"update": "update.update",
}
# handle keyboard interupt
def handle_exit(signum, frame):
from click import clear
from .utils.tools import exit_app
clear()
exit_app()
signal.signal(signal.SIGINT, handle_exit)
@click.group(
lazy_subcommands=commands,
cls=LazyGroup,
help="A command line application for streaming anime that provides a complete and featureful interface",
short_help="Stream Anime",
)
@click.version_option(__version__, "--version")
@click.option("--log", help="Allow logging to stdout", is_flag=True)
@click.option("--log-file", help="Allow logging to a file", is_flag=True)
@click.option("--rich-traceback", help="Use rich to output tracebacks", is_flag=True)
@click.option(
"-p",
"--provider",
type=click.Choice(list(anime_sources.keys()), case_sensitive=False),
help="Provider of your choice",
)
@click.option(
"-s",
"--server",
type=click.Choice([*SERVERS_AVAILABLE, "top"]),
help="Server of choice",
)
@click.option(
"-f",
"--format",
type=str,
help="yt-dlp format to use",
)
@click.option(
"-c/-no-c",
"--continue/--no-continue",
"continue_",
type=bool,
help="Continue from last episode?",
)
@click.option(
"--skip/--no-skip",
type=bool,
help="Skip opening and ending theme songs?",
)
@click.option(
"-q",
"--quality",
type=click.Choice(["360", "720", "1080", "unknown"]),
help="set the quality of the stream",
)
@click.option(
"-t",
"--translation-type",
type=click.Choice(["dub", "sub"]),
help="Anime language[dub/sub]",
)
@click.option(
"-A/-no-A",
"--auto-next/--no-auto-next",
type=bool,
help="Auto select next episode?",
)
@click.option(
"-a/-no-a",
"--auto-select/--no-auto-select",
type=bool,
help="Auto select anime title?",
)
@click.option(
"-S",
"--sort-by",
type=click.Choice(anilist_sort_normalizer.keys()), # pyright: ignore
)
@click.option("-d", "--downloads-dir", type=click.Path(), help="Downloads location")
@click.option("--fzf", is_flag=True, help="Use fzf for the ui")
@click.option("--default", is_flag=True, help="Use the default interface")
@click.option("--preview", is_flag=True, help="Show preview when using fzf")
@click.option("--no-preview", is_flag=True, help="Dont show preview when using fzf")
@click.option(
"--icons/--no-icons",
type=bool,
help="Use icons in the interfaces",
)
@click.option("--dub", help="Set the translation type to dub", is_flag=True)
@click.option("--sub", help="Set the translation type to sub", is_flag=True)
@click.option("--rofi", help="Use rofi for the ui", is_flag=True)
@click.option("--rofi-theme", help="Rofi theme to use", type=click.Path())
@click.option(
"--rofi-theme-confirm",
help="Rofi theme to use for the confirm prompt",
type=click.Path(),
)
@click.option(
"--rofi-theme-input",
help="Rofi theme to use for the user input prompt",
type=click.Path(),
)
@click.option(
"--use-mpv-mod/--use-default-player", help="Whether to use python-mpv", type=bool
)
@click.pass_context
def run_cli(
ctx: click.Context,
log,
log_file,
rich_traceback,
provider,
server,
format,
continue_,
skip,
translation_type,
quality,
auto_next,
auto_select,
sort_by,
downloads_dir,
fzf,
default,
preview,
no_preview,
icons,
dub,
sub,
rofi,
rofi_theme,
rofi_theme_confirm,
rofi_theme_input,
use_mpv_mod,
):
from .config import Config
ctx.obj = Config()
if log:
import logging
from rich.logging import RichHandler
FORMAT = "%(message)s"
logging.basicConfig(
level="NOTSET", format=FORMAT, datefmt="[%X]", handlers=[RichHandler()]
)
logger = logging.getLogger(__name__)
logger.info("logging has been initialized")
elif log_file:
import logging
from ..constants import NOTIFIER_LOG_FILE_PATH
format = "%(asctime)s%(levelname)s: %(message)s"
logging.basicConfig(
level=logging.DEBUG,
filename=NOTIFIER_LOG_FILE_PATH,
format=format,
datefmt="[%d/%m/%Y@%H:%M:%S]",
filemode="w",
)
if rich_traceback:
from rich.traceback import install
install()
if provider:
ctx.obj.provider = provider
if server:
ctx.obj.server = server
if format:
ctx.obj.format = format
if ctx.get_parameter_source("continue_") == click.core.ParameterSource.COMMANDLINE:
ctx.obj.continue_from_history = continue_
if ctx.get_parameter_source("skip") == click.core.ParameterSource.COMMANDLINE:
ctx.obj.skip = skip
if quality:
ctx.obj.quality = quality
if ctx.get_parameter_source("auto_next") == click.core.ParameterSource.COMMANDLINE:
ctx.obj.auto_next = auto_next
if ctx.get_parameter_source("icons") == click.core.ParameterSource.COMMANDLINE:
ctx.obj.icons = icons
if (
ctx.get_parameter_source("auto_select")
== click.core.ParameterSource.COMMANDLINE
):
ctx.obj.auto_select = auto_select
if (
ctx.get_parameter_source("use_mpv_mod")
== click.core.ParameterSource.COMMANDLINE
):
ctx.obj.use_mpv_mod = use_mpv_mod
if sort_by:
ctx.obj.sort_by = sort_by
if downloads_dir:
ctx.obj.downloads_dir = downloads_dir
if translation_type:
ctx.obj.translation_type = translation_type
if fzf:
ctx.obj.use_fzf = True
if default:
ctx.obj.use_fzf = False
if preview:
ctx.obj.preview = True
if no_preview:
ctx.obj.preview = False
if dub:
ctx.obj.translation_type = "dub"
if sub:
ctx.obj.translation_type = "sub"
if rofi:
ctx.obj.use_fzf = False
ctx.obj.use_rofi = True
if rofi:
from ..libs.rofi import Rofi
if rofi_theme:
ctx.obj.rofi_theme = rofi_theme
Rofi.rofi_theme = rofi_theme
if rofi_theme_input:
ctx.obj.rofi_theme_input = rofi_theme_input
Rofi.rofi_theme_input = rofi_theme_input
if rofi_theme_confirm:
ctx.obj.rofi_theme_confirm = rofi_theme_confirm
Rofi.rofi_theme_confirm = rofi_theme_confirm

View File

@@ -1,105 +0,0 @@
import pathlib
import re
import shlex
import shutil
import subprocess
import sys
import requests
from rich import print
from .. import APP_NAME, AUTHOR, GIT_REPO, __version__
API_URL = f"https://api.{GIT_REPO}/repos/{AUTHOR}/{APP_NAME}/releases/latest"
def check_for_updates():
USER_AGENT = f"{APP_NAME} user"
request = requests.get(
API_URL,
headers={
"User-Agent": USER_AGENT,
"X-GitHub-Api-Version": "2022-11-28",
"Accept": "application/vnd.github+json",
},
)
if request.status_code == 200:
release_json = request.json()
return (release_json["tag_name"] == __version__, release_json)
else:
print(request.text)
return (False, {})
def is_git_repo(author, repository):
# Check if the current directory contains a .git folder
git_dir = pathlib.Path(".git")
if not git_dir.exists() or not git_dir.is_dir():
return False
# Check if the config file exists
config_path = git_dir / "config"
if not config_path.exists():
return False
try:
# Read the .git/config file to find the remote repository URL
with config_path.open("r") as git_config:
git_config_content = git_config.read()
except (FileNotFoundError, PermissionError):
return False
# Use regex to find the repository URL in the config file
repo_name_pattern = r"url\s*=\s*.+/([^/]+/[^/]+)\.git"
match = re.search(repo_name_pattern, git_config_content)
# Return True if match found and repository name matches
return bool(match) and match.group(1) == f"{author}/{repository}"
def update_app():
is_latest, release_json = check_for_updates()
if is_latest:
print("[green]App is up to date[/]")
return False, release_json
tag_name = release_json["tag_name"]
print("[cyan]Updating app to version %s[/]" % tag_name)
if is_git_repo(AUTHOR, APP_NAME):
GIT_EXECUTABLE = shutil.which("git")
args = [
GIT_EXECUTABLE,
"pull",
]
print(f"Pulling latest changes from the repository via git: {shlex.join(args)}")
if not GIT_EXECUTABLE:
print("[red]Cannot find git please install it.[/]")
return False, release_json
process = subprocess.run(
args,
)
else:
if PIPX_EXECUTABLE := shutil.which("pipx"):
process = subprocess.run([PIPX_EXECUTABLE, "upgrade", APP_NAME])
else:
PYTHON_EXECUTABLE = sys.executable
args = [
PYTHON_EXECUTABLE,
"-m",
"pip",
"install",
APP_NAME,
"--user",
"--no-warn-script-location",
]
process = subprocess.run(args)
if process.returncode == 0:
return True, release_json
else:
return False, release_json

View File

@@ -1,51 +0,0 @@
import click
from ...utils.tools import FastAnimeRuntimeState
from .__lazyloader__ import LazyGroup
commands = {
"trending": "trending.trending",
"recent": "recent.recent",
"search": "search.search",
"upcoming": "upcoming.upcoming",
"scores": "scores.scores",
"popular": "popular.popular",
"favourites": "favourites.favourites",
"random": "random_anime.random_anime",
"login": "login.login",
"watching": "watching.watching",
"paused": "paused.paused",
"rewatching": "rewatching.rewatching",
"dropped": "dropped.dropped",
"completed": "completed.completed",
"planning": "planning.planning",
"notifier": "notifier.notifier",
}
@click.group(
lazy_subcommands=commands,
cls=LazyGroup,
invoke_without_command=True,
help="A beautiful interface that gives you access to a commplete streaming experience",
short_help="Access all streaming options",
)
@click.pass_context
def anilist(ctx: click.Context):
from typing import TYPE_CHECKING
from ....anilist import AniList
from ....AnimeProvider import AnimeProvider
from ...interfaces.anilist_interfaces import (
fastanime_main_menu as anilist_interface,
)
if TYPE_CHECKING:
from ...config import Config
config: Config = ctx.obj
config.anime_provider = AnimeProvider(config.provider)
if user := ctx.obj.user:
AniList.update_login_info(user, user["token"])
if ctx.invoked_subcommand is None:
fastanime_runtime_state = FastAnimeRuntimeState()
anilist_interface(ctx.obj, fastanime_runtime_state)

View File

@@ -1,42 +0,0 @@
# in lazy_group.py
import importlib
import click
class LazyGroup(click.Group):
def __init__(self, *args, lazy_subcommands=None, **kwargs):
super().__init__(*args, **kwargs)
# lazy_subcommands is a map of the form:
#
# {command-name} -> {module-name}.{command-object-name}
#
self.lazy_subcommands = lazy_subcommands or {}
def list_commands(self, ctx):
base = super().list_commands(ctx)
lazy = sorted(self.lazy_subcommands.keys())
return base + lazy
def get_command(self, ctx, cmd_name): # pyright:ignore
if cmd_name in self.lazy_subcommands:
return self._lazy_load(cmd_name)
return super().get_command(ctx, cmd_name)
def _lazy_load(self, cmd_name: str):
# lazily loading a command, first get the module name and attribute name
import_path: str = self.lazy_subcommands[cmd_name]
modname, cmd_object_name = import_path.rsplit(".", 1)
# do the import
mod = importlib.import_module(
f".{modname}", package="fastanime.cli.commands.anilist"
)
# get the Command object from that module
cmd_object = getattr(mod, cmd_object_name)
# check the result to make debugging easier
if not isinstance(cmd_object, click.BaseCommand):
raise ValueError(
f"Lazy loading of {import_path} failed by returning "
"a non-command object"
)
return cmd_object

View File

@@ -1,32 +0,0 @@
from typing import TYPE_CHECKING
import click
if TYPE_CHECKING:
from ...config import Config
@click.command(help="View anime you completed")
@click.pass_obj
def completed(config: "Config"):
from ....anilist import AniList
from ...interfaces import anilist_interfaces
from ...utils.tools import FastAnimeRuntimeState, exit_app
if not config.user:
print("Not authenticated")
print("Please run: fastanime anilist loggin")
exit_app()
anime_list = AniList.get_anime_list("COMPLETED")
if not anime_list or not anime_list[1]:
return
if not anime_list[0]:
return
media = [
mediaListItem["media"]
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
] # pyright:ignore
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
fastanime_runtime_state = FastAnimeRuntimeState()
fastanime_runtime_state.anilist_data = anime_list[1]
anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state)

View File

@@ -1,32 +0,0 @@
from typing import TYPE_CHECKING
import click
if TYPE_CHECKING:
from fastanime.cli.config import Config
@click.command(help="View anime you dropped")
@click.pass_obj
def dropped(config: "Config"):
from ....anilist import AniList
from ...interfaces import anilist_interfaces
from ...utils.tools import FastAnimeRuntimeState, exit_app
if not config.user:
print("Not authenticated")
print("Please run: fastanime anilist loggin")
exit_app()
anime_list = AniList.get_anime_list("DROPPED")
if not anime_list:
return
if not anime_list[0] or not anime_list[1]:
return
media = [
mediaListItem["media"]
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
] # pyright:ignore
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
fastanime_runtime_state = FastAnimeRuntimeState()
fastanime_runtime_state.anilist_data = anime_list[1]
anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state)

View File

@@ -1,18 +0,0 @@
import click
@click.command(
help="Fetch the top 15 most favourited anime from anilist",
short_help="View most favourited anime",
)
@click.pass_obj
def favourites(config):
from ....anilist import AniList
from ...interfaces.anilist_interfaces import anilist_results_menu
from ...utils.tools import FastAnimeRuntimeState
anime_data = AniList.get_most_favourite()
if anime_data[0]:
fastanime_runtime_state = FastAnimeRuntimeState()
fastanime_runtime_state.anilist_data = anime_data[1]
anilist_results_menu(config, fastanime_runtime_state)

View File

@@ -1,48 +0,0 @@
from typing import TYPE_CHECKING
import click
if TYPE_CHECKING:
from ...config import Config
@click.command(help="Login to your anilist account")
@click.option("--status", "-s", help="Whether you are logged in or not", is_flag=True)
@click.pass_obj
def login(config: "Config", status):
from click import launch
from rich import print
from rich.prompt import Confirm, Prompt
from ....anilist import AniList
from ...utils.tools import exit_app
if status:
is_logged_in = True if config.user else False
message = (
"You are logged in :happy:" if is_logged_in else "You arent logged in :cry"
)
print(message)
print(config.user)
exit_app()
if config.user:
print("Already logged in :confused:")
if not Confirm.ask("or would you like to reloggin", default=True):
exit_app()
# ---- new loggin -----
print(
f"A browser session will be opened ( [link]{config.fastanime_anilist_app_login_url}[/link] )",
)
launch(config.fastanime_anilist_app_login_url, wait=True)
print("Please paste the token provided here")
token = Prompt.ask("Enter token")
user = AniList.login_user(token)
if not user:
print("Sth went wrong", user)
exit_app()
return
user["token"] = token
config.update_user(user)
print("Successfully saved credentials")
print(user)
exit_app()

View File

@@ -1,124 +0,0 @@
from typing import TYPE_CHECKING
import click
if TYPE_CHECKING:
from ...config import Config
@click.command(help="Check for notifications on anime you currently watching")
@click.pass_obj
def notifier(config: "Config"):
import json
import logging
import os
import time
import requests
from plyer import notification
from ....anilist import AniList
from ....constants import APP_CACHE_DIR, APP_DATA_DIR, APP_NAME, ICON_PATH, PLATFORM
logger = logging.getLogger(__name__)
notified = os.path.join(APP_DATA_DIR, "last_notification.json")
anime_image_path = os.path.join(APP_CACHE_DIR, "notification_image")
notification_duration = config.notification_duration * 60
notification_image_path = ""
if not config.user:
print("Not Authenticated")
print("Run the following to get started: fastanime anilist loggin")
return
run = True
# WARNING: Mess around with this value at your own risk
timeout = 2 # time is in minutes
if os.path.exists(notified):
with open(notified, "r") as f:
past_notifications = json.load(f)
else:
past_notifications = {}
with open(notified, "w") as f:
json.dump(past_notifications, f)
while run:
try:
logger.info("checking for notifications")
result = AniList.get_notification()
if not result[0]:
logger.warning(
"Something went wrong this could mean anilist is down or you have lost internet connection"
)
logger.info("sleeping...")
time.sleep(timeout * 60)
continue
data = result[1]
if not data:
logger.warning(
"Something went wrong this could mean anilist is down or you have lost internet connection"
)
logger.info("sleeping...")
time.sleep(timeout * 60)
continue
notifications = data["data"]["Page"]["notifications"]
if not notifications:
logger.info("Nothing to notify")
else:
for notification_ in notifications:
anime_episode = notification_["episode"]
anime_title = notification_["media"]["title"][
config.preferred_language
]
title = f"{anime_title} Episode {anime_episode} just aired"
# pyright:ignore
message = "Be sure to watch so you are not left out of the loop."
# message = str(textwrap.wrap(message, width=50))
id = notification_["media"]["id"]
if past_notifications.get(str(id)) == notification_["episode"]:
logger.info(
f"skipping id={id} title={anime_title} episode={anime_episode} already notified"
)
else:
# windows only supports ico,
# and you still ask why linux
if PLATFORM != "Windows":
image_link = notification_["media"]["coverImage"]["medium"]
logger.info("Downloading image...")
resp = requests.get(image_link)
if resp.status_code == 200:
with open(anime_image_path, "wb") as f:
f.write(resp.content)
notification_image_path = anime_image_path
else:
logger.warn(
f"Failed to get image response_status={resp.status_code} response_content={resp.content}"
)
notification_image_path = ICON_PATH
else:
notification_image_path = ICON_PATH
past_notifications[f"{id}"] = notification_["episode"]
with open(notified, "w") as f:
json.dump(past_notifications, f)
logger.info(message)
notification.notify( # pyright:ignore
title=title,
message=message,
app_name=APP_NAME,
app_icon=notification_image_path,
hints={
"image-path": notification_image_path,
"desktop-entry": f"{APP_NAME}.desktop",
},
timeout=notification_duration,
)
time.sleep(30)
except Exception as e:
logger.error(e)
logger.info("sleeping...")
time.sleep(timeout * 60)

View File

@@ -1,32 +0,0 @@
from typing import TYPE_CHECKING
import click
if TYPE_CHECKING:
from ...config import Config
@click.command(help="View anime you paused on watching")
@click.pass_obj
def paused(config: "Config"):
from ....anilist import AniList
from ...interfaces import anilist_interfaces
from ...utils.tools import FastAnimeRuntimeState, exit_app
if not config.user:
print("Not authenticated")
print("Please run: fastanime anilist loggin")
exit_app()
anime_list = AniList.get_anime_list("PAUSED")
if not anime_list:
return
if not anime_list[0] or not anime_list[1]:
return
media = [
mediaListItem["media"]
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
] # pyright:ignore
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
anilist_config = FastAnimeRuntimeState()
anilist_config.data = anime_list[1]
anilist_interfaces.anilist_results_menu(config, anilist_config)

View File

@@ -1,32 +0,0 @@
from typing import TYPE_CHECKING
import click
if TYPE_CHECKING:
from ...config import Config
@click.command(help="View anime you are planning on watching")
@click.pass_obj
def planning(config: "Config"):
from ....anilist import AniList
from ...interfaces import anilist_interfaces
from ...utils.tools import FastAnimeRuntimeState, exit_app
if not config.user:
print("Not authenticated")
print("Please run: fastanime anilist loggin")
exit_app()
anime_list = AniList.get_anime_list("PLANNING")
if not anime_list:
return
if not anime_list[0] or not anime_list[1]:
return
media = [
mediaListItem["media"]
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
] # pyright:ignore
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
fastanime_runtime_state = FastAnimeRuntimeState()
fastanime_runtime_state.anilist_data = anime_list[1]
anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state)

View File

@@ -1,17 +0,0 @@
import click
@click.command(
help="Fetch the top 15 most popular anime", short_help="View most popular anime"
)
@click.pass_obj
def popular(config):
from ....anilist import AniList
from ...interfaces.anilist_interfaces import anilist_results_menu
from ...utils.tools import FastAnimeRuntimeState
anime_data = AniList.get_most_popular()
if anime_data[0]:
fastanime_runtime_state = FastAnimeRuntimeState()
fastanime_runtime_state.anilist_data = anime_data[1]
anilist_results_menu(config, fastanime_runtime_state)

View File

@@ -1,27 +0,0 @@
import click
@click.command(
help="Get random anime from anilist based on a range of anilist anime ids that are seected at random",
short_help="View random anime",
)
@click.pass_obj
def random_anime(config):
import random
from ....anilist import AniList
from ...interfaces.anilist_interfaces import anilist_results_menu
from ...utils.tools import FastAnimeRuntimeState
random_anime = range(1, 15000)
random_anime = random.sample(random_anime, k=50)
anime_data = AniList.search(id_in=list(random_anime))
if anime_data[0]:
fastanime_runtime_state = FastAnimeRuntimeState()
fastanime_runtime_state.anilist_data = anime_data[1]
anilist_results_menu(config, fastanime_runtime_state)
else:
print(anime_data[1])

View File

@@ -1,18 +0,0 @@
import click
@click.command(
help="Fetch the 15 most recently updated anime from anilist that are currently releasing",
short_help="View recently updated anime",
)
@click.pass_obj
def recent(config):
from ....anilist import AniList
from ...interfaces.anilist_interfaces import anilist_results_menu
from ...utils.tools import FastAnimeRuntimeState
anime_data = AniList.get_most_recently_updated()
if anime_data[0]:
fastanime_runtime_state = FastAnimeRuntimeState()
fastanime_runtime_state.anilist_data = anime_data[1]
anilist_results_menu(config, fastanime_runtime_state)

View File

@@ -1,32 +0,0 @@
from typing import TYPE_CHECKING
import click
if TYPE_CHECKING:
from ...config import Config
@click.command(help="View anime you are rewatching")
@click.pass_obj
def rewatching(config: "Config"):
from ....anilist import AniList
from ...interfaces import anilist_interfaces
from ...utils.tools import FastAnimeRuntimeState, exit_app
if not config.user:
print("Not authenticated")
print("Please run: fastanime anilist loggin")
exit_app()
anime_list = AniList.get_anime_list("REPEATING")
if not anime_list:
return
if not anime_list[0] or not anime_list[1]:
return
media = [
mediaListItem["media"]
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
] # pyright:ignore
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
fastanime_runtime_state = FastAnimeRuntimeState()
fastanime_runtime_state.anilist_data = anime_list[1]
anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state)

View File

@@ -1,17 +0,0 @@
import click
@click.command(
help="Fetch the 15 most scored anime", short_help="View most scored anime"
)
@click.pass_obj
def scores(config):
from ....anilist import AniList
from ...interfaces.anilist_interfaces import anilist_results_menu
from ...utils.tools import FastAnimeRuntimeState
anime_data = AniList.get_most_scored()
if anime_data[0]:
fastanime_runtime_state = FastAnimeRuntimeState()
fastanime_runtime_state.data = anime_data[1]
anilist_results_menu(config, fastanime_runtime_state)

View File

@@ -1,21 +0,0 @@
import click
@click.command(
help="Search for anime using anilists api and get top ~50 results",
short_help="Search for anime",
)
@click.argument(
"title",
)
@click.pass_obj
def search(config, title):
from ....anilist import AniList
from ...interfaces.anilist_interfaces import anilist_results_menu
from ...utils.tools import FastAnimeRuntimeState
success, search_results = AniList.search(title)
if success:
fastanime_runtime_state = FastAnimeRuntimeState()
fastanime_runtime_state.anilist_data = search_results
anilist_results_menu(config, fastanime_runtime_state)

View File

@@ -1,18 +0,0 @@
import click
@click.command(
help="Fetch the top 15 anime that are currently trending",
short_help="Trending anime 🔥🔥🔥",
)
@click.pass_obj
def trending(config):
from ....anilist import AniList
from ...interfaces.anilist_interfaces import anilist_results_menu
from ...utils.tools import FastAnimeRuntimeState
success, data = AniList.get_trending()
if success:
fastanime_runtime_state = FastAnimeRuntimeState()
fastanime_runtime_state.anilist_data = data
anilist_results_menu(config, fastanime_runtime_state)

View File

@@ -1,17 +0,0 @@
import click
@click.command(
help="Fetch the 15 most anticipited anime", short_help="View upcoming anime"
)
@click.pass_obj
def upcoming(config):
from ....anilist import AniList
from ...interfaces.anilist_interfaces import anilist_results_menu
from ...utils.tools import FastAnimeRuntimeState
success, data = AniList.get_upcoming_anime()
if success:
fastanime_runtime_state = FastAnimeRuntimeState()
fastanime_runtime_state.anilist_data = data
anilist_results_menu(config, fastanime_runtime_state)

View File

@@ -1,32 +0,0 @@
from typing import TYPE_CHECKING
import click
if TYPE_CHECKING:
from ...config import Config
@click.command(help="View anime you are watching")
@click.pass_obj
def watching(config: "Config"):
from ....anilist import AniList
from ...interfaces import anilist_interfaces
from ...utils.tools import FastAnimeRuntimeState, exit_app
if not config.user:
print("Not authenticated")
print("Please run: fastanime anilist loggin")
exit_app()
anime_list = AniList.get_anime_list("CURRENT")
if not anime_list:
return
if not anime_list[0] or not anime_list[1]:
return
media = [
mediaListItem["media"]
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
] # pyright:ignore
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
fastanime_runtime_state = FastAnimeRuntimeState()
fastanime_runtime_state.anilist_data = anime_list[1]
anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state)

View File

@@ -1,39 +0,0 @@
import click
@click.command(help="Helper command to manage cache")
@click.option("--clean", help="Clean the cache dir", is_flag=True)
@click.option("--path", help="The path to the cache dir", is_flag=True)
@click.option("--size", help="The size of the cache dir", is_flag=True)
def cache(clean, path, size):
from ...constants import APP_CACHE_DIR
if path:
print(APP_CACHE_DIR)
elif clean:
import shutil
from rich.prompt import Confirm
if Confirm.ask(
f"Are you sure you want to clean the following path: {APP_CACHE_DIR};(NOTE: !!The action is irreversible and will clean your cache!!)",
default=False,
):
print("Cleaning...")
shutil.rmtree(APP_CACHE_DIR)
print("Successfully removed: ", APP_CACHE_DIR)
elif size:
import os
from ..utils.utils import format_bytes_to_human
total_size = 0
for dirpath, dirnames, filenames in os.walk(APP_CACHE_DIR):
for f in filenames:
fp = os.path.join(dirpath, f)
total_size += os.path.getsize(fp)
print("Total Size: ", format_bytes_to_human(total_size))
else:
import click
click.launch(APP_CACHE_DIR)

View File

@@ -1,93 +0,0 @@
from typing import TYPE_CHECKING
import click
if TYPE_CHECKING:
from ..config import Config
@click.command(
help="Opens up your fastanime config in your preferred editor",
short_help="Edit your config",
)
@click.option("--path", "-p", help="Print the config location and exit", is_flag=True)
@click.option(
"--view", "-v", help="View the current contents of your config", is_flag=True
)
@click.option(
"--desktop-entry",
"-d",
help="Configure the desktop entry of fastanime",
is_flag=True,
)
@click.pass_obj
def config(config: "Config", path, view, desktop_entry):
import sys
from rich import print
from ... import __version__
from ...constants import APP_NAME, ICON_PATH, S_PLATFORM, USER_CONFIG_PATH
if path:
print(USER_CONFIG_PATH)
elif view:
print(config)
elif desktop_entry:
import os
import shutil
from pathlib import Path
from textwrap import dedent
from rich import print
from rich.prompt import Confirm
from ..utils.tools import exit_app
FASTANIME_EXECUTABLE = shutil.which("fastanime")
if FASTANIME_EXECUTABLE:
cmds = f"{FASTANIME_EXECUTABLE} --rofi anilist"
else:
cmds = f"{sys.executable} -m fastanime --rofi anilist"
# TODO: Get funs of the other platforms to complete this lol
if S_PLATFORM == "win32":
print(
"Not implemented; the author thinks its not straight forward so welcomes lovers of windows to try and implement it themselves or to switch to a proper os like arch linux or pray the author gets bored 😜"
)
elif S_PLATFORM == "darwin":
print(
"Not implemented; the author thinks its not straight forward so welcomes lovers of mac to try and implement it themselves or to switch to a proper os like arch linux or pray the author gets bored 😜"
)
else:
desktop_entry = dedent(
f"""
[Desktop Entry]
Name={APP_NAME}
Type=Application
version={__version__}
Path={Path().home()}
Comment=Watch anime from your terminal
Terminal=false
Icon={ICON_PATH}
Exec={cmds}
Categories=Entertainment
"""
)
base = os.path.expanduser("~/.local/share/applications")
desktop_entry_path = os.path.join(base, f"{APP_NAME}.desktop")
if os.path.exists(desktop_entry_path):
if not Confirm.ask(
f"The file already exists {desktop_entry_path}; or would you like to rewrite it",
default=False,
):
exit_app(1)
with open(desktop_entry_path, "w") as f:
f.write(desktop_entry)
with open(desktop_entry_path) as f:
print(f"Successfully wrote \n{f.read()}")
exit_app(0)
else:
import click
click.edit(filename=USER_CONFIG_PATH)

View File

@@ -1,164 +0,0 @@
import time
from typing import TYPE_CHECKING
import click
if TYPE_CHECKING:
from ..config import Config
@click.command(
help="Download anime using the anime provider for a specified range",
short_help="Download anime",
)
@click.argument(
"anime-title",
required=True,
)
@click.option(
"--episode-range",
"-r",
help="A range of episodes to download",
)
@click.option(
"--highest_priority",
"-h",
help="Choose stream indicated as highest priority",
is_flag=True,
)
@click.pass_obj
def download(config: "Config", anime_title, episode_range, highest_priority):
from click import clear
from rich import print
from rich.progress import Progress
from thefuzz import fuzz
from ...AnimeProvider import AnimeProvider
from ...libs.anime_provider.types import Anime
from ...libs.fzf import fzf
from ...Utility.downloader.downloader import downloader
from ..utils.tools import exit_app
from ..utils.utils import filter_by_quality, fuzzy_inquirer
anime_provider = AnimeProvider(config.provider)
translation_type = config.translation_type
download_dir = config.downloads_dir
# ---- search for anime ----
with Progress() as progress:
progress.add_task("Fetching Search Results...", total=None)
search_results = anime_provider.search_for_anime(
anime_title, translation_type=translation_type
)
if not search_results:
print("Search results failed")
input("Enter to retry")
download(config, anime_title, episode_range, highest_priority)
return
search_results = search_results["results"]
search_results_ = {
search_result["title"]: search_result for search_result in search_results
}
if config.auto_select:
search_result = max(
search_results_.keys(), key=lambda title: fuzz.ratio(title, anime_title)
)
print("[cyan]Auto selecting:[/] ", search_result)
else:
choices = list(search_results_.keys())
if config.use_fzf:
search_result = fzf.run(choices, "Please Select title: ", "FastAnime")
else:
search_result = fuzzy_inquirer(
choices,
"Please Select title",
)
# ---- fetch anime ----
with Progress() as progress:
progress.add_task("Fetching Anime...", total=None)
anime: Anime | None = anime_provider.get_anime(
search_results_[search_result]["id"]
)
if not anime:
print("Sth went wring anime no found")
input("Enter to continue...")
download(config, anime_title, episode_range, highest_priority)
return
episodes = anime["availableEpisodesDetail"][config.translation_type]
if episode_range:
episodes_start, episodes_end = episode_range.split("-")
else:
episodes_start, episodes_end = 0, len(episodes)
for episode in range(round(float(episodes_start)), round(float(episodes_end))):
try:
episode = str(episode)
if episode not in episodes:
print(f"[cyan]Warning[/]: Episode {episode} not found, skipping")
continue
with Progress() as progress:
progress.add_task("Fetching Episode Streams...", total=None)
streams = anime_provider.get_episode_streams(
anime, episode, config.translation_type
)
if not streams:
print("No streams skipping")
continue
# ---- fetch servers ----
if config.server == "top":
with Progress() as progress:
progress.add_task("Fetching top server...", total=None)
server = next(streams, None)
if not server:
print("Sth went wrong when fetching the server")
continue
stream_link = filter_by_quality(config.quality, server["links"])
if not stream_link:
print("Quality not found")
input("Enter to continue")
continue
link = stream_link["link"]
episode_title = server["episode_title"]
else:
with Progress() as progress:
progress.add_task("Fetching servers", total=None)
# prompt for server selection
servers = {server["server"]: server for server in streams}
servers_names = list(servers.keys())
if config.use_fzf:
server = fzf.run(servers_names, "Select an link: ")
else:
server = fuzzy_inquirer(
servers_names,
"Select link",
)
stream_link = filter_by_quality(
config.quality, servers[server]["links"]
)
if not stream_link:
print("Quality not found")
continue
link = stream_link["link"]
episode_title = servers[server]["episode_title"]
print(f"[purple]Now Downloading:[/] {search_result} Episode {episode}")
downloader._download_file(
link,
anime["title"],
episode_title,
download_dir,
True,
config.format,
)
except Exception as e:
print(e)
time.sleep(1)
print("Continuing")
clear()
print("Done Downloading")
exit_app()

View File

@@ -1,50 +0,0 @@
from typing import TYPE_CHECKING
import click
if TYPE_CHECKING:
from ..config import Config
@click.command(
help="View and watch your downloads using mpv", short_help="Watch downloads"
)
@click.option("--path", "-p", help="print the downloads folder and exit", is_flag=True)
@click.pass_obj
def downloads(config: "Config", path: bool):
import os
from ...cli.utils.mpv import run_mpv
from ...libs.fzf import fzf
from ...libs.rofi import Rofi
from ..utils.tools import exit_app
from ..utils.utils import fuzzy_inquirer
USER_VIDEOS_DIR = config.downloads_dir
if path:
print(USER_VIDEOS_DIR)
return
if not os.path.exists(USER_VIDEOS_DIR):
print("Downloads directory specified does not exist")
return
playlists = os.listdir(USER_VIDEOS_DIR)
playlists.append("Exit")
def stream():
if config.use_fzf:
playlist_name = fzf.run(playlists, "Enter Playlist Name", "Downloads")
elif config.use_rofi:
playlist_name = Rofi.run(playlists, "Enter Playlist Name")
else:
playlist_name = fuzzy_inquirer(
playlists,
"Enter Playlist Name: ",
)
if playlist_name == "Exit":
exit_app()
return
playlist = os.path.join(USER_VIDEOS_DIR, playlist_name)
run_mpv(playlist)
stream()
stream()

View File

@@ -1,180 +0,0 @@
import click
from ...cli.config import Config
@click.command(
help="This subcommand directly interacts with the provider to enable basic streaming. Useful for binging anime.",
short_help="Binge anime",
)
@click.option(
"--episode-range",
"-r",
help="A range of episodes to binge",
)
@click.argument("anime_title", required=True, type=str)
@click.pass_obj
def search(config: Config, anime_title: str, episode_range: str):
from click import clear
from rich import print
from rich.progress import Progress
from thefuzz import fuzz
from ...AnimeProvider import AnimeProvider
from ...libs.anime_provider.types import Anime
from ...libs.fzf import fzf
from ...libs.rofi import Rofi
from ..utils.mpv import run_mpv
from ..utils.tools import exit_app
from ..utils.utils import filter_by_quality, fuzzy_inquirer
anime_provider = AnimeProvider(config.provider)
# ---- search for anime ----
with Progress() as progress:
progress.add_task("Fetching Search Results...", total=None)
search_results = anime_provider.search_for_anime(
anime_title, config.translation_type
)
if not search_results:
print("Search results not found")
input("Enter to retry")
search(config, anime_title, episode_range)
return
search_results = search_results["results"]
if not search_results:
print("Anime not found :cry:")
exit_app()
search_results_ = {
search_result["title"]: search_result for search_result in search_results
}
if config.auto_select:
search_result = max(
search_results_.keys(), key=lambda title: fuzz.ratio(title, anime_title)
)
print("[cyan]Auto Selecting:[/] ", search_result)
else:
choices = list(search_results_.keys())
if config.use_fzf:
search_result = fzf.run(choices, "Please Select title: ", "FastAnime")
elif config.use_rofi:
search_result = Rofi.run(choices, "Please Select Title")
else:
search_result = fuzzy_inquirer(
choices,
"Please Select Title",
)
# ---- fetch selected anime ----
with Progress() as progress:
progress.add_task("Fetching Anime...", total=None)
anime: Anime | None = anime_provider.get_anime(
search_results_[search_result]["id"]
)
if not anime:
print("Sth went wring anime no found")
input("Enter to continue...")
search(config, anime_title, episode_range)
return
episode_range_ = None
episodes = anime["availableEpisodesDetail"][config.translation_type]
if episode_range:
episodes_start, episodes_end = episode_range.split("-")
if episodes_start and episodes_end:
episode_range_ = iter(
range(round(float(episodes_start)), round(float(episodes_end)) + 1)
)
else:
episode_range_ = iter(sorted(episodes, key=float))
def stream_anime():
clear()
episode = None
if episode_range_:
try:
episode = str(next(episode_range_))
print(
f"[cyan]Auto selecting:[/] {search_result} [cyan]Episode:[/] {episode}"
)
except StopIteration:
print("[green]Completed binge sequence[/]:smile:")
input("Enter to continue...")
if not episode or episode not in episodes:
if config.use_fzf:
episode = fzf.run(episodes, "Select an episode: ", header=search_result)
elif config.use_rofi:
episode = Rofi.run(episodes, "Select an episode")
else:
episode = fuzzy_inquirer(
episodes,
"Select episode",
)
# ---- fetch streams ----
with Progress() as progress:
progress.add_task("Fetching Episode Streams...", total=None)
streams = anime_provider.get_episode_streams(
anime, episode, config.translation_type
)
if not streams:
print("Failed to get streams")
return
try:
# ---- fetch servers ----
if config.server == "top":
with Progress() as progress:
progress.add_task("Fetching top server...", total=None)
server = next(streams, None)
if not server:
print("Sth went wrong when fetching the episode")
input("Enter to continue")
stream_anime()
return
stream_link = filter_by_quality(config.quality, server["links"])
if not stream_link:
print("Quality not found")
input("Enter to continue")
stream_anime()
return
link = stream_link["link"]
episode_title = server["episode_title"]
else:
with Progress() as progress:
progress.add_task("Fetching servers", total=None)
# prompt for server selection
servers = {server["server"]: server for server in streams}
servers_names = list(servers.keys())
if config.use_fzf:
server = fzf.run(servers_names, "Select an link: ")
elif config.use_rofi:
server = Rofi.run(servers_names, "Select an link")
else:
server = fuzzy_inquirer(
servers_names,
"Select link",
)
stream_link = filter_by_quality(
config.quality, servers[server]["links"]
)
if not stream_link:
print("Quality not found")
input("Enter to continue")
stream_anime()
return
link = stream_link["link"]
episode_title = servers[server]["episode_title"]
print(f"[purple]Now Playing:[/] {search_result} Episode {episode}")
run_mpv(link, episode_title)
except Exception as e:
print(e)
input("Enter to continue")
stream_anime()
stream_anime()

View File

@@ -1,37 +0,0 @@
import click
@click.command(help="Helper command to update fastanime to latest")
@click.option("--check", "-c", help="Check for the latest release", is_flag=True)
def update(
check,
):
from rich.console import Console
from rich.markdown import Markdown
from ..app_updater import check_for_updates, update_app
def _print_release(release_data):
console = Console()
body = Markdown(release_data["body"])
tag = github_release_data["tag_name"]
tag_title = release_data["name"]
github_page_url = release_data["html_url"]
console.print(f"Release Page: {github_page_url}")
console.print(f"Tag: {tag}")
console.print(f"Title: {tag_title}")
console.print(body)
if check:
is_update, github_release_data = check_for_updates()
if is_update:
print(
"You are running an older version of fastanime please update to get the latest features"
)
_print_release(github_release_data)
else:
print("You are running the latest version of fastanime")
_print_release(github_release_data)
else:
success, github_release_data = update_app()
_print_release(github_release_data)

View File

@@ -1,342 +0,0 @@
import json
import logging
import os
from configparser import ConfigParser
from typing import TYPE_CHECKING
from rich import print
from ..constants import USER_CONFIG_PATH, USER_DATA_PATH, USER_VIDEOS_DIR
from ..libs.rofi import Rofi
logger = logging.getLogger(__name__)
if TYPE_CHECKING:
from ..AnimeProvider import AnimeProvider
class Config(object):
"""class that handles and manages configuration and user data throughout the clis lifespan
Attributes:
anime_list: [TODO:attribute]
watch_history: [TODO:attribute]
fastanime_anilist_app_login_url: [TODO:attribute]
anime_provider: [TODO:attribute]
user_data: [TODO:attribute]
configparser: [TODO:attribute]
downloads_dir: [TODO:attribute]
provider: [TODO:attribute]
use_fzf: [TODO:attribute]
use_rofi: [TODO:attribute]
skip: [TODO:attribute]
icons: [TODO:attribute]
preview: [TODO:attribute]
translation_type: [TODO:attribute]
sort_by: [TODO:attribute]
continue_from_history: [TODO:attribute]
auto_next: [TODO:attribute]
auto_select: [TODO:attribute]
use_mpv_mod: [TODO:attribute]
quality: [TODO:attribute]
notification_duration: [TODO:attribute]
error: [TODO:attribute]
server: [TODO:attribute]
format: [TODO:attribute]
force_window: [TODO:attribute]
preferred_language: [TODO:attribute]
rofi_theme: [TODO:attribute]
rofi_theme: [TODO:attribute]
rofi_theme_input: [TODO:attribute]
rofi_theme_input: [TODO:attribute]
rofi_theme_confirm: [TODO:attribute]
rofi_theme_confirm: [TODO:attribute]
watch_history: [TODO:attribute]
anime_list: [TODO:attribute]
user: [TODO:attribute]
"""
anime_list: list
watch_history: dict
fastanime_anilist_app_login_url = (
"https://anilist.co/api/v2/oauth/authorize?client_id=20148&response_type=token"
)
anime_provider: "AnimeProvider"
user_data = {"watch_history": {}, "animelist": [], "user": {}}
def __init__(self) -> None:
self.initialize_user_data()
self.load_config()
def load_config(self):
self.configparser = ConfigParser(
{
"quality": "1080",
"auto_next": "False",
"auto_select": "True",
"sort_by": "search match",
"downloads_dir": USER_VIDEOS_DIR,
"translation_type": "sub",
"server": "top",
"continue_from_history": "True",
"use_mpv_mod": "false",
"force_window": "immediate",
"preferred_language": "english",
"use_fzf": "False",
"preview": "False",
"format": "best[height<=1080]/bestvideo[height<=1080]+bestaudio/best",
"provider": "allanime",
"error": "3",
"icons": "false",
"notification_duration": "2",
"skip": "false",
"use_rofi": "false",
"rofi_theme": "",
"rofi_theme_input": "",
"rofi_theme_confirm": "",
}
)
self.configparser.add_section("stream")
self.configparser.add_section("general")
self.configparser.add_section("anilist")
if not os.path.exists(USER_CONFIG_PATH):
with open(USER_CONFIG_PATH, "w") as config:
self.configparser.write(config)
self.configparser.read(USER_CONFIG_PATH)
# --- set config values from file or using defaults ---
self.downloads_dir = self.get_downloads_dir()
self.provider = self.get_provider()
self.use_fzf = self.get_use_fzf()
self.use_rofi = self.get_use_rofi()
self.skip = self.get_skip()
self.icons = self.get_icons()
self.preview = self.get_preview()
self.translation_type = self.get_translation_type()
self.sort_by = self.get_sort_by()
self.continue_from_history = self.get_continue_from_history()
self.auto_next = self.get_auto_next()
self.auto_select = self.get_auto_select()
self.use_mpv_mod = self.get_use_mpv_mod()
self.quality = self.get_quality()
self.notification_duration = self.get_notification_duration()
self.error = self.get_error()
self.server = self.get_server()
self.format = self.get_format()
self.force_window = self.get_force_window()
self.preferred_language = self.get_preferred_language()
self.rofi_theme = self.get_rofi_theme()
Rofi.rofi_theme = self.rofi_theme
self.rofi_theme_input = self.get_rofi_theme_input()
Rofi.rofi_theme_input = self.rofi_theme_input
self.rofi_theme_confirm = self.get_rofi_theme_confirm()
Rofi.rofi_theme_confirm = self.rofi_theme_confirm
# ---- setup user data ------
self.watch_history: dict = self.user_data.get("watch_history", {})
self.anime_list: list = self.user_data.get("animelist", [])
self.user: dict = self.user_data.get("user", {})
def update_user(self, user):
self.user = user
self.user_data["user"] = user
self._update_user_data()
def update_watch_history(
self, anime_id: int, episode: str | None, start_time="0", total_time="0"
):
self.watch_history.update(
{
str(anime_id): {
"episode": episode,
"start_time": start_time,
"total_time": total_time,
}
}
)
self.user_data["watch_history"] = self.watch_history
self._update_user_data()
def initialize_user_data(self):
try:
if os.path.isfile(USER_DATA_PATH):
with open(USER_DATA_PATH, "r") as f:
user_data = json.load(f)
self.user_data.update(user_data)
except Exception as e:
logger.error(e)
def _update_user_data(self):
"""method that updates the actual user data file"""
with open(USER_DATA_PATH, "w") as f:
json.dump(self.user_data, f)
# getters for user configuration
# --- general section ---
def get_provider(self):
return self.configparser.get("general", "provider")
def get_preferred_language(self):
return self.configparser.get("general", "preferred_language")
def get_downloads_dir(self):
return self.configparser.get("general", "downloads_dir")
def get_icons(self):
return self.configparser.getboolean("general", "icons")
def get_preview(self):
return self.configparser.getboolean("general", "preview")
def get_use_fzf(self):
return self.configparser.getboolean("general", "use_fzf")
# rofi conifiguration
def get_use_rofi(self):
return self.configparser.getboolean("general", "use_rofi")
def get_rofi_theme(self):
return self.configparser.get("general", "rofi_theme")
def get_rofi_theme_input(self):
return self.configparser.get("general", "rofi_theme_input")
def get_rofi_theme_confirm(self):
return self.configparser.get("general", "rofi_theme_confirm")
# --- stream section ---
def get_skip(self):
return self.configparser.getboolean("stream", "skip")
def get_auto_next(self):
return self.configparser.getboolean("stream", "auto_next")
def get_auto_select(self):
return self.configparser.getboolean("stream", "auto_select")
def get_continue_from_history(self):
return self.configparser.getboolean("stream", "continue_from_history")
def get_use_mpv_mod(self):
return self.configparser.getboolean("stream", "use_mpv_mod")
def get_notification_duration(self):
return self.configparser.getint("general", "notification_duration")
def get_error(self):
return self.configparser.getint("stream", "error")
def get_force_window(self):
return self.configparser.get("stream", "force_window")
def get_translation_type(self):
return self.configparser.get("stream", "translation_type")
def get_quality(self):
return self.configparser.get("stream", "quality")
def get_server(self):
return self.configparser.get("stream", "server")
def get_format(self):
return self.configparser.get("stream", "format")
def get_sort_by(self):
return self.configparser.get("anilist", "sort_by")
def update_config(self, section: str, key: str, value: str):
self.configparser.set(section, key, value)
with open(USER_CONFIG_PATH, "w") as config:
self.configparser.write(config)
def __repr__(self):
current_config_state = f"""
[stream]
# Auto continue from watch history
continue_from_history = {self.continue_from_history}
# Preferred language for anime (options: dub, sub)
translation_type = {self.translation_type}
# Default server (options: dropbox, sharepoint, wetransfer.gogoanime, top, wixmp)
server = {self.server}
# Auto-select next episode
auto_next = {self.auto_next}
# Auto select the anime provider results with fuzzy find.
# Note this wont always be correct.But 99% of the time will be.
auto_select = {self.auto_select}
# whether to skip the opening and ending theme songs
# NOTE: requires ani-skip to be in path
skip = {self.skip}
# the maximum delta time in minutes after which the episode should be considered as completed
# used in the continue from time stamp
error = {self.error}
# whether to use python-mpv
# to enable superior control over the player
# adding more options to it
use_mpv_mod = {self.use_mpv_mod}
# the format of downloaded anime and trailer
# based on yt-dlp format and passed directly to it
# learn more by looking it up on their site
# only works for downloaded anime if server=gogoanime
# since its the only one that offers different formats
# the others tend not to
format = {self.format}
[general]
# can be [allanime,animepahe]
provider = {self.provider}
# Display language (options: english, romaji)
preferred_language = {self.preferred_language}
# Download directory
downloads_dir = {self.downloads_dir}
# whether to show a preview window when using fzf or rofi
preview = {self.preview}
# whether to use fzf as the interface for the anilist command and others.
use_fzf = {self.use_fzf}
# whether to use rofi for the ui
use_rofi = {self.use_rofi}
# rofi theme to use
rofi_theme = {self.rofi_theme}
rofi_theme_input = {self.rofi_theme_input}
rofi_theme_confirm = {self.rofi_theme_confirm}
# whether to show the icons
icons = {self.icons}
# the duration in minutes a notification will stay in the screen
# used by notifier command
notification_duration = {self.notification_duration}
"""
return current_config_state
def __str__(self):
return self.__repr__()
# WARNING: depracated and will probably be removed
def update_anime_list(self, anime_id: int, remove=False):
if remove:
try:
self.anime_list.remove(anime_id)
print("Succesfully removed :cry:")
except Exception:
print(anime_id, "Nothing to remove :confused:")
else:
self.anime_list.append(anime_id)
self.user_data["animelist"] = list(set(self.anime_list))
self._update_user_data()
print("Succesfully added :smile:")
input("Enter to continue...")

File diff suppressed because it is too large Load Diff

View File

@@ -1,288 +0,0 @@
import concurrent.futures
import logging
import os
import shutil
import subprocess
import textwrap
from threading import Thread
import requests
from yt_dlp.utils import clean_html
from ...constants import APP_CACHE_DIR
from ...libs.anilist.types import AnilistBaseMediaDataSchema
from ...Utility import anilist_data_helper
from ..utils.utils import get_true_fg
logger = logging.getLogger(__name__)
# this script was written by the fzf devs as an example on how to preview images
# its only here for convinience
fzf_preview = r"""
#
# The purpose of this script is to demonstrate how to preview a file or an
# image in the preview window of fzf.
#
# Dependencies:
# - https://github.com/sharkdp/bat
# - https://github.com/hpjansson/chafa
# - https://iterm2.com/utilities/imgcat
fzf-preview(){
if [[ $# -ne 1 ]]; then
>&2 echo "usage: $0 FILENAME"
exit 1
fi
file=${1/#\~\//$HOME/}
type=$(file --dereference --mime -- "$file")
if [[ ! $type =~ image/ ]]; then
if [[ $type =~ =binary ]]; then
file "$1"
exit
fi
# Sometimes bat is installed as batcat.
if command -v batcat > /dev/null; then
batname="batcat"
elif command -v bat > /dev/null; then
batname="bat"
else
cat "$1"
exit
fi
${batname} --style="${BAT_STYLE:-numbers}" --color=always --pager=never -- "$file"
exit
fi
dim=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}
if [[ $dim = x ]]; then
dim=$(stty size < /dev/tty | awk '{print $2 "x" $1}')
elif ! [[ $KITTY_WINDOW_ID ]] && (( FZF_PREVIEW_TOP + FZF_PREVIEW_LINES == $(stty size < /dev/tty | awk '{print $1}') )); then
# Avoid scrolling issue when the Sixel image touches the bottom of the screen
# * https://github.com/junegunn/fzf/issues/2544
dim=${FZF_PREVIEW_COLUMNS}x$((FZF_PREVIEW_LINES - 1))
fi
# 1. Use kitty icat on kitty terminal
if [[ $KITTY_WINDOW_ID ]]; then
# 1. 'memory' is the fastest option but if you want the image to be scrollable,
# you have to use 'stream'.
#
# 2. The last line of the output is the ANSI reset code without newline.
# This confuses fzf and makes it render scroll offset indicator.
# So we remove the last line and append the reset code to its previous line.
kitty icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed '$d' | sed $'$s/$/\e[m/'
# 2. Use chafa with Sixel output
elif command -v chafa > /dev/null; then
chafa -f sixel -s "$dim" "$file"
# Add a new line character so that fzf can display multiple images in the preview window
echo
# 3. If chafa is not found but imgcat is available, use it on iTerm2
elif command -v imgcat > /dev/null; then
# NOTE: We should use https://iterm2.com/utilities/it2check to check if the
# user is running iTerm2. But for the sake of simplicity, we just assume
# that's the case here.
imgcat -W "${dim%%x*}" -H "${dim##*x}" "$file"
# 4. Cannot find any suitable method to preview the image
else
file "$file"
fi
}
"""
# ---- aniskip intergration ----
def aniskip(mal_id: int, episode: str):
"""helper function to be used for setting and getting skip data
Args:
mal_id: mal id of the anime
episode: episode number
Returns:
mpv chapter options
"""
ANISKIP = shutil.which("ani-skip")
if not ANISKIP:
print("Aniskip not found, please install and try again")
return
args = [ANISKIP, "-q", str(mal_id), "-e", str(episode)]
aniskip_result = subprocess.run(args, text=True, stdout=subprocess.PIPE)
if aniskip_result.returncode != 0:
return
mpv_skip_args = aniskip_result.stdout.strip()
return mpv_skip_args.split(" ")
# ---- prevew stuff ----
# import tempfile
# NOTE: May change this to a temp dir but there were issues so later
WORKING_DIR = APP_CACHE_DIR # tempfile.gettempdir()
IMAGES_CACHE_DIR = os.path.join(WORKING_DIR, "images")
if not os.path.exists(IMAGES_CACHE_DIR):
os.mkdir(IMAGES_CACHE_DIR)
ANIME_INFO_CACHE_DIR = os.path.join(WORKING_DIR, "info")
if not os.path.exists(ANIME_INFO_CACHE_DIR):
os.mkdir(ANIME_INFO_CACHE_DIR)
def save_image_from_url(url: str, file_name: str):
"""Helper function that downloads an image to the FastAnime images cache dir given its url and filename
Args:
url: image url to download
file_name: filename to use
"""
image = requests.get(url)
with open(f"{IMAGES_CACHE_DIR}/{file_name}", "wb") as f:
f.write(image.content)
def save_info_from_str(info: str, file_name: str):
"""Helper function that writes text (anime details and info) to a file given its filename
Args:
info: the information anilist has on the anime
file_name: the filename to use
"""
with open(f"{ANIME_INFO_CACHE_DIR}/{file_name}", "w") as f:
f.write(info)
def write_search_results(
anilist_results: list[AnilistBaseMediaDataSchema],
titles: list[str],
workers: int | None = None,
):
"""A helper function used by and run in a background thread by get_fzf_preview function inorder to get the actual preview data to be displayed by fzf
Args:
anilist_results: the anilist results from an anilist action
titles: sanitized anime titles
workers:number of threads to use defaults to as many as possible
"""
# NOTE: Will probably make this a configuraable option
HEADER_COLOR = 215, 0, 95
SEPARATOR_COLOR = 208, 208, 208
SEPARATOR_WIDTH = 45
# use concurency to download and write as fast as possible
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
future_to_task = {}
for anime, title in zip(anilist_results, titles):
# actual image url
image_url = anime["coverImage"]["large"]
future_to_task[executor.submit(save_image_from_url, image_url, title)] = (
image_url
)
# handle the text data
template = f"""
{get_true_fg("-"*SEPARATOR_WIDTH,*SEPARATOR_COLOR,bold=False)}
{get_true_fg('Title(jp):',*HEADER_COLOR)} {anime['title']['romaji']}
{get_true_fg('Title(eng):',*HEADER_COLOR)} {anime['title']['english']}
{get_true_fg('Popularity:',*HEADER_COLOR)} {anime['popularity']}
{get_true_fg('Favourites:',*HEADER_COLOR)} {anime['favourites']}
{get_true_fg('Status:',*HEADER_COLOR)} {anime['status']}
{get_true_fg('Episodes:',*HEADER_COLOR)} {anime['episodes']}
{get_true_fg('Genres:',*HEADER_COLOR)} {anilist_data_helper.format_list_data_with_comma(anime['genres'])}
{get_true_fg('Next Episode:',*HEADER_COLOR)} {anilist_data_helper.extract_next_airing_episode(anime['nextAiringEpisode'])}
{get_true_fg('Start Date:',*HEADER_COLOR)} {anilist_data_helper.format_anilist_date_object(anime['startDate'])}
{get_true_fg('End Date:',*HEADER_COLOR)} {anilist_data_helper.format_anilist_date_object(anime['endDate'])}
{get_true_fg("-"*SEPARATOR_WIDTH,*SEPARATOR_COLOR,bold=False)}
{get_true_fg('Description:',*HEADER_COLOR)}
"""
template = textwrap.dedent(template)
template = f"""
{template}
{textwrap.fill(clean_html(
str(anime['description'])), width=45)}
"""
future_to_task[executor.submit(save_info_from_str, template, title)] = title
# execute the jobs
for future in concurrent.futures.as_completed(future_to_task):
task = future_to_task[future]
try:
future.result()
except Exception as exc:
logger.error("%r generated an exception: %s" % (task, exc))
# get rofi icons
def get_rofi_icons(
anilist_results: list[AnilistBaseMediaDataSchema], titles, workers=None
):
"""A helper function to make sure that the images are downloaded so they can be used as icons
Args:
titles (list[str]): sanitized titles of the anime; NOTE: its important that they are sanitized since they are used as the filenames of the images
workers ([TODO:parameter]): Number of threads to use to download the images; defaults to as many as possible
anilist_results: the anilist results from an anilist action
"""
# use concurrency to download the images as fast as possible
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
# load the jobs
future_to_url = {}
for anime, title in zip(anilist_results, titles):
# actual link to download image from
image_url = anime["coverImage"]["large"]
future_to_url[executor.submit(save_image_from_url, image_url, title)] = (
image_url
)
# execute the jobs
for future in concurrent.futures.as_completed(future_to_url):
url = future_to_url[future]
try:
future.result()
except Exception as e:
logger.error("%r generated an exception: %s" % (url, e))
def get_fzf_preview(
anilist_results: list[AnilistBaseMediaDataSchema], titles, wait=False
):
"""A helper function that constructs data to be used for the fzf preview
Args:
titles (list[str]): The sanitized titles to use, NOTE: its important that they are sanitized since thay will be used as filenames
wait (bool): whether to block the ui as we wait for preview defaults to false
anilist_results: the anilist results got from an anilist action
Returns:
THe fzf preview script to use
"""
# ensure images and info exists
background_worker = Thread(
target=write_search_results, args=(anilist_results, titles)
)
background_worker.daemon = True
background_worker.start()
# the preview script is in bash so making sure fzf doesnt use any other shell lang to process the preview script
os.environ["SHELL"] = shutil.which("bash") or "bash"
preview = """
%s
if [ -s %s/{} ]; then fzf-preview %s/{}
else echo Loading...
fi
if [ -s %s/{} ]; then cat %s/{}
else echo Loading...
fi
""" % (
fzf_preview,
IMAGES_CACHE_DIR,
IMAGES_CACHE_DIR,
ANIME_INFO_CACHE_DIR,
ANIME_INFO_CACHE_DIR,
)
if wait:
background_worker.join()
return preview

View File

@@ -1,120 +0,0 @@
import re
import shutil
import subprocess
def stream_video(MPV, url, mpv_args, custom_args):
process = subprocess.Popen(
[MPV, url, *mpv_args, *custom_args],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
last_time = None
av_time_pattern = re.compile(r"AV: ([0-9:]*) / ([0-9:]*) \(([0-9]*)%\)")
last_time = "0"
total_time = "0"
try:
while True:
if not process.stderr:
continue
output = process.stderr.readline()
if output:
# Match the timestamp in the output
match = av_time_pattern.search(output.strip())
if match:
current_time = match.group(1)
total_time = match.group(2)
match.group(3)
last_time = current_time
# print(f"Current stream time: {current_time}, Total time: {total_time}, Progress: {percentage}%")
# Check if the process has terminated
retcode = process.poll()
if retcode is not None:
print("Finshed at: ", last_time)
break
except Exception as e:
print(f"An error occurred: {e}")
finally:
process.terminate()
return last_time, total_time
def run_mpv(
link: str,
title: str | None = "",
start_time: str = "0",
ytdl_format="",
custom_args=[],
):
# Determine if mpv is available
MPV = shutil.which("mpv")
# If title is None, set a default value
# Regex to check if the link is a YouTube URL
youtube_regex = r"(https?://)?(www\.)?(youtube|youtu|youtube-nocookie)\.(com|be)/.+"
if not MPV:
# Determine if the link is a YouTube URL
if re.match(youtube_regex, link):
# Android specific commands to launch mpv with a YouTube URL
args = [
"nohup",
"am",
"start",
"--user",
"0",
"-a",
"android.intent.action.VIEW",
"-d",
link,
"-n",
"com.google.android.youtube/.UrlActivity",
]
return "0", "0"
else:
# Android specific commands to launch mpv with a regular URL
args = [
"nohup",
"am",
"start",
"--user",
"0",
"-a",
"android.intent.action.VIEW",
"-d",
link,
"-n",
"is.xyz.mpv/.MPVActivity",
]
subprocess.run(args)
return "0", "0"
else:
# General mpv command with custom arguments
mpv_args = []
if start_time != "0":
mpv_args.append(f"--start={start_time}")
if title:
mpv_args.append(f"--title={title}")
if ytdl_format:
mpv_args.append(f"--ytdl-format={ytdl_format}")
stop_time, total_time = stream_video(MPV, link, mpv_args, custom_args)
return stop_time, total_time
# Example usage
if __name__ == "__main__":
run_mpv(
"https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"Example Video",
"--fullscreen",
"--volume=50",
)

View File

@@ -1,326 +0,0 @@
from typing import TYPE_CHECKING
import mpv
from ...anilist import AniList
from .utils import filter_by_quality
if TYPE_CHECKING:
from typing import Literal
from ...AnimeProvider import AnimeProvider
from ..config import Config
def format_time(duration_in_secs: float):
h = duration_in_secs // 3600
m = duration_in_secs // 60
s = duration_in_secs - ((h * 3600) + (m * 60))
return f"{int(h):2d}:{int(m):2d}:{int(s):2d}".replace(" ", "0")
class MpvPlayer(object):
anime_provider: "AnimeProvider"
config: "Config"
mpv_player: "mpv.MPV"
last_stop_time: str = "0"
last_total_time: str = "0"
last_stop_time_secs = 0
last_total_time_secs = 0
current_media_title = ""
player_fetching = False
def get_episode(
self,
type: "Literal['next','previous','reload','custom']",
ep_no=None,
server="top",
):
fastanime_runtime_state = self.fastanime_runtime_state
config = self.config
current_episode_number: str = (
fastanime_runtime_state.provider_current_episode_number
)
quality = config.quality
total_episodes: list = sorted(
fastanime_runtime_state.provider_available_episodes, key=float
)
anime_id_anilist: int = fastanime_runtime_state.selected_anime_id_anilist
provider_anime = fastanime_runtime_state.provider_anime
translation_type = config.translation_type
anime_provider = config.anime_provider
self.last_stop_time: str = "0"
self.last_total_time: str = "0"
self.last_stop_time_secs = 0
self.last_total_time_secs = 0
# next or prev
if type == "next":
self.mpv_player.show_text("Fetching next episode...")
next_episode = total_episodes.index(current_episode_number) + 1
if next_episode >= len(total_episodes):
next_episode = len(total_episodes) - 1
fastanime_runtime_state.provider_current_episode_number = total_episodes[
next_episode
]
current_episode_number = (
fastanime_runtime_state.provider_current_episode_number
)
config.update_watch_history(anime_id_anilist, str(current_episode_number))
elif type == "reload":
if current_episode_number not in total_episodes:
self.mpv_player.show_text("Episode not available")
return
self.mpv_player.show_text("Replaying Episode...")
elif type == "custom":
if not ep_no or ep_no not in total_episodes:
self.mpv_player.show_text("Episode number not specified or invalid")
self.mpv_player.show_text(
f"Acceptable episodes are: {total_episodes}",
)
return
self.mpv_player.show_text(f"Fetching episode {ep_no}")
current_episode_number = ep_no
config.update_watch_history(anime_id_anilist, str(ep_no))
fastanime_runtime_state.provider_current_episode_number = str(ep_no)
else:
self.mpv_player.show_text("Fetching previous episode...")
prev_episode = total_episodes.index(current_episode_number) - 1
if prev_episode <= 0:
prev_episode = 0
fastanime_runtime_state.provider_current_episode_number = total_episodes[
prev_episode
]
current_episode_number = (
fastanime_runtime_state.provider_current_episode_number
)
config.update_watch_history(anime_id_anilist, str(current_episode_number))
# update episode progress
if config.user and current_episode_number:
AniList.update_anime_list(
{
"mediaId": anime_id_anilist,
"progress": current_episode_number,
}
)
# get them juicy streams
episode_streams = anime_provider.get_episode_streams(
provider_anime,
current_episode_number,
translation_type,
fastanime_runtime_state.selected_anime_anilist,
)
if not episode_streams:
self.mpv_player.show_text("No streams were found")
return None
# always select the first
if server == "top":
selected_server = next(episode_streams, None)
if not selected_server:
self.mpv_player.show_text("Sth went wrong when loading the episode")
return
else:
episode_streams_dict = {
episode_stream["server"]: episode_stream
for episode_stream in episode_streams
}
selected_server = episode_streams_dict.get(server)
if selected_server is None:
self.mpv_player.show_text(
f"Invalid server!!; servers available are: {episode_streams_dict.keys()}",
)
return None
self.current_media_title = selected_server["episode_title"]
links = selected_server["links"]
stream_link_ = filter_by_quality(quality, links)
if not stream_link_:
self.mpv_player.show_text("Quality not found")
return
self.mpv_player._set_property("start", "0")
stream_link = stream_link_["link"]
return stream_link
def create_player(
self,
stream_link,
anime_provider: "AnimeProvider",
fastanime_runtime_state,
config: "Config",
title,
):
self.anime_provider = anime_provider
self.fastanime_runtime_state = fastanime_runtime_state
self.config = config
self.last_stop_time: str = "0"
self.last_total_time: str = "0"
self.last_stop_time_secs = 0
self.last_total_time_secs = 0
self.current_media_title = ""
mpv_player = mpv.MPV(
log_handler=print,
loglevel="error",
config=True,
input_default_bindings=True,
input_vo_keyboard=True,
osc=True,
ytdl=True,
)
mpv_player.force_window = config.force_window
# mpv_player.cache = "yes"
# mpv_player.cache_pause = "no"
mpv_player.title = title
mpv_player.play(stream_link)
# -- events --
@mpv_player.event_callback("file-loaded")
def set_total_time(event, *args):
d = mpv_player._get_property("duration")
self.player_fetching = False
if isinstance(d, float):
self.last_total_time = format_time(d)
@mpv_player.property_observer("time-pos")
def handle_time_start_update(*args):
if len(args) > 1:
value = args[1]
if value is not None:
self.last_stop_time = format_time(value)
@mpv_player.property_observer("time-remaining")
def handle_time_remaining_update(
property, time_remaining: float | None = None, *args
):
if time_remaining is not None:
if time_remaining < 1 and config.auto_next and not self.player_fetching:
print("Auto Fetching Next Episode")
self.player_fetching = True
url = self.get_episode("next")
if url:
mpv_player.loadfile(
url,
)
mpv_player.title = self.current_media_title
# -- keybindings --
@mpv_player.on_key_press("shift+n")
def _next_episode():
url = self.get_episode("next")
if url:
mpv_player.loadfile(url, options=f"title={self.current_media_title}")
mpv_player.title = self.current_media_title
@mpv_player.on_key_press("shift+p")
def _previous_episode():
url = self.get_episode("previous")
if url:
mpv_player.loadfile(
url,
)
mpv_player.title = self.current_media_title
@mpv_player.on_key_press("shift+a")
def _toggle_auto_next():
config.auto_next = not config.auto_next
if config.auto_next:
mpv_player.show_text("Auto next enabled")
else:
mpv_player.show_text("Auto next disabled")
@mpv_player.on_key_press("shift+t")
def _toggle_translation_type():
translation_type = "sub" if config.translation_type == "dub" else "dub"
mpv_player.show_text("Changing translation type...")
anime = anime_provider.get_anime(
fastanime_runtime_state.provider_anime_search_result["id"],
fastanime_runtime_state.selected_anime_anilist,
)
if not anime:
mpv_player.show_text("Failed to update translation type")
return
fastanime_runtime_state.provider_available_episodes = anime[
"availableEpisodesDetail"
][translation_type]
config.translation_type = translation_type
if config.translation_type == "dub":
mpv_player.show_text("Translation Type set to dub")
else:
mpv_player.show_text("Translation Type set to sub")
@mpv_player.on_key_press("shift+r")
def _reload():
url = self.get_episode("reload")
if url:
mpv_player.loadfile(
url,
)
mpv_player.title = self.current_media_title
# -- script messages --
@mpv_player.message_handler("select-episode")
def select_episode(episode: bytes | None = None, *args):
if not episode:
mpv_player.show_text("No episode was selected")
return
url = self.get_episode("custom", episode.decode())
if url:
mpv_player.loadfile(
url,
)
mpv_player.title = self.current_media_title
@mpv_player.message_handler("select-server")
def select_server(server: bytes | None = None, *args):
if not server:
mpv_player.show_text("No server was selected")
return
url = self.get_episode("reload", server=server.decode())
if url:
mpv_player.loadfile(
url,
)
mpv_player.title = self.current_media_title
else:
pass
@mpv_player.message_handler("select-quality")
def select_quality(quality_raw: bytes | None = None, *args):
if not quality_raw:
mpv_player.show_text("No quality was selected")
return
q = ["360", "720", "1080"]
quality = quality_raw.decode()
links: list = fastanime_runtime_state.provider_server_episode_streams
q = [link["quality"] for link in links]
if quality in q:
config.quality = quality
stream_link_ = filter_by_quality(quality, links)
if not stream_link_:
mpv_player.show_text("Quality not found")
return
mpv_player.show_text(f"Changing to stream of quality {quality}")
stream_link = stream_link_["link"]
mpv_player.loadfile(stream_link)
else:
mpv_player.show_text(f"invalid quality!! Valid quality includes: {q}")
# -- events --
mpv_player.observe_property("time-pos", handle_time_start_update)
mpv_player.observe_property("time-remaining", handle_time_remaining_update)
mpv_player.register_event_callback(set_total_time)
# --script-messages --
mpv_player.register_message_handler("select-episode", select_episode)
mpv_player.register_message_handler("select-server", select_server)
mpv_player.register_message_handler("select-quality", select_quality)
self.mpv_player = mpv_player
return mpv_player
player = MpvPlayer()

View File

@@ -1,27 +0,0 @@
import shutil
import subprocess
import requests
def print_img(url: str):
"""helper funtion to print an image given its url
Args:
url: [TODO:description]
"""
if EXECUTABLE := shutil.which("icat"):
subprocess.run([EXECUTABLE, url])
else:
EXECUTABLE = shutil.which("chafa")
if EXECUTABLE is None:
print("chafanot found")
return
res = requests.get(url)
if res.status_code != 200:
print("Error fetching image")
return
img_bytes = res.content
subprocess.run([EXECUTABLE, url, "--size=15x15"], input=img_bytes)

View File

@@ -1,49 +0,0 @@
# TODO: add typing
class FastAnimeRuntimeState(dict):
"""A class that manages fastanime runtime during anilist command runtime"""
def __getattr__(self, attr):
try:
return self.__getitem__(attr)
except KeyError:
raise AttributeError(
"%r object has no attribute %r" % (self.__class__.__name__, attr)
)
def __setattr__(self, attr, value):
self.__setitem__(attr, value)
def exit_app(exit_code=0, *args):
import os
import shutil
import sys
from ...constants import APP_NAME, ICON_PATH, USER_NAME
def is_running_in_terminal():
try:
shutil.get_terminal_size()
return (
sys.stdin
and sys.stdin.isatty()
and sys.stdout.isatty()
and os.getenv("TERM") is not None
)
except OSError:
return False
if not is_running_in_terminal():
from plyer import notification
notification.notify(
app_name=APP_NAME,
app_icon=ICON_PATH,
message=f"Have a good day {USER_NAME}",
title="Shutting down",
) # pyright:ignore
else:
from rich import print
print("Have a good day :smile:", USER_NAME)
sys.exit(exit_code)

View File

@@ -1,112 +0,0 @@
import logging
from typing import TYPE_CHECKING
from InquirerPy import inquirer
logger = logging.getLogger(__name__)
if TYPE_CHECKING:
from ...libs.anime_provider.types import EpisodeStream
# Define ANSI escape codes as constants
RESET = "\033[0m"
BOLD = "\033[1m"
INVISIBLE_CURSOR = "\033[?25l"
VISIBLE_CURSOR = "\033[?25h"
UNDERLINE = "\033[4m"
# ESC[38;2;{r};{g};{b}m
BG_GREEN = "\033[48;2;120;233;12;m"
GREEN = "\033[38;2;45;24;45;m"
def filter_by_quality(quality: str, stream_links: "list[EpisodeStream]", default=True):
"""Helper function used to filter a list of EpisodeStream objects to one that has a corresponding quality
Args:
quality: the quality to use
stream_links: a list of EpisodeStream objects
Returns:
an EpisodeStream object or None incase the quality was not found
"""
for stream_link in stream_links:
q = float(quality)
Q = float(stream_link["quality"])
# some providers have inaccurate eg qualities 718 instead of 720
if Q < q + 80 and Q > q - 80:
return stream_link
else:
if stream_links and default:
try:
print("Qualities were: ", stream_links)
print("Using default of quality: ", stream_links[0]["quality"])
return stream_links[0]
except Exception as e:
print(e)
return
def format_bytes_to_human(num_of_bytes: float, suffix: str = "B"):
"""Helper function usedd to format bytes to human
Args:
num_of_bytes: the number of bytes to format
suffix: the suffix to use
Returns:
formated bytes
"""
for unit in ("", "K", "M", "G", "T", "P", "E", "Z"):
if abs(num_of_bytes) < 1024.0:
return f"{num_of_bytes:3.1f}{unit}{suffix}"
num_of_bytes /= 1024.0
return f"{num_of_bytes:.1f}Yi{suffix}"
def get_true_fg(string: str, r: int, g: int, b: int, bold: bool = True) -> str:
"""Custom helper function that enables colored text in the terminal
Args:
bold: whether to bolden the text
string: string to color
r: red
g: green
b: blue
Returns:
colored string
"""
# NOTE: Currently only supports terminals that support true color
if bold:
return f"{BOLD}\033[38;2;{r};{g};{b};m{string}{RESET}"
else:
return f"\033[38;2;{r};{g};{b};m{string}{RESET}"
def get_true_bg(string, r: int, g: int, b: int) -> str:
return f"\033[48;2;{r};{g};{b};m{string}{RESET}"
def fuzzy_inquirer(choices: list, prompt: str, **kwargs):
"""helper function that enables easier interaction with InquirerPy lib
Args:
choices: the choices to prompt
prompt: the prompt string to use
**kwargs: other options to pass to fuzzy_inquirer
Returns:
a choice
"""
from click import clear
clear()
action = inquirer.fuzzy(
prompt,
choices,
height="100%",
border=True,
validate=lambda result: result in choices,
**kwargs,
).execute()
return action

View File

@@ -1,84 +0,0 @@
import os
import sys
from pathlib import Path
from platform import system
from . import APP_NAME, AUTHOR, __version__
PLATFORM = system()
# ---- app deps ----
APP_DIR = os.path.abspath(os.path.dirname(__file__))
CONFIGS_DIR = os.path.join(APP_DIR, "configs")
ASSETS_DIR = os.path.join(APP_DIR, "assets")
# --- icon stuff ---
if PLATFORM == "Windows":
ICON_PATH = os.path.join(ASSETS_DIR, "logo.ico")
else:
ICON_PATH = os.path.join(ASSETS_DIR, "logo.png")
PREVIEW_IMAGE = os.path.join(ASSETS_DIR, "preview")
# ----- user configs and data -----
S_PLATFORM = sys.platform
if S_PLATFORM == "win32":
# app data
app_data_dir_base = os.getenv("LOCALAPPDATA")
if not app_data_dir_base:
raise RuntimeError("Could not determine app data dir please report to devs")
APP_DATA_DIR = os.path.join(app_data_dir_base, AUTHOR, APP_NAME)
# cache dir
APP_CACHE_DIR = os.path.join(APP_DATA_DIR, "cache")
# videos dir
video_dir_base = os.path.expanduser("~/Videos")
USER_VIDEOS_DIR = os.path.join(video_dir_base, APP_NAME)
elif S_PLATFORM == "darwin":
# app data
app_data_dir_base = os.path.expanduser("~/Library/Application Support")
APP_DATA_DIR = os.path.join(app_data_dir_base, APP_NAME, __version__)
# cache dir
cache_dir_base = os.path.expanduser("~/Library/Caches")
APP_CACHE_DIR = os.path.join(cache_dir_base, APP_NAME, __version__)
# videos dir
video_dir_base = os.path.expanduser("~/Movies")
USER_VIDEOS_DIR = os.path.join(video_dir_base, APP_NAME)
else:
# app data
app_data_dir_base = os.environ.get("XDG_CONFIG_HOME", "")
if not app_data_dir_base.strip():
app_data_dir_base = os.path.expanduser("~/.config")
APP_DATA_DIR = os.path.join(app_data_dir_base, APP_NAME)
# cache dir
cache_dir_base = os.environ.get("XDG_CACHE_HOME", "")
if not cache_dir_base.strip():
cache_dir_base = os.path.expanduser("~/.cache")
APP_CACHE_DIR = os.path.join(cache_dir_base, APP_NAME)
# videos dir
video_dir_base = os.environ.get("XDG_VIDEOS_DIR", "")
if not video_dir_base.strip():
video_dir_base = os.path.expanduser("~/Videos")
USER_VIDEOS_DIR = os.path.join(video_dir_base, APP_NAME)
# ensure paths exist
Path(APP_DATA_DIR).mkdir(parents=True, exist_ok=True)
Path(APP_CACHE_DIR).mkdir(parents=True, exist_ok=True)
Path(USER_VIDEOS_DIR).mkdir(parents=True, exist_ok=True)
# useful paths
USER_DATA_PATH = os.path.join(APP_DATA_DIR, "user_data.json")
USER_CONFIG_PATH = os.path.join(APP_DATA_DIR, "config.ini")
NOTIFIER_LOG_FILE_PATH = os.path.join(APP_DATA_DIR, "notifier.log")
USER_NAME = os.environ.get("USERNAME", "Anime fun")

View File

@@ -1,3 +0,0 @@
"""
his module contains an abstraction for interaction with the anilist api making it easy and efficient
"""

View File

@@ -1,405 +0,0 @@
"""
This is the core module availing all the abstractions of the anilist api
"""
import logging
from typing import TYPE_CHECKING
import requests
from .queries_graphql import (
airing_schedule_query,
anime_characters_query,
anime_query,
anime_relations_query,
delete_list_entry_query,
get_logged_in_user_query,
get_medialist_item_query,
media_list_mutation,
media_list_query,
most_favourite_query,
most_popular_query,
most_recently_updated_query,
most_scored_query,
notification_query,
recommended_query,
search_query,
trending_query,
upcoming_anime_query,
)
if TYPE_CHECKING:
from .types import (
AnilistDataSchema,
AnilistMediaLists,
AnilistMediaListStatus,
AnilistNotifications,
AnilistUser,
AnilistUserData,
)
logger = logging.getLogger(__name__)
ANILIST_ENDPOINT = "https://graphql.anilist.co"
class AniListApi:
"""An abstraction over the anilist api offering an easy and simple interface
Attributes:
session: [TODO:attribute]
session: [TODO:attribute]
token: [TODO:attribute]
headers: [TODO:attribute]
user_id: [TODO:attribute]
token: [TODO:attribute]
headers: [TODO:attribute]
user_id: [TODO:attribute]
"""
session: requests.Session
def __init__(self) -> None:
self.session = requests.session()
def login_user(self, token: str):
"""methosd used to login a new user enabling authenticated requests
Args:
token: anilist app token
Returns:
the logged in user
"""
self.token = token
self.headers = {"Authorization": f"Bearer {self.token}"}
self.session.headers.update(self.headers)
success, user = self.get_logged_in_user()
if not user:
return
if not success or not user:
return
user_info: AnilistUser = user["data"]["Viewer"]
self.user_id = user_info["id"]
return user_info
def get_notification(
self,
) -> tuple[bool, "AnilistNotifications"] | tuple[bool, None]:
"""get the top five latest notifications for anime thats airing
Returns:
airing notifications
"""
return self._make_authenticated_request(notification_query)
def update_login_info(self, user: "AnilistUser", token: str):
"""method used to login a user enabling authenticated requests
Args:
user: an anilist user object
token: the login token
"""
self.token = token
self.headers = {"Authorization": f"Bearer {self.token}"}
self.session.headers.update(self.headers)
self.user_id = user["id"]
def get_logged_in_user(self) -> tuple[bool, "AnilistUserData"] | tuple[bool, None]:
"""get the details of the user who is currently logged in
Returns:
an anilist user
"""
if not self.headers:
return (False, None)
return self._make_authenticated_request(get_logged_in_user_query)
def update_anime_list(self, values_to_update: dict):
"""a powerful method for managing mediaLists giving full power to the user
Args:
values_to_update: a dict containing valid media list options
Returns:
an anilist object indicating success
"""
variables = {"userId": self.user_id, **values_to_update}
return self._make_authenticated_request(media_list_mutation, variables)
def get_anime_list(
self,
status: "AnilistMediaListStatus",
type="ANIME",
) -> tuple[bool, "AnilistMediaLists"] | tuple[bool, None]:
"""gets an anime list from your media list given the list status
Args:
status: the mediaListStatus of the anime list
Returns:
a media list
"""
variables = {"status": status, "userId": self.user_id, "type": type}
return self._make_authenticated_request(media_list_query, variables)
def get_medialist_entry(
self, mediaId: int
) -> tuple[bool, dict] | tuple[bool, None]:
"""Get the id entry of the items in an Anilist MediaList
Args:
mediaId: The mediaList item entry mediaId
Returns:
a boolean indicating whether the request succeeded and either a dict object containing the id of the media list entry
"""
variables = {"mediaId": mediaId}
return self._make_authenticated_request(get_medialist_item_query, variables)
def delete_medialist_entry(self, mediaId: int):
"""Deletes a mediaList item given its mediaId
Args:
mediaId: the media id of the anime
Returns:
a tuple containing a boolean whether the operation was successful and either an anilist object or none depending on success
"""
result = self.get_medialist_entry(mediaId)
data = result[1]
if not result[0] or not data:
return result
id = data["data"]["MediaList"]["id"]
variables = {"id": id}
return self._make_authenticated_request(delete_list_entry_query, variables)
# TODO: unify the _make_authenticated_request with original since sessions are now in use
def _make_authenticated_request(self, query: str, variables: dict = {}):
"""the abstraction over all authenticated requests
Args:
query: the anilist query to make
variables: the anilist variables to use
Returns:
an anilist object containing the queried data or none and a boolean indicating whether the request was successful
"""
try:
response = self.session.post(
ANILIST_ENDPOINT,
json={"query": query, "variables": variables},
timeout=10,
headers=self.headers,
)
anilist_data = response.json()
# ensuring you dont get blocked
if (
int(response.headers.get("X-RateLimit-Remaining", 0)) < 30
and not response.status_code == 500
):
print(
"Warning you are exceeding the allowed number of calls per minute"
)
logger.warning(
"You are exceeding the allowed number of calls per minute for the AniList api enforcing timeout"
)
print("Forced timeout will now be initiated")
import time
print("sleeping...")
time.sleep(1 * 60)
if response.status_code == 200:
return (True, anilist_data)
else:
return (False, anilist_data)
except requests.exceptions.Timeout:
logger.warning(
"Timeout has been exceeded this could mean anilist is down or you have lost internet connection"
)
return (False, None)
except requests.exceptions.ConnectionError:
logger.warning(
"ConnectionError this could mean anilist is down or you have lost internet connection"
)
return (False, None)
except Exception as e:
logger.error(f"Something unexpected occured {e}")
return (False, None) # type: ignore
def get_data(
self, query: str, variables: dict = {}
) -> tuple[bool, "AnilistDataSchema"]:
"""the abstraction over all none authenticated requests and that returns data of a similar type
Args:
query: the anilist query
variables: the anilist api variables
Returns:
a boolean indicating success and none or an anilist object depending on success
"""
try:
response = self.session.post(
ANILIST_ENDPOINT,
json={"query": query, "variables": variables},
timeout=10,
)
anilist_data: AnilistDataSchema = response.json()
# ensuring you dont get blocked
if (
int(response.headers.get("X-RateLimit-Remaining", 0)) < 30
and not response.status_code == 500
):
print(
"Warning you are exceeding the allowed number of calls per minute"
)
logger.warning(
"You are exceeding the allowed number of calls per minute for the AniList api enforcing timeout"
)
print("Forced timeout will now be initiated")
import time
print("sleeping...")
time.sleep(1 * 60)
if response.status_code == 200:
return (True, anilist_data)
else:
return (False, anilist_data)
except requests.exceptions.Timeout:
logger.warning(
"Timeout has been exceeded this could mean anilist is down or you have lost internet connection"
)
return (
False,
{
"Error": "Timeout Exceeded for connection there might be a problem with your internet or anilist is down."
},
) # type: ignore
except requests.exceptions.ConnectionError:
logger.warning(
"ConnectionError this could mean anilist is down or you have lost internet connection"
)
return (
False,
{
"Error": "There might be a problem with your internet or anilist is down."
},
) # type: ignore
except Exception as e:
logger.error(f"Something unexpected occured {e}")
return (False, {"Error": f"{e}"}) # type: ignore
def search(
self,
query: str | None = None,
sort: str | None = None,
genre_in: list[str] | None = None,
id_in: list[int] | None = None,
genre_not_in: list[str] = ["hentai"],
popularity_greater: int | None = None,
popularity_lesser: int | None = None,
averageScore_greater: int | None = None,
averageScore_lesser: int | None = None,
tag_in: list[str] | None = None,
tag_not_in: list[str] | None = None,
status: str | None = None,
status_in: list[str] | None = None,
status_not_in: list[str] | None = None,
endDate_greater: int | None = None,
endDate_lesser: int | None = None,
start_greater: int | None = None,
start_lesser: int | None = None,
page: int | None = None,
type="ANIME",
**kwargs,
):
"""
A powerful method abstracting all of anilist media queries
"""
variables = {}
for key, val in list(locals().items())[1:]:
if val is not None and key not in ["variables"]:
variables[key] = val
search_results = self.get_data(search_query, variables=variables)
return search_results
def get_anime(self, id: int):
"""
Gets a single anime by a valid anilist anime id
"""
variables = {"id": id}
return self.get_data(anime_query, variables)
def get_trending(self, type="ANIME", *_, **kwargs):
"""
Gets the currently trending anime
"""
variables = {"type": type}
trending = self.get_data(trending_query, variables)
return trending
def get_most_favourite(self, type="ANIME", *_, **kwargs):
"""
Gets the most favoured anime on anilist
"""
variables = {"type": type}
most_favourite = self.get_data(most_favourite_query, variables)
return most_favourite
def get_most_scored(self, type="ANIME", *_, **kwargs):
"""
Gets most scored anime on anilist
"""
variables = {"type": type}
most_scored = self.get_data(most_scored_query, variables)
return most_scored
def get_most_recently_updated(self, type="ANIME", *_, **kwargs):
"""
Gets most recently updated anime from anilist
"""
variables = {"type": type}
most_recently_updated = self.get_data(most_recently_updated_query, variables)
return most_recently_updated
def get_most_popular(
self,
type="ANIME",
):
"""
Gets most popular anime on anilist
"""
variables = {"type": type}
most_popular = self.get_data(most_popular_query, variables)
return most_popular
def get_upcoming_anime(self, type="ANIME", page: int = 1, *_, **kwargs):
"""
Gets upcoming anime from anilist
"""
variables = {"page": page, "type": type}
upcoming_anime = self.get_data(upcoming_anime_query, variables)
return upcoming_anime
# NOTE: THe following methods will probably be scraped soon
def get_recommended_anime_for(self, id: int, type="ANIME", *_, **kwargs):
variables = {"type": type}
recommended_anime = self.get_data(recommended_query, variables)
return recommended_anime
def get_charcters_of(self, id: int, type="ANIME", *_, **kwargs):
variables = {"id": id}
characters = self.get_data(anime_characters_query, variables)
return characters
def get_related_anime_for(self, id: int, type="ANIME", *_, **kwargs):
variables = {"id": id}
related_anime = self.get_data(anime_relations_query, variables)
return related_anime
def get_airing_schedule_for(self, id: int, type="ANIME", *_, **kwargs):
variables = {"id": id}
airing_schedule = self.get_data(airing_schedule_query, variables)
return airing_schedule

View File

@@ -1,980 +0,0 @@
"""
This module contains all the preset queries for the sake of neatness and convinience
Mostly for internal usage
"""
# TODO: Format the queries
mark_as_read_mutation = """
mutation{
UpdateUser{
unreadNotificationCount
}
}
"""
reviews_query = """
query($id:Int){
Page{
pageInfo{
total
}
reviews(mediaId:$id){
summary
user{
name
avatar {
large
medium
}
}
body
}
}
}
"""
notification_query = """
query{
Page(perPage:5){
pageInfo {
total
}
notifications(resetNotificationCount:true,type:AIRING) {
... on AiringNotification {
id
type
episode
contexts
createdAt
media {
id
idMal
title {
romaji
english
}
coverImage{
medium
}
}
}
}
}
}
"""
get_medialist_item_query = """
query($mediaId:Int){
MediaList(mediaId:$mediaId){
id
}
}
"""
delete_list_entry_query = """
mutation($id:Int){
DeleteMediaListEntry(id:$id){
deleted
}
}
"""
get_logged_in_user_query = """
query{
Viewer{
id
name
bannerImage
avatar {
large
medium
}
}
}
"""
media_list_mutation = """
mutation($mediaId:Int,$scoreRaw:Int,$repeat:Int,$progress:Int,$status:MediaListStatus){
SaveMediaListEntry(mediaId:$mediaId,scoreRaw:$scoreRaw,progress:$progress,repeat:$repeat,status:$status){
id
status
mediaId
score
progress
repeat
startedAt {
year
month
day
}
completedAt {
year
month
day
}
}
}
"""
media_list_query = """
query ($userId: Int, $status: MediaListStatus,$type:MediaType) {
Page {
pageInfo {
currentPage
total
}
mediaList(userId: $userId, status: $status, type: $type) {
mediaId
media {
id
idMal
title {
romaji
english
}
coverImage {
medium
large
}
trailer {
site
id
}
popularity
favourites
averageScore
episodes
genres
studios {
nodes {
name
isAnimationStudio
}
}
tags {
name
}
startDate {
year
month
day
}
endDate {
year
month
day
}
status
description
mediaListEntry{
id
progress
}
nextAiringEpisode {
timeUntilAiring
airingAt
episode
}
}
status
progress
score
repeat
notes
startedAt {
year
month
day
}
completedAt {
year
month
day
}
createdAt
}
}
}
"""
optional_variables = "\
$page:Int,\
$sort:[MediaSort],\
$id_in:[Int],\
$genre_in:[String],\
$genre_not_in:[String],\
$tag_in:[String],\
$tag_not_in:[String],\
$status_in:[MediaStatus],\
$status:MediaStatus,\
$status_not_in:[MediaStatus],\
$popularity_greater:Int,\
$popularity_lesser:Int,\
$averageScore_greater:Int,\
$averageScore_lesser:Int,\
$startDate_greater:FuzzyDateInt,\
$startDate_lesser:FuzzyDateInt,\
$endDate_greater:FuzzyDateInt,\
$endDate_lesser:FuzzyDateInt,\
$type:MediaType\
"
# FuzzyDateInt = (yyyymmdd)
# MediaStatus = (FINISHED,RELEASING,NOT_YET_RELEASED,CANCELLED,HIATUS)
search_query = (
"""
query($query:String,%s){
Page(perPage:30,page:$page){
pageInfo{
total
currentPage
hasNextPage
}
media(
search:$query,
id_in:$id_in,
genre_in:$genre_in,
genre_not_in:$genre_not_in,
tag_in:$tag_in,
tag_not_in:$tag_not_in,
status_in:$status_in,
status:$status,
status_not_in:$status_not_in,
popularity_greater:$popularity_greater,
popularity_lesser:$popularity_lesser,
averageScore_greater:$averageScore_greater,
averageScore_lesser:$averageScore_lesser,
startDate_greater:$startDate_greater,
startDate_lesser:$startDate_lesser,
endDate_greater:$endDate_greater,
endDate_lesser:$endDate_lesser,
sort:$sort,
type:$type
)
{
id
idMal
title{
romaji
english
}
coverImage{
medium
large
}
trailer {
site
id
}
mediaListEntry{
id
progress
}
popularity
favourites
averageScore
episodes
genres
studios{
nodes{
name
isAnimationStudio
}
}
tags {
name
}
startDate {
year
month
day
}
endDate {
year
month
day
}
status
description
nextAiringEpisode {
timeUntilAiring
airingAt
episode
}
}
}
}
"""
% optional_variables
)
trending_query = """
query($type:MediaType){
Page(perPage:15){
media(sort:TRENDING_DESC,type:$type,genre_not_in:["hentai"]){
id
idMal
title{
romaji
english
}
coverImage{
medium
large
}
trailer {
site
id
}
popularity
favourites
averageScore
genres
episodes
description
studios {
nodes {
name
isAnimationStudio
}
}
tags {
name
}
startDate {
year
month
day
}
mediaListEntry{
id
progress
}
endDate {
year
month
day
}
status
nextAiringEpisode {
timeUntilAiring
airingAt
episode
}
}
}
}
"""
# mosts
most_favourite_query = """
query($type:MediaType){
Page(perPage:15){
media(sort:FAVOURITES_DESC,type:$type,genre_not_in:["hentai"]){
id
idMal
title{
romaji
english
}
coverImage{
medium
large
}
trailer {
site
id
}
mediaListEntry{
id
progress
}
popularity
favourites
averageScore
episodes
description
genres
studios {
nodes {
name
isAnimationStudio
}
}
tags {
name
}
startDate {
year
month
day
}
endDate {
year
month
day
}
status
nextAiringEpisode {
timeUntilAiring
airingAt
episode
}
}
}
}
"""
most_scored_query = """
query($type:MediaType){
Page(perPage:15){
media(sort:SCORE_DESC,type:$type,genre_not_in:["hentai"]){
id
idMal
title{
romaji
english
}
coverImage{
medium
large
}
trailer {
site
id
}
mediaListEntry{
id
progress
}
popularity
episodes
favourites
averageScore
description
genres
studios {
nodes {
name
isAnimationStudio
}
}
tags {
name
}
startDate {
year
month
day
}
endDate {
year
month
day
}
status
nextAiringEpisode {
timeUntilAiring
airingAt
episode
}
}
}
}
"""
most_popular_query = """
query($type:MediaType){
Page(perPage:15){
media(sort:POPULARITY_DESC,type:$type,genre_not_in:["hentai"]){
id
idMal
title{
romaji
english
}
coverImage{
medium
large
}
trailer {
site
id
}
popularity
favourites
averageScore
description
episodes
genres
mediaListEntry{
id
progress
}
studios {
nodes {
name
isAnimationStudio
}
}
tags {
name
}
startDate {
year
month
day
}
endDate {
year
month
day
}
status
nextAiringEpisode {
timeUntilAiring
airingAt
episode
}
}
}
}
"""
most_recently_updated_query = """
query($type:MediaType){
Page(perPage:15){
media(sort:UPDATED_AT_DESC,type:$type,averageScore_greater:50,genre_not_in:["hentai"],status:RELEASING){
id
idMal
title{
romaji
english
}
coverImage{
medium
large
}
trailer {
site
id
}
mediaListEntry{
id
progress
}
popularity
favourites
averageScore
description
genres
episodes
studios {
nodes {
name
isAnimationStudio
}
}
tags {
name
}
startDate {
year
month
day
}
endDate {
year
month
day
}
status
nextAiringEpisode {
timeUntilAiring
airingAt
episode
}
}
}
}
"""
recommended_query = """
query($type:MediaType){
Page(perPage:15) {
media( type: $type,genre_not_in:["hentai"]) {
recommendations(sort:RATING_DESC){
nodes{
media{
id
idMal
title{
english
romaji
native
}
coverImage{
medium
large
}
mediaListEntry{
id
progress
}
description
episodes
trailer{
site
id
}
genres
averageScore
popularity
favourites
tags {
name
}
startDate {
year
month
day
}
endDate {
year
month
day
}
status
nextAiringEpisode {
timeUntilAiring
airingAt
episode
}
}
}
}
}
}
}
"""
anime_characters_query = """
query($id:Int,$type:MediaType){
Page {
media(id:$id, type: $type) {
characters {
nodes {
name {
first
middle
last
full
native
}
image {
medium
large
}
description
gender
dateOfBirth {
year
month
day
}
age
bloodType
favourites
}
}
}
}
}
"""
anime_relations_query = """
query ($id: Int,$type:MediaType) {
Page(perPage: 20) {
media(id: $id, sort: POPULARITY_DESC, type: $type,genre_not_in:["hentai"]) {
relations {
nodes {
id
idMal
title {
english
romaji
native
}
coverImage {
medium
large
}
mediaListEntry{
id
progress
}
description
episodes
trailer {
site
id
}
genres
averageScore
popularity
favourites
tags {
name
}
startDate {
year
month
day
}
endDate {
year
month
day
}
status
nextAiringEpisode {
timeUntilAiring
airingAt
episode
}
}
}
}
}
}
"""
airing_schedule_query = """
query ($id: Int,$type:MediaType) {
Page {
media(id: $id, sort: POPULARITY_DESC, type: $type) {
airingSchedule(notYetAired:true){
nodes{
airingAt
timeUntilAiring
episode
}
}
}
}
}
"""
upcoming_anime_query = """
query ($page: Int,$type:MediaType) {
Page(page: $page) {
pageInfo {
total
perPage
currentPage
hasNextPage
}
media(type: $type, status: NOT_YET_RELEASED,sort:POPULARITY_DESC,genre_not_in:["hentai"]) {
id
idMal
title {
romaji
english
}
coverImage {
medium
large
}
trailer {
site
id
}
mediaListEntry{
id
progress
}
popularity
favourites
averageScore
genres
episodes
description
studios {
nodes {
name
isAnimationStudio
}
}
tags {
name
}
startDate {
year
month
day
}
endDate {
year
month
day
}
status
nextAiringEpisode {
timeUntilAiring
airingAt
episode
}
}
}
}
"""
anime_query = """
query($id:Int){
Page{
media(id:$id) {
id
idMal
title {
romaji
english
}
mediaListEntry{
id
progress
}
nextAiringEpisode {
timeUntilAiring
airingAt
episode
}
coverImage {
extraLarge
}
characters(perPage: 5, sort: FAVOURITES_DESC) {
edges {
node {
name {
full
}
gender
dateOfBirth {
year
month
day
}
age
image {
medium
large
}
description
}
voiceActors {
name {
full
}
image {
medium
large
}
}
}
}
studios {
nodes {
name
isAnimationStudio
}
}
season
format
status
seasonYear
description
genres
synonyms
startDate {
year
month
day
}
endDate {
year
month
day
}
duration
countryOfOrigin
averageScore
popularity
favourites
source
hashtag
siteUrl
tags {
name
rank
}
reviews(sort: SCORE_DESC, perPage: 3) {
nodes {
summary
user {
name
avatar {
medium
large
}
}
}
}
recommendations(sort: RATING_DESC, perPage: 10) {
nodes {
mediaRecommendation {
title {
romaji
english
}
}
}
}
relations {
nodes {
title {
romaji
english
native
}
}
}
externalLinks {
url
site
icon
}
rankings {
rank
context
}
bannerImage
episodes
}
}
}
"""

View File

@@ -1,12 +0,0 @@
anime_sources = {
"allanime": "api.AllAnimeAPI",
"animepahe": "api.AnimePaheApi",
}
SERVERS_AVAILABLE = [
"sharepoint",
"dropbox",
"gogoanime",
"weTransfer",
"wixmp",
"kwik",
]

View File

@@ -1,377 +0,0 @@
"""a module that handles the scraping of allanime
abstraction over allanime api
"""
import json
import logging
from typing import TYPE_CHECKING
from requests.exceptions import Timeout
from ...anime_provider.base_provider import AnimeProvider
from ..utils import decode_hex_string, give_random_quality
from .constants import (
ALLANIME_API_ENDPOINT,
ALLANIME_BASE,
ALLANIME_REFERER,
USER_AGENT,
)
from .gql_queries import ALLANIME_EPISODES_GQL, ALLANIME_SEARCH_GQL, ALLANIME_SHOW_GQL
if TYPE_CHECKING:
from typing import Iterator
from ....libs.anime_provider.allanime.types import AllAnimeEpisode
from ....libs.anime_provider.types import Anime, Server
logger = logging.getLogger(__name__)
# TODO: create tests for the api
#
# ** Based on ani-cli **
class AllAnimeAPI(AnimeProvider):
"""
Provides a fast and effective interface to AllAnime site.
"""
api_endpoint = ALLANIME_API_ENDPOINT
def _fetch_gql(self, query: str, variables: dict):
"""main abstraction over all requests to the allanime api
Args:
query: [TODO:description]
variables: [TODO:description]
Returns:
[TODO:return]
"""
try:
response = self.session.get(
self.api_endpoint,
params={
"variables": json.dumps(variables),
"query": query,
},
headers={"Referer": ALLANIME_REFERER, "User-Agent": USER_AGENT},
timeout=10,
)
if response.status_code == 200:
return response.json()["data"]
else:
logger.error("allanime(ERROR): ", response.text)
return {}
except Timeout:
logger.error(
"allanime(Error):Timeout exceeded this could mean allanime is down or you have lost internet connection"
)
return {}
except Exception as e:
logger.error(f"allanime:Error: {e}")
return {}
def search_for_anime(
self,
user_query: str,
translation_type: str = "sub",
nsfw=True,
unknown=True,
**kwargs,
):
"""search for an anime title using allanime provider
Args:
nsfw ([TODO:parameter]): [TODO:description]
unknown ([TODO:parameter]): [TODO:description]
user_query: [TODO:description]
translation_type: [TODO:description]
**kwargs: [TODO:args]
Returns:
[TODO:return]
"""
search = {"allowAdult": nsfw, "allowUnknown": unknown, "query": user_query}
limit = 40
translationtype = translation_type
countryorigin = "all"
page = 1
variables = {
"search": search,
"limit": limit,
"page": page,
"translationtype": translationtype,
"countryorigin": countryorigin,
}
try:
search_results = self._fetch_gql(ALLANIME_SEARCH_GQL, variables)
page_info = search_results["shows"]["pageInfo"]
results = []
for result in search_results["shows"]["edges"]:
normalized_result = {
"id": result["_id"],
"title": result["name"],
"type": result["__typename"],
"availableEpisodes": result["availableEpisodes"],
}
results.append(normalized_result)
normalized_search_results = {
"pageInfo": page_info,
"results": results,
}
return normalized_search_results
except Exception as e:
logger.error(f"FA(AllAnime): {e}")
return {}
def get_anime(self, allanime_show_id: str):
"""get an anime details given its id
Args:
allanime_show_id: [TODO:description]
Returns:
[TODO:return]
"""
variables = {"showId": allanime_show_id}
try:
anime = self._fetch_gql(ALLANIME_SHOW_GQL, variables)
id: str = anime["show"]["_id"]
title: str = anime["show"]["name"]
availableEpisodesDetail = anime["show"]["availableEpisodesDetail"]
type = anime.get("__typename")
normalized_anime = {
"id": id,
"title": title,
"availableEpisodesDetail": availableEpisodesDetail,
"type": type,
}
return normalized_anime
except Exception as e:
logger.error(f"AllAnime(get_anime): {e}")
return None
def _get_anime_episode(
self, allanime_show_id: str, episode_string: str, translation_type: str = "sub"
) -> "AllAnimeEpisode | dict":
"""get the episode details and sources info
Args:
allanime_show_id: [TODO:description]
episode_string: [TODO:description]
translation_type: [TODO:description]
Returns:
[TODO:return]
"""
variables = {
"showId": allanime_show_id,
"translationType": translation_type,
"episodeString": episode_string,
}
try:
episode = self._fetch_gql(ALLANIME_EPISODES_GQL, variables)
return episode["episode"]
except Exception as e:
logger.error(f"FA(AllAnime): {e}")
return {}
def get_episode_streams(
self, anime: "Anime", episode_number: str, translation_type="sub"
) -> "Iterator[Server] | None":
"""get the streams of an episode
Args:
translation_type ([TODO:parameter]): [TODO:description]
anime: [TODO:description]
episode_number: [TODO:description]
Yields:
[TODO:description]
"""
anime_id = anime["id"]
allanime_episode = self._get_anime_episode(
anime_id, episode_number, translation_type
)
if not allanime_episode:
return []
embeds = allanime_episode["sourceUrls"]
try:
for embed in embeds:
try:
# filter the working streams no need to get all since the others are mostly hsl
# TODO: should i just get all the servers and handle the hsl??
if embed.get("sourceName", "") not in (
"Sak",
"Kir",
"S-mp4",
"Luf-mp4",
"Default",
):
continue
url = embed.get("sourceUrl")
if not url:
continue
if url.startswith("--"):
url = url[2:]
# get the stream url for an episode of the defined source names
parsed_url = decode_hex_string(url)
embed_url = f"https://{ALLANIME_BASE}{parsed_url.replace('clock', 'clock.json')}"
resp = self.session.get(
embed_url,
headers={
"Referer": ALLANIME_REFERER,
"User-Agent": USER_AGENT,
},
timeout=10,
)
if resp.status_code == 200:
match embed["sourceName"]:
case "Luf-mp4":
logger.debug("allanime:Found streams from gogoanime")
yield {
"server": "gogoanime",
"episode_title": (
allanime_episode["notes"] or f'{anime["title"]}'
)
+ f"; Episode {episode_number}",
"links": give_random_quality(resp.json()["links"]),
} # pyright:ignore
case "Kir":
logger.debug("allanime:Found streams from wetransfer")
yield {
"server": "wetransfer",
"episode_title": (
allanime_episode["notes"] or f'{anime["title"]}'
)
+ f"; Episode {episode_number}",
"links": give_random_quality(resp.json()["links"]),
} # pyright:ignore
case "S-mp4":
logger.debug("allanime:Found streams from sharepoint")
yield {
"server": "sharepoint",
"episode_title": (
allanime_episode["notes"] or f'{anime["title"]}'
)
+ f"; Episode {episode_number}",
"links": give_random_quality(resp.json()["links"]),
} # pyright:ignore
case "Sak":
logger.debug("allanime:Found streams from dropbox")
yield {
"server": "dropbox",
"episode_title": (
allanime_episode["notes"] or f'{anime["title"]}'
)
+ f"; Episode {episode_number}",
"links": give_random_quality(resp.json()["links"]),
} # pyright:ignore
case "Default":
logger.debug("allanime:Found streams from wixmp")
yield {
"server": "wixmp",
"episode_title": (
allanime_episode["notes"] or f'{anime["title"]}'
)
+ f"; Episode {episode_number}",
"links": give_random_quality(resp.json()["links"]),
} # pyright:ignore
except Timeout:
logger.error(
"Timeout has been exceeded this could mean allanime is down or you have lost internet connection"
)
return []
except Exception as e:
logger.error(f"FA(Allanime): {e}")
return []
except Exception as e:
logger.error(f"FA(Allanime): {e}")
return []
if __name__ == "__main__":
anime_provider = AllAnimeAPI()
# lets see if it works :)
import subprocess
import sys
from InquirerPy import inquirer, validator
anime = input("Enter the anime name: ")
translation = input("Enter the translation type: ")
search_results = anime_provider.search_for_anime(
anime, translation_type=translation.strip()
)
if not search_results:
raise Exception("No results found")
search_results = search_results["results"]
options = {show["title"]: show for show in search_results}
anime = inquirer.fuzzy(
"Enter the anime title",
list(options.keys()),
validate=validator.EmptyInputValidator(),
).execute()
if anime is None:
print("No anime was selected")
sys.exit(1)
anime_result = options[anime]
anime_data = anime_provider.get_anime(anime_result["id"])
if not anime_data:
raise Exception("Anime not found")
availableEpisodesDetail = anime_data["availableEpisodesDetail"]
if not availableEpisodesDetail.get(translation.strip()):
raise Exception("No episodes found")
stream_link = True
while stream_link != "quit":
print("select episode")
episode = inquirer.fuzzy(
"Choose an episode",
availableEpisodesDetail[translation.strip()],
validate=validator.EmptyInputValidator(),
).execute()
if episode is None:
print("No episode was selected")
sys.exit(1)
if not anime_data:
print("Sth went wrong")
break
episode_streams_ = anime_provider.get_episode_streams(
anime_data, # pyright: ignore
episode,
translation.strip(),
)
if episode_streams_ is None:
raise Exception("Episode not found")
episode_streams = list(episode_streams_)
stream_links = []
for server in episode_streams:
stream_links.extend([link["link"] for link in server["links"]])
stream_links.append("back")
stream_link = inquirer.fuzzy(
"Choose a link to stream",
stream_links,
validate=validator.EmptyInputValidator(),
).execute()
if stream_link == "quit":
print("Have a nice day")
sys.exit()
if not stream_link:
raise Exception("No stream was selected")
title = episode_streams[0].get(
"episode_title", "%s: Episode %s" % (anime_data["title"], episode)
)
subprocess.run(["mpv", f"--title={title}", stream_link])

View File

@@ -1,7 +0,0 @@
from yt_dlp.utils.networking import random_user_agent
ALLANIME_BASE = "allanime.day"
ALLANIME_REFERER = "https://allanime.to/"
ALLANIME_API_ENDPOINT = "https://api.{}/api/".format(ALLANIME_BASE)
USER_AGENT = random_user_agent()
SERVERS_AVAILABLE = ["sharepoint", "dropbox", "gogoanime", "weTransfer", "wixmp"]

View File

@@ -1,56 +0,0 @@
ALLANIME_SEARCH_GQL = """
query(
$search: SearchInput
$limit: Int
$page: Int
$translationType: VaildTranslationTypeEnumType
$countryOrigin: VaildCountryOriginEnumType
) {
shows(
search: $search
limit: $limit
page: $page
translationType: $translationType
countryOrigin: $countryOrigin
) {
pageInfo {
total
}
edges {
_id
name
availableEpisodes
__typename
}
}
}
"""
ALLANIME_EPISODES_GQL = """\
query ($showId: String!, $translationType: VaildTranslationTypeEnumType!, $episodeString: String!) {
episode(
showId: $showId
translationType: $translationType
episodeString: $episodeString
) {
episodeString
sourceUrls
notes
}
}"""
ALLANIME_SHOW_GQL = """
query ($showId: String!) {
show(
_id: $showId
) {
_id
name
availableEpisodesDetail
}
}
"""

View File

@@ -1,73 +0,0 @@
from typing import Literal, TypedDict
class AllAnimeEpisodesInfo(TypedDict):
dub: int
sub: int
raw: int
class AllAnimePageInfo(TypedDict):
total: int
class AllAnimeShow(TypedDict):
_id: str
name: str
availableEpisodesDetail: AllAnimeEpisodesInfo
__typename: str
class AllAnimeSearchResult(TypedDict):
_id: str
name: str
availableEpisodes: list[str]
__typename: str | None
class AllAnimeShows(TypedDict):
pageInfo: AllAnimePageInfo
edges: list[AllAnimeSearchResult]
class AllAnimeSearchResults(TypedDict):
shows: AllAnimeShows
class AllAnimeSourcesDownloads(TypedDict):
sourceName: str
dowloadUrl: str
class AllAnimeSources(TypedDict):
sourceUrl: str
priority: float
sandbox: str
sourceName: str
type: str
className: str
streamerId: str
downloads: AllAnimeSourcesDownloads
Server = Literal["gogoanime", "dropbox", "wetransfer", "sharepoint"]
class AllAnimeEpisode(TypedDict):
episodeString: str
sourceUrls: list[AllAnimeSources]
notes: str | None
class AllAnimeStream:
link: str
mp4: bool
hls: bool | None
resolutionStr: str
fromCache: str
priority: int
headers: dict | None
class AllAnimeStreams:
links: list[AllAnimeStream]

View File

@@ -1,252 +0,0 @@
import logging
import random
import re
import shutil
import subprocess
import time
from typing import TYPE_CHECKING
from yt_dlp.utils import (
extract_attributes,
get_element_by_id,
get_element_text_and_html_by_tag,
get_elements_html_by_class,
)
from ..base_provider import AnimeProvider
from .constants import (
ANIMEPAHE_BASE,
ANIMEPAHE_ENDPOINT,
REQUEST_HEADERS,
SERVER_HEADERS,
)
if TYPE_CHECKING:
from ..types import Anime
from .types import AnimePaheAnimePage, AnimePaheSearchPage, AnimeSearchResult
JUICY_STREAM_REGEX = re.compile(r"source='(.*)';")
logger = logging.getLogger(__name__)
# TODO: hack this to completion
class AnimePaheApi(AnimeProvider):
search_page: "AnimePaheSearchPage"
anime: "AnimePaheAnimePage"
def search_for_anime(self, user_query: str, *args):
try:
url = f"{ANIMEPAHE_ENDPOINT}m=search&q={user_query}"
headers = {**REQUEST_HEADERS}
response = self.session.get(url, headers=headers)
if not response.status_code == 200:
return
data: "AnimePaheSearchPage" = response.json()
self.search_page = data
return {
"pageInfo": {
"total": data["total"],
"perPage": data["per_page"],
"currentPage": data["current_page"],
},
"results": [
{
"availableEpisodes": list(range(result["episodes"])),
"id": result["session"],
"title": result["title"],
"type": result["type"],
"year": result["year"],
"score": result["score"],
"status": result["status"],
"season": result["season"],
"poster": result["poster"],
}
for result in data["data"]
],
}
except Exception as e:
logger.error(f"AnimePahe(search): {e}")
return {}
def get_anime(self, session_id: str, *args):
page = 1
try:
anime_result: "AnimeSearchResult" = [
anime
for anime in self.search_page["data"]
if anime["session"] == session_id
][0]
data: "AnimePaheAnimePage" = {} # pyright:ignore
url = f"{ANIMEPAHE_ENDPOINT}m=release&id={session_id}&sort=episode_asc&page={page}"
def _pages_loader(
url,
page,
):
response = self.session.get(url, headers=REQUEST_HEADERS)
if response.status_code == 200:
if not data:
data.update(response.json())
if ep_data := response.json().get("data"):
data["data"].extend(ep_data)
if data["next_page_url"]:
# TODO: Refine this
time.sleep(
random.choice(
[
0.25,
0.1,
0.5,
0.75,
1,
]
)
)
page += 1
url = f"{ANIMEPAHE_ENDPOINT}m=release&id={session_id}&sort=episode_asc&page={page}"
_pages_loader(
url,
page,
)
_pages_loader(
url,
page,
)
if not data:
return {}
self.anime = data # pyright:ignore
episodes = list(map(str, range(data["total"])))
title = ""
return {
"id": session_id,
"title": anime_result["title"],
"year": anime_result["year"],
"season": anime_result["season"],
"poster": anime_result["poster"],
"score": anime_result["score"],
"availableEpisodesDetail": {
"sub": episodes,
"dub": episodes,
"raw": episodes,
},
"episodesInfo": [
{
"title": episode["title"] or f"{title};{episode['episode']}",
"episode": episode["episode"],
"id": episode["session"],
"translation_type": episode["audio"],
"duration": episode["duration"],
"poster": episode["snapshot"],
}
for episode in data["data"]
],
}
except Exception as e:
logger.error(f"AnimePahe(anime): {e}")
return {}
def get_episode_streams(
self, anime: "Anime", episode_number: str, translation_type, *args
):
# extract episode details from memory
episode = [
episode
for episode in self.anime["data"]
if float(episode["episode"]) == float(episode_number)
]
if not episode:
logger.error(f"AnimePahe(streams): episode {episode_number} doesn't exist")
return []
episode = episode[0]
anime_id = anime["id"]
# fetch the episode page
url = f"{ANIMEPAHE_BASE}/play/{anime_id}/{episode['session']}"
response = self.session.get(url, headers=REQUEST_HEADERS)
# get the element containing links to juicy streams
c = get_element_by_id("resolutionMenu", response.text)
resolutionMenuItems = get_elements_html_by_class("dropdown-item", c)
# convert the elements containing embed links to a neat dict containing:
# data-src
# data-audio
# data-resolution
res_dicts = [extract_attributes(item) for item in resolutionMenuItems]
# get the episode title
episode_title = (
episode["title"] or f"{anime['title']}; Episode {episode['episode']}"
)
# get all links
streams = {"server": "kwik", "links": [], "episode_title": episode_title}
for res_dict in res_dicts:
# get embed url
embed_url = res_dict["data-src"]
data_audio = "dub" if res_dict["data-audio"] == "eng" else "sub"
# filter streams by translation_type
if data_audio != translation_type:
continue
if not embed_url:
logger.warn(
"AnimePahe: embed url not found please report to the developers"
)
return []
# get embed page
embed_response = self.session.get(embed_url, headers=SERVER_HEADERS)
embed = embed_response.text
# search for the encoded js
encoded_js = None
for _ in range(7):
content, html = get_element_text_and_html_by_tag("script", embed)
if not content:
embed = embed.replace(html, "")
continue
encoded_js = content
break
if not encoded_js:
logger.warn(
"AnimePahe: Encoded js not found please report to the developers"
)
return []
# execute the encoded js with node for now or maybe forever in odrder to get a more workable info
NODE = shutil.which("node")
if not NODE:
logger.warn(
"AnimePahe: animepahe currently requires node js to extract them juicy streams"
)
return []
result = subprocess.run(
[NODE, "-e", encoded_js],
text=True,
capture_output=True,
)
# decoded js
evaluted_js = result.stderr
if not evaluted_js:
logger.warn(
"AnimePahe: could not decode encoded js using node please report to developers"
)
return []
# get that juicy stream
match = JUICY_STREAM_REGEX.search(evaluted_js)
if not match:
logger.warn(
"AnimePahe: could not find the juicy stream please report to developers"
)
return []
# get the actual hls stream link
juicy_stream = match.group(1)
# add the link
streams["links"].append(
{
"quality": res_dict["data-resolution"],
"translation_type": data_audio,
"link": juicy_stream,
}
)
yield streams

View File

@@ -1,61 +0,0 @@
from typing import Literal, TypedDict
class AnimeSearchResult(TypedDict):
id: int
title: str
type: str
episodes: int
status: str
season: str
year: int
score: int
poster: str
session: str
class AnimePaheSearchPage(TypedDict):
total: int
per_page: int
current_page: int
last_page: int
_from: int
to: int
data: list[AnimeSearchResult]
class Episode(TypedDict):
id: int
anime_id: int
episode: int
episode2: int
edition: str
title: str
snapshot: str # episode image
disc: str
audio: Literal["eng", "jpn"]
duration: str # time 00:00:00
session: str
filler: int
created_at: str
class AnimePaheAnimePage(TypedDict):
total: int
per_page: int
current_page: int
last_page: int
next_page_url: str | None
prev_page_url: str | None
_from: int
to: int
data: list[Episode]
class Server:
type: str
data_src = "https://kwik.si/e/PImJ0u7Y3M0G"
data_fansub: str
data_resolution: Literal["360", "720", "1080"]
data_audio: Literal["eng", "jpn"]
data_av1: str

View File

@@ -1,8 +0,0 @@
import requests
class AnimeProvider:
session: requests.Session
def __init__(self) -> None:
self.session = requests.session()

View File

@@ -1,71 +0,0 @@
from typing import Literal, TypedDict
class PageInfo(TypedDict):
total: int
perPage: int
currentPage: int
#
# class EpisodesDetail(TypedDict):
# dub: int
# sub: int
# raw: int
#
# search data
class SearchResult(TypedDict):
id: str
title: str
availableEpisodes: list[str]
type: str
score: int
status: str
season: str
poster: str
class SearchResults(TypedDict):
pageInfo: PageInfo
results: list[SearchResult]
# anime data
class AnimeEpisodeDetails(TypedDict):
dub: list[str]
sub: list[str]
raw: list[str]
class AnimeEpisode(TypedDict):
id: str
title: str
class Anime(TypedDict):
id: str
title: str
availableEpisodesDetail: AnimeEpisodeDetails
type: str | None
episodesInfo: list[AnimeEpisode] | None
poster: str
year: str
class EpisodeStream(TypedDict):
resolution: str | None
link: str
hls: bool | None
mp4: bool | None
priority: int | None
headers: dict | None
quality: Literal["360", "720", "1080", "unknown"]
translation_type: Literal["dub", "sub"]
class Server(TypedDict):
server: str
episode_title: str
links: list[EpisodeStream]

View File

@@ -1,62 +0,0 @@
import re
from itertools import cycle
# Dictionary to map hex values to characters
hex_to_char = {
"01": "9",
"08": "0",
"05": "=",
"0a": "2",
"0b": "3",
"0c": "4",
"07": "?",
"00": "8",
"5c": "d",
"0f": "7",
"5e": "f",
"17": "/",
"54": "l",
"09": "1",
"48": "p",
"4f": "w",
"0e": "6",
"5b": "c",
"5d": "e",
"0d": "5",
"53": "k",
"1e": "&",
"5a": "b",
"59": "a",
"4a": "r",
"4c": "t",
"4e": "v",
"57": "o",
"51": "i",
}
def give_random_quality(links: list[dict]):
qualities = cycle(["1080", "720", "360"])
return [
{"link": link["link"], "quality": quality}
for link, quality in zip(links, qualities)
]
def decode_hex_string(hex_string):
"""some of the sources encrypt the urls into hex codes this function decrypts the urls
Args:
hex_string ([TODO:parameter]): [TODO:description]
Returns:
[TODO:return]
"""
# Split the hex string into pairs of characters
hex_pairs = re.findall("..", hex_string)
# Decode each hex pair
decoded_chars = [hex_to_char.get(pair.lower(), pair) for pair in hex_pairs]
return "".join(decoded_chars)

View File

@@ -1,22 +0,0 @@
import requests
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 = requests.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,191 +0,0 @@
import logging
import os
import shutil
import subprocess
import sys
from typing import Callable, List
# TODO: will probably scrap art not to useful
from click import clear
from rich import print
logger = logging.getLogger(__name__)
FZF_DEFAULT_OPTS = """
--color=fg:#d0d0d0,fg+:#d0d0d0,bg:#121212,bg+:#262626
--color=hl:#5f87af,hl+:#5fd7ff,info:#afaf87,marker:#87ff00
--color=prompt:#d7005f,spinner:#af5fff,pointer:#af5fff,header:#87afaf
--color=border:#262626,label:#aeaeae,query:#d9d9d9
--border="rounded" --border-label="" --preview-window="border-rounded" --prompt="> "
--marker=">" --pointer="" --separator="" --scrollbar=""
"""
HEADER = """
███████╗░█████╗░░██████╗████████╗░█████╗░███╗░░██╗██╗███╗░░░███╗███████╗
██╔════╝██╔══██╗██╔════╝╚══██╔══╝██╔══██╗████╗░██║██║████╗░████║██╔════╝
█████╗░░███████║╚█████╗░░░░██║░░░███████║██╔██╗██║██║██╔████╔██║█████╗░░
██╔══╝░░██╔══██║░╚═══██╗░░░██║░░░██╔══██║██║╚████║██║██║╚██╔╝██║██╔══╝░░
██║░░░░░██║░░██║██████╔╝░░░██║░░░██║░░██║██║░╚███║██║██║░╚═╝░██║███████╗
╚═╝░░░░░╚═╝░░╚═╝╚═════╝░░░░╚═╝░░░╚═╝░░╚═╝╚═╝░░╚══╝╚═╝╚═╝░░░░░╚═╝╚══════╝
"""
class FZF:
"""an abstraction over the fzf commandline utility
Attributes:
FZF_EXECUTABLE: [TODO:attribute]
default_options: [TODO:attribute]
stdout: [TODO:attribute]
"""
if not os.getenv("FZF_DEFAULT_OPTS"):
os.environ["FZF_DEFAULT_OPTS"] = FZF_DEFAULT_OPTS
FZF_EXECUTABLE = shutil.which("fzf")
default_options = [
"--cycle",
"--info=hidden",
"--layout=reverse",
"--height=100%",
"--bind=right:accept",
"--no-margin",
"+m",
"-i",
"--exact",
"--tabstop=1",
"--preview-window=left,35%,wrap",
"--wrap",
]
def _with_filter(self, command: str, work: Callable) -> List[str]:
"""ported from the fzf docs demo
Args:
command: [TODO:description]
work: [TODO:description]
Returns:
[TODO:return]
"""
try:
process = subprocess.Popen(
command,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
text=True,
shell=True,
)
except subprocess.SubprocessError as e:
print(f"Failed to start subprocess: {e}", file=sys.stderr)
return []
original_stdout = sys.stdout
sys.stdout = process.stdin
try:
work()
if process.stdin:
process.stdin.close()
except Exception as e:
print(f"Exception during work execution: {e}", file=sys.stderr)
finally:
sys.stdout = original_stdout
output = []
if process.stdout:
output = process.stdout.read().splitlines()
process.stdout.close()
return output
def _run_fzf(self, commands: list[str], _fzf_input) -> str:
"""core abstraction
Args:
_fzf_input ([TODO:parameter]): [TODO:description]
commands: [TODO:description]
Raises:
Exception: [TODO:throw]
Returns:
[TODO:return]
"""
fzf_input = "\n".join(_fzf_input)
if not self.FZF_EXECUTABLE:
raise Exception("fzf executable not found")
result = subprocess.run(
[self.FZF_EXECUTABLE, *commands],
input=fzf_input,
stdout=subprocess.PIPE,
text=True,
)
if not result or result.returncode != 0 or not result.stdout:
print("sth went wrong:confused:")
input("press enter to try again...")
clear()
return self._run_fzf(commands, _fzf_input)
clear()
return result.stdout.strip()
def run(
self,
fzf_input: list[str],
prompt: str,
header: str = HEADER,
preview: str | None = None,
expect: str | None = None,
validator: Callable | None = None,
) -> str:
"""a helper method that wraps common use cases over the fzf utility
Args:
fzf_input: [TODO:description]
prompt: [TODO:description]
header: [TODO:description]
preview: [TODO:description]
expect: [TODO:description]
validator: [TODO:description]
Returns:
[TODO:return]
"""
_commands = [
*self.default_options,
"--header",
HEADER,
"--header-first",
"--prompt",
prompt.title(),
] # pyright:ignore
if preview:
_commands.append(f"--preview={preview}")
if expect:
_commands.append(f"--expect={expect}")
result = self._run_fzf(_commands, fzf_input) # pyright:ignore
if not result:
print("Please enter a value")
input("Enter to do it right")
return self.run(fzf_input, prompt, header, preview, expect, validator)
elif validator:
success, info = validator(result)
if not success:
print(info)
input("Enter to try again")
return self.run(fzf_input, prompt, header, preview, expect, validator)
return result
fzf = FZF()
if __name__ == "__main__":
action = fzf.run([*os.listdir(), "exit"], "Prompt: ", "Header", preview="bat {}")
print(action)

View File

@@ -1,138 +0,0 @@
import subprocess
from shutil import which
from sys import exit
from plyer import notification
from fastanime import APP_NAME
from ...constants import ICON_PATH
class RofiApi:
ROFI_EXECUTABLE = which("rofi")
rofi_theme = ""
rofi_theme_confirm = ""
rofi_theme_input = ""
def run_with_icons(self, options: list[str], prompt_text: str) -> str:
rofi_input = "\n".join(options)
if not self.ROFI_EXECUTABLE:
raise Exception("Rofi not found")
args = [self.ROFI_EXECUTABLE]
if self.rofi_theme:
args.extend(["-no-config", "-theme", self.rofi_theme])
args.extend(["-p", prompt_text, "-i", "-show-icons", "-dmenu"])
result = subprocess.run(
args,
input=rofi_input,
stdout=subprocess.PIPE,
text=True,
)
choice = result.stdout.strip()
if not choice:
notification.notify(
app_name=APP_NAME,
app_icon=ICON_PATH,
message="FastAnime is shutting down",
title="No Valid Input Provided",
) # pyright:ignore
exit(1)
return choice
def run(self, options: list[str], prompt_text: str) -> str:
rofi_input = "\n".join(options)
if not self.ROFI_EXECUTABLE:
raise Exception("Rofi not found")
args = [self.ROFI_EXECUTABLE]
if self.rofi_theme:
args.extend(["-no-config", "-theme", self.rofi_theme])
args.extend(["-p", prompt_text, "-i", "-dmenu"])
result = subprocess.run(
args,
input=rofi_input,
stdout=subprocess.PIPE,
text=True,
)
choice = result.stdout.strip()
if not choice or choice not in options:
notification.notify(
app_name=APP_NAME,
app_icon=ICON_PATH,
message="FastAnime is shutting down",
title="No Valid Input Provided",
) # pyright:ignore
exit(1)
return choice
def confirm(self, prompt_text: str) -> bool:
rofi_choices = "Yes\nNo"
if not self.ROFI_EXECUTABLE:
raise Exception("Rofi not found")
args = [self.ROFI_EXECUTABLE]
if self.rofi_theme_confirm:
args.extend(["-no-config", "-theme", self.rofi_theme_confirm])
args.extend(["-p", prompt_text, "-i", "", "-no-fixed-num-lines", "-dmenu"])
result = subprocess.run(
args,
input=rofi_choices,
stdout=subprocess.PIPE,
text=True,
)
choice = result.stdout.strip()
if not choice:
notification.notify(
app_name=APP_NAME,
app_icon=ICON_PATH,
message="FastAnime is shutting down",
title="No Valid Input Provided",
) # pyright:ignore
exit(1)
if choice == "Yes":
return True
else:
return False
def ask(
self, prompt_text: str, is_int: bool = False, is_float: bool = False
) -> str | float | int:
if not self.ROFI_EXECUTABLE:
raise Exception("Rofi not found")
args = [self.ROFI_EXECUTABLE]
if self.rofi_theme_input:
args.extend(["-no-config", "-theme", self.rofi_theme_input])
args.extend(["-p", prompt_text, "-i", "-no-fixed-num-lines", "-dmenu"])
result = subprocess.run(
args,
stdout=subprocess.PIPE,
text=True,
)
user_input = result.stdout.strip()
if not user_input:
notification.notify(
app_name=APP_NAME,
app_icon=ICON_PATH,
message="FastAnime is shutting down",
title="No Valid Input Provided",
) # pyright:ignore
exit(1)
if is_float:
user_input = float(user_input)
elif is_int:
user_input = int(user_input)
return user_input
Rofi = RofiApi()

61
flake.lock generated Normal file
View File

@@ -0,0 +1,61 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1756386758,
"narHash": "sha256-1wxxznpW2CKvI9VdniaUnTT2Os6rdRJcRUf65ZK9OtE=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "dfb2f12e899db4876308eba6d93455ab7da304cd",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

101
flake.nix Normal file
View File

@@ -0,0 +1,101 @@
{
description = "Viu Project Flake";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs =
{
self,
nixpkgs,
flake-utils,
}:
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = nixpkgs.legacyPackages.${system};
inherit (pkgs) lib python312Packages;
version = "3.1.0";
in
{
packages.default = python312Packages.buildPythonApplication {
pname = "viu";
inherit version;
pyproject = true;
src = self;
build-system = with python312Packages; [ hatchling ];
dependencies = with python312Packages; [
click
inquirerpy
requests
rich
thefuzz
yt-dlp
dbus-python
hatchling
plyer
mpv
fastapi
pycryptodome
pypresence
httpx
];
postPatch = ''
substituteInPlace pyproject.toml \
--replace-fail "pydantic>=2.11.7" "pydantic>=2.11.4"
'';
makeWrapperArgs = [
"--prefix PATH : ${
lib.makeBinPath (
with pkgs;
[
mpv
]
)
}"
];
# Needs to be adapted for the nix derivation build
doCheck = false;
meta = {
description = "Your browser anime experience from the terminal";
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 ];
};
};
devShells.default = pkgs.mkShell {
venvDir = ".venv";
env.LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ pkgs.libxcrypt-legacy ];
packages =
with pkgs;
[
mpv
fzf
rofi
uv
pyright
]
++ (with python3Packages; [
venvShellHook
hatchling
])
++ self.packages.${system}.default.dependencies;
};
}
);
}

View File

@@ -1,14 +0,0 @@
#!/usr/bin/env bash
APP_DIR="$(
cd -- "$(dirname "$0")" >/dev/null 2>&1
pwd -P
)"
# fish shell completions
_FASTANIME_COMPLETE=fish_source fastanime >"$APP_DIR/completions/fastanime.fish"
# zsh completions
_FASTANIME_COMPLETE=zsh_source fastanime >"$APP_DIR/completions/fastanime.zsh"
# bash completions
_FASTANIME_COMPLETE=bash_source fastanime >"$APP_DIR/completions/fastanime.bash"

1359
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,35 +1,65 @@
[tool.poetry]
name = "fastanime"
version = "1.1.6.dev1"
[project]
name = "viu-media"
version = "3.3.0"
description = "A browser anime site experience from the terminal"
authors = ["Benextempest <benextempest@gmail.com>"]
license = "UNLICENSE"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"click>=8.1.7",
"httpx>=0.28.1",
"inquirerpy>=0.3.4",
"pydantic>=2.11.7",
"rich>=13.9.2",
]
[tool.poetry.dependencies]
python = "^3.10"
yt-dlp = "^2024.5.27"
rich = "^13.7.1"
click = "^8.1.7"
inquirerpy = "^0.3.4"
thefuzz = "^0.22.1"
requests = "^2.32.3"
plyer = "^2.1.0"
[project.scripts]
viu = 'viu_media:Cli'
mpv = "^1.0.7"
[tool.poetry.group.dev.dependencies]
black = "^24.4.2"
isort = "^5.13.2"
pytest = "^8.2.2"
ruff = "^0.4.10"
pre-commit = "^3.7.1"
autoflake = "^2.3.1"
tox = "^4.16.0"
[project.optional-dependencies]
standard = [
"thefuzz>=0.22.1",
"yt-dlp>=2025.7.21",
"pycryptodomex>=3.23.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"
]
notifications = [
"dbus-python>=1.4.0",
"plyer>=2.1.0",
]
mpv = [
"mpv>=1.0.7",
]
torrent = ["libtorrent>=2.0.11"]
lxml = ["lxml>=6.0.0"]
discord = ["pypresence>=4.3.0"]
download = [
"pycryptodomex>=3.23.0",
"yt-dlp>=2025.7.21",
]
torrents = [
"libtorrent>=2.0.11",
]
pyright = "^1.1.374"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.poetry.scripts]
fastanime = 'fastanime:FastAnime'
[dependency-groups]
dev = [
"pre-commit>=4.0.1",
"pyinstaller>=6.11.1",
"pyright>=1.1.384",
"pytest>=8.3.3",
"pytest-httpx>=0.35.0",
"ruff>=0.6.9",
]
[tool.pytest.ini_options]
markers = [
"integration: marks tests as integration tests that require a live network connection",
]

View File

@@ -1,4 +1,5 @@
{
"typeCheckingMode": "standard",
"reportPrivateImportUsage": false
"venvPath": ".",
"venv": ".venv",
"pythonVersion": "3.12"
}

View File

@@ -1,145 +0,0 @@
# TODO: Write tests to make sure all click commands work
import pytest
from click.testing import CliRunner
from fastanime.cli import run_cli
@pytest.fixture
def runner():
return CliRunner()
def test_main_help(runner: CliRunner):
result = runner.invoke(run_cli, ["--help"])
assert result.exit_code == 0
def test_config_help(runner: CliRunner):
result = runner.invoke(run_cli, ["config", "--help"])
assert result.exit_code == 0
def test_config_path(runner: CliRunner):
result = runner.invoke(run_cli, ["config", "--path"])
assert result.exit_code == 0
def test_downloads_help(runner: CliRunner):
result = runner.invoke(run_cli, ["downloads", "--help"])
assert result.exit_code == 0
def test_downloads_path(runner: CliRunner):
result = runner.invoke(run_cli, ["downloads", "--path"])
assert result.exit_code == 0
def test_download_help(runner: CliRunner):
result = runner.invoke(run_cli, ["download", "--help"])
assert result.exit_code == 0
def test_search_help(runner: CliRunner):
result = runner.invoke(run_cli, ["search", "--help"])
assert result.exit_code == 0
def test_cache_help(runner: CliRunner):
result = runner.invoke(run_cli, ["cache", "--help"])
assert result.exit_code == 0
def test_completions_help(runner: CliRunner):
result = runner.invoke(run_cli, ["completions", "--help"])
assert result.exit_code == 0
def test_update_help(runner: CliRunner):
result = runner.invoke(run_cli, ["update", "--help"])
assert result.exit_code == 0
def test_anilist_help(runner: CliRunner):
result = runner.invoke(run_cli, ["anilist", "--help"])
assert result.exit_code == 0
def test_anilist_completed_help(runner: CliRunner):
result = runner.invoke(run_cli, ["anilist", "completed", "--help"])
assert result.exit_code == 0
def test_anilist_dropped_help(runner: CliRunner):
result = runner.invoke(run_cli, ["anilist", "dropped", "--help"])
assert result.exit_code == 0
def test_anilist_favourites_help(runner: CliRunner):
result = runner.invoke(run_cli, ["anilist", "favourites", "--help"])
assert result.exit_code == 0
def test_anilist_login_help(runner: CliRunner):
result = runner.invoke(run_cli, ["anilist", "login", "--help"])
assert result.exit_code == 0
def test_anilist_notifier_help(runner: CliRunner):
result = runner.invoke(run_cli, ["anilist", "notifier", "--help"])
assert result.exit_code == 0
def test_anilist_paused_help(runner: CliRunner):
result = runner.invoke(run_cli, ["anilist", "paused", "--help"])
assert result.exit_code == 0
def test_anilist_planning_help(runner: CliRunner):
result = runner.invoke(run_cli, ["anilist", "planning", "--help"])
assert result.exit_code == 0
def test_anilist_popular_help(runner: CliRunner):
result = runner.invoke(run_cli, ["anilist", "popular", "--help"])
assert result.exit_code == 0
def test_anilist_random_anime_help(runner: CliRunner):
result = runner.invoke(run_cli, ["anilist", "random", "--help"])
assert result.exit_code == 0
def test_anilist_recent_help(runner: CliRunner):
result = runner.invoke(run_cli, ["anilist", "recent", "--help"])
assert result.exit_code == 0
def test_anilist_rewatching_help(runner: CliRunner):
result = runner.invoke(run_cli, ["anilist", "rewatching", "--help"])
assert result.exit_code == 0
def test_anilist_scores_help(runner: CliRunner):
result = runner.invoke(run_cli, ["anilist", "scores", "--help"])
assert result.exit_code == 0
def test_anilist_search_help(runner: CliRunner):
result = runner.invoke(run_cli, ["anilist", "search", "--help"])
assert result.exit_code == 0
def test_anilist_trending_help(runner: CliRunner):
result = runner.invoke(run_cli, ["anilist", "trending", "--help"])
assert result.exit_code == 0
def test_anilist_upcoming_help(runner: CliRunner):
result = runner.invoke(run_cli, ["anilist", "upcoming", "--help"])
assert result.exit_code == 0
def test_anilist_watching_help(runner: CliRunner):
result = runner.invoke(run_cli, ["anilist", "watching", "--help"])
assert result.exit_code == 0

20
tox.ini
View File

@@ -1,27 +1,27 @@
[tox]
requires =
tox>=4
env_list = lint, pyright, py{310,311}
env_list = lint, pyright, py{311,312}
[testenv]
description = run unit tests
deps =poetry
deps =uv
commands =
poetry install
poetry run pytest
uv sync --dev --all-extras
uv run pytest
[testenv:lint]
description = run linters
skip_install = true
deps =poetry
deps =uv
commands =
poetry install
poetry run black .
uv sync --dev --all-extras
uv run ruff format .
[testenv:pyright]
description = run type checking
skip_install = true
deps =poetry
deps =uv
commands =
poetry install --no-root
poetry run pyright
uv sync --dev --all-extras
uv run pyright

3854
uv.lock generated Normal file

File diff suppressed because it is too large Load Diff

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