Compare commits

...

193 Commits

Author SHA1 Message Date
Benex254
9d62915f2b chore: bump version 2024-07-30 17:14:19 +03:00
Benex254
a4e9e5f29e chore: bump version 2024-07-30 17:12:27 +03:00
Benex254
d00c958ff2 docs: update readme 2024-07-30 17:12:11 +03:00
Benex254
bc2ac69b9a fix(interface): escape sequence warning 2024-07-30 17:01:59 +03:00
Benex254
01fa96c27a feat: update fa script 2024-07-30 16:53:36 +03:00
Benex254
6c1bbfe50a feat: add aniskip intergration and scoring of anime 2024-07-30 16:52:33 +03:00
Benex254
ecc4e85079 feat(anilist): ensure rate limit is not exceeded 2024-07-30 16:34:18 +03:00
Benex254
1cd743acdf feat(anilist): include mal ids in queries 2024-07-30 16:27:43 +03:00
Benex254
23dd969d37 feat?: create custom aniskip functionality 2024-07-30 16:26:43 +03:00
Benex254
d21f6b5ab0 feat: ensure correct python version 2024-07-30 16:25:58 +03:00
Benex254
640bb12c44 feat: remove unused functions 2024-07-30 16:25:28 +03:00
Benex254
453e4c1b74 feat(notifier): improve error handling 2024-07-30 16:24:34 +03:00
Benex254
4dc3d1b0bb feat: rename watchlist to watching and repeating to rewatching 2024-07-30 16:23:52 +03:00
Benex254
4df57f9410 feat: remove unused print statement 2024-07-30 16:22:53 +03:00
Benex254
baa94efc24 docs: update readme 2024-07-30 10:33:06 +03:00
Benex254
f5d18512f8 feat(cli): add top as an option for servers 2024-07-30 10:32:53 +03:00
Benex254
72037eea07 feat: show anime cover image for notifications on none windows systems 2024-07-30 09:37:17 +03:00
Benex254
f5c120ebb8 feat: handle none logged in user 2024-07-30 09:36:23 +03:00
Benex254
5f2b88bd9b feat(anilist_api): handle none 200 status code 2024-07-30 09:35:28 +03:00
Benex254
b346801dba feat(allanime): handle none 200 status code 2024-07-30 09:34:45 +03:00
benex
1b1a05e2b3 feat(notifier): add icon 2024-07-29 13:21:32 +03:00
benex
8716fb2e1d fix: use platform.system to correctly detect the os 2024-07-29 12:38:37 +03:00
benex
12a38d6d48 feat(anilist): make icons optional 2024-07-29 12:29:27 +03:00
Benex254
e6aa508644 chore: update pyproject.toml 2024-07-29 13:42:45 +03:00
Benex254
584a2ee3f1 feat(allanime): add server 2024-07-29 13:42:45 +03:00
Benex254
385dd4337d feat(anilist): add notifier command 2024-07-29 13:42:45 +03:00
Benex254
1c70a2122d feat(anilist): add notifier command 2024-07-29 13:42:45 +03:00
BenedictX
46b9b844d4 Update README.md 2024-07-29 13:11:25 +03:00
Benex254
272042ec35 fix(anilist_interface): trailer not loading 2024-07-28 16:43:38 +03:00
Benex254
56632cf77c feat(tui): improve the ui 2024-07-28 15:32:31 +03:00
Benex254
e8dacf0722 feat(anilist): only update episode progress in their is actual progress 2024-07-28 11:24:31 +03:00
Benex254
b95d49429c feat(anilist): add update your anilist feature 2024-07-28 10:42:32 +03:00
Benex254
ca087b2e94 feat(player): implement continue from timestamp 2024-07-28 02:23:54 +03:00
Benex254
3f33ae3738 feat(anilist): change media animelist status for anime you currently watching 2024-07-28 00:32:24 +03:00
Benex254
94a282a320 feat(anilist): implement viewing of your anilist animelist 2024-07-28 00:08:44 +03:00
Benex254
0b379ec813 feat(anilist): add account intergration 2024-07-27 22:57:40 +03:00
Benex254
6b0a013705 feat(provider): add animepahe as new provider 2024-07-27 22:54:17 +03:00
Benex254
6c1f8d09e6 chore: update package info 2024-07-26 14:13:50 +03:00
Benex254
6bb2c89a8c feat(mpv): improve streaming on mobile 2024-07-26 14:10:49 +03:00
Benex254
9f56b74ff0 feat(utils): add logging 2024-07-26 14:10:12 +03:00
Benex254
4d03b86498 chore(anime_provider): remove print statements from provider and switch to logging 2024-07-26 14:09:44 +03:00
Benex254
fab86090a3 chore: remove legacy code 2024-07-26 14:07:57 +03:00
Benex254
71d258385c chore(constants): create constants module to store useful constants 2024-07-26 14:07:37 +03:00
Benex254
bc55ed6e81 chore(updater): update updater info 2024-07-26 14:04:53 +03:00
Benex254
197bfa9f8a chore: update pyproject.toml 2024-07-26 09:27:56 +03:00
Benex254
f84c60e6bc chore: update dependencies 2024-07-26 09:24:15 +03:00
Benex254
d8b94cbbca update pyproject.toml file 2024-07-26 09:19:49 +03:00
Benex254
dd4462f42a chore: reorganize imports 2024-07-26 09:07:49 +03:00
Benex254
0f9e08b9fa chore: reorganize codebase to make anilist top level 2024-07-26 09:07:09 +03:00
Benex254
01333ab1d1 chore: clean up legacy files 2024-07-26 08:42:19 +03:00
Benex254
d8bf9e18c4 chore: clean up legacy code 2024-07-26 08:41:39 +03:00
BenedictX
bc909397d5 Update pyproject.toml 2024-07-25 19:29:33 +03:00
Benex254
f3d88f9825 feat(cli): switch to using AnimeProvider obj 2024-07-25 18:12:27 +03:00
Benex254
eb7bef72b3 feat(anilist_interface): remove legacy methods 2024-07-25 18:10:11 +03:00
Benex254
f6ec094bc7 feat(allanime): change typing from generator to iterator 2024-07-25 18:03:29 +03:00
Benex254
3f1bf1781a feat: implement AnimeProvider obj to manage providers 2024-07-25 17:59:32 +03:00
Benex254
21167fc208 docs: update readme 2024-07-25 14:23:34 +03:00
Benex254
c7c6ff92c4 feat(config): set default format to accept .mp4 2024-07-25 13:22:53 +03:00
Benex254
78319731c0 feat: add yt-dlp format option 2024-07-25 13:10:21 +03:00
Benex254
b619a11db1 chore: use latest version of python for publising 2024-07-25 11:25:28 +03:00
Benex254
022420aa4c feat(anilist): implement anilist random subcommand 2024-07-25 11:23:24 +03:00
Benex254
a7e46d9c18 feat(anilist_interface): ensure the app does not exit when trailer not found 2024-07-25 11:23:24 +03:00
Benex254
5e2826be4e feat(mpv): add typing for mpv title option 2024-07-25 11:23:24 +03:00
Benex254
5e314e2bca docs: update readme 2024-07-25 11:23:24 +03:00
Benex254
3d23854d89 feat(cli): rename translation_type option to translation-type 2024-07-25 11:23:24 +03:00
Benex254
80a25d24a3 chore: renamed build action 2024-07-25 11:23:24 +03:00
BenedictX
1ad7929c66 ci: Update publish.yml 2024-07-25 02:21:33 +03:00
BenedictX
0670bd735c ci: Create publish.yml 2024-07-25 02:14:53 +03:00
Benex254
400a600bfe chore: update lock file 2024-07-24 21:28:30 +03:00
Benex254
b9a3f170ab fix(anilist): correct graphql query for most scored 2024-07-24 21:25:50 +03:00
Benex254
9309ba15b5 fix(mpv): correct order of args 2024-07-24 21:21:09 +03:00
Benex254
b2971e0233 refactor: remove all traces of the gui and api sub packages 2024-07-24 21:06:04 +03:00
Benex254
06f67624d4 refactor: remove config unused config dir 2024-07-24 21:05:35 +03:00
Benex254
597c1bc9fd refactor: remove unused mpv lib 2024-07-24 21:05:05 +03:00
Benex254
6fccd08e96 chore: update config removing gui dependencies 2024-07-24 21:04:41 +03:00
Benex254
0e9294d7a2 feat(anilist_interface): add random anime option for main interforce 2024-07-24 20:54:36 +03:00
Benex254
c76a354d1b refactor: remove the gui and api then move to separate project 2024-07-24 20:53:29 +03:00
Benex254
215def909e feat(cli): include the fzf preview script inside the project for convinience 2024-07-24 20:34:07 +03:00
Benex254
edd394ca74 feat(cli): make preview window for fzf optional 2024-07-24 20:21:38 +03:00
Benex254
af69046025 feat(cli): make the downloads command use the config download path 2024-07-24 20:04:56 +03:00
Benex254
6379c28fed build: increase retention and rename built artifact 2024-07-24 17:36:17 +03:00
Benex254
23b22dfc70 feat: exclude the wheel distribution since its platform dependent 2024-07-24 17:27:55 +03:00
Benex254
da06b0b6e1 feat: add github workflow to build app after every push 2024-07-24 17:19:55 +03:00
Benex254
68640202c3 chore: remove unused github workflows 2024-07-24 17:06:11 +03:00
Benex254
2595ac5bf7 feat: make the downloads command use the mpv module to enable compatibility with mobile 2024-07-24 17:05:10 +03:00
Benex254
19f2898b73 feat: add --path option that prints config location and exits 2024-07-24 17:00:05 +03:00
Benex254
69ec3ebfd7 feat: make the mpv player module work on android 2024-07-24 16:52:43 +03:00
Benex254
d048bccaa1 fix: drop curl_cffi as dependency due to issues on android 2024-07-24 16:51:40 +03:00
Benex254
2c2f2be26d docs: add pip and pipx installation instructions 2024-07-23 10:36:56 +03:00
Benex254
7e2c03d54c feat: use curl_cffi to enable browser impersonation 2024-07-22 22:51:20 +03:00
Benex254
62619421d6 docs: correct discord widget 2024-07-22 20:28:37 +03:00
Benex254
84cea644e7 docs: correct discord link 2024-07-22 20:26:22 +03:00
Benex254
85326b9bc6 docs: add links for respective projects 2024-07-22 20:18:28 +03:00
Benex254
06c602e663 feat(allanime-provider): add episode number to avoid confusion when streaming downloaded content 2024-07-22 10:13:23 +03:00
Benex254
54161f13e4 docs: update readme to include changes in the codebase 2024-07-22 09:54:46 +03:00
Benex254
d74d93da59 chore: arrange dependencies to groups and make some opt in 2024-07-21 22:37:44 +03:00
Benex254
0a5fc0fa3c feat(cli): make it opt in to use fzf and instead make fuzzy inquirer as default 2024-07-21 21:55:09 +03:00
Benex254
52fa6912be feat(cli): add bing mode to search subcommand using episode ranges 2024-07-19 15:00:04 +03:00
Benex254
62bb1f7944 fix(cli): fix bool options not editing config at runtime 2024-07-19 12:15:23 +03:00
Benex254
6fa88dd959 feat(cli): add auto-select provider results 2024-07-19 10:56:22 +03:00
Benex254
a853c01e52 Merge remote-tracking branch 'origin'
keep to date with origin
2024-07-19 10:50:41 +03:00
Benex254
a971b22d72 Revert "feat(cli): add auto-select provider results"
This reverts commit 0d64a9bd32.
2024-07-19 10:45:48 +03:00
Benex254
0d64a9bd32 feat(cli): add auto-select provider results 2024-07-19 10:42:51 +03:00
BenedictX
16dc63c177 docs: Update README.md 2024-07-18 00:37:44 +03:00
BenedictX
b5456635c7 docs: Update README.md 2024-07-18 00:18:33 +03:00
Benex254
d865086a50 docs: update readme 2024-07-18 00:12:01 +03:00
Benex254
82272cdf4e docs: update readme 2024-07-17 23:45:42 +03:00
Benex254
81aac99da8 docs: Update readme 2024-07-17 23:32:59 +03:00
Benex254
962bde00a7 feat(cli): add downloads subcommand 2024-07-17 17:52:30 +03:00
Benex254
1d9c911ea1 feat(cli): add download subcommand 2024-07-17 16:28:19 +03:00
Benex254
cf3a963173 feat(cli): add graceful exit 2024-07-17 15:01:03 +03:00
Benex254
a88e72e4c2 feat(cli): add search subcommand 2024-07-17 14:42:47 +03:00
Benex254
269b1447f6 feat(cli): normalize allanime api output 2024-07-17 14:07:35 +03:00
Benex254
e589a92147 feat(cli): improve ui and ux 2024-07-17 00:42:55 +03:00
Benex254
7fcd5c3475 feat(cli): add help messages to anilist command and subcommands 2024-07-17 00:17:34 +03:00
Benex254
e695577881 feat(cli): complete config command 2024-07-17 00:03:24 +03:00
Benex254
bcd8637b31 feat(cli): add script to generate shell completions 2024-07-16 21:32:22 +03:00
Benex254
8d4f2a8f04 style: format code and sort imports 2024-07-16 20:43:51 +03:00
Benex254
6d077fd3e2 feat: minor ui and ux improvements plus add edit config option 2024-07-16 19:33:35 +03:00
Benex254
73ce357789 index on master: 53823f0 chore:recover lost stash changes 2024-07-12 16:32:29 +03:00
Benex254
53823f02c1 chore:recover lost stash changes 2024-07-11 17:04:15 +03:00
Benex254
148619029d feat:switch to pure fzf for menus 2024-07-11 16:15:47 +03:00
Benex254
f08062ee71 feat:standardize the user data helper to kork for both cli and gui 2024-07-07 15:59:00 +03:00
Benex254
2aa02d6ab9 feat(cli):complete subcommands for anilist command 2024-06-30 23:14:01 +03:00
Benex254
520bfcbb52 feat(cli):improve anilist interfaces api 2024-06-29 22:00:48 +03:00
Benex254
7d82a356b1 feat(cli):finsh player controls sub interfaces 2024-06-28 23:43:19 +03:00
Benex254
be4cacf9dc feat(updater):implement basic script to update app 2024-06-28 18:05:00 +03:00
Benex254
f3b398d344 chore(deps):drop plyer as a dependency and switch to platformdirs 2024-06-28 15:46:48 +03:00
Benex254
1ffb122cec feat(cli):add quality and translation type selection 2024-06-28 15:11:27 +03:00
Benex254
84b8bd9950 feat(cli):add quality and translation type selection 2024-06-28 15:11:27 +03:00
Benex254
ab76689f07 chore:add bandit as pre-commit-hook 2024-06-28 15:11:27 +03:00
BenedictX
8c838a82f7 chore:Create stale.yml 2024-06-23 21:12:50 +03:00
BenedictX
9996af900f chore:Create greetings.yml 2024-06-23 21:12:15 +03:00
Benex254
4f0a752033 chore:update pre-commit-hook deps to latest 2024-06-23 20:27:33 +03:00
Benex254
3b8a565843 chore:switch to poetry as build tool and package manager 2024-06-23 20:13:09 +03:00
Benex254
4b5ff6348e feat:switch to poetry as build tool and package manager 2024-06-23 17:46:01 +03:00
Benex254
4a2c981dff feat:create cli subpackage 2024-06-19 20:43:23 +03:00
Benex254
f93d524f68 chore:updated setup.py 2024-06-17 15:37:34 +03:00
Benex254
03a3d32ce4 feat:add new anime to normalizer 2024-06-17 15:37:34 +03:00
Benex254
8615960300 feat:implement work around for packaging 2024-06-17 15:37:34 +03:00
BenedictX
1442346f07 Update LICENSE 2024-06-14 12:02:10 +03:00
BenedictX
89df10e377 Create python-package.yml 2024-06-14 11:56:03 +03:00
BenedictX
7bab3d63e6 Update README.md 2024-06-13 22:07:50 +03:00
BenedictX
4bdfe5449e Update README.md 2024-06-13 21:49:40 +03:00
BenedictX
d8afdce467 Update README.md 2024-06-13 20:22:21 +03:00
BenedictX
14f486a66f Update README.md 2024-06-13 20:18:40 +03:00
BenedictX
711686da92 Update README.md 2024-06-13 20:05:26 +03:00
BenedictX
37b7702db1 Update README.md 2024-06-13 20:02:15 +03:00
BenedictX
18356c41ec Update README.md 2024-06-13 20:00:52 +03:00
BenedictX
3bc9a09b1e Update README.md 2024-06-13 20:00:22 +03:00
BenedictX
4e705a9d0b Update README.md 2024-06-13 19:55:15 +03:00
BenedictX
814646115b Update README.md 2024-06-13 19:52:06 +03:00
BenedictX
516333cb13 Update README.md 2024-06-13 19:50:45 +03:00
BenedictX
3cd84e2f6f Update README.md 2024-06-13 19:49:36 +03:00
BenedictX
a3b8af4a30 Update README.md 2024-06-13 19:46:19 +03:00
Benex254
67c07d350a chore:created demo video 2024-06-13 19:42:50 +03:00
Benex254
821eb38170 feat:update readme 2024-06-13 15:29:05 +03:00
Benex254
b265d66859 feat(readme):updated readme 2024-06-13 15:25:32 +03:00
Benex254
d11ab7c881 feat:(anime screen):add basic error handling 2024-06-12 20:27:56 +03:00
Benex254
62b1e3260a chore:removed python 3.10 as requirement 2024-06-12 18:27:26 +03:00
Benex254
912736c66a feat(anime screen):implement anime dub functionality 2024-06-12 18:07:08 +03:00
Benex254
4a989b995e feat:add basic error handling to allanime provider 2024-06-12 17:41:49 +03:00
Benex254
e09b3f1fc8 fix(my list):make my list screen work with new media cards 2024-06-12 17:27:26 +03:00
Benex254
ef0083fa5a feat(download screen):implement download capabilities 2024-06-12 16:06:45 +03:00
Benex254
018bccbaab feat(search screen):implement work around for trending anime sidebar 2024-06-10 22:24:48 +03:00
Benex254
2fc4faab05 feat(anime screen):optimize auto select server 2024-06-10 22:17:03 +03:00
Benex254
380c2fa42c feat:update current episode when using episode buttons and normalize anime titles from allanime 2024-06-10 22:02:44 +03:00
Benex254
93d94a15ef refactor(allanime provider):clean up and move allanime api to allanime folder 2024-06-10 19:17:34 +03:00
Benex254
9e40683e86 chore: remove animdl and pyaml from requirements 2024-06-10 18:11:55 +03:00
Benex254
290167fbb5 refactor:remove animdl dependency and dependants 2024-06-10 18:11:07 +03:00
Benex254
7d029619b5 refactor:removed help screen and crash screen 2024-06-10 17:32:46 +03:00
Benex254
d10e739f07 feat(anime screen): add mpv player fallback 2024-06-10 14:00:31 +03:00
Benex254
97cb32ce4a feat(anime screen):add auto play and auto select server 2024-06-10 12:48:26 +03:00
Benex254
036be30309 perf(anime screen): cache results of the episodes fetched 2024-06-10 12:40:34 +03:00
Benex254
30ca30d34e feat(anime screen): add next and previous anime controls 2024-06-10 12:33:08 +03:00
Benex254
7d0b507b2d feat(home screen): implement load more anime functionality to improve start up time
This feature only gets anime of a particular category and only preloads
trending category
2024-06-10 11:58:31 +03:00
Benex254
a11a73cf8f chore(media card loader): drop pytube as dependency and switch to yt-dlp for trailers 2024-06-10 11:20:12 +03:00
Benex254
aec2278749 feat(anime screen, search screen): finish basic ui for anime screen with ep selection and update search screen to use recycle view 2024-06-09 20:24:26 +03:00
Benex254
1284ff1c4c feat(anime_screen):implement crude streaming with allanime api 2024-06-08 21:51:30 +03:00
Benex254
79418141bb perf(media card): use recyleboxlayout for more efficiency and better performance 2024-06-08 16:32:05 +03:00
Benex254
03e7d67266 refactor:move allanime_scraper to main codebase 2024-06-08 10:22:58 +03:00
Benex254
48d29bcfc2 feat(scraper): add allanime_api based on ani-cli
This is the main api thats going to interact with the allanime site to
scrape stream links. This will thus make the getting of video streams
faster and more efficient than using animdl as has been previously done.
2024-06-08 03:59:39 +03:00
Benex254
4cb5a5455c fix:Bugs caused by renaming 2024-06-05 00:43:36 +03:00
Benex254
96f10faf6b refactor:renamed anixstream package to fastanime 2024-06-05 00:07:28 +03:00
Benex254
b6e6ab13a6 style:Formatted codebase to pep8 2024-06-05 00:03:33 +03:00
Benex254
8e6dad3732 feat: implemented load trailer when needed and also one media popup for all 2024-06-04 18:32:39 +03:00
Benex254
9aeb96a252 refactor:removed unecesarry files 2024-06-04 14:09:04 +03:00
BenedictX
257faaa3bb Update README.md 2024-06-02 13:02:18 +03:00
Benedict Xavier wanyonyi
950e403f07 Update README.md 2024-06-01 12:31:32 +03:00
Benedict Xavier wanyonyi
4ca1c70e86 Update README.md 2024-06-01 12:29:45 +03:00
Benedict Xavier Wanyonyi
76f22795d9 Merge branch 'master' of https://github.com/Benex254/aniXstream 2024-06-01 12:20:02 +03:00
Benedict Xavier Wanyonyi
cdf507eaf2 refactor: renamed screenshots 2024-06-01 12:19:17 +03:00
Benedict Xavier wanyonyi
54bea09efb Update README.md 2024-06-01 12:03:59 +03:00
211 changed files with 6703 additions and 5237 deletions

35
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,35 @@
name: debug_build
on: push
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Python
uses: actions/setup-python@v4
# see details (matrix, python-version, python-version-file, etc.)
# https://github.com/actions/setup-python
- 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
with:
path: ./.venv
key: venv-${{ hashFiles('poetry.lock') }}
- name: Install the project dependencies
run: poetry install
- name: build app
run: poetry 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

66
.github/workflows/publish.yml vendored Normal file
View File

@@ -0,0 +1,66 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# GitHub recommends pinning actions to a commit SHA.
# To get a newer version, you will need to update the SHA.
# You can also reference a tag or branch, but the action may change without warning.
name: Upload Python Package
on:
release:
types: [published]
permissions:
contents: read
jobs:
release-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Build release distributions
run: |
# NOTE: put your own distribution build steps here.
python -m pip install build
python -m build
- name: Upload distributions
uses: actions/upload-artifact@v4
with:
name: release-dists
path: dist/
pypi-publish:
runs-on: ubuntu-latest
needs:
- release-build
permissions:
# IMPORTANT: this permission is mandatory for trusted publishing
id-token: write
# Dedicated environments with protections for publishing are strongly recommended.
environment:
name: pypi
# OPTIONAL: uncomment and update to include your PyPI project URL in the deployment status:
# url: https://pypi.org/p/YOURPROJECT
steps:
- name: Retrieve release distributions
uses: actions/download-artifact@v4
with:
name: release-dists
path: dist/
- name: Publish release distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1

11
.gitignore vendored
View File

@@ -2,9 +2,11 @@
*.mp4
*.mp3
*.ass
user_data.json
yt_cache.json
vids/*
vids
data/
.project/
fastanime.ini
crashdump.txt
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
@@ -12,12 +14,12 @@ __pycache__/
anixstream.ini
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
bin/
downloads/
eggs/
.eggs/
@@ -173,3 +175,4 @@ app/user_data.json
app/View/SearchScreen/.search_screen.py.un~
app/View/SearchScreen/search_screen.py~
app/user_data.json
.buildozer

37
.pre-commit-config.yaml Normal file
View File

@@ -0,0 +1,37 @@
default_language_version:
python: python3.10
repos:
- repo: https://github.com/pycqa/isort
rev: 5.12.0 # You can replace this with the latest version
hooks:
- id: isort
name: isort (python)
args: ["--profile", "black"] # Ensure compatibility with Black
- repo: https://github.com/PyCQA/autoflake
rev: v2.2.1
hooks:
- id: autoflake
args: ["--in-place","--remove-unused-variables", "--remove-all-unused-imports"]
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.4.10
hooks:
# Run the linter.
- 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
# ------ TODO: re-add this -----
# - repo: https://github.com/PyCQA/bandit
# rev: 1.7.9 # Update me!
# hooks:
# - id: bandit
# args: ["-c", "pyproject.toml"]
# additional_dependencies: ["bandit[toml]"]

View File

@@ -1 +0,0 @@
3.10

37
LICENSE
View File

@@ -1,19 +1,24 @@
Copyright (c) 2018 The Python Packaging Authority
This is free and unencumbered software released into the public domain.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <https://unlicense.org>

377
README.md
View File

@@ -1,24 +1,381 @@
# FastAnime
# AniXStream
Welcome to **FastAnime**, anime site experience from the terminal.
This project was created as a gui solution to any and all anime cli's. Thus freeing the cli developer from worrying about making it look good and instead focus on functionality.
[fa_demo.webm](https://github.com/user-attachments/assets/bb46642c-176e-42b3-a533-ff55d4dac111)
Heavily inspired by [animdl](https://github.com/justfoolingaround/animdl), [magic-tape](https://gitlab.com/christosangel/magic-tape/-/tree/main?ref_type=heads) and [ani-cli](https://github.com/pystardust/ani-cli).
<!--toc:start-->
- [FastAnime](#fastanime)
- [Installation](#installation)
- [Installation using your favourite package manager](#installation-using-your-favourite-package-manager)
- [Using pipx](#using-pipx)
- [Using pip](#using-pip)
- [Installing the bleeding edge version](#installing-the-bleeding-edge-version)
- [Building from the source](#building-from-the-source)
- [External Dependencies](#external-dependencies)
- [Usage](#usage)
- [The Commandline interface :fire:](#the-commandline-interface-fire)
- [The anilist command](#the-anilist-command)
- [Running without any subcommand](#running-without-any-subcommand)
- [Subcommands](#subcommands)
- [download subcommand](#download-subcommand)
- [search subcommand](#search-subcommand)
- [downloads subcommand](#downloads-subcommand)
- [config subcommand](#config-subcommand)
- [Configuration](#configuration)
- [Contributing](#contributing)
- [Receiving Support](#receiving-support)
- [Supporting the Project](#supporting-the-project)
<!--toc:end-->
## Authors
- [@Benex254](https://github.com/Benex254/aniXstream)
> [!IMPORTANT]
>
> This project currently scrapes allanime and is in no way related to them. The site is in the public domain and can be access by any one with a browser.
> [!NOTE]
>
> The docs are still being worked on and are far from completion.
## Installation
installing using the zip file
The app can run wherever python can run. So all you need to have is python installed on your device.
On android you can use [termux](https://github.com/termux/termux-app).
If you have any difficulty consult for help on the [discord channel](https://discord.gg/HRjySFjQ)
Go to releases and download the zip file.Then unzip it and run the following commands in the same path as the unzipped foder:
### Installation using your favourite package manager
Currently the app is only published on [pypi](https://pypi.org/project/fastanime/).
The app is published approximately after every 14 days, which will include accumulative changes during that period.
#### Using pipx
Preferred method of installation since [Pipx](https://github.com/pypa/pipx) creates an isolated environment for each app it installs.
```bash
python -m venv .venv
pipx install fastanime
```
#### Using pip
```bash
pip install fastanime
```
### Installing the bleeding edge version
To install the latest build which are created on every push by GitHub actions, download the [fastanime_debug_build](https://github.com/Benex254/FastAnime/actions) of your choosing from the GitHub actions page.
Then:
```bash
unzip fastanime_debug_build
# outputs fastanime<version>.tar.gz
pipx install fastanime<version>.tar.gz
# --- or ---
pip install fastanime<version>.tar.gz
```
### Building from the source
Requirements:
- [git](https://git-scm.com/)
- [python 3.10 and above](https://www.python.org/)
- [poetry](https://python-poetry.org/docs/#installation)
To build from the source, follow these steps:
1. Clone the repository: `git clone https://github.com/Benex254/FastAnime.git`
2. Navigate into the folder: `cd FastAnime`
3. Then build and Install the app:
```bash
# Normal Installation
poetry build
cd dist
pip install fastanime<version>.whl
# Editable installation (easiest for updates)
# just do a git pull in the Project dir
# the latter will require rebuilding the app
pip install -e .
```
4. Enjoy! Verify installation with:
```bash
fastanime --version
```
> [!Tip]
>
> Download the completions from [here](https://github.com/Benex254/FastAnime/tree/master/completions) for your shell.
> To add completions:
>
> - Fish Users: `cp $FASTANIME_PATH/completions/fastanime.fish ~/.config/fish/completions/`
> - Bash Users: Add `source $FASTANIME_PATH/completions/fastanime.bash` to your `.bashrc`
> - Zsh Users: Add `source $FASTANIME_PATH/completions/fastanime.zsh` to your `.zshrc`
### External Dependencies
The only required external dependency, unless you won't be streaming, is [MPV](https://mpv.io/installation/), which i recommend installing with [uosc](https://github.com/tomasklaen/uosc) and [thumbfast](https://github.com/po5/thumbfast) for the best experience since they add a better interface to it.
> [!NOTE]
>
> The project currently sees no reason to support any other video
> player because we believe nothing beats **MPV** and it provides
> everything you could ever need with a small footprint.
> But if you have a reason feel free to encourage as to do so.
**Other dependencies that will just make your experience better:**
- [fzf](https://github.com/junegunn/fzf) :fire: which is used as a better alternative to the ui.
- [chafa](https://github.com/hpjansson/chafa) currently the best cross platform and cross terminal image viewer for the terminal.
- [icat](https://sw.kovidgoyal.net/kitty/kittens/icat/) an image viewer that only works in [kitty terminal](https://sw.kovidgoyal.net/kitty/), which is currently the best terminal in my opinion, and by far the best image renderer for the terminal thanks to kitty's terminal graphics protocol. Its terminal graphics is so op that you can [run a browser on it](https://github.com/chase/awrit?tab=readme-ov-file)!!
- [bash](https://www.gnu.org/software/bash/) is used as the preview script language.
- [ani-skip](https://github.com/synacktraa/ani-skip) :fire: used for skipping the opening and ending theme songs
## Usage
The app offers both a graphical interface (under development) and a robust command-line interface.
> [!NOTE]
>
> The GUI is mostly in hiatus; use the CLI for now.
> However, you can try it out before i decided to change my objective by checking out this [release](https://github.com/Benex254/FastAnime/tree/v0.20.0).
> But be reassured for those who aren't terminal chads, i will still complete the GUI for the fun of it
### The Commandline interface :fire:
Designed for power users who prefer efficiency over browser-based streaming and still want the experience in their terminal.
Overview of main commands:
- `fastanime anilist`: Powerful command for browsing and exploring anime due to AniList integration.
- `fastanime download`: Download anime.
- `fastanime search`: Powerful command meant for binging since it doesn't require the interfaces
- `fastanime downloads`: View downloaded anime and watch with MPV.
- `fastanime config`: Quickly edit configuration settings.
Configuration is directly passed into this command at run time to override your config.
Available options include:
- `--server;-s <server>` set the default server to auto select
- `--continue;-c/--no-continue;-no-c` whether to continue from the last episode you were watching
- `--quality;-q <0|1|2|3>` the link to choose from server
- `--translation-type;- <dub|sub` what language for anime
- `--auto-select;-a/--no-auto-select;-no-a` auto select title from provider results
- `--auto-next;-A;/--no-auto-next;-no-A` auto select next episode
- `-downloads-dir;-d <path>` set the folder to download anime into
- `--fzf` use fzf for the ui
- `--default` use the default ui
- `--preview` show a preview when using fzf
- `--no-preview` dont show a preview when using fzf
- `--format <yt-dlp format string>` set the format of anime downloaded and streamed based on yt-dlp format. Works when `--server gogoanime`
- `--icons/--no-icons` toggle the visibility of the icons
- `--skip/--no-skip` whether to skip the opening and ending theme songs.
#### The anilist command :fire: :fire: :fire:
Stream, browse, and discover anime efficiently from the terminal using the [AniList API](https://github.com/AniList/ApiV2-GraphQL-Docs).
##### Running without any subcommand
Run `fastanime anilist` to access the main interface.
##### Subcommands
The subcommands are mainly their as convenience. Since all the features already exist in the main interface.
- `fastanime anilist trending`: Top 15 trending anime.
- `fastanime anilist recent`: Top 15 recently updated anime.
- `fastanime anilist search`: Search for anime (top 50 results).
- `fastanime anilist upcoming`: Top 15 upcoming anime.
- `fastanime anilist popular`: Top 15 popular anime.
- `fastanime anilist favourites`: Top 15 favorite anime.
- `fastanime anilist random`: get random anime
The following are commands you can only run if you are signed in to your AniList account:
- `fastanime anilist watching`
- `fastanime anilist planning`
- `fastanime anilist repeating`
- `fastanime anilist dropped`
- `fastanime anilist paused`
- `fastanime anilist completed`
Plus: `fastanime anilist notifier` :fire:
```bash
# basic form
fastanime anilist notifier
# with logging to stdout
fastanime --log anilist notifier
# with logging to a file. stored in the same place as your config
fastanime --log-file anilist notifier
```
The above commands will start a loop that checks every 2 minutes if any of the anime in your watch list that are aireing has just released a new episode.
The notification will consist of a cover image of the anime in none windows systems.
You can place the command among your machines startup scripts.
For fish users for example you can decide to put this in your `~/.config/fish/config.fish`:
```fish
if ! ps aux | grep -q '[f]astanime .* notifier'
echo initializing fastanime anilist notifier
fastanime --log-file anilist notifier>/dev/null &
end
```
> [!NOTE]
> To sign in just run `fastanime anilist login` and follow the instructions.
> To view your login status `fastanime anilist login --status`
#### download subcommand
Download anime to watch later dub or sub with this one command.
Its optimized for scripting due to fuzzy matching.
So every step of the way has been and can be automated.
> [!NOTE]
>
> The download feature is powered by [yt-dlp](https://github.com/yt-dlp/yt-dlp) so all the bells and whistles that it provides are readily available in the project.
> Like continuing from where you left of while downloading, after lets say you lost your internet connection.
**Syntax:**
```bash
# Download all available episodes
fastanime download <anime-title>
# Download specific episode range
# be sure to observe the range Syntax
fastanime download <anime-title> -r <episodes-start>-<episodes-end>
```
#### search subcommand
Powerful command mainly aimed at binging anime. Since it doesn't require interaction with the interfaces.
**Syntax:**
```bash
# basic form where you will still be promted for the episode number
fastanime search <anime-title>
# binge all episodes with this command
fastanime search <anime-title> -
# binge a specific episode range with this command
# be sure to observe the range Syntax
fastanime search <anime-title> <episodes-start>-<episodes-end>
```
#### downloads subcommand
View and stream the anime you downloaded using MPV.
**Syntax:**
```bash
fastanime downloads
# to get the path to the downloads folder set
fastanime downloads --path
# useful when you want to use the value for other programs
```
#### config subcommand
Edit FastAnime configuration settings using your preferred editor (based on `$EDITOR` environment variable so be sure to set it).
**Syntax:**
```bash
fastanime config
# to get config path which is useful if you want to use it for another program.
fastanime config --path
```
> [!Note]
>
> If it opens [vim](https://www.vim.org/download.php) you can exit by typing `:q` in case you don't know.
## Configuration
The app includes sensible defaults but can be customized extensively. Configuration is stored in `.ini` format at `~/.config/FastAnime/config.ini` on Linux and mac or somewhere on windows; you can check by running `fastanime config --path`.
```ini
[stream]
continue_from_history = True # Auto continue from watch history
translation_type = sub # Preferred language for anime (options: dub, sub)
server = top # Default server (options: dropbox, sharepoint, wetransfer.gogoanime, top, wixmp)
auto_next = False # Auto-select next episode
# Auto select the anime provider results with fuzzy find.
# Note this wont always be correct.But 99% of the time will be.
auto_select=True
# whether to skip the opening and ending theme songs
# note requires ani-skip to be in path
skip=false
# the maximum delta time in minutes after which the episode should be considered as completed
# used in the continue from time stamp
error=3
# the format of downloaded anime and trailer
# based on yt-dlp format and passed directly to it
# learn more by looking it up on their site
# only works for downloaded anime if server=gogoanime
# since its the only one that offers different formats
# the others tend not to
format=best[height<=1080]/bestvideo[height<=1080]+bestaudio/best # default
[general]
preferred_language = romaji # Display language (options: english, romaji)
downloads_dir = <Default-videos-dir>/FastAnime # Download directory
use_fzf=False # whether to use fzf as the interface for the anilist command and others.
preview=false # whether to show a preview window when using fzf
# whether to show the icons
icons=false
# the duration in minutes a notification will stay in the screen
# used by notifier command
notification_duration=2
[anilist]
# Not implemented yet
```
## Contributing
We welcome your issues and feature requests. However, due to time constraints, we currently do not plan to add another provider.
If you wish to contribute directly, please first open an issue describing your proposed changes so it can be discussed.
## Receiving Support
For inquiries, join our [Discord Server](https://discord.gg/4NUTj5Pt).
<p align="center">
<a href="https://discord.gg/HRjySFjQ">
<img src="https://invidget.switchblade.xyz/HRjySFjQ">
</a>
</p>
## Supporting the Project
Show your support by starring our GitHub repository or [buying us a coffee](https://ko-fi.com/benex254).

View File

@@ -1,4 +0,0 @@
{
"python.analysis.typeCheckingMode": "basic",
"python.analysis.autoImportCompletions": true
}

View File

@@ -1,7 +0,0 @@
from .home_screen import HomeScreenController
from .search_screen import SearchScreenController
from .my_list_screen import MyListScreenController
from .anime_screen import AnimeScreenController
from .downloads_screen import DownloadsScreenController
from .help_screen import HelpScreenController
from .crashlog_screen import CrashLogScreenController

View File

@@ -1,43 +0,0 @@
from kivy.clock import Clock
from kivy.logger import Logger
from kivy.cache import Cache
from anixstream.Model import AnimeScreenModel
from anixstream.View import AnimeScreenView
Cache.register("data.anime", limit=20, timeout=600)
class AnimeScreenController:
"""The controller for the anime screen
"""
def __init__(self, model: AnimeScreenModel):
self.model = model
self.view = AnimeScreenView(controller=self, model=self.model)
def get_view(self) -> AnimeScreenView:
return self.view
def update_anime_view(self, id: int, caller_screen_name: str):
"""method called to update the anime screen when a new
Args:
id (int): the anilst id of the anime
caller_screen_name (str): the screen thats calling this method; used internally to switch back to this screen
"""
if self.model.anime_id != id:
if cached_anime_data := Cache.get("data.anime", f"{id}"):
data = cached_anime_data
else:
data = self.model.get_anime_data(id)
if data[0]:
self.model.anime_id = id
Clock.schedule_once(
lambda _: self.view.update_layout(
data[1]["data"]["Page"]["media"][0], caller_screen_name
)
)
Logger.info(f"Anime Screen:Success in opening anime of id: {id}")
Cache.append("data.anime", f"{id}", data)

View File

@@ -1,16 +0,0 @@
from anixstream.View import CrashLogScreenView
from anixstream.Model import CrashLogScreenModel
class CrashLogScreenController:
"""The crash log screen controller
"""
def __init__(self, model:CrashLogScreenModel):
self.model = model
self.view = CrashLogScreenView(controller=self, model=self.model)
# self.update_anime_view()
def get_view(self) -> CrashLogScreenView:
return self.view

View File

@@ -1,15 +0,0 @@
from anixstream.View import DownloadsScreenView
from anixstream.Model import DownloadsScreenModel
class DownloadsScreenController:
"""The controller for the download screen
"""
def __init__(self, model:DownloadsScreenModel):
self.model = model
self.view = DownloadsScreenView(controller=self, model=self.model)
def get_view(self) -> DownloadsScreenView:
return self.view

View File

@@ -1,14 +0,0 @@
from anixstream.View import HelpScreenView
from anixstream.Model import HelpScreenModel
class HelpScreenController:
"""The help screen controller
"""
def __init__(self, model:HelpScreenModel):
self.model = model
self.view = HelpScreenView(controller=self, model=self.model)
def get_view(self) -> HelpScreenView:
return self.view

View File

@@ -1,121 +0,0 @@
from inspect import isgenerator
from kivy.clock import Clock
from kivy.logger import Logger
from anixstream.View import HomeScreenView
from anixstream.Model import HomeScreenModel
from anixstream.View.components import MediaCardsContainer
from anixstream.Utility import show_notification
# TODO:Move the update home screen to homescreen.py
class HomeScreenController:
"""
The `HomeScreenController` class represents a controller implementation.
Coordinates work of the view with the model.
The controller implements the strategy pattern. The controller connects to
the view to control its actions.
"""
populate_errors = []
def __init__(self, model:HomeScreenModel):
self.model = model # Model.main_screen.MainScreenModel
self.view = HomeScreenView(controller=self, model=self.model)
# if self.view.app.config.get("Preferences","is_startup_anime_enable")=="1": # type: ignore
# Clock.schedule_once(lambda _:self.populate_home_screen())
def get_view(self) -> HomeScreenView:
return self.view
def popular_anime(self):
most_popular_cards_container = MediaCardsContainer()
most_popular_cards_container.list_name = "Most Popular"
most_popular_cards_generator = self.model.get_most_popular_anime()
if isgenerator(most_popular_cards_generator):
for card in most_popular_cards_generator:
card.screen = self.view
most_popular_cards_container.container.add_widget(card)
self.view.main_container.add_widget(most_popular_cards_container)
else:
Logger.error("Home Screen:Failed to load most popular anime")
self.populate_errors.append("Most Popular Anime")
def favourite_anime(self):
most_favourite_cards_container = MediaCardsContainer()
most_favourite_cards_container.list_name = "Most Favourites"
most_favourite_cards_generator = self.model.get_most_favourite_anime()
if isgenerator(most_favourite_cards_generator):
for card in most_favourite_cards_generator:
card.screen = self.view
most_favourite_cards_container.container.add_widget(card)
self.view.main_container.add_widget(most_favourite_cards_container)
else:
Logger.error("Home Screen:Failed to load most favourite anime")
self.populate_errors.append("Most favourite Anime")
def trending_anime(self):
trending_cards_container = MediaCardsContainer()
trending_cards_container.list_name = "Trending"
trending_cards_generator = self.model.get_trending_anime()
if isgenerator(trending_cards_generator):
for card in trending_cards_generator:
card.screen = self.view
trending_cards_container.container.add_widget(card)
self.view.main_container.add_widget(trending_cards_container)
else:
Logger.error("Home Screen:Failed to load trending anime")
self.populate_errors.append("trending Anime")
def highest_scored_anime(self):
most_scored_cards_container = MediaCardsContainer()
most_scored_cards_container.list_name = "Most Scored"
most_scored_cards_generator = self.model.get_most_scored_anime()
if isgenerator(most_scored_cards_generator):
for card in most_scored_cards_generator:
card.screen = self.view
most_scored_cards_container.container.add_widget(card)
self.view.main_container.add_widget(most_scored_cards_container)
else:
Logger.error("Home Screen:Failed to load highest scored anime")
self.populate_errors.append("Most scored Anime")
def recently_updated_anime(self):
most_recently_updated_cards_container = MediaCardsContainer()
most_recently_updated_cards_container.list_name = "Most Recently Updated"
most_recently_updated_cards_generator = self.model.get_most_recently_updated_anime()
if isgenerator(most_recently_updated_cards_generator):
for card in most_recently_updated_cards_generator:
card.screen = self.view
most_recently_updated_cards_container.container.add_widget(card)
self.view.main_container.add_widget(most_recently_updated_cards_container)
else:
Logger.error("Home Screen:Failed to load recently updated anime")
self.populate_errors.append("Most recently updated Anime")
def upcoming_anime(self):
upcoming_cards_container = MediaCardsContainer()
upcoming_cards_container.list_name = "Upcoming Anime"
upcoming_cards_generator = self.model.get_upcoming_anime()
if isgenerator(upcoming_cards_generator):
for card in upcoming_cards_generator:
card.screen = self.view
upcoming_cards_container.container.add_widget(card)
self.view.main_container.add_widget(upcoming_cards_container)
else:
Logger.error("Home Screen:Failed to load upcoming anime")
self.populate_errors.append("upcoming Anime")
def populate_home_screen(self):
self.populate_errors = []
Clock.schedule_once(lambda _:self.trending_anime(),1)
Clock.schedule_once(lambda _:self.highest_scored_anime(),2)
Clock.schedule_once(lambda _:self.popular_anime(),3)
Clock.schedule_once(lambda _: self.favourite_anime(),4)
Clock.schedule_once(lambda _:self.recently_updated_anime(),5)
Clock.schedule_once(lambda _:self.upcoming_anime(),6)
if self.populate_errors:
show_notification(f"Failed to fetch all home screen data",f"Theres probably a problem with your internet connection or anilist servers are down.\nFailed include:{', '.join(self.populate_errors)}")

View File

@@ -1,53 +0,0 @@
from inspect import isgenerator
from kivy.logger import Logger
# from kivy.clock import Clock
from kivy.utils import difference
from anixstream.View import MyListScreenView
from anixstream.Model import MyListScreenModel
from anixstream.Utility import user_data_helper
class MyListScreenController:
"""
The `MyListScreenController` class represents a controller implementation.
Coordinates work of the view with the model.
The controller implements the strategy pattern. The controller connects to
the view to control its actions.
"""
def __init__(self, model:MyListScreenModel):
self.model = model
self.view = MyListScreenView(controller=self, model=self.model)
if len(self.requested_update_my_list_screen())>30:
self.requested_update_my_list_screen(2)
def get_view(self) -> MyListScreenView:
return self.view
def requested_update_my_list_screen(self,page=None):
user_anime_list = user_data_helper.get_user_anime_list()
if animes_to_add:=difference(user_anime_list,self.model.already_in_user_anime_list):
Logger.info("My List Screen:User anime list change;updating screen")
# if thirty:=len(animes_to_add)>30:
# self.model.already_in_user_anime_list = user_anime_list[:30]
# else:
anime_cards = self.model.update_my_anime_list_view(animes_to_add,page)
self.model.already_in_user_anime_list = user_anime_list
if isgenerator(anime_cards):
for result_card in anime_cards:
result_card.screen = self.view
self.view.update_layout(result_card)
return animes_to_add
elif page:
anime_cards = self.model.update_my_anime_list_view(self.model.already_in_user_anime_list,page)
# self.model.already_in_user_anime_list = user_anime_list
if isgenerator(anime_cards):
for result_card in anime_cards:
result_card.screen = self.view
self.view.update_layout(result_card)
return []
else:
return []

View File

@@ -1,47 +0,0 @@
from inspect import isgenerator
from kivy.clock import Clock
from kivy.logger import Logger
from anixstream.View import SearchScreenView
from anixstream.Model import SearchScreenModel
class SearchScreenController:
"""The search screen controller
"""
def __init__(self, model: SearchScreenModel):
self.model = model
self.view = SearchScreenView(controller=self, model=self.model)
def get_view(self) -> SearchScreenView:
return self.view
def update_trending_anime(self):
"""Gets and adds the trending anime to the search screen
"""
trending_cards_generator = self.model.get_trending_anime()
if isgenerator(trending_cards_generator):
self.view.trending_anime_sidebar.clear_widgets()
for card in trending_cards_generator:
card.screen = self.view
card.pos_hint = {"center_x": 0.5}
self.view.update_trending_sidebar(card)
else:
Logger.error("Home Screen:Failed to load trending anime")
def requested_search_for_anime(self, anime_title, **kwargs):
self.view.is_searching = True
search_Results = self.model.search_for_anime(anime_title, **kwargs)
if isgenerator(search_Results):
for result_card in search_Results:
result_card.screen = self.view
self.view.update_layout(result_card)
Clock.schedule_once(
lambda _: self.view.update_pagination(self.model.pagination_info)
)
self.update_trending_anime()
else:
Logger.error(f"Home Screen:Failed to search for {anime_title}")
self.view.is_searching = False

View File

@@ -1,7 +0,0 @@
from .home_screen import HomeScreenModel
from .search_screen import SearchScreenModel
from .my_list_screen import MyListScreenModel
from .anime_screen import AnimeScreenModel
from .crashlog_screen import CrashLogScreenModel
from .help_screen import HelpScreenModel
from .download_screen import DownloadsScreenModel

View File

@@ -1,13 +0,0 @@
from .base_model import BaseScreenModel
from anixstream.libs.anilist import AniList
class AnimeScreenModel(BaseScreenModel):
"""the Anime screen model
"""
data = {}
anime_id = 0
def get_anime_data(self,id:int):
return AniList.get_anime(id)

View File

@@ -1,33 +0,0 @@
# The model implements the observer pattern. This means that the class must
# support adding, removing, and alerting observers. In this case, the model is
# completely independent of controllers and views. It is important that all
# registered observers implement a specific method that will be called by the
# model when they are notified (in this case, it is the `model_is_changed`
# method). For this, observers must be descendants of an abstract class,
# inheriting which, the `model_is_changed` method must be overridden.
class BaseScreenModel:
"""Implements a base class for model modules."""
_observers = []
def add_observer(self, observer) -> None:
self._observers.append(observer)
def remove_observer(self, observer) -> None:
self._observers.remove(observer)
def notify_observers(self, name_screen: str) -> None:
"""
Method that will be called by the observer when the model data changes.
:param name_screen:
name of the view for which the method should be called
:meth:`model_is_changed`.
"""
for observer in self._observers:
if observer.name == name_screen:
observer.model_is_changed()
break

View File

@@ -1,7 +0,0 @@
from .base_model import BaseScreenModel
class CrashLogScreenModel(BaseScreenModel):
"""
Handles the crashlog screen logic
"""

View File

@@ -1,8 +0,0 @@
from .base_model import BaseScreenModel
class DownloadsScreenModel(BaseScreenModel):
"""
Handles the download screen logic
"""

View File

@@ -1,8 +0,0 @@
from .base_model import BaseScreenModel
class HelpScreenModel(BaseScreenModel):
"""
Handles the help screen logic
"""

View File

@@ -1,79 +0,0 @@
from .base_model import BaseScreenModel
from anixstream.libs.anilist import AniList
from anixstream.Utility.media_card_loader import MediaCardLoader
class HomeScreenModel(BaseScreenModel):
"""The home screen model"""
def get_trending_anime(self):
success, data = AniList.get_trending()
if success:
def _data_generator():
for anime_item in data["data"]["Page"]["media"]:
yield MediaCardLoader.media_card(anime_item)
return _data_generator()
else:
return data
def get_most_favourite_anime(self):
success, data = AniList.get_most_favourite()
if success:
def _data_generator():
for anime_item in data["data"]["Page"]["media"]:
yield MediaCardLoader.media_card(anime_item)
return _data_generator()
else:
return data
def get_most_recently_updated_anime(self):
success, data = AniList.get_most_recently_updated()
if success:
def _data_generator():
for anime_item in data["data"]["Page"]["media"]:
yield MediaCardLoader.media_card(anime_item)
return _data_generator()
else:
return data
def get_most_popular_anime(self):
success, data = AniList.get_most_popular()
if success:
def _data_generator():
for anime_item in data["data"]["Page"]["media"]:
yield MediaCardLoader.media_card(anime_item)
return _data_generator()
else:
return data
def get_most_scored_anime(self):
success, data = AniList.get_most_scored()
if success:
def _data_generator():
for anime_item in data["data"]["Page"]["media"]:
yield MediaCardLoader.media_card(anime_item)
return _data_generator()
else:
return data
def get_upcoming_anime(self):
success, data = AniList.get_upcoming_anime(1)
if success:
def _data_generator():
for anime_item in data["data"]["Page"]["media"]:
yield MediaCardLoader.media_card(anime_item)
return _data_generator()
else:
return data

View File

@@ -1,18 +0,0 @@
from anixstream.libs.anilist import AniList
from .base_model import BaseScreenModel
from anixstream.Utility import MediaCardLoader,show_notification
class MyListScreenModel(BaseScreenModel):
already_in_user_anime_list = []
def update_my_anime_list_view(self,not_yet_in_user_anime_list:list,page=None):
success,self.data = AniList.search(id_in=not_yet_in_user_anime_list,page=page,sort="SCORE_DESC")
if success:
return self.media_card_generator()
else:
show_notification(f"Failed to update my list screen view",self.data["Error"])
return None
def media_card_generator(self):
for anime_item in self.data["data"]["Page"]["media"]:
yield MediaCardLoader.media_card(anime_item)

View File

@@ -1,29 +0,0 @@
from .base_model import BaseScreenModel
from anixstream.libs.anilist import AniList
from anixstream.Utility import MediaCardLoader, show_notification
class SearchScreenModel(BaseScreenModel):
data = {}
def get_trending_anime(self):
success,data = AniList.get_trending()
if success:
def _data_generator():
for anime_item in data["data"]["Page"]["media"]:
yield MediaCardLoader.media_card(anime_item)
return _data_generator()
else:
return data
def search_for_anime(self,anime_title,**kwargs):
success,self.data = AniList.search(query=anime_title,**kwargs)
if success:
return self.media_card_generator()
else:
show_notification(f"Failed to search for {anime_title}",self.data.get("Error"))
def media_card_generator(self):
for anime_item in self.data["data"]["Page"]["media"]:
yield MediaCardLoader.media_card(anime_item)
self.pagination_info = self.data["data"]["Page"]["pageInfo"]

View File

@@ -1,4 +0,0 @@
from .media_card_loader import MediaCardLoader
from .show_notification import show_notification
from .data import themes_available
from .utils import write_crash

View File

@@ -1,41 +0,0 @@
from typing import TypedDict
import plyer
import os
from .yaml_parser import YamlParser
class AnimdlConfig(TypedDict):
default_player: str
default_provider: str
quality_string: str
if local_data_path:=os.getenv("LOCALAPPDATA"):
config_dir = os.path.join(local_data_path,".config")
if not os.path.exists(config_dir):
os.mkdir(config_dir)
animdl_config_folder_location = os.path.join(config_dir, "animdl")
else:
user_profile_path = plyer.storagepath.get_home_dir() # type: ignore
animdl_config_folder_location = os.path.join(user_profile_path, ".animdl")
if not os.path.exists(animdl_config_folder_location):
os.mkdir(animdl_config_folder_location)
animdl_config_location = os.path.join(animdl_config_folder_location, "config.yml")
# print(animdl_config_location)
animdl_config = YamlParser(
animdl_config_location,
{"default_player": "mpv", "default_provider": "allanime", "quality_string": "best"},
AnimdlConfig,
)
def update_animdl_config(field_to_update: str, value):
current_data = animdl_config.data
current_data[f"{field_to_update}"] = value
animdl_config.write(current_data)
def get_animdl_config() -> AnimdlConfig:
return animdl_config.data

View File

@@ -1,6 +0,0 @@
"""
Just contains some useful data used across the codebase
"""
themes_available = ['Aliceblue', 'Antiquewhite', 'Aqua', 'Aquamarine', 'Azure', 'Beige', 'Bisque', 'Black', 'Blanchedalmond', 'Blue', 'Blueviolet', 'Brown', 'Burlywood', 'Cadetblue', 'Chartreuse', 'Chocolate', 'Coral', 'Cornflowerblue', 'Cornsilk', 'Crimson', 'Cyan', 'Darkblue', 'Darkcyan', 'Darkgoldenrod', 'Darkgray', 'Darkgrey', 'Darkgreen', 'Darkkhaki', 'Darkmagenta', 'Darkolivegreen', 'Darkorange', 'Darkorchid', 'Darkred', 'Darksalmon', 'Darkseagreen', 'Darkslateblue', 'Darkslategray', 'Darkslategrey', 'Darkturquoise', 'Darkviolet', 'Deeppink', 'Deepskyblue', 'Dimgray', 'Dimgrey', 'Dodgerblue', 'Firebrick', 'Floralwhite', 'Forestgreen', 'Fuchsia', 'Gainsboro', 'Ghostwhite', 'Gold', 'Goldenrod', 'Gray', 'Grey', 'Green', 'Greenyellow', 'Honeydew', 'Hotpink', 'Indianred', 'Indigo', 'Ivory', 'Khaki', 'Lavender', 'Lavenderblush', 'Lawngreen', 'Lemonchiffon', 'Lightblue', 'Lightcoral', 'Lightcyan', 'Lightgoldenrodyellow', 'Lightgreen', 'Lightgray', 'Lightgrey', 'Lightpink', 'Lightsalmon', 'Lightseagreen', 'Lightskyblue', 'Lightslategray', 'Lightslategrey', 'Lightsteelblue', 'Lightyellow', 'Lime', 'Limegreen', 'Linen', 'Magenta', 'Maroon', 'Mediumaquamarine', 'Mediumblue', 'Mediumorchid', 'Mediumpurple', 'Mediumseagreen', 'Mediumslateblue', 'Mediumspringgreen', 'Mediumturquoise', 'Mediumvioletred', 'Midnightblue', 'Mintcream', 'Mistyrose', 'Moccasin', 'Navajowhite', 'Navy', 'Oldlace', 'Olive', 'Olivedrab', 'Orange', 'Orangered', 'Orchid', 'Palegoldenrod', 'Palegreen', 'Paleturquoise', 'Palevioletred', 'Papayawhip', 'Peachpuff', 'Peru', 'Pink', 'Plum', 'Powderblue', 'Purple', 'Red', 'Rosybrown', 'Royalblue', 'Saddlebrown', 'Salmon', 'Sandybrown', 'Seagreen', 'Seashell', 'Sienna', 'Silver', 'Skyblue', 'Slateblue', 'Slategray', 'Slategrey', 'Snow', 'Springgreen', 'Steelblue', 'Tan', 'Teal', 'Thistle', 'Tomato', 'Turquoise', 'Violet', 'Wheat', 'White', 'Whitesmoke', 'Yellow',
'Yellowgreen']

View File

@@ -1,54 +0,0 @@
"""
Contains helper functions to make your life easy when adding kivy markup to text
"""
from kivy.utils import get_hex_from_color
def bolden(text: str):
return f"[b]{text}[/b]"
def italicize(text: str):
return f"[i]{text}[/i]"
def underline(text: str):
return f"[u]{text}[/u]"
def strike_through(text: str):
return f"[s]{text}[/s]"
def sub_script(text: str):
return f"[sub]{text}[/sub]"
def super_script(text: str):
return f"[sup]{text}[/sup]"
def color_text(text: str, color: tuple):
hex_color = get_hex_from_color(color)
return f"[color={hex_color}]{text}[/color]"
def font(text: str, font_name: str):
return f"[font={font_name}]{text}[/font]"
def font_family(text: str, family: str):
return f"[font_family={family}]{text}[/font_family]"
def font_context(text: str, context: str):
return f"[font_context={context}]{text}[/font_context]"
def font_size(text: str, size: int):
return f"[size={size}]{text}[/size]"
def text_ref(text: str, ref: str):
return f"[ref={ref}]{text}[/ref]"

View File

@@ -1,276 +0,0 @@
from collections import deque
from time import sleep
import threading
from pytube import YouTube
from kivy.clock import Clock
from kivy.cache import Cache
from kivy.loader import _ThreadPool
from kivy.logger import Logger
from anixstream.View.components import MediaCard
from anixstream.Utility import anilist_data_helper, user_data_helper
from anixstream.libs.anilist.anilist_data_schema import AnilistBaseMediaDataSchema
# Register anime cache in memory
Cache.register("yt_stream_links.anime")
user_anime_list = user_data_helper.get_user_anime_list()
yt_stream_links = user_data_helper.get_anime_trailer_cache()
for link in yt_stream_links:
Cache.append("yt_stream_links.anime", link[0], tuple(link[1]))
# TODO: Make this process more efficient
# for youtube video links gotten from from pytube which is blocking
class MediaCardDataLoader(object):
"""this class loads an anime media card and gets the trailer url from pytube"""
def __init__(self):
self._resume_cond = threading.Condition()
self._num_workers = 5
self._max_upload_per_frame = 5
self._paused = False
self._q_load = deque()
self._q_done = deque()
self._client = []
self._running = False
self._start_wanted = False
self._trigger_update = Clock.create_trigger(self._update)
def start(self):
"""Start the loader thread/process."""
self._running = True
def run(self, *largs):
"""Main loop for the loader."""
pass
def stop(self):
"""Stop the loader thread/process."""
self._running = False
def pause(self):
"""Pause the loader, can be useful during interactions.
.. versionadded:: 1.6.0
"""
self._paused = True
def resume(self):
"""Resume the loader, after a :meth:`pause`.
.. versionadded:: 1.6.0
"""
self._paused = False
self._resume_cond.acquire()
self._resume_cond.notify_all()
self._resume_cond.release()
def _wait_for_resume(self):
while self._running and self._paused:
self._resume_cond.acquire()
self._resume_cond.wait(0.25)
self._resume_cond.release()
def cached_fetch_data(self, yt_watch_url):
data: tuple = Cache.get("yt_stream_links.anime", yt_watch_url) # type: ignore # trailer_url is the yt_watch_link
if not data[0]:
yt = YouTube(yt_watch_url)
preview_image = yt.thumbnail_url
try:
video_stream_url = yt.streams.filter(
progressive=True, file_extension="mp4"
)[-1].url
data = preview_image, video_stream_url
yt_stream_links.append((yt_watch_url, data))
user_data_helper.update_anime_trailer_cache(yt_stream_links)
except:
data = preview_image, None
return data
def _load(self, kwargs):
while len(self._q_done) >= (self._max_upload_per_frame * self._num_workers):
sleep(0.1) # type: ignore
self._wait_for_resume()
yt_watch_link = kwargs["yt_watch_link"]
try:
data = self.cached_fetch_data(yt_watch_link)
except Exception as e:
data = None
Logger.error("Pytube:{e}")
self._q_done.appendleft((yt_watch_link, data))
self._trigger_update()
def _update(self, *largs):
if self._start_wanted:
if not self._running:
self.start()
self._start_wanted = False
# in pause mode, don't unqueue anything.
if self._paused:
self._trigger_update()
return
for _ in range(self._max_upload_per_frame):
try:
yt_watch_link, data = self._q_done.pop()
except IndexError:
return
# update client
for c_yt_watch_link, client in self._client[:]:
if yt_watch_link != c_yt_watch_link:
continue
# got one client to update
if data:
trailer_url = data[1]
if trailer_url:
client.set_trailer_url(trailer_url)
Logger.info(f"Pytube:Found trailer url for {client.title}")
Cache.append("yt_stream_links.anime", yt_watch_link, data)
self._client.remove((c_yt_watch_link, client))
self._trigger_update()
def media_card(
self,
anime_item: AnilistBaseMediaDataSchema,
load_callback=None,
post_callback=None,
**kwargs,
):
media_card = MediaCard()
media_card.anime_id = anime_id = anime_item["id"]
# TODO: ADD language preference
if anime_item["title"].get("english"):
media_card.title = anime_item["title"]["english"]
else:
media_card.title = anime_item["title"]["romaji"]
media_card.cover_image_url = anime_item["coverImage"]["medium"]
media_card.popularity = str(anime_item["popularity"])
media_card.favourites = str(anime_item["favourites"])
media_card.episodes = str(anime_item["episodes"])
if anime_item.get("description"):
media_card.description = anime_item["description"]
else:
media_card.description = "None"
# TODO: switch to season and year
media_card.first_aired_on = (
f'{anilist_data_helper.format_anilist_date_object(anime_item["startDate"])}'
)
media_card.studios = anilist_data_helper.format_list_data_with_comma(
[
studio["name"]
for studio in anime_item["studios"]["nodes"]
if studio["isAnimationStudio"]
]
)
media_card.producers = anilist_data_helper.format_list_data_with_comma(
[
studio["name"]
for studio in anime_item["studios"]["nodes"]
if not studio["isAnimationStudio"]
]
)
media_card.next_airing_episode = "{}".format(
anilist_data_helper.extract_next_airing_episode(
anime_item["nextAiringEpisode"]
)
)
if anime_item.get("tags"):
media_card.tags = anilist_data_helper.format_list_data_with_comma(
[tag["name"] for tag in anime_item["tags"]]
)
media_card.media_status = anime_item["status"]
if anime_item.get("genres"):
media_card.genres = anilist_data_helper.format_list_data_with_comma(
anime_item["genres"]
)
if anime_id in user_anime_list:
media_card.is_in_my_list = True
if anime_item["averageScore"]:
stars = int(anime_item["averageScore"] / 100 * 6)
if stars:
for i in range(stars):
media_card.stars[i] = 1
# Setting up trailer info to be gotten if available
if anime_item["trailer"]:
yt_watch_link = "https://youtube.com/watch?v=" + anime_item["trailer"]["id"]
data = Cache.get("yt_stream_links.anime", yt_watch_link) # type: ignore # trailer_url is the yt_watch_link
if data:
if data[1] not in (None, False):
media_card.set_preview_image(data[0])
media_card.set_trailer_url(data[1])
return media_card
else:
# if data is None, this is really the first time
self._client.append((yt_watch_link, media_card))
self._q_load.appendleft(
{
"yt_watch_link": yt_watch_link,
"load_callback": load_callback,
"post_callback": post_callback,
"current_anime": anime_item["id"],
"kwargs": kwargs,
}
)
if not kwargs.get("nocache", False):
Cache.append("yt_stream_links.anime", yt_watch_link, (False, False))
self._start_wanted = True
self._trigger_update()
return media_card
class LoaderThreadPool(MediaCardDataLoader):
def __init__(self):
super(LoaderThreadPool, self).__init__()
self.pool: _ThreadPool | None = None
def start(self):
super(LoaderThreadPool, self).start()
self.pool = _ThreadPool(self._num_workers)
Clock.schedule_interval(self.run, 0)
def stop(self):
super(LoaderThreadPool, self).stop()
Clock.unschedule(self.run)
self.pool.stop() # type: ignore
def run(self, *largs):
while self._running:
try:
parameters = self._q_load.pop()
except:
return
self.pool.add_task(self._load, parameters) # type: ignore
MediaCardLoader = LoaderThreadPool()
Logger.info(
"MediaCardLoader: using a thread pool of {} workers".format(
MediaCardLoader._num_workers
)
)

View File

@@ -1,17 +0,0 @@
# Of course, "very flexible Python" allows you to do without an abstract
# superclass at all or use the clever exception `NotImplementedError`. In my
# opinion, this can negatively affect the architecture of the application.
# I would like to point out that using Kivy, one could use the on-signaling
# model. In this case, when the state changes, the model will send a signal
# that can be received by all attached observers. This approach seems less
# universal - you may want to use a different library in the future.
class Observer:
"""Abstract superclass for all observers."""
def model_is_changed(self):
"""
The method that will be called on the observer when the model changes.
"""

View File

@@ -1,29 +0,0 @@
from kivymd.uix.snackbar import MDSnackbar, MDSnackbarText, MDSnackbarSupportingText
from kivy.clock import Clock
def show_notification(title, details):
"""helper function to display notifications
Args:
title (str): the title of your message
details (str): the details of your message
"""
def _show(dt):
MDSnackbar(
MDSnackbarText(
text=title,
adaptive_height=True,
),
MDSnackbarSupportingText(
text=details, shorten=False, max_lines=0, adaptive_height=True
),
duration=5,
y="10dp",
pos_hint={"bottom": 1, "right": 0.99},
padding=[0, 0, "8dp", "8dp"],
size_hint_x=0.4,
).open()
Clock.schedule_once(_show, 1)

View File

@@ -1,82 +0,0 @@
"""
Contains Helper functions to read and write the user data files
"""
from kivy.storage.jsonstore import JsonStore
from datetime import date, datetime
from kivy.logger import Logger
from kivy.resources import resource_find
today = date.today()
now = datetime.now()
user_data = JsonStore(resource_find("user_data.json"))
yt_cache = JsonStore(resource_find("yt_cache.json"))
# Get the user data
def get_user_anime_list() -> list:
try:
return user_data.get("user_anime_list")[
"user_anime_list"
] # returns a list of anime ids
except Exception as e:
Logger.warning(f"User Data:Read failure:{e}")
return []
def update_user_anime_list(updated_list: list):
try:
updated_list_ = list(set(updated_list))
user_data.put("user_anime_list", user_anime_list=updated_list_)
except Exception as e:
Logger.warning(f"User Data:Update failure:{e}")
# Get the user data
def get_user_downloads() -> list:
try:
return user_data.get("user_downloads")[
"user_downloads"
] # returns a list of anime ids
except Exception as e:
Logger.warning(f"User Data:Read failure:{e}")
return []
def update_user_downloads(updated_list: list):
try:
user_data.put("user_downloads", user_downloads=list(set(updated_list)))
except Exception as e:
Logger.warning(f"User Data:Update failure:{e}")
# Yt persistent anime trailer cache
t = 1
if now.hour <= 6:
t = 1
elif now.hour <= 12:
t = 2
elif now.hour <= 18:
t = 3
else:
t = 4
yt_anime_trailer_cache_name = f"{today}{t}"
def get_anime_trailer_cache() -> list:
try:
return yt_cache["yt_stream_links"][f"{yt_anime_trailer_cache_name}"]
except Exception as e:
Logger.warning(f"User Data:Read failure:{e}")
return []
def update_anime_trailer_cache(yt_stream_links: list):
try:
yt_cache.put(
"yt_stream_links", **{f"{yt_anime_trailer_cache_name}": yt_stream_links}
)
except Exception as e:
Logger.warning(f"User Data:Update failure:{e}")

View File

@@ -1,34 +0,0 @@
from datetime import datetime
import shutil
import os
# TODO: make it use color_text instead of fixed vals
# from .kivy_markup_helper import color_text
# utility functions
def write_crash(e: Exception):
index = datetime.today()
error = f"[b][color=#fa0000][ {index} ]:[/color][/b]\n(\n\n{e}\n\n)\n"
try:
with open("crashdump.txt", "a") as file:
file.write(error)
except:
with open("crashdump.txt", "w") as file:
file.write(error)
return index
def read_crash_file():
crash_file_path = "./crashfile.txt"
if not os.path.exists(crash_file_path):
return None
else:
with open(crash_file_path,"r") as file:
return file.read()
def move_file(source_path, dest_path):
try:
path = shutil.move(source_path, dest_path)
return (1, path)
except Exception as e:
return (0, e)

View File

@@ -1,34 +0,0 @@
import yaml
import os
class YamlParser:
"""makes managing yaml files easier"""
data = {}
def __init__(self, file_path: str, default, data_type):
self.file_path: str = file_path
self.data: data_type
if os.path.exists(file_path):
try:
with open(self.file_path, "r") as yaml_file:
self.data = yaml.safe_load(yaml_file)
except:
self.data = default
with open(file_path, "w") as yaml_file:
yaml.dump(default, yaml_file)
else:
self.data = default
with open(file_path, "w") as yaml_file:
yaml.dump(default, yaml_file)
def read(self):
with open(self.file_path, "r") as yaml_file:
self.data = yaml.safe_load(yaml_file)
return self.data
def write(self, new_obj):
with open(self.file_path, "w") as yaml_file:
yaml.dump(new_obj, yaml_file)

View File

@@ -1,61 +0,0 @@
<AnimeScreenView>:
md_bg_color: self.theme_cls.backgroundColor
header:header
side_bar:side_bar
rankings_bar:rankings_bar
anime_description:anime_description
anime_characters:anime_characters
anime_reviews:anime_reviews
MDBoxLayout:
orientation: 'vertical'
MDBoxLayout:
orientation: 'vertical'
size_hint_y:None
height: self.minimum_height
MDBoxLayout:
adaptive_height:True
MDIconButton:
icon: "arrow-left"
on_release:
root.manager_screens.current = root.caller_screen_name
MDScrollView:
size_hint:1,1
MDBoxLayout:
id:main_container
size_hint_y:None
padding:"10dp"
spacing:"10dp"
height: self.minimum_height
orientation:"vertical"
AnimeHeader:
id:header
MDBoxLayout:
size_hint_y:None
height: self.minimum_height
AnimeSideBar:
id:side_bar
screen:root
size_hint_y:None
height:max(self.parent.height,self.minimum_height)
MDBoxLayout:
spacing:"10dp"
orientation:"vertical"
size_hint_y:None
height: max(self.parent.height,self.minimum_height)
RankingsBar:
id:rankings_bar
Controls:
screen:root
cols:3 if root.width < 1100 else 5
MDBoxLayout:
adaptive_height:True
padding:"20dp"
orientation:"vertical"
AnimeDescription:
id:anime_description
AnimeCharacters:
id:anime_characters
AnimeReviews:
id:anime_reviews
BoxLayout:

View File

@@ -1,135 +0,0 @@
from kivy.properties import ObjectProperty, DictProperty, StringProperty
from anixstream.Utility import anilist_data_helper
from anixstream.libs.anilist import AnilistBaseMediaDataSchema
from anixstream.View.base_screen import BaseScreenView
from .components import (
AnimeHeader,
AnimeSideBar,
AnimeDescription,
AnimeReviews,
AnimeCharacters,
AnimdlStreamDialog,
DownloadAnimeDialog,
RankingsBar,
)
class AnimeScreenView(BaseScreenView):
"""The anime screen view"""
caller_screen_name = StringProperty()
header: AnimeHeader = ObjectProperty()
side_bar: AnimeSideBar = ObjectProperty()
rankings_bar: RankingsBar = ObjectProperty()
anime_description: AnimeDescription = ObjectProperty()
anime_characters: AnimeCharacters = ObjectProperty()
anime_reviews: AnimeReviews = ObjectProperty()
data = DictProperty()
anime_id = 0
def update_layout(self, data: AnilistBaseMediaDataSchema, caller_screen_name: str):
self.caller_screen_name = caller_screen_name
self.data = data
# uitlity functions
# variables
english_title = data["title"]["english"]
jp_title = data["title"]["romaji"]
studios = data["studios"]["nodes"]
# update header
self.header.titles = f"{english_title}\n{jp_title}"
if banner_image := data["bannerImage"]:
self.header.banner_image = banner_image
# -----side bar-----
# update image
self.side_bar.image = data["coverImage"]["extraLarge"]
# update alternative titles
alternative_titles = {
"synonyms": anilist_data_helper.format_list_data_with_comma(
data["synonyms"]
), # list
"japanese": jp_title,
"english": english_title,
}
self.side_bar.alternative_titles = alternative_titles
# update information
information = {
"episodes": data["episodes"],
"status": data["status"],
"nextAiringEpisode": anilist_data_helper.extract_next_airing_episode(
data["nextAiringEpisode"]
),
"aired": f"{anilist_data_helper.format_anilist_date_object(data['startDate'])} to {anilist_data_helper.format_anilist_date_object(data['endDate'])}",
"premiered": f"{data['season']} {data['seasonYear']}",
"broadcast": data["format"],
"countryOfOrigin": data["countryOfOrigin"],
"hashtag": data["hashtag"],
"studios": anilist_data_helper.format_list_data_with_comma(
[studio["name"] for studio in studios if studio["isAnimationStudio"]]
), # { "name": "Sunrise", "isAnimationStudio": true }
"producers": anilist_data_helper.format_list_data_with_comma(
[
studio["name"]
for studio in studios
if not studio["isAnimationStudio"]
]
),
"source": data["source"],
"genres": anilist_data_helper.format_list_data_with_comma(data["genres"]),
"duration": data["duration"],
}
self.side_bar.information = information
# update statistics
statistics = [*[(stat["context"], stat["rank"]) for stat in data["rankings"]]]
self.side_bar.statistics = statistics
# update tags
self.side_bar.tags = [(tag["name"], tag["rank"]) for tag in data["tags"]]
# update external links
external_links = [
("AniList", data["siteUrl"]),
*[(site["site"], site["url"]) for site in data["externalLinks"]],
]
self.side_bar.external_links = external_links
self.rankings_bar.rankings = {
"Popularity": data["popularity"],
"Favourites": data["favourites"],
"AverageScore": data["averageScore"] if data["averageScore"] else 0,
}
self.anime_description.description = data["description"]
self.anime_characters.characters = [
(character["node"], character["voiceActors"])
for character in data["characters"]["edges"]
] # list (character,actor)
self.anime_reviews.reviews = data["reviews"]["nodes"]
def stream_anime_with_custom_cmds_dialog(self,mpv=False):
"""
Called when user wants to stream with custom commands
"""
AnimdlStreamDialog(self.data,mpv).open()
def open_download_anime_dialog(self):
"""
Opens the download anime dialog
"""
DownloadAnimeDialog(self.data).open()
def add_to_user_anime_list(self, *args):
self.app.add_anime_to_user_anime_list(self.model.anime_id)

View File

@@ -1,10 +0,0 @@
from .side_bar import AnimeSideBar
from .header import AnimeHeader
from .rankings_bar import RankingsBar
from .controls import Controls
from .description import AnimeDescription
from .characters import AnimeCharacters
from .review import AnimeReviews
from .animdl_stream_dialog import AnimdlStreamDialog
from .download_anime_dialog import DownloadAnimeDialog

View File

@@ -1,63 +0,0 @@
<StreamDialogLabel@MDLabel>:
adaptive_height:True
max_lines:0
shorten:False
markup:True
font_style: "Label"
role: "medium"
bold:True
<StreamDialogHeaderLabel@MDLabel>:
adaptive_height:True
halign:"center"
max_lines:0
shorten:False
bold:True
markup:True
font_style: "Title"
role: "medium"
md_bg_color:self.theme_cls.secondaryContainerColor
padding:"10dp"
<AnimdlStreamDialog>
md_bg_color:self.theme_cls.backgroundColor
radius:8
size_hint:None,None
height:"500dp"
width:"400dp"
MDBoxLayout:
spacing: '10dp'
padding:"10dp"
orientation:"vertical"
StreamDialogHeaderLabel:
text:"Stream Anime"
StreamDialogLabel:
text:"Title"
MDTextField:
id:title_field
required:True
StreamDialogLabel:
text:"Range"
MDTextField:
id:range_field
required:True
StreamDialogLabel:
text:"Latest"
MDTextField:
id:latest_field
required:True
StreamDialogLabel:
text:"Quality"
MDTextField:
id:quality_field
required:True
MDBoxLayout:
orientation:"vertical"
MDButton:
pos_hint: {'center_x': 0.5}
on_press:root.stream_anime(app)
MDButtonIcon:
icon:"rss"
MDButtonText:
text:"Stream"

View File

@@ -1,69 +0,0 @@
from kivy.clock import Clock
from kivy.uix.modalview import ModalView
from kivymd.uix.behaviors import (
StencilBehavior,
CommonElevationBehavior,
BackgroundColorBehavior,
)
from kivymd.theming import ThemableBehavior
class AnimdlStreamDialog(
ThemableBehavior,
StencilBehavior,
CommonElevationBehavior,
BackgroundColorBehavior,
ModalView,
):
"""The anime streaming dialog"""
def __init__(self, data, mpv, **kwargs):
super().__init__(**kwargs)
self.data = data
self.mpv = mpv
if title := data["title"].get("romaji"):
self.ids.title_field.text = title
elif title := data["title"].get("english"):
self.ids.title_field.text = title
self.ids.quality_field.text = "best"
def _stream_anime(self, app):
if self.mpv:
streaming_cmds = {}
title = self.ids.title_field.text
streaming_cmds["title"] = title
episodes_range = self.ids.range_field.text
if episodes_range:
streaming_cmds["episodes_range"] = episodes_range
quality = self.ids.quality_field.text
if quality:
streaming_cmds["quality"] = quality
else:
streaming_cmds["quality"] = "best"
app.watch_on_animdl(streaming_cmds)
else:
cmds = []
title = self.ids.title_field.text
cmds.append(title)
episodes_range = self.ids.range_field.text
if episodes_range:
cmds = [*cmds, "-r", episodes_range]
latest = self.ids.latest_field.text
if latest:
cmds = [*cmds, "-s", latest]
quality = self.ids.quality_field.text
if quality:
cmds = [*cmds, "-q", quality]
app.watch_on_animdl(custom_options=cmds)
self.dismiss()
def stream_anime(self, app):
Clock.schedule_once(lambda _: self._stream_anime(app))

View File

@@ -1,71 +0,0 @@
#:import get_hex_from_color kivy.utils.get_hex_from_color
<CharactersContainer@MDBoxLayout>:
adaptive_height:True
md_bg_color:self.theme_cls.surfaceContainerLowColor
padding:"10dp"
orientation:"vertical"
<CharacterText@MDLabel>:
adaptive_height:True
max_lines:0
shorten:False
markup:True
font_style: "Body"
role: "small"
<CharacterHeader@MDBoxLayout>:
adaptive_height:True
spacing:"10dp"
<CharacterAvatar@FitImage>
radius:50
size_hint:None,None
height:"50dp"
width:"50dp"
<CharacterSecondaryContainer@MDBoxLayout>:
adaptive_height:True
orientation:"vertical"
<AnimeCharacter>:
spacing:"5dp"
adaptive_height:True
orientation:"vertical"
CharacterHeader:
padding:"10dp"
CharacterAvatar:
source:root.character["image"]
CharacterText:
text: root.character["name"]
pos_hint:{"center_y":.5}
CharacterSecondaryContainer:
spacing:"5dp"
MDDivider:
CharacterText:
text: "Details"
MDDivider:
CharacterText:
text:"[color={}]Gender:[/color] {}".format(get_hex_from_color(self.theme_cls.primaryColor),root.character["gender"])
CharacterText:
text:"[color={}]Date Of Birth:[/color] {}".format(get_hex_from_color(self.theme_cls.primaryColor),root.character["dateOfBirth"])
CharacterText:
text:"[color={}]Age:[/color] {}".format(get_hex_from_color(self.theme_cls.primaryColor),root.character["age"])
CharacterText:
text:"[color={}]Description:[/color] {}".format(get_hex_from_color(self.theme_cls.primaryColor),root.character["description"])
max_lines:5
CharacterText:
text:"[color={}]Voice Actors:[/color] {}".format(get_hex_from_color(self.theme_cls.primaryColor),root.voice_actors["name"])
MDDivider:
<AnimeCharacters>:
adaptive_height:True
container:container
orientation:"vertical"
HeaderLabel:
text:"Characters"
halign:"left"
CharactersContainer:
id:container

View File

@@ -1,56 +0,0 @@
from kivy.clock import Clock
from kivy.properties import ObjectProperty, ListProperty
from kivymd.uix.boxlayout import MDBoxLayout
class AnimeCharacter(MDBoxLayout):
"""an Anime character data"""
voice_actors = ObjectProperty({"name": "", "image": ""})
character = ObjectProperty(
{
"name": "",
"gender": "",
"dateOfBirth": "",
"image": "",
"age": "",
"description": "",
}
)
class AnimeCharacters(MDBoxLayout):
"""The anime characters card"""
container = ObjectProperty()
characters = ListProperty()
def update_characters_card(self, instance, characters):
format_date = lambda date_: (
f"{date_['day']}/{date_['month']}/{date_['year']}" if date_ else ""
)
self.container.clear_widgets()
for character_ in characters: # character (character,actor)
character = character_[0]
actors = character_[1]
anime_character = AnimeCharacter()
anime_character.character = {
"name": character["name"]["full"],
"gender": character["gender"],
"dateOfBirth": format_date(character["dateOfBirth"]),
"image": character["image"]["medium"],
"age": character["age"],
"description": character["description"],
}
anime_character.voice_actors = {
"name": ", ".join([actor["name"]["full"] for actor in actors])
}
# anime_character.voice_actor =
self.container.add_widget(anime_character)
def on_characters(self, *args):
Clock.schedule_once(lambda _: self.update_characters_card(*args))

View File

@@ -1,32 +0,0 @@
<Controls>
adaptive_height:True
padding:"10dp"
spacing:"10dp"
pos_hint: {'center_x': 0.5}
cols:3
MDButton:
on_press:
root.screen.add_to_user_anime_list()
add_to_user_list_label.text = "Added to MyAnimeList"
MDButtonText:
id:add_to_user_list_label
text:"Add to MyAnimeList"
MDButton:
on_press:
if root.screen:root.screen.stream_anime_with_custom_cmds_dialog()
MDButtonText:
text:"Watch on Animdl"
MDButton:
on_press:
if root.screen:root.screen.stream_anime_with_custom_cmds_dialog(mpv=True)
MDButtonText:
text:"Watch on mpv"
MDButton:
on_press: app.watch_on_allanime(root.screen.data["title"]["romaji"]) if root.screen.data["title"]["romaji"] else app.watch_on_allanime(root.screen.data["title"]["english"])
MDButtonText:
text:"Watch on AllAnime"
MDButton:
on_press:
if root.screen:root.screen.open_download_anime_dialog()
MDButtonText:
text:"Download Anime"

View File

@@ -1,9 +0,0 @@
from kivy.properties import ObjectProperty
from kivymd.uix.gridlayout import MDGridLayout
class Controls(MDGridLayout):
"""The diferent controls available"""
screen = ObjectProperty()

View File

@@ -1,23 +0,0 @@
<DescriptionContainer@MDBoxLayout>:
adaptive_height:True
md_bg_color:self.theme_cls.surfaceContainerLowColor
padding:"10dp"
<DescriptionText@MDLabel>:
adaptive_height:True
max_lines:0
shorten:False
markup:True
font_style: "Body"
role: "small"
<AnimeDescription>:
orientation:"vertical"
adaptive_height:True
HeaderLabel:
halign:"left"
text:"Description"
DescriptionContainer:
DescriptionText:
text:root.description

View File

@@ -1,9 +0,0 @@
from kivy.properties import StringProperty
from kivymd.uix.boxlayout import MDBoxLayout
class AnimeDescription(MDBoxLayout):
"""The anime description"""
description = StringProperty()

View File

@@ -1,58 +0,0 @@
<DownloadDialogLabel@MDLabel>:
adaptive_height:True
max_lines:0
shorten:False
markup:True
font_style: "Label"
role: "medium"
bold:True
<DownloadDialogHeaderLabel@MDLabel>:
adaptive_height:True
halign:"center"
max_lines:0
shorten:False
bold:True
markup:True
font_style: "Title"
role: "medium"
md_bg_color:self.theme_cls.secondaryContainerColor
padding:"10dp"
<DownloadAnimeDialog>
md_bg_color:self.theme_cls.backgroundColor
radius:8
size_hint:None,None
height:"500dp"
width:"400dp"
MDBoxLayout:
spacing: '10dp'
padding:"10dp"
orientation:"vertical"
DownloadDialogHeaderLabel:
text:"Download Anime"
DownloadDialogLabel:
text:"Title"
MDTextField:
id:title_field
required:True
DownloadDialogLabel:
text:"Range"
MDTextField:
id:range_field
required:True
DownloadDialogLabel:
text:"Quality"
MDTextField:
id:quality_field
required:True
MDBoxLayout:
orientation:"vertical"
MDButton:
pos_hint: {'center_x': 0.5}
on_press:root.download_anime(app)
MDButtonIcon:
icon:"download"
MDButtonText:
text:"Download"

View File

@@ -1,42 +0,0 @@
from kivy.uix.modalview import ModalView
from kivymd.uix.behaviors import (
StencilBehavior,
CommonElevationBehavior,
BackgroundColorBehavior,
)
from kivymd.theming import ThemableBehavior
# from main import AniXStreamApp
class DownloadAnimeDialog(
ThemableBehavior,
StencilBehavior,
CommonElevationBehavior,
BackgroundColorBehavior,
ModalView,
):
"""The download anime dialog"""
def __init__(self, data, **kwargs):
super(DownloadAnimeDialog, self).__init__(**kwargs)
self.data = data
self.anime_id = self.data["id"]
if title := data["title"].get("romaji"):
self.ids.title_field.text = title
elif title := data["title"].get("english"):
self.ids.title_field.text = title
self.ids.quality_field.text = "best"
def download_anime(self, app):
default_cmds = {}
title = self.ids.title_field.text
default_cmds["title"] = title
if episodes_range := self.ids.range_field.text:
default_cmds["episodes_range"] = episodes_range
if quality := self.ids.range_field.text:
default_cmds["quality"] = quality
app.download_anime(self.anime_id, default_cmds)
self.dismiss()

View File

@@ -1,20 +0,0 @@
<AnimeHeader>:
adaptive_height:True
orientation: 'vertical'
MDBoxLayout:
adaptive_height:True
md_bg_color:self.theme_cls.secondaryContainerColor
MDLabel:
text: root.titles
adaptive_height:True
padding:"5dp"
bold:True
shorten:False
max_lines:2
font_style:"Label"
role:"large"
FitImage:
size_hint_y: None
height: dp(250)
source: root.banner_image if root.banner_image else app.default_banner_image

View File

@@ -1,8 +0,0 @@
from kivy.properties import StringProperty
from kivymd.uix.boxlayout import MDBoxLayout
class AnimeHeader(MDBoxLayout):
titles = StringProperty()
banner_image = StringProperty()

View File

@@ -1,81 +0,0 @@
#:set yellow [.9,.9,0,.9]
<RankingsLabel@MDLabel>:
max_lines:0
shorten:False
markup:True
font_style: "Label"
role: "medium"
<RankingsHeaderLabel@MDLabel>:
color:self.theme_cls.primaryColor
bold:True
max_lines:0
shorten:False
font_style: "Label"
role: "large"
<RankingsDivider@MDDivider>:
orientation:"vertical"
<RankingsBoxLayout@MDBoxLayout>:
orientation:"vertical"
padding:"20dp"
<RankingsBar>:
size_hint_y:None
height:dp(100)
line_color:self.theme_cls.secondaryColor
padding:"10dp"
RankingsBoxLayout:
size_hint_x:.4
RankingsHeaderLabel:
text:"Average Score"
MDBoxLayout:
adaptive_width:True
MDBoxLayout:
adaptive_size:True
pos_hint: {'center_y': .5}
MDIcon:
icon: "star"
color:yellow
disabled: not((root.rankings["AverageScore"]/100)*6>=1)
MDIcon:
color:yellow
disabled: not(root.rankings["AverageScore"]/100*6>=2)
icon: "star"
MDIcon:
color:yellow
disabled: not(root.rankings["AverageScore"]/100*6>=3)
icon: "star"
MDIcon:
color:yellow
disabled: not(root.rankings["AverageScore"]/100*6>=4)
icon: "star"
MDIcon:
color:yellow
icon: "star"
disabled: not(root.rankings["AverageScore"]/100*6>=5)
MDIcon:
color:yellow
icon: "star"
disabled: not(root.rankings["AverageScore"]/100*6>=6)
RankingsLabel:
adaptive_width:True
text: '{}'.format(root.rankings["AverageScore"]/10)
RankingsDivider:
RankingsBoxLayout:
size_hint_x:.3
RankingsHeaderLabel:
text:"Popularity"
RankingsLabel:
text: '{}'.format(root.rankings["Popularity"])
RankingsDivider:
RankingsBoxLayout:
size_hint_x:.3
RankingsHeaderLabel:
text:"Favourites"
RankingsLabel:
text: '{}'.format(root.rankings["Favourites"])

View File

@@ -1,13 +0,0 @@
from kivy.properties import DictProperty
from kivymd.uix.boxlayout import MDBoxLayout
class RankingsBar(MDBoxLayout):
rankings = DictProperty(
{
"Popularity": 0,
"Favourites": 0,
"AverageScore": 0,
}
)

View File

@@ -1,50 +0,0 @@
#:import get_hex_from_color kivy.utils.get_hex_from_color
<ReviewContainer@MDBoxLayout>:
adaptive_height:True
md_bg_color:self.theme_cls.surfaceContainerLowColor
padding:"10dp"
orientation:"vertical"
<ReviewText@MDLabel>:
adaptive_height:True
max_lines:0
shorten:False
markup:True
font_style: "Body"
role: "small"
<ReviewHeader@MDBoxLayout>:
adaptive_height:True
spacing:"10dp"
padding:"10dp"
<ReviewerAvatar@FitImage>
radius:50
size_hint:None,None
height:"50dp"
width:"50dp"
<AnimeReview>
orientation:"vertical"
adaptive_height:True
ReviewHeader:
ReviewerAvatar:
source:root.review["avatar"]
ReviewText:
pos_hint: {'center_y': 0.5}
text:root.review["username"]
MDDivider:
ReviewText:
text:root.review["summary"]
MDDivider:
<AnimeReviews>:
container:container
adaptive_height:True
orientation:"vertical"
HeaderLabel:
halign:"left"
text:"reviews"
ReviewContainer:
id:container

View File

@@ -1,29 +0,0 @@
from kivy.properties import ObjectProperty, ListProperty
from kivy.clock import Clock
from kivymd.uix.boxlayout import MDBoxLayout
class AnimeReview(MDBoxLayout):
review = ObjectProperty({"username": "", "avatar": "", "summary": ""})
class AnimeReviews(MDBoxLayout):
"""anime reviews"""
reviews = ListProperty()
container = ObjectProperty()
def on_reviews(self, *args):
Clock.schedule_once(lambda _: self.update_reviews_card(*args))
def update_reviews_card(self, instance, reviews):
self.container.clear_widgets()
for review in reviews:
review_ = AnimeReview()
review_.review = {
"username": review["user"]["name"],
"avatar": review["user"]["avatar"]["medium"],
"summary": review["summary"],
}
self.container.add_widget(review_)

View File

@@ -1,102 +0,0 @@
#:import get_hex_from_color kivy.utils.get_hex_from_color
<FitBoxLayout@MDBoxLayout>:
size_hint_y:None
height:self.minimum_height
padding:"10dp"
spacing:"10dp"
orientation: 'vertical'
pos_hint: {'center_x': 0.5}
<SideBarLabel>:
adaptive_height:True
max_lines:0
shorten:False
markup:True
font_style: "Label"
role: "medium"
<HeaderLabel>:
adaptive_height:True
md_bg_color:self.theme_cls.secondaryContainerColor
MDLabel:
text:root.text
adaptive_height:True
halign:root.halign
max_lines:0
shorten:False
bold:True
font_style: "Label"
role: "large"
padding:"10dp"
<AnimeSideBar>:
size_hint_x: None
width: dp(300)
orientation: 'vertical'
line_color:self.theme_cls.secondaryColor
statistics_container:statistics_container
tags_container:tags_container
external_links_container:external_links_container
FitBoxLayout:
FitImage:
source:root.image
size_hint:None,None
height:dp(250)
width:dp(200)
pos_hint: {'center_x': 0.5}
MDButton:
pos_hint: {'center_x': 0.5}
on_press:
if root.screen:root.screen.stream_anime_with_custom_cmds_dialog(mpv=True)
MDButtonText:
text:"Watch with mpv"
FitBoxLayout:
HeaderLabel:
text:"Alternative Titles"
SideBarLabel:
text: "[color={}]Synonyms:[/color] {}".format(get_hex_from_color(self.theme_cls.primaryColor),root.alternative_titles["synonyms"])
SideBarLabel:
text: "[color={}]English:[/color] {}".format(get_hex_from_color(self.theme_cls.primaryColor),root.alternative_titles["english"])
SideBarLabel:
text: "[color={}]Japanese:[/color] {}".format(get_hex_from_color(self.theme_cls.primaryColor),root.alternative_titles["japanese"])
FitBoxLayout:
HeaderLabel:
text:"Information"
SideBarLabel:
text: "[color={}]Episodes:[/color] {}".format(get_hex_from_color(self.theme_cls.primaryColor),root.information["episodes"])
SideBarLabel:
text: "[color={}]Status:[/color] {}".format(get_hex_from_color(self.theme_cls.primaryColor),root.information["status"])
SideBarLabel:
text: "[color={}]Next Airing Episode:[/color] {}".format(get_hex_from_color(self.theme_cls.primaryColor),root.information["nextAiringEpisode"])
SideBarLabel:
text: "[color={}]Aired:[/color] {}".format(get_hex_from_color(self.theme_cls.primaryColor),root.information["aired"])
SideBarLabel:
text: "[color={}]Premiered:[/color] {}".format(get_hex_from_color(self.theme_cls.primaryColor),root.information["premiered"])
SideBarLabel:
text: "[color={}]Broadcast:[/color] {}".format(get_hex_from_color(self.theme_cls.primaryColor),root.information["broadcast"])
SideBarLabel:
text: "[color={}]Country Of Origin:[/color] {}".format(get_hex_from_color(self.theme_cls.primaryColor),root.information["countryOfOrigin"])
SideBarLabel:
text: "[color={}]Hashtag:[/color] {}".format(get_hex_from_color(self.theme_cls.primaryColor),root.information["hashtag"])
SideBarLabel:
text: "[color={}]Studios:[/color] {}".format(get_hex_from_color(self.theme_cls.primaryColor),root.information["studios"])
SideBarLabel:
text: "[color={}]Producers:[/color] {}".format(get_hex_from_color(self.theme_cls.primaryColor),root.information["producers"])
SideBarLabel:
text: "[color={}]Source:[/color] {}".format(get_hex_from_color(self.theme_cls.primaryColor),root.information["source"])
SideBarLabel:
text: "[color={}]Genres:[/color] {}".format(get_hex_from_color(self.theme_cls.primaryColor),root.information["genres"])
SideBarLabel:
text: "[color={}]Duration:[/color] {} minutes".format(get_hex_from_color(self.theme_cls.primaryColor),root.information["duration"])
FitBoxLayout:
id:statistics_container
HeaderLabel:
text:"Rankings"
FitBoxLayout:
id:tags_container
HeaderLabel:
text:"Tags"
FitBoxLayout:
id:external_links_container
HeaderLabel:
text:"External Links"
BoxLayout:

View File

@@ -1,98 +0,0 @@
from kivy.properties import ObjectProperty, StringProperty, DictProperty, ListProperty
from kivy.utils import get_hex_from_color
from kivy.factory import Factory
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.label import MDLabel
class HeaderLabel(MDBoxLayout):
text = StringProperty()
halign = StringProperty("center")
Factory.register("HeaderLabel", HeaderLabel)
class SideBarLabel(MDLabel):
pass
# TODO:Switch to using the kivy_markup_module
class AnimeSideBar(MDBoxLayout):
screen = ObjectProperty()
image = StringProperty()
alternative_titles = DictProperty(
{
"synonyms": "",
"english": "",
"japanese": "",
}
)
information = DictProperty(
{
"episodes": "",
"status": "",
"aired": "",
"nextAiringEpisode": "",
"premiered": "",
"broadcast": "",
"countryOfOrigin": "",
"hashtag": "",
"studios": "", # { "name": "Sunrise", "isAnimationStudio": true }
"source": "",
"genres": "",
"duration": "",
"producers": "",
}
)
statistics = ListProperty()
statistics_container = ObjectProperty()
external_links = ListProperty()
external_links_container = ObjectProperty()
tags = ListProperty()
tags_container = ObjectProperty()
def on_statistics(self, instance, value):
self.statistics_container.clear_widgets()
header = HeaderLabel()
header.text = "Rankings"
self.statistics_container.add_widget(header)
for stat in value:
# stat (rank,context)
label = SideBarLabel()
label.text = "[color={}]{}:[/color] {}".format(
get_hex_from_color(label.theme_cls.primaryColor),
stat[0].capitalize(),
f"{stat[1]}",
)
self.statistics_container.add_widget(label)
def on_tags(self, instance, value):
self.tags_container.clear_widgets()
header = HeaderLabel()
header.text = "Tags"
self.tags_container.add_widget(header)
for tag in value:
label = SideBarLabel()
label.text = "[color={}]{}:[/color] {}".format(
get_hex_from_color(label.theme_cls.primaryColor),
tag[0].capitalize(),
f"{tag[1]} %",
)
self.tags_container.add_widget(label)
def on_external_links(self, instance, value):
self.external_links_container.clear_widgets()
header = HeaderLabel()
header.text = "External Links"
self.external_links_container.add_widget(header)
for site in value:
# stat (rank,context)
label = SideBarLabel()
label.text = "[color={}]{}:[/color] {}".format(
get_hex_from_color(label.theme_cls.primaryColor),
site[0].capitalize(),
site[1],
)
self.external_links_container.add_widget(label)

View File

@@ -1,21 +0,0 @@
#:import get_color_from_hex kivy.utils.get_color_from_hex
#:import StringProperty kivy.properties.StringProperty
<CrashLogScreenView>
md_bg_color: self.theme_cls.backgroundColor
# main_container:main_container
MDBoxLayout:
NavRail:
screen:root
MDAnchorLayout:
anchor_y: 'top'
padding:"10dp"
MDBoxLayout:
orientation: 'vertical'
SearchBar:
MDScrollView:
MDLabel:
text:root.crash_text
adaptive_height:True
markup:True
padding:"10dp"

View File

@@ -1,21 +0,0 @@
from kivy.properties import StringProperty
from anixstream.View.base_screen import BaseScreenView
from anixstream.Utility.utils import read_crash_file
from anixstream.Utility.kivy_markup_helper import color_text, bolden
class CrashLogScreenView(BaseScreenView):
"""The crash log screen"""
crash_text = StringProperty()
def __init__(self, **kw):
super().__init__(**kw)
if crashes := read_crash_file():
self.crash_text = crashes
else:
self.crash_text = color_text(
f"No Crashes so far :) and if there are any in the future {bolden('please report! Okay?')}",
self.theme_cls.primaryColor,
)

View File

@@ -1,28 +0,0 @@
#:import color_text anixstream.Utility.kivy_markup_helper.color_text
<TaskText@MDLabel>:
adaptive_height:True
max_lines:0
shorten:False
markup:True
font_style: "Label"
role: "large"
bold:True
<TaskCard>:
adaptive_height:True
radius:8
padding:"20dp"
md_bg_color:self.theme_cls.surfaceContainerHighColor
TaskText:
size_hint_x:.8
text:color_text(root.anime_task_name,root.theme_cls.primaryColor)
TaskText:
size_hint_x:.2
# color:self.theme_cls.surfaceDimColor
theme_text_color:"Secondary"
text:color_text(root.episodes_to_download,root.theme_cls.secondaryColor)
MDIcon:
icon:"download"

View File

@@ -1,12 +0,0 @@
from kivy.properties import StringProperty
from kivymd.uix.boxlayout import MDBoxLayout
# TODO: add a progress bar to show the individual progress of each task
class TaskCard(MDBoxLayout):
anime_task_name = StringProperty()
episodes_to_download = StringProperty()
def __init__(self, anime_title:str, episodes:str, *args, **kwargs):
super().__init__(*args, **kwargs)
self.anime_task_name = f"{anime_title}"
self.episodes_to_download = f"Episodes: {episodes}"

View File

@@ -1,58 +0,0 @@
#:import get_color_from_hex kivy.utils.get_color_from_hex
#:import StringProperty kivy.properties.StringProperty
<DownloadsScreenLabel@MDLabel>:
adaptive_height:True
max_lines:0
shorten:False
markup:True
font_style: "Label"
role: "large"
bold:True
<DownloadsScreenView>
md_bg_color: self.theme_cls.backgroundColor
main_container:main_container
download_progress_label:download_progress_label
progress_bar:progress_bar
MDBoxLayout:
NavRail:
screen:root
MDAnchorLayout:
anchor_y: 'top'
MDBoxLayout:
orientation: 'vertical'
SearchBar:
MDScrollView:
size_hint:.95,1
MDBoxLayout:
id:main_container
orientation:"vertical"
padding:"40dp"
pos_hint:{"center_x":.5}
spacing:"10dp"
adaptive_height:True
HeaderLabel:
text:"Download Tasks"
halign:"left"
MDIcon:
padding:"10dp"
pos_hint:{"center_y":.5}
icon:"clock"
MDBoxLayout:
size_hint_y:None
height:"40dp"
spacing:"10dp"
padding:"10dp"
md_bg_color:self.theme_cls.secondaryContainerColor
DownloadsScreenLabel:
id:download_progress_label
size_hint_x: .6
text:"Try Downloading sth :)"
pos_hint: {'center_y': .5}
MDLinearProgressIndicator:
id: progress_bar
size_hint_x: .4
size_hint_y:None
height:"10dp"
type: "determinate"
pos_hint: {'center_y': .5}

View File

@@ -1,32 +0,0 @@
from kivy.clock import Clock
from kivy.properties import ObjectProperty
from kivy.logger import Logger
from kivy.utils import format_bytes_to_human
from anixstream.View.base_screen import BaseScreenView
from .components.task_card import TaskCard
class DownloadsScreenView(BaseScreenView):
main_container = ObjectProperty()
progress_bar = ObjectProperty()
download_progress_label = ObjectProperty()
def on_new_download_task(self, anime_title: str, episodes: str | None):
if not episodes:
episodes = "All"
Clock.schedule_once(
lambda _: self.main_container.add_widget(TaskCard(anime_title, episodes))
)
def on_episode_download_progress(
self, current_bytes_downloaded, total_bytes, episode_info
):
percentage_completion = round((current_bytes_downloaded / total_bytes) * 100)
progress_text = f"Downloading: {episode_info['anime_title']} - {episode_info['episode']} ({format_bytes_to_human(current_bytes_downloaded)}/{format_bytes_to_human(total_bytes)})"
if (percentage_completion % 5) == 0:
self.progress_bar.value = max(min(percentage_completion, 100), 0)
self.download_progress_label.text = progress_text
def update_layout(self, widget):
self.user_anime_list_container.add_widget(widget)

View File

@@ -1,60 +0,0 @@
#:import get_color_from_hex kivy.utils.get_color_from_hex
#:import StringProperty kivy.properties.StringProperty
<HelpCard@MDBoxLayout>
spacing:"10dp"
orientation:"vertical"
adaptive_height:True
md_bg_color:self.theme_cls.surfaceContainerLowColor
theme_text_color:"Secondary"
<HelpHeaderLabel@HeaderLabel>
halign:"left"
<HelpDescription@MDLabel>:
adaptive_height:True
max_lines:0
shorten:False
markup:True
font_style: "Body"
padding:"10dp"
role: "large"
<HelpScreenView>
md_bg_color: self.theme_cls.backgroundColor
# main_container:main_container
MDBoxLayout:
NavRail:
screen:root
MDAnchorLayout:
anchor_y: 'top'
padding:"10dp"
MDBoxLayout:
orientation: 'vertical'
SearchBar:
MDScrollView:
size_hint_x:.95
MDBoxLayout:
adaptive_height:True
padding:"10dp"
orientation:"vertical"
HelpCard:
HelpHeaderLabel:
text:"Animdl Commands"
HelpDescription:
text:root.animdl_help
HelpCard:
HelpHeaderLabel:
text:"Installing Animdl"
HelpDescription:
text:root.installing_animdl_help
HelpCard:
HelpHeaderLabel:
text:"Available Themes"
HelpDescription:
text:root.available_themes
HelpCard:
HelpHeaderLabel:
text:"About"
HelpDescription:
text:"This app was made to be a gui wrapper for any and all anime cli tools. Inoder to solve the age old problem of getting the same experience from the cli as you would in a website"

View File

@@ -1,72 +0,0 @@
from kivy.properties import ObjectProperty, StringProperty
from anixstream.View.base_screen import BaseScreenView
from anixstream.Utility.kivy_markup_helper import bolden, color_text, underline
from anixstream.Utility.data import themes_available
class HelpScreenView(BaseScreenView):
main_container = ObjectProperty()
animdl_help = StringProperty()
installing_animdl_help = StringProperty()
available_themes = StringProperty()
def __init__(self, **kw):
super(HelpScreenView, self).__init__(**kw)
self.animdl_help = f"""
{underline(color_text(bolden('Streaming Commands'),self.theme_cls.surfaceBrightColor))}
{color_text(bolden('-r:'),self.theme_cls.primaryFixedDimColor)} specifies the range of episodes
example: {color_text(('animdl stream <Anime title> -r 1-4'),self.theme_cls.tertiaryColor)}
explanation:in this case gets 4 episodes 1 to 4
{color_text(('-s:'),self.theme_cls.primaryFixedDimColor)} special selector for the most recent episodes or basically selects from the end
example: {color_text(('animdl stream <Anime title> -s 4'),self.theme_cls.tertiaryColor)}
explanation: in this case gets the latest 4 episodes
{color_text(('-q:'),self.theme_cls.primaryFixedDimColor)} sets the quality of the stream
example: {color_text(('animdl stream <Anime title> -q best'),self.theme_cls.tertiaryColor)}
explanation: The quality of the anime stream should be the best possible others include 1080,720... plus worst
{underline(color_text(bolden('Downloading Commands'),self.theme_cls.surfaceBrightColor))}
{color_text(bolden('-r:'),self.theme_cls.primaryFixedDimColor)} specifies the range of episodes
example: {color_text(('animdl download <Anime title> -r 1-4'),self.theme_cls.tertiaryColor)}
explanation:in this case gets 4 episodes 1 to 4
{color_text(('-s:'),self.theme_cls.primaryFixedDimColor)} special selector for the most recent episodes or basically selects from the end
example: {color_text(('animdl download <Anime title> -s 4'),self.theme_cls.tertiaryColor)}
explanation: in this case gets the latest 4 episodes
{color_text(('-q:'),self.theme_cls.primaryFixedDimColor)} sets the quality of the download
example: {color_text(('animdl download <Anime title> -q best'),self.theme_cls.tertiaryColor)}
explanation: The quality of the anime download should be the best possible others include 1080,720... plus worst
"""
self.installing_animdl_help = f"""
This works on windows only and should be done in powershell
1. First install pyenv with the following command:
{color_text(('Invoke-WebRequest -UseBasicParsing -Uri "https://raw.githubusercontent.com/pyenv-win/pyenv-win/master/pyenv-win/install-pyenv-win.ps1" -OutFile "./install-pyenv-win.ps1"; &"./install-pyenv-win.ps1"'),self.theme_cls.tertiaryColor)}
2. run the following command to check if successsful:
{color_text(('pyenv --version '),self.theme_cls.tertiaryColor)}
3. run the following command to install python 3.10
{color_text(('pyenv install 3.10'),self.theme_cls.tertiaryColor)}
4. To confirm successful install of python 3.10 run the following command and check if 3.10 is listed:
{color_text(('pyenv -l'),self.theme_cls.tertiaryColor)}
5. Next run:
{color_text(('pyenv local 3.10'),self.theme_cls.tertiaryColor)} (if in anixstream directory to set python 3.10 as local interpreter)
or run:
{color_text(('pyenv global 3.10'),self.theme_cls.tertiaryColor)} (if in another directory to set python version 3.10 as global interpreter)
6. Check if success by running and checking if output is 3.10:
{color_text(('python --version'),self.theme_cls.tertiaryColor)}
7. Run:
{color_text(('python -m pip install animdl'),self.theme_cls.tertiaryColor)}
8. Check if success by running:
{color_text(('python -m animdl'),self.theme_cls.tertiaryColor)}
{color_text(('Note:'),self.theme_cls.secondaryColor)}
All this instructions should be done from the folder you choose to install
aniXstream but incase you have never installed python should work any where
{bolden('-----------------------------')}
Now enjoy :)
{bolden('-----------------------------')}
"""
self.available_themes = "\n".join(themes_available)
def model_is_changed(self) -> None:
"""
Called whenever any change has occurred in the data model.
The view in this method tracks these changes and updates the UI
according to these changes.
"""

View File

@@ -1,23 +0,0 @@
<HomeScreenView>
md_bg_color: self.theme_cls.backgroundColor
main_container:main_container
MDBoxLayout:
NavRail:
screen:root
MDAnchorLayout:
anchor_y: 'top'
padding:"10dp"
MDBoxLayout:
orientation: 'vertical'
id:p
SearchBar:
MDScrollView:
size_hint:1,1
MDBoxLayout:
id:main_container
padding:"50dp","5dp","50dp","150dp"
spacing:"10dp"
orientation: 'vertical'
size_hint_y:None
height:max(self.minimum_height,p.height,1800)

View File

@@ -1,7 +0,0 @@
from kivy.properties import ObjectProperty
from anixstream.View.base_screen import BaseScreenView
class HomeScreenView(BaseScreenView):
main_container = ObjectProperty()

View File

@@ -1,28 +0,0 @@
<MyListScreenView>
md_bg_color: self.theme_cls.backgroundColor
user_anime_list_container:user_anime_list_container
MDBoxLayout:
size_hint:1,1
NavRail:
screen:root
MDAnchorLayout:
anchor_y: 'top'
padding:"10dp"
size_hint:1,1
MDBoxLayout:
spacing:"40dp"
orientation: 'vertical'
size_hint:.95,1
SearchBar:
MDScrollView:
pos_hint:{"center_x":.5}
size_hint:1,1
MDGridLayout:
spacing: '40dp'
padding: "100dp","50dp","10dp","200dp"
id:user_anime_list_container
cols:4 if root.width<=1100 else 5
size_hint_y:None
height:self.minimum_height

View File

@@ -1,21 +0,0 @@
from kivy.properties import ObjectProperty, StringProperty, DictProperty
from kivy.clock import Clock
from anixstream.View.base_screen import BaseScreenView
class MyListScreenView(BaseScreenView):
user_anime_list_container = ObjectProperty()
def model_is_changed(self) -> None:
"""
Called whenever any change has occurred in the data model.
The view in this method tracks these changes and updates the UI
according to these changes.
"""
def on_enter(self):
Clock.schedule_once(lambda _: self.controller.requested_update_my_list_screen())
def update_layout(self, widget):
self.user_anime_list_container.add_widget(widget)

View File

@@ -1,3 +0,0 @@
from .filters import Filters
from .pagination import SearchResultsPagination
from .trending_sidebar import TrendingAnimeSideBar

View File

@@ -1,27 +0,0 @@
<FilterDropDown>:
MDDropDownItemText:
text: root.text
<FilterLabel@MDLabel>:
adaptive_width:True
<Filters>:
adaptive_height:True
spacing:"10dp"
size_hint_x:.95
pos_hint:{"center_x":.5}
padding:"10dp"
md_bg_color:self.theme_cls.surfaceContainerLowColor
FilterLabel:
text:"Sort By"
FilterDropDown:
id:sort_filter
text:root.filters["sort"]
on_release: root.open_filter_menu(self,"sort")
FilterLabel:
text:"Status"
FilterDropDown:
id:status_filter
text:root.filters["status"]
on_release: root.open_filter_menu(self,"status")

View File

@@ -1,83 +0,0 @@
from kivy.properties import StringProperty, DictProperty
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.dropdownitem import MDDropDownItem
from kivymd.uix.menu import MDDropdownMenu
class FilterDropDown(MDDropDownItem):
text: str = StringProperty()
class Filters(MDBoxLayout):
filters: dict = DictProperty({"sort": "SEARCH_MATCH", "status": "FINISHED"})
def open_filter_menu(self, menu_item, filter_name):
items = []
match filter_name:
case "sort":
items = [
"ID",
"ID_DESC",
"TITLE_ROMANJI",
"TITLE_ROMANJI_DESC",
"TITLE_ENGLISH",
"TITLE_ENGLISH_DESC",
"TITLE_NATIVE",
"TITLE_NATIVE_DESC",
"TYPE",
"TYPE_DESC",
"FORMAT",
"FORMAT_DESC",
"START_DATE",
"START_DATE_DESC",
"END_DATE",
"END_DATE_DESC",
"SCORE",
"SCORE_DESC",
"TRENDING",
"TRENDING_DESC",
"EPISODES",
"EPISODES_DESC",
"DURATION",
"DURATION_DESC",
"STATUS",
"STATUS_DESC",
"UPDATED_AT",
"UPDATED_AT_DESC",
"SEARCH_MATCH",
"POPULARITY",
"POPULARITY_DESC",
"FAVOURITES",
"FAVOURITES_DESC",
]
case "status":
items = [
"FINISHED",
"RELEASING",
"NOT_YET_RELEASED",
"CANCELLED",
"HIATUS",
]
case _:
items = []
if items:
menu_items = [
{
"text": f"{item}",
"on_release": lambda filter_value=f"{item}": self.filter_menu_callback(
filter_name, filter_value
),
}
for item in items
]
MDDropdownMenu(caller=menu_item, items=menu_items).open()
def filter_menu_callback(self, filter_name, filter_value):
match filter_name:
case "sort":
self.ids.sort_filter.text = filter_value
self.filters["sort"] = filter_value
case "status":
self.ids.status_filter.text = filter_value
self.filters["status"] = filter_value

View File

@@ -1,21 +0,0 @@
<PaginationLabel@MDLabel>:
max_lines:0
shorten:False
adaptive_height:True
font_style: "Label"
pos_hint:{"center_y":.5}
halign:"center"
role: "medium"
<SearchResultsPagination>:
md_bg_color:self.theme_cls.surfaceContainerLowColor
radius:8
adaptive_height:True
MDIconButton:
icon:"arrow-left"
on_release:root.search_view.previous_page()
PaginationLabel:
text:"Page {} of {}".format(root.current_page,root.total_pages)
MDIconButton:
icon:"arrow-right"
on_release:root.search_view.next_page()

View File

@@ -1,9 +0,0 @@
from kivy.properties import ObjectProperty, NumericProperty
from kivymd.uix.boxlayout import MDBoxLayout
class SearchResultsPagination(MDBoxLayout):
current_page = NumericProperty()
total_pages = NumericProperty()
search_view = ObjectProperty()

View File

@@ -1,6 +0,0 @@
<TrendingAnimeSideBar>:
orientation: 'vertical'
adaptive_height:True
md_bg_color:self.theme_cls.surfaceContainerLowColor
pos_hint: {'center_x': 0.5}
padding:"25dp","25dp","25dp","200dp"

View File

@@ -1,5 +0,0 @@
from kivymd.uix.boxlayout import MDBoxLayout
class TrendingAnimeSideBar(MDBoxLayout):
pass

View File

@@ -1,58 +0,0 @@
<SearchScreenView>
md_bg_color: self.theme_cls.backgroundColor
search_results_container:search_results_container
trending_anime_sidebar:trending_anime_sidebar
search_results_pagination:search_results_pagination
filters:filters
MDBoxLayout:
size_hint:1,1
NavRail:
screen:root
MDAnchorLayout:
anchor_y: 'top'
padding:"10dp"
size_hint:1,1
MDBoxLayout:
orientation: 'vertical'
size_hint:1,1
SearchBar:
MDBoxLayout:
spacing:"20dp"
padding:"75dp","10dp","100dp","0dp"
MDBoxLayout:
orientation: 'vertical'
size_hint:1,1
Filters:
id:filters
MDBoxLayout:
spacing:"20dp"
MDScrollView:
size_hint:1,1
MDBoxLayout:
orientation: 'vertical'
size_hint_y:None
height:max(self.parent.parent.height,self.minimum_height)
MDGridLayout:
pos_hint: {'center_x': 0.5}
id:search_results_container
spacing: '40dp'
padding: "25dp","50dp","75dp","200dp"
cols:3 if root.width <= 1100 else 5
size_hint_y:None
height:max(self.parent.parent.height,self.minimum_height)
SearchResultsPagination:
id:search_results_pagination
search_view:root
MDBoxLayout:
orientation:"vertical"
size_hint_y:1
size_hint_x:None
width: dp(250)
HeaderLabel:
text:"Trending"
MDScrollView:
TrendingAnimeSideBar:
id:trending_anime_sidebar
height:max(self.parent.parent.height,self.minimum_height)

View File

@@ -1,67 +0,0 @@
from kivy.properties import ObjectProperty, StringProperty
from kivy.clock import Clock
from anixstream.View.base_screen import BaseScreenView
from .components import TrendingAnimeSideBar, Filters, SearchResultsPagination
class SearchScreenView(BaseScreenView):
trending_anime_sidebar: TrendingAnimeSideBar = ObjectProperty()
search_results_pagination: SearchResultsPagination = ObjectProperty()
filters: Filters = ObjectProperty()
search_results_container = ObjectProperty()
search_term: str = StringProperty()
is_searching = False
has_next_page = False
current_page = 0
total_pages = 0
def handle_search_for_anime(self, search_widget=None, page=None):
if search_widget:
search_term = search_widget.text
elif page:
search_term = self.search_term
else:
return
if search_term and not (self.is_searching):
self.search_term = search_term
self.search_results_container.clear_widgets()
if filters := self.filters.filters:
Clock.schedule_once(
lambda _: self.controller.requested_search_for_anime(
search_term, **filters, page=page
)
)
else:
Clock.schedule_once(
lambda _: self.controller.requested_search_for_anime(
search_term, page=page
)
)
def update_layout(self, widget):
self.search_results_container.add_widget(widget)
def update_pagination(self, pagination_info):
self.search_results_pagination.current_page = self.current_page = (
pagination_info["currentPage"]
)
self.search_results_pagination.total_pages = self.total_pages = max(
int(pagination_info["total"] / 30), 1
)
self.has_next_page = pagination_info["hasNextPage"]
def next_page(self):
if self.has_next_page:
page = self.current_page + 1
self.handle_search_for_anime(page=page)
def previous_page(self):
if self.current_page > 1:
page = self.current_page - 1
self.handle_search_for_anime(page=page)
def update_trending_sidebar(self, trending_anime):
self.trending_anime_sidebar.add_widget(trending_anime)

View File

@@ -1,8 +0,0 @@
# screens
from .HomeScreen.home_screen import HomeScreenView
from .SearchScreen.search_screen import SearchScreenView
from .MylistScreen.my_list_screen import MyListScreenView
from .AnimeScreen.anime_screen import AnimeScreenView
from .CrashLogScreen.crashlog_screen import CrashLogScreenView
from .DownloadsScreen.download_screen import DownloadsScreenView
from .HelpScreen.help_screen import HelpScreenView

View File

@@ -1,72 +0,0 @@
from kivy.properties import ObjectProperty, StringProperty
from kivymd.app import MDApp
from kivymd.uix.screen import MDScreen
from kivymd.uix.navigationrail import MDNavigationRail, MDNavigationRailItem
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.button import MDIconButton
from kivymd.uix.tooltip import MDTooltip
from anixstream.Utility.observer import Observer
class NavRail(MDNavigationRail):
screen = ObjectProperty()
class SearchBar(MDBoxLayout):
screen = ObjectProperty()
class Tooltip(MDTooltip):
pass
class TooltipMDIconButton(Tooltip, MDIconButton):
tooltip_text = StringProperty()
class CommonNavigationRailItem(MDNavigationRailItem):
icon = StringProperty()
text = StringProperty()
class BaseScreenView(MDScreen, Observer):
"""
A base class that implements a visual representation of the model data.
The view class must be inherited from this class.
"""
controller = ObjectProperty()
"""
Controller object - :class:`~Controller.controller_screen.ClassScreenControler`.
:attr:`controller` is an :class:`~kivy.properties.ObjectProperty`
and defaults to `None`.
"""
model = ObjectProperty()
"""
Model object - :class:`~Model.model_screen.ClassScreenModel`.
:attr:`model` is an :class:`~kivy.properties.ObjectProperty`
and defaults to `None`.
"""
manager_screens = ObjectProperty()
"""
Screen manager object - :class:`~kivymd.uix.screenmanager.MDScreenManager`.
:attr:`manager_screens` is an :class:`~kivy.properties.ObjectProperty`
and defaults to `None`.
"""
def __init__(self, **kw):
super().__init__(**kw)
# Often you need to get access to the application object from the view
# class. You can do this using this attribute.
from anixstream.__main__ import AniXStreamApp
self.app: AniXStreamApp = MDApp.get_running_app() # type: ignore
# Adding a view class as observer.
self.model.add_observer(self)

View File

@@ -1 +0,0 @@
from .media_card import MediaCard,MediaCardsContainer

View File

@@ -1,4 +0,0 @@
from kivy.uix.modalview import ModalView
class AnimdlDialogPopup(ModalView):
pass

View File

@@ -1,3 +0,0 @@
<MDLabel>:
allow_copy:True
allow_selection:True

View File

@@ -1 +0,0 @@
from .media_card import MediaCard,MediaCardsContainer

View File

@@ -1,2 +0,0 @@
from .media_player import MediaPopupVideoPlayer
from .media_popup import MediaPopup

View File

@@ -1,19 +0,0 @@
<MediaCardsContainer>
size_hint:1,None
height:max(self.minimum_height,dp(350),container.minimum_height)
container:container
orientation: 'vertical'
padding:"10dp"
spacing:"5dp"
MDLabel:
text:root.list_name
MDScrollView:
size_hint:1,None
height:container.minimum_height
MDBoxLayout:
id:container
spacing:"10dp"
padding:"0dp","10dp","100dp","10dp"
size_hint:None,None
height:self.minimum_height
width:self.minimum_width

View File

@@ -1,10 +0,0 @@
from kivy.uix.videoplayer import VideoPlayer
# TODO: make fullscreen exp better
class MediaPopupVideoPlayer(VideoPlayer):
def __init__(self, **kwargs):
super().__init__(**kwargs)
def on_fullscreen(self, instance, value):
super().on_fullscreen(instance, value)
# self.state = "pause"

View File

@@ -1,170 +0,0 @@
#:import get_hex_from_color kivy.utils.get_hex_from_color
#:set yellow [.9,.9,0,.9]
<SingleLineLabel@MDLabel>:
shorten:True
shorten_from:"right"
adaptive_height:True
<PopupBoxLayout@MDBoxLayout>
adaptive_height:True
<Video>:
fit_mode:"fill"
# TODO: subdivide each main component to its own file
<MediaPopup>
size_hint: None, None
height: dp(530)
width: dp(400)
radius:[5,5,5,5]
md_bg_color:self.theme_cls.backgroundColor
anchor_y: 'top'
player:player
MDBoxLayout:
orientation: 'vertical'
MDRelativeLayout:
size_hint_y: None
height: dp(280)
line_color:root.caller.has_trailer_color
line_width:1
MediaPopupVideoPlayer:
id:player
source: root.caller.trailer_url
thumbnail:app.default_anime_image
state:"play" if root.caller.trailer_url else "stop"
# fit_mode:"fill"
size_hint_y: None
height: dp(280)
PopupBoxLayout:
padding: "10dp","5dp"
spacing:"5dp"
pos_hint: {'left': 1,'top': 1}
MDIcon:
icon: "star"
color:yellow
disabled: not(root.caller.stars[0])
MDIcon:
color:yellow
disabled: not(root.caller.stars[1])
icon: "star"
MDIcon:
color:yellow
disabled: not(root.caller.stars[2])
icon: "star"
MDIcon:
color:yellow
disabled: not(root.caller.stars[3])
icon: "star"
MDIcon:
color:yellow
icon: "star"
disabled: not(root.caller.stars[4])
MDIcon:
color: yellow
icon: "star"
disabled: not(root.caller.stars[5])
MDLabel:
text: f"{root.caller.episodes} Episodes"
halign:"right"
font_style:"Label"
role:"medium"
bold:True
pos_hint: {'center_y': 0.5}
adaptive_height:True
color: 0,0,0,.7
PopupBoxLayout:
padding:"5dp"
pos_hint: {'bottom': 1}
SingleLineLabel:
text:root.caller.media_status
opacity:.8
halign:"left"
font_style:"Label"
role:"medium"
bold:True
pos_hint: {'center_y': .5}
SingleLineLabel:
text:root.caller.first_aired_on
opacity:.8
halign:"right"
font_style:"Label"
role:"medium"
bold:True
pos_hint: {'center_y': .5}
# header
MDBoxLayout:
orientation: 'vertical'
padding:"10dp"
spacing:"10dp"
PopupBoxLayout:
PopupBoxLayout:
pos_hint: {'center_y': 0.5}
TooltipMDIconButton:
tooltip_text:root.caller.title
icon: "play-circle"
on_press:
root.dismiss()
app.show_anime_screen(root.caller.anime_id,root.caller.screen.name)
TooltipMDIconButton:
tooltip_text:"Add to your anime list"
icon: "plus-circle" if not(root.caller.is_in_my_list) else "check-circle"
on_release:
root.caller.is_in_my_list = not(root.caller.is_in_my_list)
self.icon = "plus-circle" if not(root.caller.is_in_my_list) else "check-circle"
TooltipMDIconButton:
disabled:True
tooltip_text:"Coming soon"
icon: "bell-circle" if not(root.caller.is_in_my_notify) else "bell-check"
PopupBoxLayout:
pos_hint: {'center_y': 0.5}
orientation: 'vertical'
SingleLineLabel:
font_style:"Label"
role:"small"
text: f"[color={get_hex_from_color(self.theme_cls.primaryColor)}]"+"Genres: "+"[/color]"+root.caller.genres
markup:True
PopupBoxLayout:
SingleLineLabel:
font_style:"Label"
role:"small"
markup:True
text: f"[color={get_hex_from_color(self.theme_cls.primaryColor)}]"+"Popularity: "+"[/color]"+root.caller.popularity
SingleLineLabel:
font_style:"Label"
markup:True
role:"small"
text: f"[color={get_hex_from_color(self.theme_cls.primaryColor)}]"+"Favourites: "+"[/color]"+root.caller.favourites
MDScrollView:
size_hint:1,1
do_scroll_y:True
MDLabel:
font_style:"Body"
role:"small"
text:root.caller.description
adaptive_height:True
# footer
PopupBoxLayout:
orientation:"vertical"
SingleLineLabel:
font_style:"Label"
markup:True
role:"small"
text: f"[color={get_hex_from_color(self.theme_cls.primaryColor)}]"+"Next Airing Episode: "+"[/color]"+root.caller.next_airing_episode
SingleLineLabel:
font_style:"Label"
role:"small"
markup:True
text: f"[color={get_hex_from_color(self.theme_cls.primaryColor)}]"+"Studios: " + "[/color]"+root.caller.studios
SingleLineLabel:
font_style:"Label"
markup:True
role:"small"
text: f"[color={get_hex_from_color(self.theme_cls.primaryColor)}]"+"Producers: " + "[/color]"+root.caller.producers
SingleLineLabel:
font_style:"Label"
markup:True
role:"small"
text: f"[color={get_hex_from_color(self.theme_cls.primaryColor)}]"+"Tags: "+"[/color]"+root.caller.tags

View File

@@ -1,87 +0,0 @@
from kivy.properties import ObjectProperty
from kivy.clock import Clock
from kivy.animation import Animation
from kivy.uix.modalview import ModalView
from kivymd.theming import ThemableBehavior
from kivymd.uix.behaviors import (
BackgroundColorBehavior,
StencilBehavior,
CommonElevationBehavior,
HoverBehavior,
)
class MediaPopup(
ThemableBehavior,
HoverBehavior,
StencilBehavior,
CommonElevationBehavior,
BackgroundColorBehavior,
ModalView,
):
caller = ObjectProperty()
player = ObjectProperty()
def __init__(self, caller, *args, **kwarg):
self.caller = caller
super(MediaPopup, self).__init__(*args, **kwarg)
self.player.bind(fullscreen=self.handle_clean_fullscreen_transition)
def open(self, *_args, **kwargs):
"""Display the modal in the Window.
When the view is opened, it will be faded in with an animation. If you
don't want the animation, use::
view.open(animation=False)
"""
from kivy.core.window import Window
if self._is_open:
return
self._window = Window
self._is_open = True
self.dispatch("on_pre_open")
Window.add_widget(self)
Window.bind(on_resize=self._align_center, on_keyboard=self._handle_keyboard)
self.center = self.caller.to_window(*self.caller.center)
self.fbind("center", self._align_center)
self.fbind("size", self._align_center)
if kwargs.get("animation", True):
ani = Animation(_anim_alpha=1.0, d=self._anim_duration)
ani.bind(on_complete=lambda *_args: self.dispatch("on_open"))
ani.start(self)
else:
self._anim_alpha = 1.0
self.dispatch("on_open")
def _align_center(self, *_args):
if self._is_open:
self.center = self.caller.to_window(*self.caller.center)
def on_leave(self, *args):
def _leave(dt):
self.player.state = "stop"
if self.player._video:
self.player._video.unload()
if not self.hovering:
self.dismiss()
Clock.schedule_once(_leave, 2)
def handle_clean_fullscreen_transition(self,instance,fullscreen):
if not fullscreen:
if not self._is_open:
instance.state = "stop"
if vid:=instance._video:
vid.unload()
else:
instance.state = "stop"
if vid:=instance._video:
vid.unload()
self.dismiss()

View File

@@ -1,3 +0,0 @@
<Tooltip>
MDTooltipPlain:
text:root.tooltip_text

View File

@@ -1,24 +0,0 @@
<MediaCard>
adaptive_height:True
spacing:"5dp"
image:"https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx163270-oxwgbe43Cpog.jpg"
size_hint_x: None
width:dp(100)
on_release:
self.open()
FitImage:
source:root.cover_image_url
fit_mode:"fill"
size_hint: None, None
width: dp(100)
height: dp(150)
MDDivider:
color:root.has_trailer_color
SingleLineLabel:
font_style:"Label"
role:"medium"
text:root.title
max_lines:2
halign:"center"
color:self.theme_cls.secondaryColor

View File

@@ -1,96 +0,0 @@
from kivy.properties import (
ObjectProperty,
StringProperty,
BooleanProperty,
ListProperty,
NumericProperty,
)
from kivy.clock import Clock
from kivy.uix.behaviors import ButtonBehavior
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.behaviors import HoverBehavior
from .components import MediaPopup
class MediaCard(ButtonBehavior, HoverBehavior, MDBoxLayout):
screen = ObjectProperty()
anime_id = NumericProperty()
title = StringProperty()
is_play = ObjectProperty()
trailer_url = StringProperty()
episodes = StringProperty()
favourites = StringProperty()
popularity = StringProperty()
media_status = StringProperty("Releasing")
is_in_my_list = BooleanProperty(False)
is_in_my_notify = BooleanProperty(False)
genres = StringProperty()
first_aired_on = StringProperty()
description = StringProperty()
producers = StringProperty()
studios = StringProperty()
next_airing_episode = StringProperty()
tags = StringProperty()
stars = ListProperty([0, 0, 0, 0, 0, 0])
cover_image_url = StringProperty()
preview_image = StringProperty()
has_trailer_color = ListProperty([.5, .5, .5, .5])
def __init__(self, trailer_url=None, **kwargs):
super().__init__(**kwargs)
self.orientation = "vertical"
if trailer_url:
self.trailer_url = trailer_url
self.adaptive_size = True
def on_enter(self):
def _open_popup(dt):
if self.hovering:
window = self.get_parent_window()
if window:
for widget in window.children: # type: ignore
if isinstance(widget, MediaPopup):
return
self.open()
Clock.schedule_once(_open_popup, 5)
def on_popup_open(self, popup: MediaPopup):
popup.center = self.center
def on_dismiss(self, popup: MediaPopup):
popup.player.state = "stop"
if popup.player._video:
popup.player._video.unload()
def set_preview_image(self, image):
self.preview_image = image
def set_trailer_url(self, trailer_url):
self.trailer_url = trailer_url
self.has_trailer_color = self.theme_cls.primaryColor
def open(self, *_):
popup = MediaPopup(self)
popup.title = self.title
popup.bind(on_dismiss=self.on_dismiss, on_open=self.on_popup_open)
popup.open(self)
# ---------------respond to user actions and call appropriate model-------------------------
def on_is_in_my_list(self, instance, in_user_anime_list):
if self.screen:
if in_user_anime_list:
self.screen.app.add_anime_to_user_anime_list(self.anime_id)
else:
self.screen.app.remove_anime_from_user_anime_list(self.anime_id)
def on_trailer_url(self, *args):
pass
class MediaCardsContainer(MDBoxLayout):
container = ObjectProperty()
list_name = StringProperty()

View File

@@ -1,45 +0,0 @@
<CommonNavigationRailItem>
MDNavigationRailItemIcon:
icon:root.icon
MDNavigationRailItemLabel:
text: root.text
<NavRail>:
anchor:"top"
type: "labeled"
md_bg_color: self.theme_cls.secondaryContainerColor
MDNavigationRailFabButton:
icon: "home"
on_press:
root.screen.manager_screens.current = "home screen"
CommonNavigationRailItem:
icon: "magnify"
text: "Search"
on_press:
root.screen.manager_screens.current = "search screen"
CommonNavigationRailItem:
icon: "bookmark"
text: "MyList"
on_press:
root.screen.manager_screens.current = "my list screen"
CommonNavigationRailItem:
icon: "download-circle"
text: "Downloads"
on_press:
root.screen.manager_screens.current = "downloads screen"
CommonNavigationRailItem:
icon: "cog"
text: "settings"
on_press:app.open_settings()
CommonNavigationRailItem:
icon: "help-circle"
text: "Help"
on_press:
root.screen.manager_screens.current = "help screen"
CommonNavigationRailItem:
icon: "bug"
text: "debug"
on_press:
root.screen.manager_screens.current = "crashlog screen"

View File

@@ -1,3 +0,0 @@
<Tooltip>
MDTooltipPlain:
text:root.tooltip_text

View File

@@ -1,18 +0,0 @@
<SearchBar>:
pos_hint: {'center_x': 0.5,'top': 1}
padding: "10dp"
adaptive_height:True
size_hint_x:.75
spacing: '20dp'
MDTextField:
size_hint_x:1
required:True
on_text_validate:
app.search_for_anime(args[0])
MDTextFieldLeadingIcon:
icon: "magnify"
MDTextFieldHintText:
text: "Search for anime"
MDIconButton:
pos_hint: {'center_y': 0.5}
icon: "account-circle"

View File

@@ -1,50 +0,0 @@
from anixstream.Controller import (
SearchScreenController,
HomeScreenController,
MyListScreenController,
AnimeScreenController,
DownloadsScreenController,
HelpScreenController,
CrashLogScreenController,
)
from anixstream.Model import (
HomeScreenModel,
SearchScreenModel,
MyListScreenModel,
AnimeScreenModel,
DownloadsScreenModel,
HelpScreenModel,
CrashLogScreenModel,
)
screens = {
"home screen": {
"model": HomeScreenModel,
"controller": HomeScreenController,
},
"search screen": {
"model": SearchScreenModel,
"controller": SearchScreenController,
},
"my list screen": {
"model": MyListScreenModel,
"controller": MyListScreenController,
},
"anime screen": {
"model": AnimeScreenModel,
"controller": AnimeScreenController,
},
"crashlog screen": {
"model": CrashLogScreenModel,
"controller": CrashLogScreenController,
},
"downloads screen": {
"model": DownloadsScreenModel,
"controller": DownloadsScreenController,
},
"help screen": {
"model": HelpScreenModel,
"controller": HelpScreenController,
},
}

View File

@@ -1,435 +0,0 @@
import os
import random
os.environ["KIVY_VIDEO"] = "ffpyplayer"
from queue import Queue
from threading import Thread
from subprocess import Popen
import webbrowser
import plyer
from kivy.config import Config
from kivy.resources import resource_find,resource_add_path,resource_remove_path
# resource_add_path("_internal")
app_dir = os.path.dirname(__file__)
# test
# test_end
# make sure we aint searching dist folder
dist_folder = os.path.join(app_dir,"dist")
resource_remove_path(dist_folder)
assets_folder = os.path.join(app_dir,"assets")
resource_add_path(assets_folder)
conigs_folder = os.path.join(app_dir,"configs")
resource_add_path(conigs_folder)
data_folder = os.path.join(app_dir,"data")
resource_add_path(data_folder)
Config.set("graphics","width","1000")
Config.set("graphics","minimum_width","1000")
Config.set('kivy', 'window_icon', resource_find("logo.ico"))
Config.write()
# from kivy.core.window import Window
from kivy.loader import Loader
Loader.num_workers = 5
Loader.max_upload_per_frame = 10
from kivy.clock import Clock
from kivy.logger import Logger
from kivy.uix.screenmanager import ScreenManager, FadeTransition
from kivy.uix.settings import SettingsWithSidebar, Settings
from kivymd.icon_definitions import md_icons
from kivymd.app import MDApp
from anixstream.View.screens import screens
from anixstream.libs.animdl import AnimdlApi
from anixstream.Utility import (
themes_available,
show_notification,
user_data_helper,
animdl_config_manager,
)
from anixstream.Utility.utils import write_crash
# Ensure the user data fields exist
if not (user_data_helper.user_data.exists("user_anime_list")):
user_data_helper.update_user_anime_list([])
if not (user_data_helper.yt_cache.exists("yt_stream_links")):
user_data_helper.update_anime_trailer_cache([])
# TODO: Confirm data integrity from user_data and yt_cache
class AniXStreamApp(MDApp):
queue = Queue()
downloads_queue = Queue()
animdl_streaming_subprocess: Popen | None = None
default_anime_image = resource_find(random.choice(["default_1.jpg","default.jpg"]))
default_banner_image = resource_find(random.choice(["banner_1.jpg","banner.jpg"]))
# default_video = resource_find("Billyhan_When you cant afford Crunchyroll to watch anime.mp4")
def worker(self, queue: Queue):
while True:
task = queue.get() # task should be a function
try:
task()
except Exception as e:
show_notification("An error occured while streaming", f"{e}")
self.queue.task_done()
def downloads_worker(self, queue: Queue):
while True:
download_task = queue.get() # task should be a function
try:
download_task()
except Exception as e:
show_notification("An error occured while downloading", f"{e}")
self.downloads_queue.task_done()
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.icon = resource_find("logo.png")
self.load_all_kv_files(self.directory)
self.theme_cls.theme_style = "Dark"
self.theme_cls.primary_palette = "Lightcoral"
self.manager_screens = ScreenManager()
self.manager_screens.transition = FadeTransition()
# initialize worker
self.worker_thread = Thread(target=self.worker, args=(self.queue,))
self.worker_thread.daemon = True
self.worker_thread.start()
Logger.info("AniXStream:Successfully started background tasks worker")
# initialize downloads worker
self.downloads_worker_thread = Thread(
target=self.downloads_worker, args=(self.downloads_queue,)
)
self.downloads_worker_thread.daemon = True
self.downloads_worker_thread.start()
Logger.info("AniXStream:Successfully started download worker")
def build(self) -> ScreenManager:
self.settings_cls = SettingsWithSidebar
self.generate_application_screens()
if config := self.config:
if theme_color := config.get("Preferences", "theme_color"):
self.theme_cls.primary_palette = theme_color
if theme_style := config.get("Preferences", "theme_style"):
self.theme_cls.theme_style = theme_style
self.anime_screen = self.manager_screens.get_screen("anime screen")
self.search_screen = self.manager_screens.get_screen("search screen")
self.download_screen = self.manager_screens.get_screen("downloads screen")
self.home_screen = self.manager_screens.get_screen("home screen")
return self.manager_screens
def on_start(self, *args):
if self.config.get("Preferences","is_startup_anime_enable")=="1": # type: ignore
Clock.schedule_once(lambda _:self.home_screen.controller.populate_home_screen(),1)
def generate_application_screens(self) -> None:
for i, name_screen in enumerate(screens.keys()):
model = screens[name_screen]["model"]()
controller = screens[name_screen]["controller"](model)
view = controller.get_view()
view.manager_screens = self.manager_screens
view.name = name_screen
self.manager_screens.add_widget(view)
def build_config(self, config):
# General settings setup
if vid_path := plyer.storagepath.get_videos_dir(): # type: ignore
downloads_dir = os.path.join(vid_path, "anixstream")
if not os.path.exists(downloads_dir):
os.mkdir(downloads_dir)
else:
downloads_dir = os.path.join(".", "videos")
if not os.path.exists(downloads_dir):
os.mkdir(downloads_dir)
config.setdefaults(
"Preferences",
{
"theme_color": "Cyan",
"theme_style": "Dark",
"downloads_dir": downloads_dir,
"is_startup_anime_enable": False,
},
)
# animdl config settings setup
animdl_config = animdl_config_manager.get_animdl_config()
config.setdefaults(
"Providers",
{
"default_provider": animdl_config["default_provider"],
},
)
config.setdefaults(
"Quality",
{
"quality_string": animdl_config["quality_string"],
},
)
config.setdefaults(
"PlayerSelection",
{
"default_player": animdl_config["default_player"],
},
)
def build_settings(self, settings: Settings):
settings.add_json_panel(
"Settings", self.config, resource_find("general_settings_panel.json")
)
settings.add_json_panel(
"Animdl Config", self.config, resource_find("animdl_config_panel.json")
)
def on_config_change(self, config, section, key, value):
# TODO: Change to match case
if section == "Preferences":
match key:
case "theme_color":
if value in themes_available:
self.theme_cls.primary_palette = value
else:
Logger.warning(
"AniXStream Settings: An invalid theme has been entered and will be ignored"
)
config.set("Preferences", "theme_color", "Cyan")
config.write()
case "theme_style":
self.theme_cls.theme_style = value
elif section == "Providers":
animdl_config_manager.update_animdl_config("default_provider", value)
elif section == "Quality":
animdl_config_manager.update_animdl_config("quality_string", value)
elif section == "PlayerSelection":
animdl_config_manager.update_animdl_config("default_player", value)
def on_stop(self):
del self.downloads_worker_thread
if self.animdl_streaming_subprocess:
self.stop_streaming = True
self.animdl_streaming_subprocess.terminate()
del self.worker_thread
Logger.info("Animdl:Successfully terminated existing animdl subprocess")
# custom methods
def search_for_anime(self, search_field, **kwargs):
if self.manager_screens.current != "search screen":
self.manager_screens.current = "search screen"
self.search_screen.handle_search_for_anime(search_field, **kwargs)
def add_anime_to_user_anime_list(self, id: int):
updated_list = user_data_helper.get_user_anime_list()
updated_list.append(id)
user_data_helper.update_user_anime_list(updated_list)
def remove_anime_from_user_anime_list(self, id: int):
updated_list = user_data_helper.get_user_anime_list()
if updated_list.count(id):
updated_list.remove(id)
user_data_helper.update_user_anime_list(updated_list)
def add_anime_to_user_downloads_list(self, id: int):
updated_list = user_data_helper.get_user_downloads()
updated_list.append(id)
user_data_helper.get_user_downloads()
def show_anime_screen(self, id: int, caller_screen_name: str):
self.manager_screens.current = "anime screen"
self.anime_screen.controller.update_anime_view(id, caller_screen_name)
def download_anime_complete(
self, successful_downloads: list, failed_downloads: list, anime_title: str
):
show_notification(
f"Finished Dowloading {anime_title}",
f"There were {len(successful_downloads)} successful downloads and {len(failed_downloads)} failed downloads",
)
Logger.info(
f"Downloader:Finished Downloading {anime_title} and there were {len(failed_downloads)} failed downloads"
)
def download_anime(self, anime_id: int, default_cmds: dict):
show_notification(
"New Download Task Queued",
f"{default_cmds.get('title')} has been queued for downloading",
)
self.add_anime_to_user_downloads_list(anime_id)
# TODO:Add custom download cmds functionality
on_progress = lambda *args: self.download_screen.on_episode_download_progress(
*args
)
output_path = self.config.get("Preferences", "downloads_dir") # type: ignore
self.download_screen.on_new_download_task(
default_cmds["title"], default_cmds.get("episodes_range")
)
if episodes_range := default_cmds.get("episodes_range"):
download_task = lambda: AnimdlApi.download_anime_by_title(
default_cmds["title"],
on_progress,
lambda anime_title, episode: show_notification(
"Finished installing an episode", f"{anime_title}-{episode}"
),
self.download_anime_complete,
output_path,
episodes_range,
) # ,default_cmds["quality"]
self.downloads_queue.put(download_task)
Logger.info(
f"Downloader:Successfully Queued {default_cmds['title']} for downloading"
)
else:
download_task = lambda: AnimdlApi.download_anime_by_title(
default_cmds["title"],
on_progress,
lambda anime_title, episode: show_notification(
"Finished installing an episode", f"{anime_title}-{episode}"
),
self.download_anime_complete,
output_path,
) # ,default_cmds.get("quality")
self.downloads_queue.put(download_task)
Logger.info(
f"Downloader:Successfully Queued {default_cmds['title']} for downloading"
)
def watch_on_allanime(self, title_):
"""
Opens the given anime in your default browser on allanimes site
Parameters:
----------
title_: The anime title requested to be opened
"""
try:
if anime := AnimdlApi.get_anime_url_by_title(title_):
title, link = anime
parsed_link = f"https://allmanga.to/bangumi/{link.split('/')[-1]}"
else:
Logger.error(
f"AniXStream:Failed to open {title_} in browser on allanime site"
)
show_notification(
"Failure", f"Failed to open {title_} in browser on allanime site"
)
if webbrowser.open(parsed_link):
Logger.info(
f"AniXStream:Successfully opened {title} in browser allanime site"
)
show_notification(
"Success", f"Successfully opened {title} in browser allanime site"
)
else:
Logger.error(
f"AniXStream:Failed to open {title} in browser on allanime site"
)
show_notification(
"Failure", f"Failed to open {title} in browser on allanime site"
)
except Exception as e:
show_notification("Something went wrong",f"{e}")
def stream_anime_with_custom_input_cmds(self, *cmds):
self.animdl_streaming_subprocess = (
AnimdlApi._run_animdl_command_and_get_subprocess(["stream", *cmds])
)
def stream_anime_by_title_with_animdl(
self, title, episodes_range: str | None = None
):
self.stop_streaming = False
self.animdl_streaming_subprocess = AnimdlApi.stream_anime_by_title_on_animdl(
title, episodes_range
)
def stream_anime_with_mpv(
self, title, episodes_range: str | None = None, quality: str = "best"
):
self.stop_streaming = False
streams = AnimdlApi.stream_anime_with_mpv(title, episodes_range, quality)
# TODO: End mpv child process properly
for stream in streams:
try:
if isinstance(stream,str):
show_notification("Failed to stream current episode",f"{stream}")
continue
self.animdl_streaming_subprocess = stream
for line in self.animdl_streaming_subprocess.stderr: # type: ignore
if self.stop_streaming:
if stream:
stream.terminate()
stream.kill()
del stream
return
except Exception as e:
show_notification("Something went wrong while streaming",f"{e}")
def watch_on_animdl(
self,
stream_with_mpv_options: dict | None = None,
episodes_range: str | None = None,
custom_options: tuple[str] | None = None,
):
"""
Enables you to stream an anime using animdl either by parsing a title or custom animdl options
parameters:
-----------
title_dict:dict["japanese","kanji"]
a dictionary containing the titles of the anime
custom_options:tuple[str]
a tuple containing valid animdl stream commands
"""
if self.animdl_streaming_subprocess:
self.animdl_streaming_subprocess.kill()
self.stop_streaming = True
if stream_with_mpv_options:
stream_func = lambda: self.stream_anime_with_mpv(
stream_with_mpv_options["title"],
stream_with_mpv_options.get("episodes_range"),
stream_with_mpv_options["quality"],
)
self.queue.put(stream_func)
Logger.info(
f"Animdl:Successfully started to stream {stream_with_mpv_options['title']}"
)
else:
stream_func = lambda: self.stream_anime_with_custom_input_cmds(
*custom_options
)
self.queue.put(stream_func)
show_notification("Streamer", "Started streaming")
def run_app():
AniXStreamApp().run()
if __name__ == "__main__":
try:
run_app()
except Exception as e:
write_crash(e)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 291 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

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