Compare commits

...

717 Commits

Author SHA1 Message Date
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
320 changed files with 29998 additions and 10662 deletions

1
.envrc Normal file
View File

@@ -0,0 +1 @@
use flake

15
.github/FUNDING.yml vendored Normal file
View File

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

View File

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

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']
---
# FastAnime: New Component Generation Mode
You are an expert on `fastanime`'s modular architecture. Your task is to help the developer add a new **Player** or **Selector** component.
**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 `fastanime/libs/player/{player_name}/`.
2. **Implement `BasePlayer`:** Create a `player.py` file with a class `NewPlayer` that inherits from `fastanime.libs.player.base.BasePlayer`. Implement the `play` and `play_with_ipc` methods. The `play` method should use `subprocess` to call the player's executable.
3. **Add Configuration:**
* Instruct to create a new Pydantic model `NewPlayerConfig(OtherConfig)` in `fastanime/core/config/model.py`.
* Add the new config model to the main `AppConfig`.
* Add defaults in `fastanime/core/config/defaults.py` and descriptions in `fastanime/core/config/descriptions.py`.
4. **Register Player:** Instruct to modify `fastanime/libs/player/player.py` by:
* 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 `fastanime/libs/selectors/{selector_name}/`.
2. **Implement `BaseSelector`:** Create a `selector.py` file with a class `NewSelector` that inherits from `fastanime.libs.selectors.base.BaseSelector`. Implement the `choose`, `confirm`, and `ask` methods.
3. **Add Configuration:** (Follow the same steps as for a Player).
4. **Register Selector:**
* Instruct to modify `fastanime/libs/selectors/selector.py` by adding the selector name to the `SELECTORS` list and the factory logic to `SelectorFactory.create`.
* Instruct to update the `Literal` type hint for the `selector` field in `GeneralConfig` (`fastanime/core/config/model.py`).

View File

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

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 fastanime project. Does not write implementation code."
tools: ['codebase', 'search', 'githubRepo', 'fetch']
model: "gpt-4o"
---
# FastAnime: Feature & Fix Planner Mode
You are a senior software architect and project planner for the `fastanime` project. You are an expert in its layered architecture (`Core`, `Libs`, `Service`, `CLI`) and its commitment to modular, testable code.
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 (`fastanime/core`):**
> - *Config (`config/model.py`):* Will a new Pydantic model or field be needed?
> - *Utils (`utils/`):* Are any new low-level, reusable functions required?
> - *Exceptions (`exceptions.py`):* Does this introduce a new failure case that needs a custom exception?
> - **Libs Layer (`fastanime/libs`):**
> - *Media API (`media_api/`):* Does this involve a new call to the AniList API?
> - *Provider (`provider/`):* Does this affect how data is scraped?
> - *Player/Selector (`player/`, `selectors/`):* Does this change how we interact with external tools?
> - **Service Layer (`fastanime/cli/service`):**
> - Which service will orchestrate this logic? (e.g., `DownloadService`, `PlayerService`). Will a new service be needed?
> - **CLI Layer (`fastanime/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 `fastanime 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 FastAnime Repository
Hello, Copilot! This document provides instructions and context to help you understand the `fastanime` codebase. Following these guidelines will help you generate code that is consistent, maintainable, and aligned with the project's architecture.
## 1. High-Level Project Goal
`fastanime` is a command-line tool that brings the anime browsing, streaming, and management experience to the terminal. It integrates with metadata providers like AniList and scrapes streaming links from various anime provider websites. The core goals are efficiency, extensibility, and providing a powerful, scriptable user experience.
## 2. Core Architectural Concepts
The project follows a clean, layered architecture. When generating code, please adhere to this structure.
#### Layer 1: CLI (`fastanime/cli`)
* **Purpose:** Handles user interaction, command parsing, and displaying output.
* **Key Libraries:** `click` for command structure, `rich` for styled output.
* **Interactive Mode:** The interactive TUI is managed by the `Session` object in `fastanime/cli/interactive/session.py`. It's a state machine where each menu is a function that returns the next `State` or an `InternalDirective` (like `BACK` or `EXIT`).
* **Guideline:** **CLI files should not contain complex business logic.** They should parse arguments and delegate tasks to the Service Layer.
#### Layer 2: Service (`fastanime/cli/service`)
* **Purpose:** Contains the core application logic. Services act as orchestrators, connecting the CLI layer with the various library components.
* **Examples:** `DownloadService`, `PlayerService`, `MediaRegistryService`, `WatchHistoryService`.
* **Guideline:** When adding new functionality (e.g., a new way to manage downloads), it should likely be implemented in a service or an existing service should be extended. Services are the "brains" of the application.
#### Layer 3: Libraries (`fastanime/libs`)
* **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 (`fastanime/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 `fastanime/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 `fastanime/libs/provider/anime/newprovider/`.
2. **Implement `BaseAnimeProvider`:** In `provider.py`, create a class `NewProvider` that inherits from `BaseAnimeProvider` and implement the `search`, `get`, and `episode_streams` methods.
3. **Create Mappers:** In `mappers.py`, write functions to convert the provider's API/HTML data into the generic Pydantic models from `fastanime/libs/provider/anime/types.py` (e.g., `SearchResult`, `Anime`, `Server`).
4. **Register Provider:**
* Add the provider's name to the `ProviderName` enum in `fastanime/libs/provider/anime/types.py`.
* Add it to the `PROVIDERS_AVAILABLE` dictionary in `fastanime/libs/provider/anime/provider.py`.
### How to Add a New Player
1. **Create Directory:** Add a new folder in `fastanime/libs/player/newplayer/`.
2. **Implement `BasePlayer`:** In `player.py`, create a class `NewPlayer` that inherits from `BasePlayer` and implement the `play` method. It should call the player's executable via `subprocess`.
3. **Add Configuration:** If the player has settings, add a `NewPlayerConfig` Pydantic model in `fastanime/core/config/model.py`, and add it to the main `AppConfig`. Also add defaults and descriptions.
4. **Register Player:** Add the player's name to the `PLAYERS` list and the factory logic in `fastanime/libs/player/player.py`.
### How to Add a New Selector
1. **Create Directory:** Add a new folder in `fastanime/libs/selectors/newselector/`.
2. **Implement `BaseSelector`:** In `selector.py`, create a class `NewSelector` that inherits from `BaseSelector` and implement `choose`, `confirm`, and `ask`.
3. **Add Configuration:** If needed, add a `NewSelectorConfig` to `fastanime/core/config/model.py`.
4. **Register Selector:** Add the selector's name to the `SELECTORS` list and the factory logic in `fastanime/libs/selectors/selector.py`. Update the `Literal` type hint for `selector` in `GeneralConfig`.
### How to Add a New CLI Command
* **Top-Level Command (`fastanime my-command`):**
1. Create `fastanime/cli/commands/my_command.py` with your `click.command()`.
2. Register it in the `commands` dictionary in `fastanime/cli/cli.py`.
* **Subcommand (`fastanime anilist my-subcommand`):**
1. Create `fastanime/cli/commands/anilist/commands/my_subcommand.py`.
2. Register it in the `lazy_subcommands` dictionary of the parent `click.group()` (e.g., in `fastanime/cli/commands/anilist/cmd.py`).
### How to Add a New Configuration Option
1. **Add to Model:** Add the field to the appropriate Pydantic model in `fastanime/core/config/model.py`.
2. **Add Default:** Add a default value in `fastanime/core/config/defaults.py`.
3. **Add Description:** Add a user-friendly description in `fastanime/core/config/descriptions.py`.
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 `fastanime/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 fastanime
run: uv build
- name: Archive production artifacts
uses: actions/upload-artifact@v4
with:
name: fastanime_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

@@ -27,11 +27,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 fastanime
run: uv build
- name: Upload distributions
uses: actions/upload-artifact@v4

View File

@@ -6,37 +6,35 @@ 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
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)
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: 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
- 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,5 +1,5 @@
default_language_version:
python: python3.10
python: python3.12
repos:
- repo: https://github.com/pycqa/isort
@@ -7,7 +7,7 @@ repos:
hooks:
- id: isort
name: isort (python)
args: ["--profile", "black"] # Ensure compatibility with Black
args: ["--profile", "black"]
- repo: https://github.com/PyCQA/autoflake
rev: v2.2.1
@@ -19,17 +19,15 @@ repos:
"--remove-unused-variables",
"--remove-all-unused-imports",
]
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.4.10
hooks:
# Run the linter.
- id: ruff
args: [--fix]
# - repo: https://github.com/astral-sh/ruff-pre-commit
# rev: v0.4.10
# hooks:
# - id: ruff
# args: [--fix]
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 24.4.2
hooks:
- id: black
name: black
language_version: python3.10 # to ensure compatibilty
#language_version: python3.10

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 FastAnime
First off, thank you for considering contributing to FastAnime! We welcome any help, whether it's reporting a bug, proposing a feature, or writing code. This document will guide you through the process.
## How Can I Contribute?
There are many ways to contribute to the FastAnime project:
* **Reporting Bugs:** If you find a bug, please create an issue in our [issue tracker](https://github.com/Benexl/FastAnime/issues).
* **Suggesting Enhancements:** Have an idea for a new feature or an improvement to an existing one? We'd love to hear it.
* **Writing Code:** Help us fix bugs or implement new features.
* **Improving Documentation:** Enhance our README, add examples, or clarify our contribution guidelines.
* **Adding a Provider, Player, or Selector:** Extend FastAnime's capabilities by integrating new tools and services.
## Contribution Workflow
We follow the standard GitHub Fork & Pull Request workflow.
1. **Create an Issue:** Before starting work on a new feature or a significant bug fix, please [create an issue](https://github.com/Benexl/FastAnime/issues/new/choose) to discuss your idea. This allows us to give feedback and prevent duplicate work. For small bugs or documentation typos, you can skip this step.
2. **Fork the Repository:** Create your own fork of the FastAnime repository.
3. **Clone Your Fork:**
```bash
git clone https://github.com/YOUR_USERNAME/FastAnime.git
cd FastAnime
```
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 FastAnime 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 `fastanime/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 `fastanime/libs/provider/anime/`, create a new directory with the provider's name (e.g., `fastanime/libs/provider/anime/newprovider/`).
2. **Implement the Provider:**
* Create a `provider.py` file.
* Define a class (e.g., `NewProviderApi`) that inherits from `BaseAnimeProvider`.
* Implement the abstract methods: `search`, `get`, and `episode_streams`.
* Create `mappers.py` to convert the provider's data structures into the generic types defined in `fastanime/libs/provider/anime/types.py`.
* Create `types.py` for any provider-specific data structures you need.
* If the provider requires complex scraping, place extractor logic in an `extractors/` subdirectory.
3. **Register the Provider:**
* Add your new provider to the `ProviderName` enum in `fastanime/libs/provider/anime/types.py`.
* Register it in the `PROVIDERS_AVAILABLE` dictionary in `fastanime/libs/provider/anime/provider.py`.
4. **Add Normalization Rules (Optional):** If the provider uses different anime titles than AniList, add mappings to `fastanime/assets/normalizer.json`.
## How to Add a New Player
1. **Create a New Player Directory:** Inside `fastanime/libs/player/`, create a directory for your player (e.g., `fastanime/libs/player/myplayer/`).
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 `fastanime/core/config/model.py`. It should inherit from `OtherConfig`.
* Add this new config model as a field in the main `AppConfig` model.
* Add default values in `defaults.py` and descriptions in `descriptions.py`.
4. **Register the Player:**
* Add your player's name to the `PLAYERS` list in `fastanime/libs/player/player.py`.
* Add the logic to instantiate your player class within the `PlayerFactory.create` method.
## How to Add a New Selector
1. **Create a New Selector Directory:** Inside `fastanime/libs/selectors/`, create a new directory (e.g., `fastanime/libs/selectors/myselector/`).
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 `fastanime/libs/selectors/selector.py`.
* Add the instantiation logic to the `SelectorFactory.create` method.
* Update the `Literal` type hint for the `selector` field in `GeneralConfig` (`fastanime/core/config/model.py`).
## How to Add a New CLI Command or Service
Our CLI uses `click` and a `LazyGroup` class to load commands on demand.
### Adding a Top-Level Command (e.g., `fastanime my-command`)
1. **Create the Command File:** Create a new Python file in `fastanime/cli/commands/` (e.g., `my_command.py`). This file should contain your `click.command()` function.
2. **Register the Command:** In `fastanime/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., `fastanime anilist my-subcommand`)
1. **Create the Command File:** Place your new command file inside the appropriate subdirectory, for example, `fastanime/cli/commands/anilist/commands/my_subcommand.py`.
2. **Register the Subcommand:** In the parent command's entry point file (e.g., `fastanime/cli/commands/anilist/cmd.py`), add your subcommand to the `commands` dictionary within the `LazyGroup`.
```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 `fastanime/cli/service/` to keep the business logic separate from the command-line interface. This service can then be instantiated and used within your `click` command function. This follows the existing pattern for services like `DownloadService` and `PlayerService`.
---
Thank you for contributing to FastAnime

38
DISCLAIMER.md Normal file
View File

@@ -0,0 +1,38 @@
<h1 align="center">Disclaimer</h1>
<div align="center">
<h2>This project: fastanime</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"]

1022
README.md

File diff suppressed because it is too large Load Diff

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 . /fastanime
ENV PATH=/root/.local/bin:$PATH
WORKDIR /fastanime
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 = [
('fastanime/assets/*', 'fastanime/assets'),
]
# Collect all required hidden imports
hiddenimports = [
'click',
'rich',
'requests',
'yt_dlp',
'python_mpv',
'fuzzywuzzy',
'fastanime',
] + collect_submodules('fastanime')
a = Analysis(
['./fastanime/fastanime.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='fastanime',
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='fastanime/assets/logo.ico'
)

File diff suppressed because it is too large Load Diff

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" ] && fastanime --version && exit 0
sed -i "s/^version.*/version = \"$VERSION\"/" "$CLI_DIR/pyproject.toml" &&
sed -i "s/__version__.*/__version__ = \"v$VERSION\"/" "$CLI_DIR/fastanime/__init__.py" &&
sed -i "s/version = .*/version = \"$VERSION\";/" "$CLI_DIR/flake.nix" &&
git stage "$CLI_DIR/pyproject.toml" "$CLI_DIR/fastanime/__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"

8
fa
View File

@@ -1,4 +1,6 @@
#!/usr/bin/env sh
# exec "${PYTHON:-python3}" -Werror -Xdev -m "$(dirname "$(realpath "$0")")/fastanime" "$@"
cd "$(dirname "$(realpath "$0")")" || exit 1
exec python -m fastanime "$@"
provider_type=$1
provider_name=$2
[ -z "$provider_type" ] && echo "Please specify provider type" && exit
[ -z "$provider_name" ] && echo "Please specify provider type" && exit
uv run python -m fastanime.libs.provider.${provider_type}.${provider_name}.provider

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,82 +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",
force_unknown_ext=False,
verbose=False,
headers={},
sub="",
):
"""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
"http_headers": headers,
"outtmpl": f"{download_dir}/{anime_title}/{episode_title}.%(ext)s",
"silent": silent,
"verbose": verbose,
"format": vid_format,
"compat_opts": ("allow-unsafe-ext",) if force_unknown_ext else tuple(),
}
urls = [url]
if sub:
urls.append(sub)
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
ydl.download(urls)
# 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,45 +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 sort_by_episode_number(filename: str):
import re
match = re.search(r"\d+", filename)
return int(match.group()) if match else 0
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

@@ -2,19 +2,11 @@ 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"
"You are using an unsupported version of Python. Only Python versions 3.10 and above are supported by FastAnime"
) # noqa: F541
__version__ = "v2.3.3"
APP_NAME = "FastAnime"
AUTHOR = "Benex254"
GIT_REPO = "github.com"
REPO = f"{GIT_REPO}/{AUTHOR}/{APP_NAME}"
def FastAnime():
def Cli():
from .cli import run_cli
run_cli()

View File

@@ -9,6 +9,6 @@ if __package__ is None and not getattr(sys, "frozen", False):
if __name__ == "__main__":
from . import FastAnime
from . import Cli
FastAnime()
Cli()

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,23 @@
--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=''
--prompt='>'
--marker='>'
--pointer='◆'
--separator='─'
--scrollbar='│'
--layout=reverse
--cycle
--info=hidden
--height=100%
--bind=right:accept,ctrl-/:toggle-preview,ctrl-space:toggle-wrap+toggle-preview-wrap
--no-margin
+m
-i
--exact
--tabstop=1
--preview-window=border-rounded,left,35%,wrap
--wrap

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
query ($showId: String!) {
show(_id: $showId) {
_id
name
availableEpisodesDetail
}
}

View File

@@ -0,0 +1,15 @@
query (
$showId: String!
$translationType: VaildTranslationTypeEnumType!
$episodeString: String!
) {
episode(
showId: $showId
translationType: $translationType
episodeString: $episodeString
) {
episodeString
sourceUrls
notes
}
}

View File

@@ -0,0 +1,25 @@
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
}
}
}

View File

@@ -0,0 +1,5 @@
mutation ($id: Int) {
DeleteMediaListEntry(id: $id) {
deleted
}
}

View File

@@ -0,0 +1,5 @@
mutation {
UpdateUser {
unreadNotificationCount
}
}

View File

@@ -0,0 +1,32 @@
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
}
}
}

View File

@@ -0,0 +1,11 @@
query {
Viewer {
id
name
bannerImage
avatar {
large
medium
}
}
}

View File

@@ -0,0 +1,13 @@
query ($id: Int, $type: MediaType) {
Page {
media(id: $id, sort: POPULARITY_DESC, type: $type) {
airingSchedule(notYetAired: true) {
nodes {
airingAt
timeUntilAiring
episode
}
}
}
}
}

View File

@@ -0,0 +1,31 @@
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
}
}
}
}
}

View File

@@ -0,0 +1,5 @@
query ($mediaId: Int) {
MediaList(mediaId: $mediaId) {
id
}
}

View File

@@ -0,0 +1,94 @@
query (
$userId: Int
$status: MediaListStatus
$type: MediaType
$page: Int
$perPage: Int
$sort: [MediaListSort]
) {
Page(perPage: $perPage, page: $page) {
pageInfo {
total
currentPage
hasNextPage
}
mediaList(userId: $userId, status: $status, type: $type, sort: $sort) {
mediaId
media {
id
idMal
format
title {
romaji
english
}
coverImage {
medium
large
}
trailer {
site
id
}
popularity
streamingEpisodes {
title
thumbnail
}
favourites
averageScore
episodes
genres
synonyms
studios {
nodes {
name
favourites
isAnimationStudio
}
}
tags {
name
}
startDate {
year
month
day
}
endDate {
year
month
day
}
status
description
mediaListEntry {
status
id
progress
}
nextAiringEpisode {
timeUntilAiring
airingAt
episode
}
}
status
progress
score
repeat
notes
startedAt {
year
month
day
}
completedAt {
year
month
day
}
createdAt
}
}
}

View File

@@ -0,0 +1,60 @@
query ($id: Int, $page: Int, $per_page: Int) {
Page(perPage: $per_page, page: $page) {
recommendations(mediaRecommendationId: $id) {
media {
id
idMal
format
mediaListEntry {
status
id
progress
}
title {
english
romaji
native
}
coverImage {
medium
large
}
description
episodes
duration
trailer {
site
id
}
genres
synonyms
averageScore
popularity
streamingEpisodes {
title
thumbnail
}
favourites
tags {
name
}
startDate {
year
month
day
}
endDate {
year
month
day
}
status
nextAiringEpisode {
timeUntilAiring
airingAt
episode
}
}
}
}
}

View File

@@ -0,0 +1,61 @@
query ($id: Int, $format_not_in: [MediaFormat]) {
Media(id: $id, format_not_in: $format_not_in) {
relations {
nodes {
id
idMal
type
format
title {
english
romaji
native
}
coverImage {
medium
large
}
mediaListEntry {
status
id
progress
}
description
episodes
duration
trailer {
site
id
}
genres
synonyms
averageScore
popularity
streamingEpisodes {
title
thumbnail
}
favourites
tags {
name
}
startDate {
year
month
day
}
endDate {
year
month
day
}
status
nextAiringEpisode {
timeUntilAiring
airingAt
episode
}
}
}
}
}

View File

@@ -0,0 +1,28 @@
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
large
}
}
}
}
}
}

View File

@@ -0,0 +1,18 @@
query ($id: Int) {
Page {
pageInfo {
total
}
reviews(mediaId: $id) {
summary
user {
name
avatar {
large
medium
}
}
body
}
}
}

View File

@@ -0,0 +1,121 @@
query (
$query: String
$per_page: Int
$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
$seasonYear: Int
$startDate_greater: FuzzyDateInt
$startDate_lesser: FuzzyDateInt
$startDate: FuzzyDateInt
$endDate_greater: FuzzyDateInt
$endDate_lesser: FuzzyDateInt
$format_in: [MediaFormat]
$type: MediaType
$season: MediaSeason
$on_list: Boolean
) {
Page(perPage: $per_page, 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
startDate: $startDate
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
format_in: $format_in
sort: $sort
season: $season
seasonYear: $seasonYear
type: $type
onList: $on_list
) {
id
idMal
format
title {
romaji
english
}
coverImage {
medium
large
}
trailer {
site
id
}
mediaListEntry {
status
id
progress
}
popularity
streamingEpisodes {
title
thumbnail
}
favourites
averageScore
duration
episodes
genres
synonyms
studios {
nodes {
name
favourites
isAnimationStudio
}
}
tags {
name
}
startDate {
year
month
day
}
endDate {
year
month
day
}
status
description
nextAiringEpisode {
timeUntilAiring
airingAt
episode
}
}
}
}

View File

@@ -0,0 +1,62 @@
query ($userId: Int) {
User(id: $userId) {
name
about
avatar {
large
medium
}
bannerImage
statistics {
anime {
count
minutesWatched
episodesWatched
genres {
count
meanScore
genre
}
tags {
tag {
id
}
count
meanScore
}
}
manga {
count
meanScore
chaptersRead
volumesRead
tags {
count
meanScore
}
genres {
count
meanScore
}
}
}
favourites {
anime {
nodes {
title {
romaji
english
}
}
}
manga {
nodes {
title {
romaji
english
}
}
}
}
}
}

View File

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 133 KiB

View File

Before

Width:  |  Height:  |  Size: 197 KiB

After

Width:  |  Height:  |  Size: 197 KiB

View File

@@ -0,0 +1,17 @@
{
"allanime": {
"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",
"Re:Zero kara Hajimeru Isekai Seikatsu Season 3": "Re:Zero kara Hajimeru Isekai Seikatsu 3rd Season"
},
"hianime": {
"My Star": "Oshi no Ko"
},
"animepahe": {
"Azumanga Daiou The Animation": "Azumanga Daioh",
"Mairimashita! Iruma-kun 2nd Season": "Mairimashita! Iruma-kun 2",
"Mairimashita! Iruma-kun 3rd Season": "Mairimashita! Iruma-kun 3"
}
}

View File

@@ -0,0 +1,22 @@
#!/bin/sh
#
# FastAnime Airing Schedule Info Script Template
# This script formats and displays airing schedule details in the FZF preview pane.
# Python injects the actual data values into the placeholders.
draw_rule
print_kv "Anime Title" "{ANIME_TITLE}"
draw_rule
print_kv "Total Episodes" "{TOTAL_EPISODES}"
print_kv "Upcoming Episodes" "{UPCOMING_EPISODES}"
draw_rule
echo "{C_KEY}Next Episodes:{RESET}"
echo
echo "{SCHEDULE_TABLE}" | fold -s -w "$WIDTH"
draw_rule

View File

@@ -0,0 +1,75 @@
#!/bin/sh
#
# FZF Airing Schedule Preview Script Template
#
# This script is a template. The placeholders in curly braces, like {NAME}
# are dynamically filled by python using .replace()
WIDTH=${FZF_PREVIEW_COLUMNS:-80} # Set a fallback width of 80
IMAGE_RENDERER="{IMAGE_RENDERER}"
generate_sha256() {
local input
# Check if input is passed as an argument or piped
if [ -n "$1" ]; then
input="$1"
else
input=$(cat)
fi
if command -v sha256sum &>/dev/null; then
echo -n "$input" | sha256sum | awk '{print $1}'
elif command -v shasum &>/dev/null; then
echo -n "$input" | shasum -a 256 | awk '{print $1}'
elif command -v sha256 &>/dev/null; then
echo -n "$input" | sha256 | awk '{print $1}'
elif command -v openssl &>/dev/null; then
echo -n "$input" | openssl dgst -sha256 | awk '{print $2}'
else
echo -n "$input" | base64 | tr '/+' '_-' | tr -d '\n'
fi
}
print_kv() {
local key="$1"
local value="$2"
local key_len=${#key}
local value_len=${#value}
local multiplier="${3:-1}"
# Correctly calculate padding by accounting for the key, the ": ", and the value.
local padding_len=$((WIDTH - key_len - 2 - value_len * multiplier))
# If the text is too long to fit, just add a single space for separation.
if [ "$padding_len" -lt 1 ]; then
padding_len=1
value=$(echo $value| fold -s -w "$((WIDTH - key_len - 3))")
printf "{C_KEY}%s:{RESET}%*s%s\\n" "$key" "$padding_len" "" " $value"
else
printf "{C_KEY}%s:{RESET}%*s%s\\n" "$key" "$padding_len" "" " $value"
fi
}
draw_rule(){
ll=2
while [ $ll -le $FZF_PREVIEW_COLUMNS ];do
echo -n -e "{C_RULE}─{RESET}"
((ll++))
done
echo
}
title={}
hash=$(generate_sha256 "$title")
if [ "{PREVIEW_MODE}" = "full" ] || [ "{PREVIEW_MODE}" = "text" ]; then
info_file="{INFO_CACHE_DIR}{PATH_SEP}$hash"
if [ -f "$info_file" ]; then
source "$info_file"
else
echo "📅 Loading airing schedule..."
fi
fi

View File

@@ -0,0 +1,41 @@
#!/bin/sh
#
# FastAnime Character Info Script Template
# This script formats and displays character details in the FZF preview pane.
# Python injects the actual data values into the placeholders.
draw_rule
print_kv "Character Name" "{CHARACTER_NAME}"
if [ -n "{CHARACTER_NATIVE_NAME}" ] && [ "{CHARACTER_NATIVE_NAME}" != "N/A" ]; then
print_kv "Native Name" "{CHARACTER_NATIVE_NAME}"
fi
draw_rule
if [ -n "{CHARACTER_GENDER}" ] && [ "{CHARACTER_GENDER}" != "Unknown" ]; then
print_kv "Gender" "{CHARACTER_GENDER}"
fi
if [ -n "{CHARACTER_AGE}" ] && [ "{CHARACTER_AGE}" != "Unknown" ]; then
print_kv "Age" "{CHARACTER_AGE}"
fi
if [ -n "{CHARACTER_BLOOD_TYPE}" ] && [ "{CHARACTER_BLOOD_TYPE}" != "N/A" ]; then
print_kv "Blood Type" "{CHARACTER_BLOOD_TYPE}"
fi
if [ -n "{CHARACTER_BIRTHDAY}" ] && [ "{CHARACTER_BIRTHDAY}" != "N/A" ]; then
print_kv "Birthday" "{CHARACTER_BIRTHDAY}"
fi
if [ -n "{CHARACTER_FAVOURITES}" ] && [ "{CHARACTER_FAVOURITES}" != "0" ]; then
print_kv "Favorites" "{CHARACTER_FAVOURITES}"
fi
draw_rule
echo "{CHARACTER_DESCRIPTION}" | fold -s -w "$WIDTH"
draw_rule

View File

@@ -0,0 +1,130 @@
#!/bin/sh
#
# FZF Character Preview Script Template
#
# This script is a template. The placeholders in curly braces, like {NAME}
# are dynamically filled by python using .replace()
WIDTH=${FZF_PREVIEW_COLUMNS:-80} # Set a fallback width of 80
IMAGE_RENDERER="{IMAGE_RENDERER}"
generate_sha256() {
local input
# Check if input is passed as an argument or piped
if [ -n "$1" ]; then
input="$1"
else
input=$(cat)
fi
if command -v sha256sum &>/dev/null; then
echo -n "$input" | sha256sum | awk '{print $1}'
elif command -v shasum &>/dev/null; then
echo -n "$input" | shasum -a 256 | awk '{print $1}'
elif command -v sha256 &>/dev/null; then
echo -n "$input" | sha256 | awk '{print $1}'
elif command -v openssl &>/dev/null; then
echo -n "$input" | openssl dgst -sha256 | awk '{print $2}'
else
echo -n "$input" | base64 | tr '/+' '_-' | tr -d '\n'
fi
}
fzf_preview() {
file=$1
dim=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}
if [ "$dim" = x ]; then
dim=$(stty size </dev/tty | awk "{print \$2 \"x\" \$1}")
fi
if ! [ "$IMAGE_RENDERER" = "icat" ] && [ -z "$KITTY_WINDOW_ID" ] && [ "$((FZF_PREVIEW_TOP + FZF_PREVIEW_LINES))" -eq "$(stty size </dev/tty | awk "{print \$1}")" ]; then
dim=${FZF_PREVIEW_COLUMNS}x$((FZF_PREVIEW_LINES - 1))
fi
if [ "$IMAGE_RENDERER" = "icat" ] && [ -z "$GHOSTTY_BIN_DIR" ]; then
if command -v kitten >/dev/null 2>&1; then
kitten icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed "\$d" | sed "$(printf "\$s/\$/\033[m/")"
elif command -v icat >/dev/null 2>&1; then
icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed "\$d" | sed "$(printf "\$s/\$/\033[m/")"
else
kitty icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed "\$d" | sed "$(printf "\$s/\$/\033[m/")"
fi
elif [ -n "$GHOSTTY_BIN_DIR" ]; then
if command -v kitten >/dev/null 2>&1; then
kitten icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed "\$d" | sed "$(printf "\$s/\$/\033[m/")"
elif command -v icat >/dev/null 2>&1; then
icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed "\$d" | sed "$(printf "\$s/\$/\033[m/")"
else
chafa -s "$dim" "$file"
fi
elif command -v chafa >/dev/null 2>&1; then
case "$PLATFORM" in
android) chafa -s "$dim" "$file" ;;
windows) chafa -f sixel -s "$dim" "$file" ;;
*) chafa -s "$dim" "$file" ;;
esac
echo
elif command -v imgcat >/dev/null; then
imgcat -W "${dim%%x*}" -H "${dim##*x}" "$file"
else
echo please install a terminal image viewer
echo either icat for kitty terminal and wezterm or imgcat or chafa
fi
}
print_kv() {
local key="$1"
local value="$2"
local key_len=${#key}
local value_len=${#value}
local multiplier="${3:-1}"
# Correctly calculate padding by accounting for the key, the ": ", and the value.
local padding_len=$((WIDTH - key_len - 2 - value_len * multiplier))
# If the text is too long to fit, just add a single space for separation.
if [ "$padding_len" -lt 1 ]; then
padding_len=1
value=$(echo $value| fold -s -w "$((WIDTH - key_len - 3))")
printf "{C_KEY}%s:{RESET}%*s%s\\n" "$key" "$padding_len" "" " $value"
else
printf "{C_KEY}%s:{RESET}%*s%s\\n" "$key" "$padding_len" "" " $value"
fi
}
draw_rule(){
ll=2
while [ $ll -le $FZF_PREVIEW_COLUMNS ];do
echo -n -e "{C_RULE}─{RESET}"
((ll++))
done
echo
}
title={}
hash=$(generate_sha256 "$title")
# FIXME: Disabled since they cover the text perhaps its aspect ratio related or image format not sure
# if [ "{PREVIEW_MODE}" = "full" ] || [ "{PREVIEW_MODE}" = "image" ]; then
# image_file="{IMAGE_CACHE_DIR}{PATH_SEP}$hash.png"
# if [ -f "$image_file" ]; then
# fzf_preview "$image_file"
# echo # Add a newline for spacing
# fi
# fi
if [ "{PREVIEW_MODE}" = "full" ] || [ "{PREVIEW_MODE}" = "text" ]; then
info_file="{INFO_CACHE_DIR}{PATH_SEP}$hash"
if [ -f "$info_file" ]; then
source "$info_file"
else
echo "👤 Loading character details..."
fi
fi

View File

@@ -0,0 +1,315 @@
#!/bin/bash
#
# FZF Dynamic Preview Script Template
#
# This script handles previews for dynamic search results by parsing the JSON
# search results file and extracting info for the selected item.
# The placeholders in curly braces are dynamically filled by Python using .replace()
WIDTH=${FZF_PREVIEW_COLUMNS:-80}
IMAGE_RENDERER="{IMAGE_RENDERER}"
SEARCH_RESULTS_FILE="{SEARCH_RESULTS_FILE}"
IMAGE_CACHE_PATH="{IMAGE_CACHE_PATH}"
INFO_CACHE_PATH="{INFO_CACHE_PATH}"
PATH_SEP="{PATH_SEP}"
# Color codes injected by Python
C_TITLE="{C_TITLE}"
C_KEY="{C_KEY}"
C_VALUE="{C_VALUE}"
C_RULE="{C_RULE}"
RESET="{RESET}"
# Selected item from fzf
SELECTED_ITEM={}
generate_sha256() {
local input="$1"
if command -v sha256sum &>/dev/null; then
echo -n "$input" | sha256sum | awk '{print $1}'
elif command -v shasum &>/dev/null; then
echo -n "$input" | shasum -a 256 | awk '{print $1}'
elif command -v sha256 &>/dev/null; then
echo -n "$input" | sha256 | awk '{print $1}'
elif command -v openssl &>/dev/null; then
echo -n "$input" | openssl dgst -sha256 | awk '{print $2}'
else
echo -n "$input" | base64 | tr '/+' '_-' | tr -d '\n'
fi
}
fzf_preview() {
file=$1
dim=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}
if [ "$dim" = x ]; then
dim=$(stty size </dev/tty | awk "{print \$2 \"x\" \$1}")
fi
if ! [ "$IMAGE_RENDERER" = "icat" ] && [ -z "$KITTY_WINDOW_ID" ] && [ "$((FZF_PREVIEW_TOP + FZF_PREVIEW_LINES))" -eq "$(stty size </dev/tty | awk "{print \$1}")" ]; then
dim=${FZF_PREVIEW_COLUMNS}x$((FZF_PREVIEW_LINES - 1))
fi
if [ "$IMAGE_RENDERER" = "icat" ] && [ -z "$GHOSTTY_BIN_DIR" ]; then
if command -v kitten >/dev/null 2>&1; then
kitten icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed "\$d" | sed "$(printf "\$s/\$/\033[m/")"
elif command -v icat >/dev/null 2>&1; then
icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed "\$d" | sed "$(printf "\$s/\$/\033[m/")"
else
kitty icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed "\$d" | sed "$(printf "\$s/\$/\033[m/")"
fi
elif [ -n "$GHOSTTY_BIN_DIR" ]; then
if command -v kitten >/dev/null 2>&1; then
kitten icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed "\$d" | sed "$(printf "\$s/\$/\033[m/")"
elif command -v icat >/dev/null 2>&1; then
icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed "\$d" | sed "$(printf "\$s/\$/\033[m/")"
else
chafa -s "$dim" "$file"
fi
elif command -v chafa >/dev/null 2>&1; then
case "$PLATFORM" in
android) chafa -s "$dim" "$file" ;;
windows) chafa -f sixel -s "$dim" "$file" ;;
*) chafa -s "$dim" "$file" ;;
esac
echo
elif command -v imgcat >/dev/null; then
imgcat -W "${dim%%x*}" -H "${dim##*x}" "$file"
else
echo please install a terminal image viewer
echo either icat for kitty terminal and wezterm or imgcat or chafa
fi
}
print_kv() {
local key="$1"
local value="$2"
local key_len=${#key}
local value_len=${#value}
local multiplier="${3:-1}"
local padding_len=$((WIDTH - key_len - 2 - value_len * multiplier))
if [ "$padding_len" -lt 1 ]; then
padding_len=1
value=$(echo $value| fold -s -w "$((WIDTH - key_len - 3))")
printf "{C_KEY}%s:{RESET}%*s%s\\n" "$key" "$padding_len" "" " $value"
else
printf "{C_KEY}%s:{RESET}%*s%s\\n" "$key" "$padding_len" "" " $value"
fi
}
draw_rule() {
ll=2
while [ $ll -le $FZF_PREVIEW_COLUMNS ];do
echo -n -e "{C_RULE}─{RESET}"
((ll++))
done
echo
}
clean_html() {
echo "$1" | sed 's/<[^>]*>//g' | sed 's/&lt;/</g' | sed 's/&gt;/>/g' | sed 's/&amp;/\&/g' | sed 's/&quot;/"/g' | sed "s/&#39;/'/g"
}
format_date() {
local date_obj="$1"
if [ "$date_obj" = "null" ] || [ -z "$date_obj" ]; then
echo "N/A"
return
fi
# Extract year, month, day from the date object
if command -v jq >/dev/null 2>&1; then
year=$(echo "$date_obj" | jq -r '.year // "N/A"' 2>/dev/null || echo "N/A")
month=$(echo "$date_obj" | jq -r '.month // ""' 2>/dev/null || echo "")
day=$(echo "$date_obj" | jq -r '.day // ""' 2>/dev/null || echo "")
else
year=$(echo "$date_obj" | python3 -c "import json, sys; data=json.load(sys.stdin); print(data.get('year', 'N/A'))" 2>/dev/null || echo "N/A")
month=$(echo "$date_obj" | python3 -c "import json, sys; data=json.load(sys.stdin); print(data.get('month', ''))" 2>/dev/null || echo "")
day=$(echo "$date_obj" | python3 -c "import json, sys; data=json.load(sys.stdin); print(data.get('day', ''))" 2>/dev/null || echo "")
fi
if [ "$year" = "N/A" ] || [ "$year" = "null" ]; then
echo "N/A"
elif [ -n "$month" ] && [ "$month" != "null" ] && [ -n "$day" ] && [ "$day" != "null" ]; then
echo "$day/$month/$year"
elif [ -n "$month" ] && [ "$month" != "null" ]; then
echo "$month/$year"
else
echo "$year"
fi
}
# If no selection or search results file doesn't exist, show placeholder
if [ -z "$SELECTED_ITEM" ] || [ ! -f "$SEARCH_RESULTS_FILE" ]; then
echo "${C_TITLE}Dynamic Search Preview${RESET}"
draw_rule
echo "Type to search for anime..."
echo "Results will appear here as you type."
echo
echo "DEBUG:"
echo "SELECTED_ITEM='$SELECTED_ITEM'"
echo "SEARCH_RESULTS_FILE='$SEARCH_RESULTS_FILE'"
if [ -f "$SEARCH_RESULTS_FILE" ]; then
echo "Search results file exists"
else
echo "Search results file missing"
fi
exit 0
fi
# Parse the search results JSON and find the matching item
if command -v jq >/dev/null 2>&1; then
MEDIA_DATA=$(cat "$SEARCH_RESULTS_FILE" | jq --arg anime_title "$SELECTED_ITEM" '
.data.Page.media[]? |
select((.title.english // .title.romaji // .title.native // "Unknown") == $anime_title )
' )
else
# Fallback to Python for JSON parsing
MEDIA_DATA=$(cat "$SEARCH_RESULTS_FILE" | python3 -c "
import json
import sys
try:
data = json.load(sys.stdin)
selected_item = '''$SELECTED_ITEM'''
if 'data' not in data or 'Page' not in data['data'] or 'media' not in data['data']['Page']:
sys.exit(1)
media_list = data['data']['Page']['media']
for media in media_list:
title = media.get('title', {})
english_title = title.get('english') or title.get('romaji') or title.get('native', 'Unknown')
year = media.get('startDate', {}).get('year', 'Unknown') if media.get('startDate') else 'Unknown'
status = media.get('status', 'Unknown')
genres = ', '.join(media.get('genres', [])[:3]) or 'Unknown'
display_format = f'{english_title} ({year}) [{status}] - {genres}'
# Debug output for matching
print(f"DEBUG: selected_item='{selected_item.strip()}' display_format='{display_format.strip()}'", file=sys.stderr)
if selected_item.strip() == display_format.strip():
json.dump(media, sys.stdout, indent=2)
sys.exit(0)
print(f"DEBUG: No match found for selected_item='{selected_item.strip()}'", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f'Error: {e}', file=sys.stderr)
sys.exit(1)
" 2>/dev/null)
fi
# If we couldn't find the media data, show error
if [ $? -ne 0 ] || [ -z "$MEDIA_DATA" ]; then
echo "${C_TITLE}Preview Error${RESET}"
draw_rule
echo "Could not load preview data for:"
echo "$SELECTED_ITEM"
echo
echo "DEBUG INFO:"
echo "Search results file: $SEARCH_RESULTS_FILE"
if [ -f "$SEARCH_RESULTS_FILE" ]; then
echo "File exists, size: $(wc -c < "$SEARCH_RESULTS_FILE") bytes"
echo "First few lines of search results:"
head -3 "$SEARCH_RESULTS_FILE" 2>/dev/null || echo "Cannot read file"
else
echo "Search results file does not exist"
fi
exit 0
fi
# Extract information from the media data
if command -v jq >/dev/null 2>&1; then
# Use jq for faster extraction
TITLE=$(echo "$MEDIA_DATA" | jq -r '.title.english // .title.romaji // .title.native // "Unknown"' 2>/dev/null || echo "Unknown")
STATUS=$(echo "$MEDIA_DATA" | jq -r '.status // "Unknown"' 2>/dev/null || echo "Unknown")
FORMAT=$(echo "$MEDIA_DATA" | jq -r '.format // "Unknown"' 2>/dev/null || echo "Unknown")
EPISODES=$(echo "$MEDIA_DATA" | jq -r '.episodes // "Unknown"' 2>/dev/null || echo "Unknown")
DURATION=$(echo "$MEDIA_DATA" | jq -r 'if .duration then "\(.duration) min" else "Unknown" end' 2>/dev/null || echo "Unknown")
SCORE=$(echo "$MEDIA_DATA" | jq -r 'if .averageScore then "\(.averageScore)/100" else "N/A" end' 2>/dev/null || echo "N/A")
FAVOURITES=$(echo "$MEDIA_DATA" | jq -r '.favourites // 0' 2>/dev/null | sed ':a;s/\B[0-9]\{3\}\>/,&/;ta' || echo "0")
POPULARITY=$(echo "$MEDIA_DATA" | jq -r '.popularity // 0' 2>/dev/null | sed ':a;s/\B[0-9]\{3\}\>/,&/;ta' || echo "0")
GENRES=$(echo "$MEDIA_DATA" | jq -r '(.genres[:5] // []) | join(", ") | if . == "" then "Unknown" else . end' 2>/dev/null || echo "Unknown")
DESCRIPTION=$(echo "$MEDIA_DATA" | jq -r '.description // "No description available."' 2>/dev/null || echo "No description available.")
# Get start and end dates as JSON objects
START_DATE_OBJ=$(echo "$MEDIA_DATA" | jq -c '.startDate' 2>/dev/null || echo "null")
END_DATE_OBJ=$(echo "$MEDIA_DATA" | jq -c '.endDate' 2>/dev/null || echo "null")
# Get cover image URL
COVER_IMAGE=$(echo "$MEDIA_DATA" | jq -r '.coverImage.large // ""' 2>/dev/null || echo "")
else
# Fallback to Python for extraction
TITLE=$(echo "$MEDIA_DATA" | python3 -c "import json, sys; data=json.load(sys.stdin); title=data.get('title',{}); print(title.get('english') or title.get('romaji') or title.get('native', 'Unknown'))" 2>/dev/null || echo "Unknown")
STATUS=$(echo "$MEDIA_DATA" | python3 -c "import json, sys; data=json.load(sys.stdin); print(data.get('status', 'Unknown'))" 2>/dev/null || echo "Unknown")
FORMAT=$(echo "$MEDIA_DATA" | python3 -c "import json, sys; data=json.load(sys.stdin); print(data.get('format', 'Unknown'))" 2>/dev/null || echo "Unknown")
EPISODES=$(echo "$MEDIA_DATA" | python3 -c "import json, sys; data=json.load(sys.stdin); print(data.get('episodes', 'Unknown'))" 2>/dev/null || echo "Unknown")
DURATION=$(echo "$MEDIA_DATA" | python3 -c "import json, sys; data=json.load(sys.stdin); duration=data.get('duration'); print(f'{duration} min' if duration else 'Unknown')" 2>/dev/null || echo "Unknown")
SCORE=$(echo "$MEDIA_DATA" | python3 -c "import json, sys; data=json.load(sys.stdin); score=data.get('averageScore'); print(f'{score}/100' if score else 'N/A')" 2>/dev/null || echo "N/A")
FAVOURITES=$(echo "$MEDIA_DATA" | python3 -c "import json, sys; data=json.load(sys.stdin); print(f\"{data.get('favourites', 0):,}\")" 2>/dev/null || echo "0")
POPULARITY=$(echo "$MEDIA_DATA" | python3 -c "import json, sys; data=json.load(sys.stdin); print(f\"{data.get('popularity', 0):,}\")" 2>/dev/null || echo "0")
GENRES=$(echo "$MEDIA_DATA" | python3 -c "import json, sys; data=json.load(sys.stdin); print(', '.join(data.get('genres', [])[:5]))" 2>/dev/null || echo "Unknown")
DESCRIPTION=$(echo "$MEDIA_DATA" | python3 -c "import json, sys; data=json.load(sys.stdin); print(data.get('description', 'No description available.'))" 2>/dev/null || echo "No description available.")
# Get start and end dates
START_DATE_OBJ=$(echo "$MEDIA_DATA" | python3 -c "import json, sys; data=json.load(sys.stdin); json.dump(data.get('startDate'), sys.stdout)" 2>/dev/null || echo "null")
END_DATE_OBJ=$(echo "$MEDIA_DATA" | python3 -c "import json, sys; data=json.load(sys.stdin); json.dump(data.get('endDate'), sys.stdout)" 2>/dev/null || echo "null")
# Get cover image URL
COVER_IMAGE=$(echo "$MEDIA_DATA" | python3 -c "import json, sys; data=json.load(sys.stdin); cover=data.get('coverImage',{}); print(cover.get('large', ''))" 2>/dev/null || echo "")
fi
# Format the dates
START_DATE=$(format_date "$START_DATE_OBJ")
END_DATE=$(format_date "$END_DATE_OBJ")
# Generate cache hash for this item (using selected item like regular preview)
CACHE_HASH=$(generate_sha256 "$SELECTED_ITEM")
# Try to show image if available
if [ "{PREVIEW_MODE}" = "full" ] || [ "{PREVIEW_MODE}" = "image" ]; then
image_file="{IMAGE_CACHE_PATH}{PATH_SEP}${CACHE_HASH}.png"
# If image not cached and we have a URL, try to download it quickly
if [ ! -f "$image_file" ] && [ -n "$COVER_IMAGE" ]; then
if command -v curl >/dev/null 2>&1; then
# Quick download with timeout
curl -s -m 3 -L "$COVER_IMAGE" -o "$image_file" 2>/dev/null || rm -f "$image_file" 2>/dev/null
fi
fi
if [ -f "$image_file" ]; then
fzf_preview "$image_file"
else
echo "🖼️ Loading image..."
fi
echo
fi
# Display text info if configured
if [ "{PREVIEW_MODE}" = "full" ] || [ "{PREVIEW_MODE}" = "text" ]; then
draw_rule
print_kv "Title" "$TITLE"
draw_rule
print_kv "Score" "$SCORE"
print_kv "Favourites" "$FAVOURITES"
print_kv "Popularity" "$POPULARITY"
print_kv "Status" "$STATUS"
draw_rule
print_kv "Episodes" "$EPISODES"
print_kv "Duration" "$DURATION"
print_kv "Format" "$FORMAT"
draw_rule
print_kv "Genres" "$GENRES"
print_kv "Start Date" "$START_DATE"
print_kv "End Date" "$END_DATE"
draw_rule
# Clean and display description
CLEAN_DESCRIPTION=$(clean_html "$DESCRIPTION")
echo "$CLEAN_DESCRIPTION" | fold -s -w "$WIDTH"
fi

View File

@@ -0,0 +1,31 @@
#!/bin/sh
#
# Episode Preview Info Script Template
# This script formats and displays episode information in the FZF preview pane.
# Some values are injected by python those with '{name}' syntax using .replace()
draw_rule
echo "{TITLE}" | fold -s -w "$WIDTH"
draw_rule
print_kv "Duration" "{DURATION}"
print_kv "Status" "{STATUS}"
draw_rule
print_kv "Total Episodes" "{EPISODES}"
print_kv "Next Episode" "{NEXT_EPISODE}"
draw_rule
print_kv "Progress" "{USER_PROGRESS}"
print_kv "List Status" "{USER_STATUS}"
draw_rule
print_kv "Start Date" "{START_DATE}"
print_kv "End Date" "{END_DATE}"
draw_rule

View File

@@ -0,0 +1,54 @@
#!/bin/sh
#
# FastAnime Preview Info Script Template
# This script formats and displays the textual information in the FZF preview pane.
# Some values are injected by python those with '{name}' syntax using .replace()
draw_rule
print_kv "Title" "{TITLE}"
draw_rule
# Emojis take up double the space
score_multiplier=1
if ! [ "{SCORE}" = "N/A" ]; then
score_multiplier=2
fi
print_kv "Score" "{SCORE}" $score_multiplier
print_kv "Favourites" "{FAVOURITES}"
print_kv "Popularity" "{POPULARITY}"
print_kv "Status" "{STATUS}"
draw_rule
print_kv "Episodes" "{EPISODES}"
print_kv "Next Episode" "{NEXT_EPISODE}"
print_kv "Duration" "{DURATION}"
draw_rule
print_kv "Genres" "{GENRES}"
print_kv "Format" "{FORMAT}"
draw_rule
print_kv "List Status" "{USER_STATUS}"
print_kv "Progress" "{USER_PROGRESS}"
draw_rule
print_kv "Start Date" "{START_DATE}"
print_kv "End Date" "{END_DATE}"
draw_rule
print_kv "Studios" "{STUDIOS}"
print_kv "Synonymns" "{SYNONYMNS}"
print_kv "Tags" "{TAGS}"
draw_rule
# Synopsis
echo "{SYNOPSIS}" | fold -s -w "$WIDTH"

View File

@@ -0,0 +1,146 @@
#!/bin/sh
#
# FZF Preview Script Template
#
# This script is a template. The placeholders in curly braces, like {NAME}
# are dynamically filled by python using .replace()
WIDTH=${FZF_PREVIEW_COLUMNS:-80} # Set a fallback width of 80
IMAGE_RENDERER="{IMAGE_RENDERER}"
generate_sha256() {
local input
# Check if input is passed as an argument or piped
if [ -n "$1" ]; then
input="$1"
else
input=$(cat)
fi
if command -v sha256sum &>/dev/null; then
echo -n "$input" | sha256sum | awk '{print $1}'
elif command -v shasum &>/dev/null; then
echo -n "$input" | shasum -a 256 | awk '{print $1}'
elif command -v sha256 &>/dev/null; then
echo -n "$input" | sha256 | awk '{print $1}'
elif command -v openssl &>/dev/null; then
echo -n "$input" | openssl dgst -sha256 | awk '{print $2}'
else
echo -n "$input" | base64 | tr '/+' '_-' | tr -d '\n'
fi
}
fzf_preview() {
file=$1
dim=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}
if [ "$dim" = x ]; then
dim=$(stty size </dev/tty | awk "{print \$2 \"x\" \$1}")
fi
if ! [ "$IMAGE_RENDERER" = "icat" ] && [ -z "$KITTY_WINDOW_ID" ] && [ "$((FZF_PREVIEW_TOP + FZF_PREVIEW_LINES))" -eq "$(stty size </dev/tty | awk "{print \$1}")" ]; then
dim=${FZF_PREVIEW_COLUMNS}x$((FZF_PREVIEW_LINES - 1))
fi
if [ "$IMAGE_RENDERER" = "icat" ] && [ -z "$GHOSTTY_BIN_DIR" ]; then
if command -v kitten >/dev/null 2>&1; then
kitten icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed "\$d" | sed "$(printf "\$s/\$/\033[m/")"
elif command -v icat >/dev/null 2>&1; then
icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed "\$d" | sed "$(printf "\$s/\$/\033[m/")"
else
kitty icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed "\$d" | sed "$(printf "\$s/\$/\033[m/")"
fi
elif [ -n "$GHOSTTY_BIN_DIR" ]; then
if command -v kitten >/dev/null 2>&1; then
kitten icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed "\$d" | sed "$(printf "\$s/\$/\033[m/")"
elif command -v icat >/dev/null 2>&1; then
icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed "\$d" | sed "$(printf "\$s/\$/\033[m/")"
else
chafa -s "$dim" "$file"
fi
elif command -v chafa >/dev/null 2>&1; then
case "$PLATFORM" in
android) chafa -s "$dim" "$file" ;;
windows) chafa -f sixel -s "$dim" "$file" ;;
*) chafa -s "$dim" "$file" ;;
esac
echo
elif command -v imgcat >/dev/null; then
imgcat -W "${dim%%x*}" -H "${dim##*x}" "$file"
else
echo please install a terminal image viewer
echo either icat for kitty terminal and wezterm or imgcat or chafa
fi
}
# --- Helper function for printing a key-value pair, aligning the value to the right ---
print_kv() {
local key="$1"
local value="$2"
local key_len=${#key}
local value_len=${#value}
local multiplier="${3:-1}"
# Correctly calculate padding by accounting for the key, the ": ", and the value.
local padding_len=$((WIDTH - key_len - 2 - value_len * multiplier))
# If the text is too long to fit, just add a single space for separation.
if [ "$padding_len" -lt 1 ]; then
padding_len=1
value=$(echo $value| fold -s -w "$((WIDTH - key_len - 3))")
printf "{C_KEY}%s:{RESET}%*s%s\\n" "$key" "$padding_len" "" " $value"
else
printf "{C_KEY}%s:{RESET}%*s%s\\n" "$key" "$padding_len" "" " $value"
fi
}
# --- Draw a rule across the screen ---
# TODO: figure out why this method does not work in fzf
draw_rule() {
local rule
# Generate the line of '─' characters, removing the trailing newline `tr` adds.
rule=$(printf '%*s' "$WIDTH" | tr ' ' '─' | tr -d '\n')
# Print the rule with colors and a single, clean newline.
printf "{C_RULE}%s{RESET}\\n" "$rule"
}
draw_rule(){
ll=2
while [ $ll -le $FZF_PREVIEW_COLUMNS ];do
echo -n -e "{C_RULE}─{RESET}"
((ll++))
done
echo
}
# Generate the same cache key that the Python worker uses
# {PREFIX} is used only on episode previews to make sure they are unique
title={}
hash=$(generate_sha256 "{PREFIX}$title")
#
# --- Display image if configured and the cached file exists ---
#
if [ "{PREVIEW_MODE}" = "full" ] || [ "{PREVIEW_MODE}" = "image" ]; then
image_file="{IMAGE_CACHE_PATH}{PATH_SEP}$hash.png"
if [ -f "$image_file" ]; then
fzf_preview "$image_file"
else
echo "🖼️ Loading image..."
fi
echo # Add a newline for spacing
fi
# Display text info if configured and the cached file exists
if [ "{PREVIEW_MODE}" = "full" ] || [ "{PREVIEW_MODE}" = "text" ]; then
info_file="{INFO_CACHE_PATH}{PATH_SEP}$hash"
if [ -f "$info_file" ]; then
source "$info_file"
else
echo "📝 Loading details..."
fi
fi

View File

@@ -0,0 +1,19 @@
#!/bin/sh
#
# FastAnime Review Info Script Template
# This script formats and displays review details in the FZF preview pane.
# Python injects the actual data values into the placeholders.
draw_rule
print_kv "Review By" "{REVIEWER_NAME}"
draw_rule
print_kv "Summary" "{REVIEW_SUMMARY}"
draw_rule
echo "{REVIEW_BODY}" | fold -s -w "$WIDTH"
draw_rule

View File

@@ -0,0 +1,75 @@
#!/bin/sh
#
# FZF Preview Script Template
#
# This script is a template. The placeholders in curly braces, like {NAME}
# are dynamically filled by python using .replace()
WIDTH=${FZF_PREVIEW_COLUMNS:-80} # Set a fallback width of 80
IMAGE_RENDERER="{IMAGE_RENDERER}"
generate_sha256() {
local input
# Check if input is passed as an argument or piped
if [ -n "$1" ]; then
input="$1"
else
input=$(cat)
fi
if command -v sha256sum &>/dev/null; then
echo -n "$input" | sha256sum | awk '{print $1}'
elif command -v shasum &>/dev/null; then
echo -n "$input" | shasum -a 256 | awk '{print $1}'
elif command -v sha256 &>/dev/null; then
echo -n "$input" | sha256 | awk '{print $1}'
elif command -v openssl &>/dev/null; then
echo -n "$input" | openssl dgst -sha256 | awk '{print $2}'
else
echo -n "$input" | base64 | tr '/+' '_-' | tr -d '\n'
fi
}
print_kv() {
local key="$1"
local value="$2"
local key_len=${#key}
local value_len=${#value}
local multiplier="${3:-1}"
# Correctly calculate padding by accounting for the key, the ": ", and the value.
local padding_len=$((WIDTH - key_len - 2 - value_len * multiplier))
# If the text is too long to fit, just add a single space for separation.
if [ "$padding_len" -lt 1 ]; then
padding_len=1
value=$(echo $value| fold -s -w "$((WIDTH - key_len - 3))")
printf "{C_KEY}%s:{RESET}%*s%s\\n" "$key" "$padding_len" "" " $value"
else
printf "{C_KEY}%s:{RESET}%*s%s\\n" "$key" "$padding_len" "" " $value"
fi
}
draw_rule(){
ll=2
while [ $ll -le $FZF_PREVIEW_COLUMNS ];do
echo -n -e "{C_RULE}─{RESET}"
((ll++))
done
echo
}
title={}
hash=$(generate_sha256 "$title")
if [ "{PREVIEW_MODE}" = "full" ] || [ "{PREVIEW_MODE}" = "text" ]; then
info_file="{INFO_CACHE_DIR}{PATH_SEP}$hash"
if [ -f "$info_file" ]; then
source "$info_file"
else
echo "📝 Loading details..."
fi
fi

View File

@@ -0,0 +1,118 @@
#!/bin/bash
#
# FZF Dynamic Search Script Template
#
# This script is a template for dynamic search functionality in fzf.
# The placeholders in curly braces, like {QUERY} are dynamically filled by Python using .replace()
# Configuration variables (injected by Python)
GRAPHQL_ENDPOINT="{GRAPHQL_ENDPOINT}"
CACHE_DIR="{CACHE_DIR}"
SEARCH_RESULTS_FILE="{SEARCH_RESULTS_FILE}"
AUTH_HEADER="{AUTH_HEADER}"
# Get the current query from fzf
QUERY="{{q}}"
# If query is empty, exit with empty results
if [ -z "$QUERY" ]; then
echo ""
exit 0
fi
# Create GraphQL variables
VARIABLES=$(cat <<EOF
{
"query": "$QUERY",
"type": "ANIME",
"per_page": 50,
"genre_not_in": ["Hentai"]
}
EOF
)
# The GraphQL query is injected here as a properly escaped string
GRAPHQL_QUERY='{GRAPHQL_QUERY}'
# Create the GraphQL request payload
PAYLOAD=$(cat <<EOF
{
"query": $GRAPHQL_QUERY,
"variables": $VARIABLES
}
EOF
)
# Make the GraphQL request and save raw results
if [ -n "$AUTH_HEADER" ]; then
RESPONSE=$(curl -s -X POST \
-H "Content-Type: application/json" \
-H "Authorization: $AUTH_HEADER" \
-d "$PAYLOAD" \
"$GRAPHQL_ENDPOINT")
else
RESPONSE=$(curl -s -X POST \
-H "Content-Type: application/json" \
-d "$PAYLOAD" \
"$GRAPHQL_ENDPOINT")
fi
# Check if the request was successful
if [ $? -ne 0 ] || [ -z "$RESPONSE" ]; then
echo "❌ Search failed"
exit 1
fi
# Save the raw response for later processing
echo "$RESPONSE" > "$SEARCH_RESULTS_FILE"
# Parse and display results
if command -v jq >/dev/null 2>&1; then
# Use jq for faster and more reliable JSON parsing
echo "$RESPONSE" | jq -r '
if .errors then
"❌ Search error: " + (.errors | tostring)
elif (.data.Page.media // []) | length == 0 then
"❌ No results found"
else
.data.Page.media[] | (.title.english // .title.romaji // .title.native // "Unknown")
end
' 2>/dev/null || echo "❌ Parse error"
else
# Fallback to Python for JSON parsing
echo "$RESPONSE" | python3 -c "
import json
import sys
try:
data = json.load(sys.stdin)
if 'errors' in data:
print('❌ Search error: ' + str(data['errors']))
sys.exit(1)
if 'data' not in data or 'Page' not in data['data'] or 'media' not in data['data']['Page']:
print('❌ No results found')
sys.exit(0)
media_list = data['data']['Page']['media']
if not media_list:
print('❌ No results found')
sys.exit(0)
for media in media_list:
title = media.get('title', {})
english_title = title.get('english') or title.get('romaji') or title.get('native', 'Unknown')
year = media.get('startDate', {}).get('year', 'Unknown') if media.get('startDate') else 'Unknown'
status = media.get('status', 'Unknown')
genres = ', '.join(media.get('genres', [])[:3]) or 'Unknown'
# Format: Title (Year) [Status] - Genres
print(f'{english_title} ({year}) [{status}] - {genres}')
except Exception as e:
print(f'❌ Parse error: {str(e)}')
sys.exit(1)
"
fi

View File

@@ -1,291 +1,3 @@
import signal
from .cli import cli as run_cli
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",
"grab": "grab.grab",
}
# 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(
"--local-history/--remote-history",
type=bool,
help="Whether to continue from local history or remote history",
)
@click.option(
"--skip/--no-skip",
type=bool,
help="Skip opening and ending theme songs?",
)
@click.option(
"-q",
"--quality",
type=click.Choice(
[
"360",
"480",
"720",
"1080",
]
),
help="set the quality of the stream",
)
@click.option(
"-t",
"--translation-type",
type=click.Choice(["dub", "sub"]),
help="Anime language[dub/sub]",
)
@click.option(
"-sl",
"--sub-lang",
help="Set the preferred language for subs",
)
@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.option("--sync-play", "-sp", help="Use sync play", is_flag=True)
@click.pass_context
def run_cli(
ctx: click.Context,
log,
log_file,
rich_traceback,
provider,
server,
format,
continue_,
local_history,
skip,
translation_type,
sub_lang,
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,
sync_play,
):
from .config import Config
ctx.obj = Config()
if log:
import logging
from rich.logging import RichHandler
FORMAT = "%(message)s"
logging.basicConfig(
level="debug", format=FORMAT, datefmt="[%X]", handlers=[RichHandler()]
)
logger = logging.getLogger(__name__)
logger.info("logging has been initialized")
elif log_file:
import logging
from ..constants import LOG_FILE_PATH
format = "%(asctime)s%(levelname)s: %(message)s"
logging.basicConfig(
level=logging.DEBUG,
filename=LOG_FILE_PATH,
format=format,
datefmt="[%d/%m/%Y@%H:%M:%S]",
filemode="w",
)
else:
import logging
logging.basicConfig(level=logging.CRITICAL)
if rich_traceback:
from rich.traceback import install
install()
if sync_play:
ctx.obj.sync_play = sync_play
if provider:
ctx.obj.provider = provider
if server:
ctx.obj.server = server
if format:
ctx.obj.format = format
if sub_lang:
ctx.obj.sub_lang = sub_lang
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("local_history")
== click.core.ParameterSource.COMMANDLINE
):
ctx.obj.preferred_history = "local" if local_history else "remote"
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
__all__ = ["run_cli"]

108
fastanime/cli/cli.py Normal file
View File

@@ -0,0 +1,108 @@
import logging
import sys
from typing import TYPE_CHECKING
import click
from click.core import ParameterSource
from ..core.config import AppConfig
from ..core.constants import PROJECT_NAME, USER_CONFIG, __version__
from .config import ConfigLoader
from .options import options_from_model
from .utils.exception import setup_exceptions_handler
from .utils.lazyloader import LazyGroup
from .utils.logging import setup_logging
if TYPE_CHECKING:
from typing import TypedDict
from typing_extensions import Unpack
class Options(TypedDict):
no_config: bool | None
trace: bool | None
dev: bool | None
log: bool | None
rich_traceback: bool | None
rich_traceback_theme: str
logger = logging.getLogger(__name__)
commands = {
"config": "config.config",
"search": "search.search",
"anilist": "anilist.anilist",
"download": "download.download",
"update": "update.update",
"registry": "registry.registry",
"worker": "worker.worker",
"queue": "queue.queue",
}
@click.group(
cls=LazyGroup,
root="fastanime.cli.commands",
lazy_subcommands=commands,
context_settings=dict(auto_envvar_prefix=PROJECT_NAME),
)
@click.version_option(__version__, "--version")
@click.option("--no-config", is_flag=True, help="Don't load the user config file.")
@click.option(
"--trace", is_flag=True, help="Controls Whether to display tracebacks or not"
)
@click.option("--dev", is_flag=True, help="Controls Whether the app is in dev mode")
@click.option("--log", is_flag=True, help="Controls Whether to log")
@click.option(
"--rich-traceback",
is_flag=True,
help="Controls Whether to display a rich traceback",
)
@click.option(
"--rich-traceback-theme",
default="github-dark",
help="Controls Whether to display a rich traceback",
)
@options_from_model(AppConfig)
@click.pass_context
def cli(ctx: click.Context, **options: "Unpack[Options]"):
"""
The main entry point for the FastAnime CLI.
"""
setup_logging(options["log"])
setup_exceptions_handler(
options["trace"],
options["dev"],
options["rich_traceback"],
options["rich_traceback_theme"],
)
logger.info(f"Current Command: {' '.join(sys.argv)}")
cli_overrides = {}
param_lookup = {p.name: p for p in ctx.command.params}
for param_name, param_value in ctx.params.items():
source = ctx.get_parameter_source(param_name)
if source in (ParameterSource.ENVIRONMENT, ParameterSource.COMMANDLINE):
parameter = param_lookup.get(param_name)
if (
parameter
and hasattr(parameter, "model_name")
and hasattr(parameter, "field_name")
):
model_name = getattr(parameter, "model_name")
field_name = getattr(parameter, "field_name")
if model_name not in cli_overrides:
cli_overrides[model_name] = {}
cli_overrides[model_name][field_name] = param_value
loader = ConfigLoader(config_path=USER_CONFIG)
config = (
AppConfig.model_validate(cli_overrides)
if options["no_config"]
else loader.load(cli_overrides)
)
ctx.obj = config

View File

@@ -1,40 +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")
# 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,51 +1 @@
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)
from .cmd import anilist

View File

@@ -0,0 +1,42 @@
import click
from ...utils.lazyloader import LazyGroup
from . import examples
commands = {
# "trending": "trending.trending",
# "recent": "recent.recent",
"search": "search.search",
"download": "download.download",
"auth": "auth.auth",
"stats": "stats.stats",
"notifications": "notifications.notifications",
}
@click.group(
cls=LazyGroup,
name="anilist",
root="fastanime.cli.commands.anilist.commands",
invoke_without_command=True,
help="A beautiful interface that gives you access to a commplete streaming experience",
short_help="Access all streaming options",
lazy_subcommands=commands,
epilog=examples.main,
)
@click.option(
"--resume", is_flag=True, help="Resume from the last session (Not yet implemented)."
)
@click.pass_context
def anilist(ctx: click.Context, resume: bool):
"""
The entry point for the 'anilist' command. If no subcommand is invoked,
it launches the interactive TUI mode.
"""
from ...interactive.session import session
config = ctx.obj
if ctx.invoked_subcommand is None:
session.load_menus_from_folder("media")
session.run(config, resume=resume)

View File

@@ -0,0 +1,64 @@
import click
from .....core.config.model import AppConfig
@click.command(help="Login to your AniList account to enable progress tracking.")
@click.option("--status", "-s", is_flag=True, help="Check current login status.")
@click.option("--logout", "-l", is_flag=True, help="Log out and erase credentials.")
@click.pass_obj
def auth(config: AppConfig, status: bool, logout: bool):
"""Handles user authentication and credential management."""
from .....core.constants import ANILIST_AUTH
from .....libs.media_api.api import create_api_client
from .....libs.selectors.selector import create_selector
from ....service.auth import AuthService
from ....service.feedback import FeedbackService
auth_service = AuthService("anilist")
feedback = FeedbackService(config)
selector = create_selector(config)
feedback.clear_console()
if status:
user_data = auth_service.get_auth()
if user_data:
feedback.info(f"Logged in as: {user_data.user_profile}")
else:
feedback.error("Not logged in.")
return
if logout:
if selector.confirm("Are you sure you want to log out and erase your token?"):
auth_service.clear_user_profile()
feedback.info("You have been logged out.")
return
if auth_profile := auth_service.get_auth():
if not selector.confirm(
f"You are already logged in as {auth_profile.user_profile.name}.Would you like to relogin"
):
return
api_client = create_api_client("anilist", config)
# TODO: stop the printing of opening browser session to stderr
click.launch(ANILIST_AUTH)
feedback.info("Your browser has been opened to obtain an AniList token.")
feedback.info(
"After authorizing, copy the token from the address bar and paste it below."
)
token = selector.ask("Enter your AniList Access Token")
if not token:
feedback.error("Login cancelled.")
return
# Use the API client to validate the token and get profile info
profile = api_client.authenticate(token.strip())
if profile:
# If successful, use the manager to save the credentials
auth_service.save_user_profile(profile, token)
feedback.info(f"Successfully logged in as {profile.name}! ✨")
else:
feedback.error("Login failed. The token may be invalid or expired.")

View File

@@ -0,0 +1,265 @@
from typing import TYPE_CHECKING, Dict, List
import click
from fastanime.cli.utils.completion import anime_titles_shell_complete
from fastanime.core.config import AppConfig
from fastanime.core.exceptions import FastAnimeError
from fastanime.libs.media_api.types import (
MediaFormat,
MediaGenre,
MediaItem,
MediaSeason,
MediaSort,
MediaStatus,
MediaTag,
MediaType,
MediaYear,
)
from .. import examples
if TYPE_CHECKING:
from typing import TypedDict
from typing_extensions import Unpack
class DownloadOptions(TypedDict, total=False):
title: str | None
episode_range: str | None
page: int
per_page: int | None
season: str | None
status: tuple[str, ...]
status_not: tuple[str, ...]
sort: str | None
genres: tuple[str, ...]
genres_not: tuple[str, ...]
tags: tuple[str, ...]
tags_not: tuple[str, ...]
media_format: tuple[str, ...]
media_type: str | None
year: str | None
popularity_greater: int | None
popularity_lesser: int | None
score_greater: int | None
score_lesser: int | None
start_date_greater: int | None
start_date_lesser: int | None
end_date_greater: int | None
end_date_lesser: int | None
on_list: bool | None
yes: bool
@click.command(
help="Search for anime on AniList and download episodes.",
short_help="Search and download anime.",
epilog=examples.download,
)
# --- Re-using all search options ---
@click.option("--title", "-t", shell_complete=anime_titles_shell_complete)
@click.option("--page", "-p", type=click.IntRange(min=1), default=1)
@click.option("--per-page", type=click.IntRange(min=1, max=50))
@click.option("--season", type=click.Choice([s.value for s in MediaSeason]))
@click.option(
"--status", "-S", multiple=True, type=click.Choice([s.value for s in MediaStatus])
)
@click.option(
"--status-not", multiple=True, type=click.Choice([s.value for s in MediaStatus])
)
@click.option("--sort", "-s", type=click.Choice([s.value for s in MediaSort]))
@click.option(
"--genres", "-g", multiple=True, type=click.Choice([g.value for g in MediaGenre])
)
@click.option(
"--genres-not", multiple=True, type=click.Choice([g.value for g in MediaGenre])
)
@click.option(
"--tags", "-T", multiple=True, type=click.Choice([t.value for t in MediaTag])
)
@click.option(
"--tags-not", multiple=True, type=click.Choice([t.value for t in MediaTag])
)
@click.option(
"--media-format",
"-f",
multiple=True,
type=click.Choice([f.value for f in MediaFormat]),
)
@click.option("--media-type", type=click.Choice([t.value for t in MediaType]))
@click.option("--year", "-y", type=click.Choice([y.value for y in MediaYear]))
@click.option("--popularity-greater", type=click.IntRange(min=0))
@click.option("--popularity-lesser", type=click.IntRange(min=0))
@click.option("--score-greater", type=click.IntRange(min=0, max=100))
@click.option("--score-lesser", type=click.IntRange(min=0, max=100))
@click.option("--start-date-greater", type=int)
@click.option("--start-date-lesser", type=int)
@click.option("--end-date-greater", type=int)
@click.option("--end-date-lesser", type=int)
@click.option("--on-list/--not-on-list", "-L/-no-L", type=bool, default=None)
# --- Download specific options ---
@click.option(
"--episode-range",
"-r",
help="Range of episodes to download (e.g., '1-10', '5', '8:12'). Required.",
required=True,
)
@click.option(
"--yes",
"-Y",
is_flag=True,
help="Automatically download from all found anime without prompting for selection.",
)
@click.pass_obj
def download(config: AppConfig, **options: "Unpack[DownloadOptions]"):
from fastanime.cli.service.download.service import DownloadService
from fastanime.cli.service.feedback import FeedbackService
from fastanime.cli.service.registry import MediaRegistryService
from fastanime.cli.service.watch_history import WatchHistoryService
from fastanime.cli.utils.parser import parse_episode_range
from fastanime.libs.media_api.api import create_api_client
from fastanime.libs.media_api.params import MediaSearchParams
from fastanime.libs.provider.anime.provider import create_provider
from fastanime.libs.selectors import create_selector
from rich.progress import Progress
feedback = FeedbackService(config)
selector = create_selector(config)
media_api = create_api_client(config.general.media_api, config)
provider = create_provider(config.general.provider)
registry = MediaRegistryService(config.general.media_api, config.media_registry)
watch_history = WatchHistoryService(config, registry, media_api)
download_service = DownloadService(config, registry, media_api, provider)
try:
sort_val = options.get("sort")
status_val = options.get("status")
status_not_val = options.get("status_not")
genres_val = options.get("genres")
genres_not_val = options.get("genres_not")
tags_val = options.get("tags")
tags_not_val = options.get("tags_not")
media_format_val = options.get("media_format")
media_type_val = options.get("media_type")
season_val = options.get("season")
year_val = options.get("year")
search_params = MediaSearchParams(
query=options.get("title"),
page=options.get("page", 1),
per_page=options.get("per_page"),
sort=MediaSort(sort_val) if sort_val else None,
status_in=[MediaStatus(s) for s in status_val] if status_val else None,
status_not_in=[MediaStatus(s) for s in status_not_val]
if status_not_val
else None,
genre_in=[MediaGenre(g) for g in genres_val] if genres_val else None,
genre_not_in=[MediaGenre(g) for g in genres_not_val]
if genres_not_val
else None,
tag_in=[MediaTag(t) for t in tags_val] if tags_val else None,
tag_not_in=[MediaTag(t) for t in tags_not_val] if tags_not_val else None,
format_in=[MediaFormat(f) for f in media_format_val]
if media_format_val
else None,
type=MediaType(media_type_val) if media_type_val else None,
season=MediaSeason(season_val) if season_val else None,
seasonYear=int(year_val) if year_val else None,
popularity_greater=options.get("popularity_greater"),
popularity_lesser=options.get("popularity_lesser"),
averageScore_greater=options.get("score_greater"),
averageScore_lesser=options.get("score_lesser"),
startDate_greater=options.get("start_date_greater"),
startDate_lesser=options.get("start_date_lesser"),
endDate_greater=options.get("end_date_greater"),
endDate_lesser=options.get("end_date_lesser"),
on_list=options.get("on_list"),
)
with Progress() as progress:
progress.add_task("Searching AniList...", total=None)
search_result = media_api.search_media(search_params)
if not search_result or not search_result.media:
raise FastAnimeError("No anime found matching your search criteria.")
anime_to_download: List[MediaItem]
if options.get("yes"):
anime_to_download = search_result.media
else:
choice_map: Dict[str, MediaItem] = {
(item.title.english or item.title.romaji or f"ID: {item.id}"): item
for item in search_result.media
}
preview_command = None
if config.general.preview != "none":
from ....utils.preview import create_preview_context
with create_preview_context() as preview_ctx:
preview_command = preview_ctx.get_anime_preview(
list(choice_map.values()),
list(choice_map.keys()),
config,
)
selected_titles = selector.choose_multiple(
"Select anime to download",
list(choice_map.keys()),
preview=preview_command,
)
else:
selected_titles = selector.choose_multiple(
"Select anime to download",
list(choice_map.keys()),
)
if not selected_titles:
feedback.warning("No anime selected. Aborting download.")
return
anime_to_download = [choice_map[title] for title in selected_titles]
total_downloaded = 0
episode_range_str = options.get("episode_range")
if not episode_range_str:
raise FastAnimeError("--episode-range is required.")
for media_item in anime_to_download:
watch_history.add_media_to_list_if_not_present(media_item)
available_episodes = [str(i + 1) for i in range(media_item.episodes or 0)]
if not available_episodes:
feedback.warning(
f"No episode information for '{media_item.title.english}', skipping."
)
continue
try:
episodes_to_download = list(
parse_episode_range(episode_range_str, available_episodes)
)
if not episodes_to_download:
feedback.warning(
f"Episode range '{episode_range_str}' resulted in no episodes for '{media_item.title.english}'."
)
continue
feedback.info(
f"Preparing to download {len(episodes_to_download)} episodes for '{media_item.title.english}'."
)
download_service.download_episodes_sync(
media_item, episodes_to_download
)
total_downloaded += len(episodes_to_download)
except (ValueError, IndexError) as e:
feedback.error(
f"Invalid episode range for '{media_item.title.english}': {e}"
)
continue
feedback.success(
f"Finished. Successfully downloaded a total of {total_downloaded} episodes."
)
except FastAnimeError as e:
feedback.error("Download command failed", str(e))
except Exception as e:
feedback.error("An unexpected error occurred", str(e))

View File

@@ -0,0 +1,56 @@
import click
from fastanime.core.config import AppConfig
from rich.console import Console
from rich.table import Table
@click.command(help="Check for new AniList notifications (e.g., for airing episodes).")
@click.pass_obj
def notifications(config: AppConfig):
"""
Displays unread notifications from AniList.
Running this command will also mark the notifications as read on the AniList website.
"""
from fastanime.cli.service.feedback import FeedbackService
from fastanime.libs.media_api.api import create_api_client
from ....service.auth import AuthService
feedback = FeedbackService(config)
console = Console()
auth = AuthService(config.general.media_api)
api_client = create_api_client(config.general.media_api, config)
if profile := auth.get_auth():
api_client.authenticate(profile.token)
if not api_client.is_authenticated():
feedback.error(
"Authentication Required", "Please log in with 'fastanime anilist auth'."
)
return
with feedback.progress("Fetching notifications..."):
notifs = api_client.get_notifications()
if not notifs:
feedback.success("All caught up!", "You have no new notifications.")
return
table = Table(
title="🔔 AniList Notifications", show_header=True, header_style="bold magenta"
)
table.add_column("Date", style="dim", width=12)
table.add_column("Anime Title", style="cyan")
table.add_column("Details", style="green")
for notif in sorted(notifs, key=lambda n: n.created_at, reverse=True):
title = notif.media.title.english or notif.media.title.romaji or "Unknown"
date_str = notif.created_at.strftime("%Y-%m-%d")
details = f"Episode {notif.episode} has aired!"
table.add_row(date_str, title, details)
console.print(table)
feedback.info(
"Notifications have been marked as read on AniList.",
)

View File

@@ -0,0 +1,334 @@
from typing import TYPE_CHECKING
import click
from .....core.config import AppConfig
from .....core.exceptions import FastAnimeError
from .....libs.media_api.types import (
MediaFormat,
MediaGenre,
MediaSeason,
MediaSort,
MediaStatus,
MediaTag,
MediaType,
MediaYear,
)
from ....utils.completion import anime_titles_shell_complete
from .. import examples
if TYPE_CHECKING:
from typing import TypedDict
from typing_extensions import Unpack
class SearchOptions(TypedDict, total=False):
title: str | None
dump_json: bool
page: int
per_page: int | None
season: str | None
status: tuple[str, ...]
status_not: tuple[str, ...]
sort: str | None
genres: tuple[str, ...]
genres_not: tuple[str, ...]
tags: tuple[str, ...]
tags_not: tuple[str, ...]
media_format: tuple[str, ...]
media_type: str | None
year: str | None
popularity_greater: int | None
popularity_lesser: int | None
score_greater: int | None
score_lesser: int | None
start_date_greater: int | None
start_date_lesser: int | None
end_date_greater: int | None
end_date_lesser: int | None
on_list: bool | None
@click.command(
help="Search for anime using anilists api and get top ~50 results",
short_help="Search for anime",
epilog=examples.search,
)
@click.option("--title", "-t", shell_complete=anime_titles_shell_complete)
@click.option(
"--dump-json",
"-d",
is_flag=True,
help="Only print out the results dont open anilist menu",
)
@click.option(
"--page",
"-p",
type=click.IntRange(min=1),
default=1,
help="Page number for pagination",
)
@click.option(
"--per-page",
type=click.IntRange(min=1, max=50),
help="Number of results per page (max 50)",
)
@click.option(
"--season",
help="The season the media was released",
type=click.Choice([season.value for season in MediaSeason]),
)
@click.option(
"--status",
"-S",
help="The media status of the anime",
multiple=True,
type=click.Choice([status.value for status in MediaStatus]),
)
@click.option(
"--status-not",
help="Exclude media with these statuses",
multiple=True,
type=click.Choice([status.value for status in MediaStatus]),
)
@click.option(
"--sort",
"-s",
help="What to sort the search results on",
type=click.Choice([sort.value for sort in MediaSort]),
)
@click.option(
"--genres",
"-g",
multiple=True,
help="the genres to filter by",
type=click.Choice([genre.value for genre in MediaGenre]),
)
@click.option(
"--genres-not",
multiple=True,
help="Exclude these genres",
type=click.Choice([genre.value for genre in MediaGenre]),
)
@click.option(
"--tags",
"-T",
multiple=True,
help="the tags to filter by",
type=click.Choice([tag.value for tag in MediaTag]),
)
@click.option(
"--tags-not",
multiple=True,
help="Exclude these tags",
type=click.Choice([tag.value for tag in MediaTag]),
)
@click.option(
"--media-format",
"-f",
multiple=True,
help="Media format",
type=click.Choice([format.value for format in MediaFormat]),
)
@click.option(
"--media-type",
help="Media type (ANIME or MANGA)",
type=click.Choice([media_type.value for media_type in MediaType]),
)
@click.option(
"--year",
"-y",
type=click.Choice([year.value for year in MediaYear]),
help="the year the media was released",
)
@click.option(
"--popularity-greater",
type=click.IntRange(min=0),
help="Minimum popularity score",
)
@click.option(
"--popularity-lesser",
type=click.IntRange(min=0),
help="Maximum popularity score",
)
@click.option(
"--score-greater",
type=click.IntRange(min=0, max=100),
help="Minimum average score (0-100)",
)
@click.option(
"--score-lesser",
type=click.IntRange(min=0, max=100),
help="Maximum average score (0-100)",
)
@click.option(
"--start-date-greater",
type=click.IntRange(min=10000101, max=99991231),
help="Minimum start date (YYYYMMDD format, e.g., 20240101)",
)
@click.option(
"--start-date-lesser",
type=click.IntRange(min=10000101, max=99991231),
help="Maximum start date (YYYYMMDD format, e.g., 20241231)",
)
@click.option(
"--end-date-greater",
type=click.IntRange(min=10000101, max=99991231),
help="Minimum end date (YYYYMMDD format, e.g., 20240101)",
)
@click.option(
"--end-date-lesser",
type=click.IntRange(min=10000101, max=99991231),
help="Maximum end date (YYYYMMDD format, e.g., 20241231)",
)
@click.option(
"--on-list/--not-on-list",
"-L/-no-L",
help="Whether the anime should be in your list or not",
type=bool,
)
@click.pass_obj
def search(config: AppConfig, **options: "Unpack[SearchOptions]"):
import json
from rich.progress import Progress
from .....libs.media_api.api import create_api_client
from .....libs.media_api.params import MediaSearchParams
from ....service.feedback import FeedbackService
feedback = FeedbackService(config)
try:
# Create API client
api_client = create_api_client(config.general.media_api, config)
# Extract options
title = options.get("title")
dump_json = options.get("dump_json", False)
page = options.get("page", 1)
per_page = options.get("per_page") or config.anilist.per_page or 50
season = options.get("season")
status = options.get("status", ())
status_not = options.get("status_not", ())
sort = options.get("sort")
genres = options.get("genres", ())
genres_not = options.get("genres_not", ())
tags = options.get("tags", ())
tags_not = options.get("tags_not", ())
media_format = options.get("media_format", ())
media_type = options.get("media_type")
year = options.get("year")
popularity_greater = options.get("popularity_greater")
popularity_lesser = options.get("popularity_lesser")
score_greater = options.get("score_greater")
score_lesser = options.get("score_lesser")
start_date_greater = options.get("start_date_greater")
start_date_lesser = options.get("start_date_lesser")
end_date_greater = options.get("end_date_greater")
end_date_lesser = options.get("end_date_lesser")
on_list = options.get("on_list")
# Validate logical relationships
if (
score_greater is not None
and score_lesser is not None
and score_greater > score_lesser
):
raise FastAnimeError("Minimum score cannot be higher than maximum score")
if (
popularity_greater is not None
and popularity_lesser is not None
and popularity_greater > popularity_lesser
):
raise FastAnimeError(
"Minimum popularity cannot be higher than maximum popularity"
)
if (
start_date_greater is not None
and start_date_lesser is not None
and start_date_greater > start_date_lesser
):
raise FastAnimeError(
"Start date greater cannot be later than start date lesser"
)
if (
end_date_greater is not None
and end_date_lesser is not None
and end_date_greater > end_date_lesser
):
raise FastAnimeError(
"End date greater cannot be later than end date lesser"
)
# Build search parameters
search_params = MediaSearchParams(
query=title,
page=page,
per_page=per_page,
sort=MediaSort(sort) if sort else None,
status_in=[MediaStatus(s) for s in status] if status else None,
status_not_in=[MediaStatus(s) for s in status_not] if status_not else None,
genre_in=[MediaGenre(g) for g in genres] if genres else None,
genre_not_in=[MediaGenre(g) for g in genres_not] if genres_not else None,
tag_in=[MediaTag(t) for t in tags] if tags else None,
tag_not_in=[MediaTag(t) for t in tags_not] if tags_not else None,
format_in=[MediaFormat(f) for f in media_format] if media_format else None,
type=MediaType(media_type) if media_type else None,
season=MediaSeason(season) if season else None,
seasonYear=int(year) if year else None,
popularity_greater=popularity_greater,
popularity_lesser=popularity_lesser,
averageScore_greater=score_greater,
averageScore_lesser=score_lesser,
startDate_greater=start_date_greater,
startDate_lesser=start_date_lesser,
endDate_greater=end_date_greater,
endDate_lesser=end_date_lesser,
on_list=on_list,
)
# Search for anime
with Progress() as progress:
progress.add_task("Searching anime...", total=None)
search_result = api_client.search_media(search_params)
if not search_result or not search_result.media:
raise FastAnimeError("No anime found matching your search criteria")
if dump_json:
# Use Pydantic's built-in serialization
print(json.dumps(search_result.model_dump(mode="json")))
else:
# Launch interactive session for browsing results
from ....interactive.session import session
from ....interactive.state import MediaApiState, MenuName, State
feedback.info(
f"Found {len(search_result.media)} anime matching your search. Launching interactive mode..."
)
# Create initial state with search results
initial_state = State(
menu_name=MenuName.RESULTS,
media_api=MediaApiState(
search_result={
media_item.id: media_item for media_item in search_result.media
},
search_params=search_params,
page_info=search_result.page_info,
),
)
session.load_menus_from_folder("media")
session.run(config, history=[initial_state])
except FastAnimeError as e:
feedback.error("Search failed", str(e))
raise click.Abort()
except Exception as e:
feedback.error("Unexpected error occurred", str(e))
raise click.Abort()

View File

@@ -0,0 +1,94 @@
from typing import TYPE_CHECKING
import click
if TYPE_CHECKING:
from fastanime.core.config import AppConfig
@click.command(help="Print out your anilist stats")
@click.pass_obj
def stats(config: "AppConfig"):
import shutil
import subprocess
from rich.console import Console
from rich.markdown import Markdown
from rich.panel import Panel
from .....libs.media_api.api import create_api_client
from ....service.auth import AuthService
from ....service.feedback import FeedbackService
from ....service.registry import MediaRegistryService
console = Console()
feedback = FeedbackService(config)
auth = AuthService(config.general.media_api)
registry_service = MediaRegistryService(
config.general.media_api, config.media_registry
)
media_api_client = create_api_client(config.general.media_api, config)
try:
# Check authentication
if profile := auth.get_auth():
if not media_api_client.authenticate(profile.token):
feedback.error(
"Authentication Required",
f"You must be logged in to {config.general.media_api} to sync your media list.",
)
feedback.info(
"Run this command to authenticate:",
f"fastanime {config.general.media_api} auth",
)
raise click.Abort()
# Check if kitten is available for image display
KITTEN_EXECUTABLE = shutil.which("kitten")
if not KITTEN_EXECUTABLE:
feedback.warning(
"Kitten not found - profile image will not be displayed"
)
else:
# Display profile image using kitten icat
if profile.user_profile.avatar_url:
console.clear()
image_x = int(console.size.width * 0.1)
image_y = int(console.size.height * 0.1)
img_w = console.size.width // 3
img_h = console.size.height // 3
image_process = subprocess.run(
[
KITTEN_EXECUTABLE,
"icat",
"--clear",
"--place",
f"{img_w}x{img_h}@{image_x}x{image_y}",
profile.user_profile.avatar_url,
],
check=False,
)
if image_process.returncode != 0:
feedback.warning("Failed to display profile image")
# Display user information
about_text = getattr(profile, "about", "") or "No description available"
console.print(
Panel(
Markdown(about_text),
title=f"📊 {profile.user_profile.name}'s Profile",
)
)
# You can add more stats here if the API provides them
feedback.success("User profile displayed successfully")
except Exception as e:
feedback.error("Unexpected error occurred", str(e))
raise click.Abort()

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

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

View File

@@ -1,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,61 +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.option("--erase", "-e", help="Erase your login details", is_flag=True)
@click.pass_obj
def login(config: "Config", status, erase):
from rich import print
from rich.prompt import Confirm, Prompt
from ...utils.tools import exit_app
if status:
is_logged_in = True if config.user else False
message = (
"You are logged in :smile:" if is_logged_in else "You arent logged in :cry:"
)
print(message)
print(config.user)
exit_app()
elif erase:
if Confirm.ask(
"Are you sure you want to erase your login status", default=False
):
config.update_user({})
print("Success")
exit_app(0)
else:
exit_app(1)
else:
from click import launch
from ....anilist import AniList
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
from ...completion_functions import anime_titles_shell_complete
@click.command(
help="Search for anime using anilists api and get top ~50 results",
short_help="Search for anime",
)
@click.argument("title", shell_complete=anime_titles_shell_complete)
@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,7 +1,24 @@
import click
@click.command(help="Helper command to get shell completions")
@click.command(
help="Helper command to get shell completions",
epilog="""
\b
\b\bExamples:
# try to detect your shell and print completions
fastanime completions
\b
# print fish completions
fastanime completions --fish
\b
# print bash completions
fastanime completions --bash
\b
# print zsh completions
fastanime completions --zsh
""",
)
@click.option("--fish", is_flag=True, help="print fish completions")
@click.option("--zsh", is_flag=True, help="print zsh completions")
@click.option("--bash", is_flag=True, help="print bash completions")
@@ -20,7 +37,7 @@ def completions(fish, zsh, bash):
current_shell = None
else:
current_shell = None
if fish or current_shell == "fish" and not zsh and not bash:
if fish or (current_shell == "fish" and not zsh and not bash):
print(
"""
function _fastanime_completion;
@@ -42,7 +59,7 @@ end;
complete --no-files --command fastanime --arguments "(_fastanime_completion)";
"""
)
elif zsh or current_shell == "zsh" and not bash:
elif zsh or (current_shell == "zsh" and not bash):
print(
"""
#compdef fastanime

View File

@@ -1,93 +1,164 @@
from typing import TYPE_CHECKING
import click
if TYPE_CHECKING:
from ..config import Config
from ...core.config import AppConfig
@click.command(
help="Opens up your fastanime config in your preferred editor",
help="Manage your config with ease",
short_help="Edit your config",
epilog="""
\b
\b\bExamples:
# Edit your config in your default editor
# NB: If it opens vim or vi exit with `:q`
fastanime config
\b
# Start the interactive configuration wizard
fastanime config --interactive
\b
# get the path of the config file
fastanime config --path
\b
# print desktop entry info
fastanime config --desktop-entry
\b
# update your config without opening an editor
fastanime --icons --fzf --preview config --update
\b
# view the current contents of your config
fastanime config --view
""",
)
@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(
"--view-json",
"-vj",
help="View the current contents of your config in json format",
is_flag=True,
)
@click.option(
"--desktop-entry",
"-d",
help="Configure the desktop entry of fastanime",
is_flag=True,
)
@click.option(
"--update",
"-u",
help="Persist all the config options passed to fastanime to your config file",
is_flag=True,
)
@click.option(
"--interactive",
"-i",
is_flag=True,
help="Start the interactive configuration wizard.",
)
@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
def config(
user_config: AppConfig, path, view, view_json, desktop_entry, update, interactive
):
from ...core.constants import USER_CONFIG
from ..config.editor import InteractiveConfigEditor
from ..config.generate import generate_config_ini_from_app_model
if path:
print(USER_CONFIG_PATH)
print(USER_CONFIG)
elif view:
print(config)
from rich.console import Console
from rich.syntax import Syntax
console = Console()
config_ini = generate_config_ini_from_app_model(user_config)
syntax = Syntax(
config_ini,
"ini",
theme=user_config.general.pygment_style,
line_numbers=True,
word_wrap=True,
)
console.print(syntax)
elif view_json:
import json
print(json.dumps(user_config.model_dump(mode="json")))
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)
_generate_desktop_entry()
elif interactive:
editor = InteractiveConfigEditor(current_config=user_config)
new_config = editor.run()
with open(USER_CONFIG, "w", encoding="utf-8") as file:
file.write(generate_config_ini_from_app_model(new_config))
click.echo(f"Configuration saved successfully to {USER_CONFIG}")
elif update:
with open(USER_CONFIG, "w", encoding="utf-8") as file:
file.write(generate_config_ini_from_app_model(user_config))
print("update successfull")
else:
import click
click.edit(filename=str(USER_CONFIG))
click.edit(filename=USER_CONFIG_PATH)
def _generate_desktop_entry():
"""
Generates a desktop entry for FastAnime.
"""
import shutil
import sys
from pathlib import Path
from textwrap import dedent
from rich import print
from rich.prompt import Confirm
from ...core.constants import (
ICON_PATH,
PLATFORM,
PROJECT_NAME,
USER_APPLICATIONS,
__version__,
)
EXECUTABLE = shutil.which("fastanime")
if EXECUTABLE:
cmds = f"{EXECUTABLE} --rofi anilist"
else:
cmds = f"{sys.executable} -m fastanime --rofi anilist"
# TODO: Get funs of the other platforms to complete this lol
if 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 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={PROJECT_NAME}
Type=Application
version={__version__}
Path={Path().home()}
Comment=Watch anime from your terminal
Terminal=false
Icon={ICON_PATH}
Exec={cmds}
Categories=Entertainment
"""
)
desktop_entry_path = USER_APPLICATIONS / f"{PROJECT_NAME}.desktop"
if desktop_entry_path.exists():
if not Confirm.ask(
f"The file already exists {desktop_entry_path}; or would you like to rewrite it",
default=False,
):
return
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()}")

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