mirror of
https://github.com/Benexl/FastAnime.git
synced 2025-12-08 13:50:40 -08:00
Compare commits
79 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5fb9747285 | ||
|
|
394228d391 | ||
|
|
5d3c0cc6ec | ||
|
|
3ef7c5248c | ||
|
|
8bebc401fd | ||
|
|
215b28457b | ||
|
|
dfd2bfc857 | ||
|
|
f991292e94 | ||
|
|
d837457f80 | ||
|
|
343bdba31b | ||
|
|
1c1c2457e8 | ||
|
|
b083bfb074 | ||
|
|
ea1abcb2ae | ||
|
|
001030ba2b | ||
|
|
eda8984781 | ||
|
|
d8dc6f0a34 | ||
|
|
2d711a7a7f | ||
|
|
30ca25626a | ||
|
|
b1f5a558c8 | ||
|
|
8062c8dc83 | ||
|
|
cb7eed46bc | ||
|
|
4626eca89e | ||
|
|
0d549c5915 | ||
|
|
33c518ed4c | ||
|
|
8e155dcc74 | ||
|
|
7743b0423e | ||
|
|
6346ea7343 | ||
|
|
32de01047f | ||
|
|
35c7f81afb | ||
|
|
2dbbb1c4df | ||
|
|
6a6efa9d56 | ||
|
|
e510dc3a11 | ||
|
|
9639fd8c05 | ||
|
|
add35ce682 | ||
|
|
6bcc77ea44 | ||
|
|
1a72f88be3 | ||
|
|
1a9f1120b8 | ||
|
|
c2fc807688 | ||
|
|
2b0ade093c | ||
|
|
a26193706e | ||
|
|
ff3c57ef9b | ||
|
|
3b987bd07a | ||
|
|
e8474c0428 | ||
|
|
c78a759aa1 | ||
|
|
d1aad70c48 | ||
|
|
62b36f3e58 | ||
|
|
c5b905fb0d | ||
|
|
7d3dc671ed | ||
|
|
0ec3c7a5bb | ||
|
|
8e0619863a | ||
|
|
e8a05ec4b8 | ||
|
|
34e8b2abd1 | ||
|
|
161b6eb961 | ||
|
|
dd2090f85d | ||
|
|
8b1595a5da | ||
|
|
77ffa27ed8 | ||
|
|
15f79b65c9 | ||
|
|
33c3af0241 | ||
|
|
9badde62fb | ||
|
|
4e401dca40 | ||
|
|
25422b1b7d | ||
|
|
e8463f13b4 | ||
|
|
556f42e41f | ||
|
|
b99a4f7efc | ||
|
|
f6f45cf322 | ||
|
|
ae6db1847a | ||
|
|
20d04ea07b | ||
|
|
8f3834453c | ||
|
|
7ad8b8a0e3 | ||
|
|
80b41f06da | ||
|
|
e79321ed50 | ||
|
|
f7b5898dfa | ||
|
|
144bf53081 | ||
|
|
16dded9724 | ||
|
|
c47b158bff | ||
|
|
9a36e15d9d | ||
|
|
d6b2bd7761 | ||
|
|
2346552dc4 | ||
|
|
ba275055db |
327
README.md
327
README.md
@@ -2,11 +2,14 @@
|
|||||||
|
|
||||||
Welcome to **FastAnime**, anime site experience from the terminal.
|
Welcome to **FastAnime**, anime site experience from the terminal.
|
||||||
|
|
||||||
**fzf mode**
|

|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>fzf mode</b></summary>
|
||||||
|
|
||||||
[fa_fzf_demo.webm](https://github.com/user-attachments/assets/b1fecf25-e358-4e8b-a144-bcb7947210cf)
|
[fa_fzf_demo.webm](https://github.com/user-attachments/assets/b1fecf25-e358-4e8b-a144-bcb7947210cf)
|
||||||
|
|
||||||
**other modes:**
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><b>rofi mode</b></summary>
|
<summary><b>rofi mode</b></summary>
|
||||||
@@ -22,7 +25,7 @@ Welcome to **FastAnime**, anime site experience from the terminal.
|
|||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
Heavily inspired by [animdl](https://github.com/justfoolingaround/animdl), [magic-tape](https://gitlab.com/christosangel/magic-tape/-/tree/main?ref_type=heads) and [ani-cli](https://github.com/pystardust/ani-cli).
|
Heavily inspired by [animdl](https://github.com/justfoolingaround/animdl), [jerry](https://github.com/justchokingaround/jerry/tree/main),[magic-tape](https://gitlab.com/christosangel/magic-tape/-/tree/main?ref_type=heads) and [ani-cli](https://github.com/pystardust/ani-cli).
|
||||||
|
|
||||||
<!--toc:start-->
|
<!--toc:start-->
|
||||||
|
|
||||||
@@ -50,6 +53,7 @@ Heavily inspired by [animdl](https://github.com/justfoolingaround/animdl), [magi
|
|||||||
- [MPV specific commands](#mpv-specific-commands)
|
- [MPV specific commands](#mpv-specific-commands)
|
||||||
- [Key Bindings](#key-bindings)
|
- [Key Bindings](#key-bindings)
|
||||||
- [Script Messages](#script-messages)
|
- [Script Messages](#script-messages)
|
||||||
|
- [styling the default interface](#styling-the-default-interface)
|
||||||
- [Configuration](#configuration)
|
- [Configuration](#configuration)
|
||||||
- [Contributing](#contributing)
|
- [Contributing](#contributing)
|
||||||
- [Receiving Support](#receiving-support)
|
- [Receiving Support](#receiving-support)
|
||||||
@@ -174,10 +178,12 @@ The only required external dependency, unless you won't be streaming, is [MPV](h
|
|||||||
- [ani-skip](https://github.com/synacktraa/ani-skip) used for skipping the opening and ending theme songs
|
- [ani-skip](https://github.com/synacktraa/ani-skip) used for skipping the opening and ending theme songs
|
||||||
- [ffmpegthumbnailer](https://github.com/dirkvdb/ffmpegthumbnailer) used for local previews of downloaded anime
|
- [ffmpegthumbnailer](https://github.com/dirkvdb/ffmpegthumbnailer) used for local previews of downloaded anime
|
||||||
- [syncplay](https://syncplay.pl/) to enable watch together.
|
- [syncplay](https://syncplay.pl/) to enable watch together.
|
||||||
|
- [feh]() used in manga mode
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
The project offers a featureful command-line interface and MPV interface through the use of python-mpv.
|
The project offers a featureful command-line interface and MPV interface through the use of python-mpv.
|
||||||
|
The project also offers subs in different languages thanks to aniwatch provider.
|
||||||
|
|
||||||
### The Commandline interface :fire:
|
### The Commandline interface :fire:
|
||||||
|
|
||||||
@@ -220,7 +226,7 @@ Available options for the fastanime include:
|
|||||||
- `--default` use the default ui
|
- `--default` use the default ui
|
||||||
- `--preview` show a preview when using fzf
|
- `--preview` show a preview when using fzf
|
||||||
- `--no-preview` dont show a preview when using fzf
|
- `--no-preview` dont show a preview when using fzf
|
||||||
- `--format <yt-dlp format string>` or `-f <yt-dlp format string>` set the format of anime downloaded and streamed based on yt-dlp format. Works when `--server gogoanime`
|
- `--format <yt-dlp format string>` or `-f <yt-dlp format string>` set the format of anime downloaded and streamed based on [yt-dlp format](https://github.com/yt-dlp/yt-dlp#format-selection). Works when `--server gogoanime` or on providers that provide multi quality streams eg aniwatch
|
||||||
- `--icons/--no-icons` toggle the visibility of the icons
|
- `--icons/--no-icons` toggle the visibility of the icons
|
||||||
- `--skip/--no-skip` whether to skip the opening and ending theme songs.
|
- `--skip/--no-skip` whether to skip the opening and ending theme songs.
|
||||||
- `--rofi` use rofi for the ui
|
- `--rofi` use rofi for the ui
|
||||||
@@ -233,7 +239,9 @@ Available options for the fastanime include:
|
|||||||
- `--use-mpv-mod/--use-default-player` whether to use python-mpv
|
- `--use-mpv-mod/--use-default-player` whether to use python-mpv
|
||||||
- `--provider <allanime/animepahe>` anime site of choice to scrape from
|
- `--provider <allanime/animepahe>` anime site of choice to scrape from
|
||||||
- `--sync-play` or `-sp` use syncplay for streaming anime so you can watch with your friends
|
- `--sync-play` or `-sp` use syncplay for streaming anime so you can watch with your friends
|
||||||
- `--sub_lang <en/or any other common shortform for country>` regex is used to determine the appropriate. Only works when provider is aniwatch.
|
- `--sub-lang <en/or any other common shortform for country>` regex is used to determine the appropriate. Only works when provider is aniwatch.
|
||||||
|
- `--normalize-titles/--no-normalize-titles` whether to normalize provider titles
|
||||||
|
- `--manga` toggle experimental manga mode
|
||||||
|
|
||||||
Example usage of the above options
|
Example usage of the above options
|
||||||
|
|
||||||
@@ -247,13 +255,16 @@ fastanime --sync-play --server sharepoint search -t <anime-title>
|
|||||||
fastanime --sync-play --server sharepoint anilist
|
fastanime --sync-play --server sharepoint anilist
|
||||||
|
|
||||||
# downloading dubbed anime
|
# downloading dubbed anime
|
||||||
fastanime --dub download <anime>
|
fastanime --dub download -t <anime>
|
||||||
|
|
||||||
# use icons and fzf for a more elegant ui with preview
|
# use icons and fzf for a more elegant ui with preview
|
||||||
fastanime --icons --preview --fzf anilist
|
fastanime --icons --preview --fzf anilist
|
||||||
|
|
||||||
# use icons with default ui
|
# use icons with default ui
|
||||||
fastanime --icons --default anilist
|
fastanime --icons --default anilist
|
||||||
|
|
||||||
|
# viewing manga
|
||||||
|
fastanime --manga search -t <manga-title>
|
||||||
```
|
```
|
||||||
|
|
||||||
#### The anilist command :fire: :fire: :fire:
|
#### The anilist command :fire: :fire: :fire:
|
||||||
@@ -267,6 +278,7 @@ Run `fastanime anilist` to access the main interface.
|
|||||||
##### Subcommands
|
##### Subcommands
|
||||||
|
|
||||||
The subcommands are mainly their as convenience. Since all the features already exist in the main interface.
|
The subcommands are mainly their as convenience. Since all the features already exist in the main interface.
|
||||||
|
Most of the subcommands share the common option `--dump-json` or `-d` which will print only the json data and suppress the ui.
|
||||||
|
|
||||||
- `fastanime anilist trending`: Top 15 trending anime.
|
- `fastanime anilist trending`: Top 15 trending anime.
|
||||||
- `fastanime anilist recent`: Top 15 recently updated anime.
|
- `fastanime anilist recent`: Top 15 recently updated anime.
|
||||||
@@ -276,6 +288,49 @@ The subcommands are mainly their as convenience. Since all the features already
|
|||||||
- `fastanime anilist favourites`: Top 15 favorite anime.
|
- `fastanime anilist favourites`: Top 15 favorite anime.
|
||||||
- `fastanime anilist random`: get random anime
|
- `fastanime anilist random`: get random anime
|
||||||
|
|
||||||
|
**FastAnime Anilist Search subcommand** 🔥 🔥 🔥
|
||||||
|
|
||||||
|
It is by far one of the most powerful commands.
|
||||||
|
It offers the following options:
|
||||||
|
|
||||||
|
- `--sort <MediaSort>` or `-s <MediaSort>`
|
||||||
|
- `--title <anime-title>` or `-t <anime-title>`
|
||||||
|
- `--tags <tag>` or `-T <tag>` can be specified multiple times for different tags to filter by.
|
||||||
|
- `--year <year>` or `-y <year>`
|
||||||
|
- `--status <MediaStatus>` or `-S <MediaStatus>` can be specified multiple times
|
||||||
|
- `--media-format <MediaFormat>` or `-f <MediaFormat>`
|
||||||
|
- `--season <MediaSeason>`
|
||||||
|
- `--genres <genre>` or `-g <genre>` can be specified multiple times.
|
||||||
|
- `--on-list/--not-on-list`
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# get anime with the tag of isekai
|
||||||
|
fastanime anilist search -T isekai
|
||||||
|
|
||||||
|
# get anime of 2024 and sort by popularity
|
||||||
|
# that has already finished airing or is releasing
|
||||||
|
# and is not in your anime lists
|
||||||
|
fastanime anilist search -y 2024 -s POPULARITY_DESC --status RELEASING --status FINISHED --not-on-list
|
||||||
|
|
||||||
|
# get anime of 2024 season WINTER
|
||||||
|
fastanime anilist search -y 2024 --season WINTER
|
||||||
|
|
||||||
|
# get anime genre action and tag isekai,magic
|
||||||
|
fastanime anilist search -g Action -T Isekai -T Magic
|
||||||
|
|
||||||
|
# get anime of 2024 thats finished airing
|
||||||
|
fastanime anilist search -y 2024 -S FINISHED
|
||||||
|
|
||||||
|
# get the most favourite anime movies
|
||||||
|
fastanime anilist search -f MOVIE -s FAVOURITES_DESC
|
||||||
|
```
|
||||||
|
|
||||||
|
For more details visit the anilist docs or just get the completions which will improve the experience.
|
||||||
|
|
||||||
|
Like seriously **[get the completions](https://github.com/Benex254/FastAnime#completions-subcommand)** and the experience will be a 💯 💯 better.
|
||||||
|
|
||||||
The following are commands you can only run if you are signed in to your AniList account:
|
The following are commands you can only run if you are signed in to your AniList account:
|
||||||
|
|
||||||
- `fastanime anilist watching`
|
- `fastanime anilist watching`
|
||||||
@@ -285,7 +340,7 @@ The following are commands you can only run if you are signed in to your AniList
|
|||||||
- `fastanime anilist paused`
|
- `fastanime anilist paused`
|
||||||
- `fastanime anilist completed`
|
- `fastanime anilist completed`
|
||||||
|
|
||||||
Plus: `fastanime anilist notifier` :fire:
|
Plus: `fastanime anilist notifier` 🔥
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# basic form
|
# basic form
|
||||||
@@ -362,6 +417,24 @@ fastanime download -t <anime-title> -r ':<episodes-end>'
|
|||||||
# remember python indexing starts at 0
|
# remember python indexing starts at 0
|
||||||
fastanime download -t <anime-title> -r '<episode-1>:<episode>'
|
fastanime download -t <anime-title> -r '<episode-1>:<episode>'
|
||||||
|
|
||||||
|
# merge subtitles with ffmpeg to mkv format; aniwatch tends to give subs as separate files
|
||||||
|
# and dont prompt for anything
|
||||||
|
# eg existing file in destination instead remove
|
||||||
|
# and clean
|
||||||
|
# ie remove original files (sub file and vid file)
|
||||||
|
# only keep merged files
|
||||||
|
fastanime download -t <anime-title> --merge --clean --no-prompt
|
||||||
|
|
||||||
|
# EOF is used since -t always expects a title
|
||||||
|
# you can supply anime titles from file or -t at the same time
|
||||||
|
#
|
||||||
|
# from stdin
|
||||||
|
echo -e "<anime-title>\n<anime-title>\n<anime-title>" | fastanime download -t "EOF" -r <range> -f -
|
||||||
|
|
||||||
|
# from file
|
||||||
|
fastanime download -t "EOF" -r <range> -f <file-path>
|
||||||
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### search subcommand
|
#### search subcommand
|
||||||
@@ -465,6 +538,10 @@ fastanime downloads --time-to-seek <intRange(-1,100)>
|
|||||||
# --- or ---
|
# --- or ---
|
||||||
fastanime downloads -t <intRange(-1,100)>
|
fastanime downloads -t <intRange(-1,100)>
|
||||||
|
|
||||||
|
# to watch a specific title
|
||||||
|
# be sure to get the completions for the best experience
|
||||||
|
fastanime downloads --title <title>
|
||||||
|
|
||||||
# to get the path to the downloads folder set
|
# to get the path to the downloads folder set
|
||||||
fastanime downloads --path
|
fastanime downloads --path
|
||||||
# useful when you want to use the value for other programs
|
# useful when you want to use the value for other programs
|
||||||
@@ -579,79 +656,213 @@ script-message select-server <server-name>
|
|||||||
script-message select-quality <1080/720/480/360>
|
script-message select-quality <1080/720/480/360>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## styling the default interface
|
||||||
|
|
||||||
|
The default interface uses inquirerPy which is customizable. Read here to findout more <https://inquirerpy.readthedocs.io/en/latest/pages/env.html>
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
The app includes sensible defaults but can be customized extensively. Configuration is stored in `.ini` format at `~/.config/FastAnime/config.ini` on arch linux; for the other operating systems you can check by running `fastanime config --path`.
|
The app includes sensible defaults but can be customized extensively. Configuration is stored in `.ini` format at `~/.config/FastAnime/config.ini` on arch linux; for the other operating systems you can check by running `fastanime config --path`.
|
||||||
|
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> You can now use the option `--update` to update your config file from the command-line
|
||||||
|
> For Example:
|
||||||
|
> `fastanime --icons --fzf --preview config --update`
|
||||||
|
> the above will set icons to true, use_fzf to true and preview to true in your config file
|
||||||
|
>
|
||||||
|
|
||||||
|
|
||||||
|
The default config:
|
||||||
|
|
||||||
```ini
|
```ini
|
||||||
|
#
|
||||||
|
# ███████╗░█████╗░░██████╗████████╗░█████╗░███╗░░██╗██╗███╗░░░███╗███████╗ ░█████╗░░█████╗░███╗░░██╗███████╗██╗░██████╗░
|
||||||
|
# ██╔════╝██╔══██╗██╔════╝╚══██╔══╝██╔══██╗████╗░██║██║████╗░████║██╔════╝ ██╔══██╗██╔══██╗████╗░██║██╔════╝██║██╔════╝░
|
||||||
|
# █████╗░░███████║╚█████╗░░░░██║░░░███████║██╔██╗██║██║██╔████╔██║█████╗░░ ██║░░╚═╝██║░░██║██╔██╗██║█████╗░░██║██║░░██╗░
|
||||||
|
# ██╔══╝░░██╔══██║░╚═══██╗░░░██║░░░██╔══██║██║╚████║██║██║╚██╔╝██║██╔══╝░░ ██║░░██╗██║░░██║██║╚████║██╔══╝░░██║██║░░╚██╗
|
||||||
|
# ██║░░░░░██║░░██║██████╔╝░░░██║░░░██║░░██║██║░╚███║██║██║░╚═╝░██║███████╗ ╚█████╔╝╚█████╔╝██║░╚███║██║░░░░░██║╚██████╔╝
|
||||||
|
# ╚═╝░░░░░╚═╝░░╚═╝╚═════╝░░░░╚═╝░░░╚═╝░░╚═╝╚═╝░░╚══╝╚═╝╚═╝░░░░░╚═╝╚══════╝ ░╚════╝░░╚════╝░╚═╝░░╚══╝╚═╝░░░░░╚═╝░╚═════╝░
|
||||||
|
#
|
||||||
|
[general]
|
||||||
|
# whether to show the icons in the tui [True/False]
|
||||||
|
# more like emojis
|
||||||
|
# by the way if you have any recommendations to which should be used where please
|
||||||
|
# don't hesitate to share your opinion
|
||||||
|
# cause it's a lot of work to look for the right one for each menu option
|
||||||
|
# be sure to also give the replacement emoji
|
||||||
|
icons = False
|
||||||
|
|
||||||
|
# the quality of the stream [1080,720,480,360]
|
||||||
|
# this option is usually only reliable when:
|
||||||
|
# provider=animepahe
|
||||||
|
# since it provides links that actually point to streams of different qualities
|
||||||
|
# while the rest just point to another link that can provide the anime from the same server
|
||||||
|
quality = 1080
|
||||||
|
|
||||||
|
# whether to normalize provider titles [True/False]
|
||||||
|
# basically takes the provider titles and finds the corresponding anilist title then changes the title to that
|
||||||
|
# useful for uniformity especially when downloading from different providers
|
||||||
|
# this also applies to episode titles
|
||||||
|
normalize_titles = True
|
||||||
|
|
||||||
|
# can be [allanime, animepahe, aniwatch]
|
||||||
|
# allanime is the most realible
|
||||||
|
# animepahe provides different links to streams of different quality so a quality can be selected reliably with --quality option
|
||||||
|
# aniwatch which is now hianime usually provides subs in different languuages and its servers are generally faster
|
||||||
|
provider = allanime
|
||||||
|
|
||||||
|
# Display language [english, romaji]
|
||||||
|
# this is passed to anilist directly and is used to set the language which the anime titles will be in
|
||||||
|
# when using the anilist interface
|
||||||
|
preferred_language = english
|
||||||
|
|
||||||
|
# Download directory
|
||||||
|
# where you will find your videos after downloading them with 'fastanime download' command
|
||||||
|
downloads_dir = ~/Videos/FastAnime
|
||||||
|
|
||||||
|
# whether to show a preview window when using fzf or rofi [True/False]
|
||||||
|
# the preview requires you have a commandline image viewer as documented in the README
|
||||||
|
# this is only when usinf fzf
|
||||||
|
# if you dont care about image previews it doesnt matter
|
||||||
|
# though its awesome
|
||||||
|
# try it and you will see
|
||||||
|
preview = False
|
||||||
|
|
||||||
|
# the time to seek when using ffmpegthumbnailer [-1 to 100]
|
||||||
|
# -1 means random and is the default
|
||||||
|
# ffmpegthumbnailer is used to generate previews and you can select at what time in the video to extract an image
|
||||||
|
# random makes things quite exciting cause you never no at what time it will extract the image from
|
||||||
|
ffmpegthumbnailer_seek_time = -1
|
||||||
|
|
||||||
|
# whether to use fzf as the interface for the anilist command and others. [True/False]
|
||||||
|
use_fzf = False
|
||||||
|
|
||||||
|
# whether to use rofi for the ui [True/False]
|
||||||
|
# it's more useful if you want to create a desktop entry
|
||||||
|
# which can be setup with 'fastanime config --desktop-entry'
|
||||||
|
# though if you want it to be your sole interface even when fastanime is run directly from the terminal
|
||||||
|
use_rofi = False
|
||||||
|
|
||||||
|
# rofi themes to use
|
||||||
|
# the values of this option is the path to the rofi config files to use
|
||||||
|
# i choose to split it into three since it gives the best look and feel
|
||||||
|
# you can refer to the rofi demo on github to see for your self
|
||||||
|
# by the way i recommend getting the rofi themes from this project;
|
||||||
|
rofi_theme =
|
||||||
|
|
||||||
|
rofi_theme_input =
|
||||||
|
|
||||||
|
rofi_theme_confirm =
|
||||||
|
|
||||||
|
# the duration in minutes a notification will stay in the screen
|
||||||
|
# used by notifier command
|
||||||
|
notification_duration = 2
|
||||||
|
|
||||||
|
# used when the provider gives subs of different languages
|
||||||
|
# currently its the case for:
|
||||||
|
# aniwatch
|
||||||
|
# the values for this option are the short names for countries
|
||||||
|
# regex is used to determine what you selected
|
||||||
|
sub_lang = eng
|
||||||
|
|
||||||
|
|
||||||
[stream]
|
[stream]
|
||||||
continue_from_history = True # Auto continue from watch history
|
# Auto continue from watch history [True/False]
|
||||||
|
# this will make fastanime to choose the episode that you last watched to completion
|
||||||
|
# and increment it by one
|
||||||
|
# and use that to auto select the episode you want to watch
|
||||||
|
continue_from_history = True
|
||||||
|
|
||||||
# which history to use [local/remote]
|
# which history to use [local/remote]
|
||||||
|
# local history means it will just use the watch history stored locally in your device
|
||||||
|
# the file that stores it is called watch_history.json and is stored next to your config file
|
||||||
|
# remote means it ignores the last episode stored locally and instead uses the one in your anilist anime list
|
||||||
|
# this config option is useful if you want to overwrite your local history or import history covered from another device or platform
|
||||||
|
# since remote history will take precendence over whats available locally
|
||||||
preferred_history = local
|
preferred_history = local
|
||||||
|
|
||||||
# force mpv window
|
# Preferred language for anime [dub/sub]
|
||||||
# passed directly to mpv so values are same
|
translation_type = sub
|
||||||
force_window = immediate
|
|
||||||
|
|
||||||
translation_type = sub # Preferred language for anime (options: dub, sub)
|
# what server to use for a particular provider
|
||||||
|
# allanime: [dropbox, sharepoint, wetransfer, gogoanime, wixmp]
|
||||||
|
# animepahe: [kwik]
|
||||||
|
# aniwatch: [HD1, HD2, StreamSB, StreamTape]
|
||||||
|
# 'top' can also be used as a value for this option
|
||||||
|
# 'top' will cause fastanime to auto select the first server it sees
|
||||||
|
# this saves on resources and is faster since not all servers are being fetched
|
||||||
|
server = top
|
||||||
|
|
||||||
server = top # Default server (options: dropbox, sharepoint, wetransfer.gogoanime, top, wixmp)
|
# Auto select next episode [True/False]
|
||||||
|
# this makes fastanime increment the current episode number
|
||||||
|
# then after using that value to fetch the next episode instead of prompting
|
||||||
|
# this option is useful for binging
|
||||||
|
auto_next = False
|
||||||
|
|
||||||
auto_next = False # Auto-select next episode
|
# Auto select the anime provider results with fuzzy find. [True/False]
|
||||||
|
# Note this won't always be correct
|
||||||
|
# this is because the providers sometime use non-standard names
|
||||||
|
# that are there own preference rather than the official names
|
||||||
|
# But 99% of the time will be accurate
|
||||||
|
# if this happens just turn of auto_select in the menus or from the commandline and manually select the correct anime title
|
||||||
|
# and then please open an issue at <> highlighting the normalized title and the title given by the provider for the anime you wished to watch
|
||||||
|
# or even better edit this file <> and open a pull request
|
||||||
|
auto_select = True
|
||||||
|
|
||||||
# Auto select the anime provider results with fuzzy find.
|
# whether to skip the opening and ending theme songs [True/False]
|
||||||
# Note this wont always be correct.But 99% of the time will be.
|
# NOTE: requires ani-skip to be in path
|
||||||
auto_select=True
|
# for python-mpv users am planning to create this functionality n python without the use of an external script
|
||||||
|
# so its disabled for now
|
||||||
# whether to skip the opening and ending theme songs
|
skip = False
|
||||||
# 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
|
# the maximum delta time in minutes after which the episode should be considered as completed
|
||||||
# used in the continue from time stamp
|
# used in the continue from time stamp
|
||||||
error=3
|
error = 3
|
||||||
|
|
||||||
use_mpv_mod=False
|
# whether to use python-mpv [True/False]
|
||||||
|
# to enable superior control over the player
|
||||||
|
# adding more options to it
|
||||||
|
# Enable this one and you will be wonder why you did not discover fastanime sooner
|
||||||
|
# Since you basically don't have to close the player window to go to the next or previous episode, switch servers, change translation type or
|
||||||
|
change to a given episode x
|
||||||
|
# so try it if you haven't already
|
||||||
|
# if you have any issues setting it up
|
||||||
|
# don't be afraid to ask
|
||||||
|
# especially on windows
|
||||||
|
# honestly it can be a pain to set it up there
|
||||||
|
# personally it took me quite sometime to figure it out
|
||||||
|
# this is because of how windows handles shared libraries
|
||||||
|
# so just ask when you find yourself stuck
|
||||||
|
# or just switch to arch linux
|
||||||
|
use_python_mpv = False
|
||||||
|
|
||||||
|
# force mpv window
|
||||||
|
# the default 'immediate' just makes mpv to open the window even if the video has not yet loaded
|
||||||
|
# done for asthetics
|
||||||
|
# passed directly to mpv so values are same
|
||||||
|
force_window = immediate
|
||||||
|
|
||||||
# the format of downloaded anime and trailer
|
# the format of downloaded anime and trailer
|
||||||
# based on yt-dlp format and passed directly to it
|
# based on yt-dlp format and passed directly to it
|
||||||
# learn more by looking it up on their site
|
# learn more by looking it up on their site
|
||||||
# only works for downloaded anime if server=gogoanime
|
# only works for downloaded anime if:
|
||||||
# since its the only one that offers different formats
|
# provider=allanime, server=gogoanime
|
||||||
# the others tend not to
|
# provider=allanime, server=wixmp
|
||||||
format=best[height<=1080]/bestvideo[height<=1080]+bestaudio/best # default
|
# provider=aniwatch
|
||||||
|
# this is because they provider a m3u8 file that contans multiple quality streams
|
||||||
|
format = best[height<=1080]/bestvideo[height<=1080]+bestaudio/best
|
||||||
|
|
||||||
[general]
|
# NOTE:
|
||||||
# can be [allanime,animepahe]
|
# if you have any trouble setting up your config
|
||||||
provider = allanime
|
# please don't be afraid to ask in our discord
|
||||||
|
# plus if there are any errors, improvements or suggestions please tell us in the discord
|
||||||
preferred_language = romaji # Display language (options: english, romaji)
|
# or help us by contributing
|
||||||
|
# we appreciate all the help we can get
|
||||||
downloads_dir = <Default-videos-dir>/FastAnime # Download directory
|
# since we may not always have the time to immediately implement the changes
|
||||||
|
#
|
||||||
preview=false # whether to show a preview window when using fzf or rofi
|
# HOPE YOU ENJOY FASTANIME AND BE SURE TO STAR THE PROJECT ON GITHUB
|
||||||
|
#
|
||||||
use_fzf=False # whether to use fzf as the interface for the anilist command and others.
|
|
||||||
|
|
||||||
use_rofi=false # whether to use rofi for the ui
|
|
||||||
|
|
||||||
rofi_theme=<path-to-rofi-theme-file>
|
|
||||||
|
|
||||||
rofi_theme_input=<path-to-rofi-theme-file>
|
|
||||||
|
|
||||||
rofi_theme_confirm=<path-to-rofi-theme-file>
|
|
||||||
|
|
||||||
|
|
||||||
# whether to show the icons
|
|
||||||
icons=false
|
|
||||||
|
|
||||||
# the duration in minutes a notification will stay in the screen
|
|
||||||
# used by notifier command
|
|
||||||
notification_duration=2
|
|
||||||
|
|
||||||
[anilist]
|
|
||||||
# Not implemented yet
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
@@ -660,6 +871,8 @@ We welcome your issues and feature requests. However, due to time constraints, w
|
|||||||
|
|
||||||
If you wish to contribute directly, please first open an issue describing your proposed changes so it can be discussed or if you are in a rush for the feature to be merged just open a pr.
|
If you wish to contribute directly, please first open an issue describing your proposed changes so it can be discussed or if you are in a rush for the feature to be merged just open a pr.
|
||||||
|
|
||||||
|
If you find an anime title that does not correspond with a provider or is just weird just [edit the data file](https://github.com/Benex254/FastAnime/blob/master/fastanime/Utility/data.py) and open a pr or if you don't want to do that open an issue.
|
||||||
|
|
||||||
## Receiving Support
|
## Receiving Support
|
||||||
|
|
||||||
For inquiries, join our [Discord Server](https://discord.gg/C4rhMA4mmK).
|
For inquiries, join our [Discord Server](https://discord.gg/C4rhMA4mmK).
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ from .libs.anime_provider import anime_sources
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from typing import Iterator
|
from typing import Iterator
|
||||||
|
|
||||||
from .libs.anilist.types import AnilistBaseMediaDataSchema
|
|
||||||
from .libs.anime_provider.types import Anime, SearchResults, Server
|
from .libs.anime_provider.types import Anime, SearchResults, Server
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -37,12 +36,12 @@ class AnimeProvider:
|
|||||||
self.provider = provider
|
self.provider = provider
|
||||||
self.dynamic = dynamic
|
self.dynamic = dynamic
|
||||||
self.retries = retries
|
self.retries = retries
|
||||||
self.lazyload_provider()
|
self.lazyload_provider(self.provider)
|
||||||
|
|
||||||
def lazyload_provider(self):
|
def lazyload_provider(self, provider):
|
||||||
"""updates the current provider being used"""
|
"""updates the current provider being used"""
|
||||||
_, anime_provider_cls_name = anime_sources[self.provider].split(".", 1)
|
_, anime_provider_cls_name = anime_sources[provider].split(".", 1)
|
||||||
package = f"fastanime.libs.anime_provider.{self.provider}"
|
package = f"fastanime.libs.anime_provider.{provider}"
|
||||||
provider_api = importlib.import_module(".api", package)
|
provider_api = importlib.import_module(".api", package)
|
||||||
anime_provider = getattr(provider_api, anime_provider_cls_name)
|
anime_provider = getattr(provider_api, anime_provider_cls_name)
|
||||||
self.anime_provider = anime_provider()
|
self.anime_provider = anime_provider()
|
||||||
@@ -51,7 +50,6 @@ class AnimeProvider:
|
|||||||
self,
|
self,
|
||||||
user_query,
|
user_query,
|
||||||
translation_type,
|
translation_type,
|
||||||
anilist_obj: "AnilistBaseMediaDataSchema | None" = None,
|
|
||||||
nsfw=True,
|
nsfw=True,
|
||||||
unknown=True,
|
unknown=True,
|
||||||
) -> "SearchResults | None":
|
) -> "SearchResults | None":
|
||||||
@@ -73,14 +71,14 @@ class AnimeProvider:
|
|||||||
user_query, translation_type, nsfw, unknown
|
user_query, translation_type, nsfw, unknown
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(e)
|
logger.error(f"[ANIMEPROVIDER-ERROR]: {e}")
|
||||||
results = None
|
results = None
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def get_anime(
|
def get_anime(
|
||||||
self,
|
self,
|
||||||
anime_id: str,
|
anime_id: str,
|
||||||
anilist_obj: "AnilistBaseMediaDataSchema | None" = None,
|
|
||||||
) -> "Anime | None":
|
) -> "Anime | None":
|
||||||
"""core abstraction over getting info of an anime from all providers
|
"""core abstraction over getting info of an anime from all providers
|
||||||
|
|
||||||
@@ -95,7 +93,8 @@ class AnimeProvider:
|
|||||||
try:
|
try:
|
||||||
results = anime_provider.get_anime(anime_id)
|
results = anime_provider.get_anime(anime_id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(e)
|
logger.error(f"[ANIMEPROVIDER-ERROR]: {e}")
|
||||||
|
|
||||||
results = None
|
results = None
|
||||||
return results
|
return results
|
||||||
|
|
||||||
@@ -104,7 +103,6 @@ class AnimeProvider:
|
|||||||
anime,
|
anime,
|
||||||
episode: str,
|
episode: str,
|
||||||
translation_type: str,
|
translation_type: str,
|
||||||
anilist_obj: "AnilistBaseMediaDataSchema|None" = None,
|
|
||||||
) -> "Iterator[Server] | None":
|
) -> "Iterator[Server] | None":
|
||||||
"""core abstractions for getting juicy streams from all providers
|
"""core abstractions for getting juicy streams from all providers
|
||||||
|
|
||||||
@@ -123,6 +121,7 @@ class AnimeProvider:
|
|||||||
anime, episode, translation_type
|
anime, episode, translation_type
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(e)
|
logger.error(f"[ANIMEPROVIDER-ERROR]: {e}")
|
||||||
|
|
||||||
results = None
|
results = None
|
||||||
return results # pyright:ignore
|
return results
|
||||||
|
|||||||
105
fastanime/MangaProvider.py
Normal file
105
fastanime/MangaProvider.py
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
"""An abstraction over all providers offering added features with a simple and well typed api
|
||||||
|
|
||||||
|
[TODO:description]
|
||||||
|
"""
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
import logging
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from .libs.manga_provider import manga_sources
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class MangaProvider:
|
||||||
|
"""Class that manages all anime sources adding some extra functionality to them.
|
||||||
|
Attributes:
|
||||||
|
PROVIDERS: [TODO:attribute]
|
||||||
|
provider: [TODO:attribute]
|
||||||
|
provider: [TODO:attribute]
|
||||||
|
dynamic: [TODO:attribute]
|
||||||
|
retries: [TODO:attribute]
|
||||||
|
manga_provider: [TODO:attribute]
|
||||||
|
"""
|
||||||
|
|
||||||
|
PROVIDERS = list(manga_sources.keys())
|
||||||
|
provider = PROVIDERS[0]
|
||||||
|
|
||||||
|
def __init__(self, provider="mangadex", dynamic=False, retries=0) -> None:
|
||||||
|
self.provider = provider
|
||||||
|
self.dynamic = dynamic
|
||||||
|
self.retries = retries
|
||||||
|
self.lazyload_provider(self.provider)
|
||||||
|
|
||||||
|
def lazyload_provider(self, provider):
|
||||||
|
"""updates the current provider being used"""
|
||||||
|
_, anime_provider_cls_name = manga_sources[provider].split(".", 1)
|
||||||
|
package = f"fastanime.libs.manga_provider.{provider}"
|
||||||
|
provider_api = importlib.import_module(".api", package)
|
||||||
|
manga_provider = getattr(provider_api, anime_provider_cls_name)
|
||||||
|
self.manga_provider = manga_provider()
|
||||||
|
|
||||||
|
def search_for_manga(
|
||||||
|
self,
|
||||||
|
user_query,
|
||||||
|
nsfw=True,
|
||||||
|
unknown=True,
|
||||||
|
):
|
||||||
|
"""core abstraction over all providers search functionality
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_query ([TODO:parameter]): [TODO:description]
|
||||||
|
translation_type ([TODO:parameter]): [TODO:description]
|
||||||
|
nsfw ([TODO:parameter]): [TODO:description]
|
||||||
|
manga_provider ([TODO:parameter]): [TODO:description]
|
||||||
|
anilist_obj: [TODO:description]
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
[TODO:return]
|
||||||
|
"""
|
||||||
|
manga_provider = self.manga_provider
|
||||||
|
try:
|
||||||
|
results = manga_provider.search_for_manga(user_query, nsfw, unknown)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
results = None
|
||||||
|
return results
|
||||||
|
|
||||||
|
def get_manga(
|
||||||
|
self,
|
||||||
|
anime_id: str,
|
||||||
|
):
|
||||||
|
"""core abstraction over getting info of an anime from all providers
|
||||||
|
|
||||||
|
Args:
|
||||||
|
anime_id: [TODO:description]
|
||||||
|
anilist_obj: [TODO:description]
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
[TODO:return]
|
||||||
|
"""
|
||||||
|
manga_provider = self.manga_provider
|
||||||
|
try:
|
||||||
|
results = manga_provider.get_manga(anime_id)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
results = None
|
||||||
|
return results
|
||||||
|
|
||||||
|
def get_chapter_thumbnails(
|
||||||
|
self,
|
||||||
|
manga_id: str,
|
||||||
|
chapter: str,
|
||||||
|
):
|
||||||
|
manga_provider = self.manga_provider
|
||||||
|
try:
|
||||||
|
results = manga_provider.get_chapter_thumbnails(manga_id, chapter)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
results = None
|
||||||
|
return results # pyright:ignore
|
||||||
@@ -3,12 +3,24 @@ Just contains some useful data used across the codebase
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# useful incases where the anilist title is too different from the provider title
|
# useful incases where the anilist title is too different from the provider title
|
||||||
anime_normalizer = {
|
anime_normalizer_raw = {
|
||||||
"1P": "one piece",
|
"allanime": {
|
||||||
"Magia Record: Mahou Shoujo Madoka☆Magica Gaiden (TV)": "Mahou Shoujo Madoka☆Magica",
|
"1P": "one piece",
|
||||||
"Dungeon ni Deai o Motomeru no wa Machigatte Iru Darouka": "Dungeon ni Deai wo Motomeru no wa Machigatteiru Darou ka",
|
"Magia Record: Mahou Shoujo Madoka☆Magica Gaiden (TV)": "Mahou Shoujo Madoka☆Magica",
|
||||||
'Hazurewaku no "Joutai Ijou Skill" de Saikyou ni Natta Ore ga Subete wo Juurin suru made': "Hazure Waku no [Joutai Ijou Skill] de Saikyou ni Natta Ore ga Subete wo Juurin Suru made",
|
"Dungeon ni Deai o Motomeru no wa Machigatte Iru Darouka": "Dungeon ni Deai wo Motomeru no wa Machigatteiru Darou ka",
|
||||||
|
'Hazurewaku no "Joutai Ijou Skill" de Saikyou ni Natta Ore ga Subete wo Juurin suru made': "Hazure Waku no [Joutai Ijou Skill] de Saikyou ni Natta Ore ga Subete wo Juurin Suru made",
|
||||||
|
},
|
||||||
|
"aniwatch": {"My Star": "Oshi no Ko"},
|
||||||
|
"animepahe": {"Azumanga Daiou The Animation": "Azumanga Daioh"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
anilist_sort_normalizer = {"search match": "SEARCH_MATCH"}
|
def get_anime_normalizer():
|
||||||
|
"""Used because there are different providers"""
|
||||||
|
import os
|
||||||
|
|
||||||
|
current_provider = os.environ["CURRENT_FASTANIME_PROVIDER"]
|
||||||
|
return anime_normalizer_raw[current_provider]
|
||||||
|
|
||||||
|
|
||||||
|
anime_normalizer = get_anime_normalizer()
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
from queue import Queue
|
from queue import Queue
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
|
|
||||||
import yt_dlp
|
import yt_dlp
|
||||||
|
from rich import print
|
||||||
|
from rich.prompt import Confirm
|
||||||
from yt_dlp.utils import sanitize_filename
|
from yt_dlp.utils import sanitize_filename
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -25,8 +31,6 @@ class YtDLPDownloader:
|
|||||||
self._thread.daemon = True
|
self._thread.daemon = True
|
||||||
self._thread.start()
|
self._thread.start()
|
||||||
|
|
||||||
# Function to download the file
|
|
||||||
# TODO: untpack the title to its actual values episode_title and anime_title
|
|
||||||
def _download_file(
|
def _download_file(
|
||||||
self,
|
self,
|
||||||
url: str,
|
url: str,
|
||||||
@@ -39,6 +43,9 @@ class YtDLPDownloader:
|
|||||||
verbose=False,
|
verbose=False,
|
||||||
headers={},
|
headers={},
|
||||||
sub="",
|
sub="",
|
||||||
|
merge=False,
|
||||||
|
clean=False,
|
||||||
|
prompt=True,
|
||||||
):
|
):
|
||||||
"""Helper function that downloads anime given url and path details
|
"""Helper function that downloads anime given url and path details
|
||||||
|
|
||||||
@@ -64,8 +71,82 @@ class YtDLPDownloader:
|
|||||||
urls = [url]
|
urls = [url]
|
||||||
if sub:
|
if sub:
|
||||||
urls.append(sub)
|
urls.append(sub)
|
||||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
vid_path = ""
|
||||||
ydl.download(urls)
|
sub_path = ""
|
||||||
|
for i, url in enumerate(urls):
|
||||||
|
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||||
|
info = ydl.extract_info(url, download=True)
|
||||||
|
if not info:
|
||||||
|
continue
|
||||||
|
if i == 0:
|
||||||
|
vid_path = info["requested_downloads"][0]["filepath"]
|
||||||
|
else:
|
||||||
|
sub_path = info["requested_downloads"][0]["filepath"]
|
||||||
|
if sub_path and vid_path and merge:
|
||||||
|
self.merge_subtitles(vid_path, sub_path, clean, prompt)
|
||||||
|
|
||||||
|
def merge_subtitles(self, video_path, sub_path, clean, prompt):
|
||||||
|
# Extract the directory and filename
|
||||||
|
video_dir = os.path.dirname(video_path)
|
||||||
|
video_name = os.path.basename(video_path)
|
||||||
|
video_name, _ = os.path.splitext(video_name)
|
||||||
|
video_name += ".mkv"
|
||||||
|
|
||||||
|
FFMPEG_EXECUTABLE = shutil.which("ffmpeg")
|
||||||
|
if not FFMPEG_EXECUTABLE:
|
||||||
|
print("[yellow bold]WARNING: [/]FFmpeg not found")
|
||||||
|
return
|
||||||
|
# Create a temporary directory
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
# Temporary output path in the temporary directory
|
||||||
|
temp_output_path = os.path.join(temp_dir, video_name)
|
||||||
|
# FFmpeg command to merge subtitles
|
||||||
|
command = [
|
||||||
|
FFMPEG_EXECUTABLE,
|
||||||
|
"-hide_banner",
|
||||||
|
"-i",
|
||||||
|
video_path,
|
||||||
|
"-i",
|
||||||
|
sub_path,
|
||||||
|
"-c",
|
||||||
|
"copy",
|
||||||
|
"-map",
|
||||||
|
"0",
|
||||||
|
"-map",
|
||||||
|
"1",
|
||||||
|
temp_output_path,
|
||||||
|
]
|
||||||
|
|
||||||
|
# Run the command
|
||||||
|
try:
|
||||||
|
subprocess.run(command, check=True)
|
||||||
|
|
||||||
|
# Move the file back to the original directory with the original name
|
||||||
|
final_output_path = os.path.join(video_dir, video_name)
|
||||||
|
|
||||||
|
if os.path.exists(final_output_path):
|
||||||
|
if not prompt or Confirm.ask(
|
||||||
|
f"File exists({final_output_path}) would you like to overwrite it",
|
||||||
|
default=True,
|
||||||
|
):
|
||||||
|
# move file to dest
|
||||||
|
os.remove(final_output_path)
|
||||||
|
shutil.move(temp_output_path, final_output_path)
|
||||||
|
else:
|
||||||
|
shutil.move(temp_output_path, final_output_path)
|
||||||
|
# clean up
|
||||||
|
if clean:
|
||||||
|
print("[cyan]Cleaning original files...[/]")
|
||||||
|
os.remove(video_path)
|
||||||
|
os.remove(sub_path)
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"[green bold]Subtitles merged successfully.[/] Output file: {final_output_path}"
|
||||||
|
)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"[red bold]Error[/] during merging subtitles: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[red bold]An error[/] occurred: {e}")
|
||||||
|
|
||||||
# WARN: May remove this legacy functionality
|
# WARN: May remove this legacy functionality
|
||||||
def download_file(self, url: str, title, silent=True):
|
def download_file(self, url: str, title, silent=True):
|
||||||
|
|||||||
@@ -30,10 +30,9 @@ def anime_title_percentage_match(
|
|||||||
Returns:
|
Returns:
|
||||||
int: the percentage match
|
int: the percentage match
|
||||||
"""
|
"""
|
||||||
if normalized_anime_title := anime_normalizer.get(
|
possible_user_requested_anime_title = anime_normalizer.get(
|
||||||
possible_user_requested_anime_title
|
possible_user_requested_anime_title, possible_user_requested_anime_title
|
||||||
):
|
)
|
||||||
possible_user_requested_anime_title = normalized_anime_title
|
|
||||||
# compares both the romaji and english names and gets highest Score
|
# compares both the romaji and english names and gets highest Score
|
||||||
title_a = str(anime["title"]["romaji"])
|
title_a = str(anime["title"]["romaji"])
|
||||||
title_b = str(anime["title"]["english"])
|
title_b = str(anime["title"]["english"])
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ if sys.version_info < (3, 10):
|
|||||||
) # noqa: F541
|
) # noqa: F541
|
||||||
|
|
||||||
|
|
||||||
__version__ = "v2.3.1"
|
__version__ = "v2.5.1"
|
||||||
|
|
||||||
APP_NAME = "FastAnime"
|
APP_NAME = "FastAnime"
|
||||||
AUTHOR = "Benex254"
|
AUTHOR = "Benex254"
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import click
|
|||||||
|
|
||||||
from .. import __version__
|
from .. import __version__
|
||||||
from ..libs.anime_provider import SERVERS_AVAILABLE, anime_sources
|
from ..libs.anime_provider import SERVERS_AVAILABLE, anime_sources
|
||||||
from ..Utility.data import anilist_sort_normalizer
|
|
||||||
from .commands import LazyGroup
|
from .commands import LazyGroup
|
||||||
|
|
||||||
commands = {
|
commands = {
|
||||||
@@ -41,6 +40,7 @@ signal.signal(signal.SIGINT, handle_exit)
|
|||||||
short_help="Stream Anime",
|
short_help="Stream Anime",
|
||||||
)
|
)
|
||||||
@click.version_option(__version__, "--version")
|
@click.version_option(__version__, "--version")
|
||||||
|
@click.option("--manga", "-m", help="Enable manga mode", is_flag=True)
|
||||||
@click.option("--log", help="Allow logging to stdout", is_flag=True)
|
@click.option("--log", help="Allow logging to stdout", is_flag=True)
|
||||||
@click.option("--log-file", help="Allow logging to a file", is_flag=True)
|
@click.option("--log-file", help="Allow logging to a file", is_flag=True)
|
||||||
@click.option("--rich-traceback", help="Use rich to output tracebacks", is_flag=True)
|
@click.option("--rich-traceback", help="Use rich to output tracebacks", is_flag=True)
|
||||||
@@ -116,9 +116,9 @@ signal.signal(signal.SIGINT, handle_exit)
|
|||||||
help="Auto select anime title?",
|
help="Auto select anime title?",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"-S",
|
"--normalize-titles/--no-normalize-titles",
|
||||||
"--sort-by",
|
type=bool,
|
||||||
type=click.Choice(anilist_sort_normalizer.keys()), # pyright: ignore
|
help="whether to normalize anime and episode titls given by providers",
|
||||||
)
|
)
|
||||||
@click.option("-d", "--downloads-dir", type=click.Path(), help="Downloads location")
|
@click.option("-d", "--downloads-dir", type=click.Path(), help="Downloads location")
|
||||||
@click.option("--fzf", is_flag=True, help="Use fzf for the ui")
|
@click.option("--fzf", is_flag=True, help="Use fzf for the ui")
|
||||||
@@ -145,12 +145,13 @@ signal.signal(signal.SIGINT, handle_exit)
|
|||||||
type=click.Path(),
|
type=click.Path(),
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--use-mpv-mod/--use-default-player", help="Whether to use python-mpv", type=bool
|
"--use-python-mpv/--use-default-player", help="Whether to use python-mpv", type=bool
|
||||||
)
|
)
|
||||||
@click.option("--sync-play", "-sp", help="Use sync play", is_flag=True)
|
@click.option("--sync-play", "-sp", help="Use sync play", is_flag=True)
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def run_cli(
|
def run_cli(
|
||||||
ctx: click.Context,
|
ctx: click.Context,
|
||||||
|
manga,
|
||||||
log,
|
log,
|
||||||
log_file,
|
log_file,
|
||||||
rich_traceback,
|
rich_traceback,
|
||||||
@@ -165,7 +166,7 @@ def run_cli(
|
|||||||
quality,
|
quality,
|
||||||
auto_next,
|
auto_next,
|
||||||
auto_select,
|
auto_select,
|
||||||
sort_by,
|
normalize_titles,
|
||||||
downloads_dir,
|
downloads_dir,
|
||||||
fzf,
|
fzf,
|
||||||
default,
|
default,
|
||||||
@@ -178,12 +179,13 @@ def run_cli(
|
|||||||
rofi_theme,
|
rofi_theme,
|
||||||
rofi_theme_confirm,
|
rofi_theme_confirm,
|
||||||
rofi_theme_input,
|
rofi_theme_input,
|
||||||
use_mpv_mod,
|
use_python_mpv,
|
||||||
sync_play,
|
sync_play,
|
||||||
):
|
):
|
||||||
from .config import Config
|
from .config import Config
|
||||||
|
|
||||||
ctx.obj = Config()
|
ctx.obj = Config()
|
||||||
|
ctx.obj.manga = manga
|
||||||
if log:
|
if log:
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -192,7 +194,7 @@ def run_cli(
|
|||||||
FORMAT = "%(message)s"
|
FORMAT = "%(message)s"
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level="NOTSET", format=FORMAT, datefmt="[%X]", handlers=[RichHandler()]
|
level=logging.DEBUG, format=FORMAT, datefmt="[%X]", handlers=[RichHandler()]
|
||||||
)
|
)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
logger.info("logging has been initialized")
|
logger.info("logging has been initialized")
|
||||||
@@ -209,6 +211,10 @@ def run_cli(
|
|||||||
datefmt="[%d/%m/%Y@%H:%M:%S]",
|
datefmt="[%d/%m/%Y@%H:%M:%S]",
|
||||||
filemode="w",
|
filemode="w",
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.CRITICAL)
|
||||||
if rich_traceback:
|
if rich_traceback:
|
||||||
from rich.traceback import install
|
from rich.traceback import install
|
||||||
|
|
||||||
@@ -217,7 +223,10 @@ def run_cli(
|
|||||||
if sync_play:
|
if sync_play:
|
||||||
ctx.obj.sync_play = sync_play
|
ctx.obj.sync_play = sync_play
|
||||||
if provider:
|
if provider:
|
||||||
|
import os
|
||||||
|
|
||||||
ctx.obj.provider = provider
|
ctx.obj.provider = provider
|
||||||
|
os.environ["CURRENT_FASTANIME_PROVIDER"] = provider
|
||||||
if server:
|
if server:
|
||||||
ctx.obj.server = server
|
ctx.obj.server = server
|
||||||
if format:
|
if format:
|
||||||
@@ -228,6 +237,11 @@ def run_cli(
|
|||||||
ctx.obj.continue_from_history = continue_
|
ctx.obj.continue_from_history = continue_
|
||||||
if ctx.get_parameter_source("skip") == click.core.ParameterSource.COMMANDLINE:
|
if ctx.get_parameter_source("skip") == click.core.ParameterSource.COMMANDLINE:
|
||||||
ctx.obj.skip = skip
|
ctx.obj.skip = skip
|
||||||
|
if (
|
||||||
|
ctx.get_parameter_source("normalize_titles")
|
||||||
|
== click.core.ParameterSource.COMMANDLINE
|
||||||
|
):
|
||||||
|
ctx.obj.normalize_titles = normalize_titles
|
||||||
|
|
||||||
if quality:
|
if quality:
|
||||||
ctx.obj.quality = quality
|
ctx.obj.quality = quality
|
||||||
@@ -246,20 +260,19 @@ def run_cli(
|
|||||||
):
|
):
|
||||||
ctx.obj.auto_select = auto_select
|
ctx.obj.auto_select = auto_select
|
||||||
if (
|
if (
|
||||||
ctx.get_parameter_source("use_mpv_mod")
|
ctx.get_parameter_source("use_python_mpv")
|
||||||
== click.core.ParameterSource.COMMANDLINE
|
== click.core.ParameterSource.COMMANDLINE
|
||||||
):
|
):
|
||||||
ctx.obj.use_mpv_mod = use_mpv_mod
|
ctx.obj.use_python_mpv = use_python_mpv
|
||||||
if sort_by:
|
|
||||||
ctx.obj.sort_by = sort_by
|
|
||||||
if downloads_dir:
|
if downloads_dir:
|
||||||
ctx.obj.downloads_dir = downloads_dir
|
ctx.obj.downloads_dir = downloads_dir
|
||||||
if translation_type:
|
if translation_type:
|
||||||
ctx.obj.translation_type = translation_type
|
ctx.obj.translation_type = translation_type
|
||||||
if fzf:
|
|
||||||
ctx.obj.use_fzf = True
|
|
||||||
if default:
|
if default:
|
||||||
ctx.obj.use_fzf = False
|
ctx.obj.use_fzf = False
|
||||||
|
ctx.obj.use_rofi = False
|
||||||
|
if fzf:
|
||||||
|
ctx.obj.use_fzf = True
|
||||||
if preview:
|
if preview:
|
||||||
ctx.obj.preview = True
|
ctx.obj.preview = True
|
||||||
if no_preview:
|
if no_preview:
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ commands = {
|
|||||||
"completed": "completed.completed",
|
"completed": "completed.completed",
|
||||||
"planning": "planning.planning",
|
"planning": "planning.planning",
|
||||||
"notifier": "notifier.notifier",
|
"notifier": "notifier.notifier",
|
||||||
|
"stats": "stats.stats",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,16 +7,23 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
@click.command(help="View anime you completed")
|
@click.command(help="View anime you completed")
|
||||||
|
@click.option(
|
||||||
|
"--dump-json",
|
||||||
|
"-d",
|
||||||
|
is_flag=True,
|
||||||
|
help="Only print out the results dont open anilist menu",
|
||||||
|
)
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def completed(config: "Config"):
|
def completed(config: "Config", dump_json):
|
||||||
|
from sys import exit
|
||||||
|
|
||||||
from ....anilist import AniList
|
from ....anilist import AniList
|
||||||
from ...interfaces import anilist_interfaces
|
from ...utils.tools import FastAnimeRuntimeState
|
||||||
from ...utils.tools import FastAnimeRuntimeState, exit_app
|
|
||||||
|
|
||||||
if not config.user:
|
if not config.user:
|
||||||
print("Not authenticated")
|
print("Not authenticated")
|
||||||
print("Please run: fastanime anilist loggin")
|
print("Please run: fastanime anilist loggin")
|
||||||
exit_app()
|
exit(1)
|
||||||
anime_list = AniList.get_anime_list("COMPLETED")
|
anime_list = AniList.get_anime_list("COMPLETED")
|
||||||
if not anime_list or not anime_list[1]:
|
if not anime_list or not anime_list[1]:
|
||||||
return
|
return
|
||||||
@@ -27,6 +34,13 @@ def completed(config: "Config"):
|
|||||||
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
|
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
|
||||||
] # pyright:ignore
|
] # pyright:ignore
|
||||||
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
|
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
|
||||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
if dump_json:
|
||||||
fastanime_runtime_state.anilist_data = anime_list[1]
|
import json
|
||||||
anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state)
|
|
||||||
|
print(json.dumps(anime_list))
|
||||||
|
else:
|
||||||
|
from ...interfaces import anilist_interfaces
|
||||||
|
|
||||||
|
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||||
|
fastanime_runtime_state.anilist_results_data = anime_list[1]
|
||||||
|
anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state)
|
||||||
|
|||||||
@@ -7,26 +7,40 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
@click.command(help="View anime you dropped")
|
@click.command(help="View anime you dropped")
|
||||||
|
@click.option(
|
||||||
|
"--dump-json",
|
||||||
|
"-d",
|
||||||
|
is_flag=True,
|
||||||
|
help="Only print out the results dont open anilist menu",
|
||||||
|
)
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def dropped(config: "Config"):
|
def dropped(config: "Config", dump_json):
|
||||||
|
from sys import exit
|
||||||
|
|
||||||
from ....anilist import AniList
|
from ....anilist import AniList
|
||||||
from ...interfaces import anilist_interfaces
|
|
||||||
from ...utils.tools import FastAnimeRuntimeState, exit_app
|
|
||||||
|
|
||||||
if not config.user:
|
if not config.user:
|
||||||
print("Not authenticated")
|
print("Not authenticated")
|
||||||
print("Please run: fastanime anilist loggin")
|
print("Please run: fastanime anilist loggin")
|
||||||
exit_app()
|
exit(1)
|
||||||
anime_list = AniList.get_anime_list("DROPPED")
|
anime_list = AniList.get_anime_list("DROPPED")
|
||||||
if not anime_list:
|
if not anime_list:
|
||||||
return
|
exit(1)
|
||||||
if not anime_list[0] or not anime_list[1]:
|
if not anime_list[0] or not anime_list[1]:
|
||||||
return
|
exit(1)
|
||||||
media = [
|
media = [
|
||||||
mediaListItem["media"]
|
mediaListItem["media"]
|
||||||
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
|
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
|
||||||
] # pyright:ignore
|
] # pyright:ignore
|
||||||
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
|
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
|
||||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
if dump_json:
|
||||||
fastanime_runtime_state.anilist_data = anime_list[1]
|
import json
|
||||||
anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state)
|
|
||||||
|
print(json.dumps(anime_list[1]))
|
||||||
|
else:
|
||||||
|
from ...interfaces import anilist_interfaces
|
||||||
|
from ...utils.tools import FastAnimeRuntimeState
|
||||||
|
|
||||||
|
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||||
|
fastanime_runtime_state.anilist_results_data = anime_list[1]
|
||||||
|
anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state)
|
||||||
|
|||||||
@@ -5,14 +5,30 @@ import click
|
|||||||
help="Fetch the top 15 most favourited anime from anilist",
|
help="Fetch the top 15 most favourited anime from anilist",
|
||||||
short_help="View most favourited anime",
|
short_help="View most favourited anime",
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
"--dump-json",
|
||||||
|
"-d",
|
||||||
|
is_flag=True,
|
||||||
|
help="Only print out the results dont open anilist menu",
|
||||||
|
)
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def favourites(config):
|
def favourites(config, dump_json):
|
||||||
from ....anilist import AniList
|
from ....anilist import AniList
|
||||||
from ...interfaces.anilist_interfaces import anilist_results_menu
|
|
||||||
from ...utils.tools import FastAnimeRuntimeState
|
|
||||||
|
|
||||||
anime_data = AniList.get_most_favourite()
|
anime_data = AniList.get_most_favourite()
|
||||||
if anime_data[0]:
|
if anime_data[0]:
|
||||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
if dump_json:
|
||||||
fastanime_runtime_state.anilist_data = anime_data[1]
|
import json
|
||||||
anilist_results_menu(config, fastanime_runtime_state)
|
|
||||||
|
print(json.dumps(anime_data[1]))
|
||||||
|
else:
|
||||||
|
from ...interfaces.anilist_interfaces import anilist_results_menu
|
||||||
|
from ...utils.tools import FastAnimeRuntimeState
|
||||||
|
|
||||||
|
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||||
|
fastanime_runtime_state.anilist_results_data = anime_data[1]
|
||||||
|
anilist_results_menu(config, fastanime_runtime_state)
|
||||||
|
else:
|
||||||
|
from sys import exit
|
||||||
|
|
||||||
|
exit(1)
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ if TYPE_CHECKING:
|
|||||||
@click.option("--erase", "-e", help="Erase your login details", is_flag=True)
|
@click.option("--erase", "-e", help="Erase your login details", is_flag=True)
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def login(config: "Config", status, erase):
|
def login(config: "Config", status, erase):
|
||||||
|
from sys import exit
|
||||||
|
|
||||||
from rich import print
|
from rich import print
|
||||||
from rich.prompt import Confirm, Prompt
|
from rich.prompt import Confirm, Prompt
|
||||||
|
|
||||||
from ...utils.tools import exit_app
|
|
||||||
|
|
||||||
if status:
|
if status:
|
||||||
is_logged_in = True if config.user else False
|
is_logged_in = True if config.user else False
|
||||||
message = (
|
message = (
|
||||||
@@ -23,16 +23,16 @@ def login(config: "Config", status, erase):
|
|||||||
)
|
)
|
||||||
print(message)
|
print(message)
|
||||||
print(config.user)
|
print(config.user)
|
||||||
exit_app()
|
exit(0)
|
||||||
elif erase:
|
elif erase:
|
||||||
if Confirm.ask(
|
if Confirm.ask(
|
||||||
"Are you sure you want to erase your login status", default=False
|
"Are you sure you want to erase your login status", default=False
|
||||||
):
|
):
|
||||||
config.update_user({})
|
config.update_user({})
|
||||||
print("Success")
|
print("Success")
|
||||||
exit_app(0)
|
exit(0)
|
||||||
else:
|
else:
|
||||||
exit_app(1)
|
exit(1)
|
||||||
else:
|
else:
|
||||||
from click import launch
|
from click import launch
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ def login(config: "Config", status, erase):
|
|||||||
if config.user:
|
if config.user:
|
||||||
print("Already logged in :confused:")
|
print("Already logged in :confused:")
|
||||||
if not Confirm.ask("or would you like to reloggin", default=True):
|
if not Confirm.ask("or would you like to reloggin", default=True):
|
||||||
exit_app()
|
exit(0)
|
||||||
# ---- new loggin -----
|
# ---- new loggin -----
|
||||||
print(
|
print(
|
||||||
f"A browser session will be opened ( [link]{config.fastanime_anilist_app_login_url}[/link] )",
|
f"A browser session will be opened ( [link]{config.fastanime_anilist_app_login_url}[/link] )",
|
||||||
@@ -52,10 +52,10 @@ def login(config: "Config", status, erase):
|
|||||||
user = AniList.login_user(token)
|
user = AniList.login_user(token)
|
||||||
if not user:
|
if not user:
|
||||||
print("Sth went wrong", user)
|
print("Sth went wrong", user)
|
||||||
exit_app()
|
exit(1)
|
||||||
return
|
return
|
||||||
user["token"] = token
|
user["token"] = token
|
||||||
config.update_user(user)
|
config.update_user(user)
|
||||||
print("Successfully saved credentials")
|
print("Successfully saved credentials")
|
||||||
print(user)
|
print(user)
|
||||||
exit_app()
|
exit(0)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ def notifier(config: "Config"):
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
|
from sys import exit
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from plyer import notification
|
from plyer import notification
|
||||||
@@ -30,7 +31,7 @@ def notifier(config: "Config"):
|
|||||||
if not config.user:
|
if not config.user:
|
||||||
print("Not Authenticated")
|
print("Not Authenticated")
|
||||||
print("Run the following to get started: fastanime anilist loggin")
|
print("Run the following to get started: fastanime anilist loggin")
|
||||||
return
|
exit(1)
|
||||||
run = True
|
run = True
|
||||||
# WARNING: Mess around with this value at your own risk
|
# WARNING: Mess around with this value at your own risk
|
||||||
timeout = 2 # time is in minutes
|
timeout = 2 # time is in minutes
|
||||||
|
|||||||
@@ -7,26 +7,40 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
@click.command(help="View anime you paused on watching")
|
@click.command(help="View anime you paused on watching")
|
||||||
|
@click.option(
|
||||||
|
"--dump-json",
|
||||||
|
"-d",
|
||||||
|
is_flag=True,
|
||||||
|
help="Only print out the results dont open anilist menu",
|
||||||
|
)
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def paused(config: "Config"):
|
def paused(config: "Config", dump_json):
|
||||||
|
from sys import exit
|
||||||
|
|
||||||
from ....anilist import AniList
|
from ....anilist import AniList
|
||||||
from ...interfaces import anilist_interfaces
|
|
||||||
from ...utils.tools import FastAnimeRuntimeState, exit_app
|
|
||||||
|
|
||||||
if not config.user:
|
if not config.user:
|
||||||
print("Not authenticated")
|
print("Not authenticated")
|
||||||
print("Please run: fastanime anilist loggin")
|
print("Please run: fastanime anilist loggin")
|
||||||
exit_app()
|
exit(1)
|
||||||
anime_list = AniList.get_anime_list("PAUSED")
|
anime_list = AniList.get_anime_list("PAUSED")
|
||||||
if not anime_list:
|
if not anime_list:
|
||||||
return
|
exit(1)
|
||||||
if not anime_list[0] or not anime_list[1]:
|
if not anime_list[0] or not anime_list[1]:
|
||||||
return
|
exit(1)
|
||||||
media = [
|
media = [
|
||||||
mediaListItem["media"]
|
mediaListItem["media"]
|
||||||
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
|
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
|
||||||
] # pyright:ignore
|
] # pyright:ignore
|
||||||
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
|
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
|
||||||
anilist_config = FastAnimeRuntimeState()
|
if dump_json:
|
||||||
anilist_config.data = anime_list[1]
|
import json
|
||||||
anilist_interfaces.anilist_results_menu(config, anilist_config)
|
|
||||||
|
print(json.dumps(anime_list[1]))
|
||||||
|
else:
|
||||||
|
from ...interfaces import anilist_interfaces
|
||||||
|
from ...utils.tools import FastAnimeRuntimeState
|
||||||
|
|
||||||
|
anilist_config = FastAnimeRuntimeState()
|
||||||
|
anilist_config.anilist_results_data = anime_list[1]
|
||||||
|
anilist_interfaces.anilist_results_menu(config, anilist_config)
|
||||||
|
|||||||
@@ -7,26 +7,40 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
@click.command(help="View anime you are planning on watching")
|
@click.command(help="View anime you are planning on watching")
|
||||||
|
@click.option(
|
||||||
|
"--dump-json",
|
||||||
|
"-d",
|
||||||
|
is_flag=True,
|
||||||
|
help="Only print out the results dont open anilist menu",
|
||||||
|
)
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def planning(config: "Config"):
|
def planning(config: "Config", dump_json):
|
||||||
|
from sys import exit
|
||||||
|
|
||||||
from ....anilist import AniList
|
from ....anilist import AniList
|
||||||
from ...interfaces import anilist_interfaces
|
|
||||||
from ...utils.tools import FastAnimeRuntimeState, exit_app
|
|
||||||
|
|
||||||
if not config.user:
|
if not config.user:
|
||||||
print("Not authenticated")
|
print("Not authenticated")
|
||||||
print("Please run: fastanime anilist loggin")
|
print("Please run: fastanime anilist loggin")
|
||||||
exit_app()
|
exit(1)
|
||||||
anime_list = AniList.get_anime_list("PLANNING")
|
anime_list = AniList.get_anime_list("PLANNING")
|
||||||
if not anime_list:
|
if not anime_list:
|
||||||
return
|
exit(1)
|
||||||
if not anime_list[0] or not anime_list[1]:
|
if not anime_list[0] or not anime_list[1]:
|
||||||
return
|
exit(1)
|
||||||
media = [
|
media = [
|
||||||
mediaListItem["media"]
|
mediaListItem["media"]
|
||||||
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
|
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
|
||||||
] # pyright:ignore
|
] # pyright:ignore
|
||||||
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
|
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
|
||||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
if dump_json:
|
||||||
fastanime_runtime_state.anilist_data = anime_list[1]
|
import json
|
||||||
anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state)
|
|
||||||
|
print(json.dumps(anime_list[1]))
|
||||||
|
else:
|
||||||
|
from ...interfaces import anilist_interfaces
|
||||||
|
from ...utils.tools import FastAnimeRuntimeState
|
||||||
|
|
||||||
|
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||||
|
fastanime_runtime_state.anilist_results_data = anime_list[1]
|
||||||
|
anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state)
|
||||||
|
|||||||
@@ -4,14 +4,30 @@ import click
|
|||||||
@click.command(
|
@click.command(
|
||||||
help="Fetch the top 15 most popular anime", short_help="View most popular anime"
|
help="Fetch the top 15 most popular anime", short_help="View most popular anime"
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
"--dump-json",
|
||||||
|
"-d",
|
||||||
|
is_flag=True,
|
||||||
|
help="Only print out the results dont open anilist menu",
|
||||||
|
)
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def popular(config):
|
def popular(config, dump_json):
|
||||||
from ....anilist import AniList
|
from ....anilist import AniList
|
||||||
from ...interfaces.anilist_interfaces import anilist_results_menu
|
|
||||||
from ...utils.tools import FastAnimeRuntimeState
|
|
||||||
|
|
||||||
anime_data = AniList.get_most_popular()
|
anime_data = AniList.get_most_popular()
|
||||||
if anime_data[0]:
|
if anime_data[0]:
|
||||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
if dump_json:
|
||||||
fastanime_runtime_state.anilist_data = anime_data[1]
|
import json
|
||||||
anilist_results_menu(config, fastanime_runtime_state)
|
|
||||||
|
print(json.dumps(anime_data[1]))
|
||||||
|
else:
|
||||||
|
from ...interfaces.anilist_interfaces import anilist_results_menu
|
||||||
|
from ...utils.tools import FastAnimeRuntimeState
|
||||||
|
|
||||||
|
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||||
|
fastanime_runtime_state.anilist_results_data = anime_data[1]
|
||||||
|
anilist_results_menu(config, fastanime_runtime_state)
|
||||||
|
else:
|
||||||
|
from sys import exit
|
||||||
|
|
||||||
|
exit(1)
|
||||||
|
|||||||
@@ -5,23 +5,35 @@ import click
|
|||||||
help="Get random anime from anilist based on a range of anilist anime ids that are seected at random",
|
help="Get random anime from anilist based on a range of anilist anime ids that are seected at random",
|
||||||
short_help="View random anime",
|
short_help="View random anime",
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
"--dump-json",
|
||||||
|
"-d",
|
||||||
|
is_flag=True,
|
||||||
|
help="Only print out the results dont open anilist menu",
|
||||||
|
)
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def random_anime(config):
|
def random_anime(config, dump_json):
|
||||||
import random
|
import random
|
||||||
|
|
||||||
from ....anilist import AniList
|
from ....anilist import AniList
|
||||||
from ...interfaces.anilist_interfaces import anilist_results_menu
|
|
||||||
from ...utils.tools import FastAnimeRuntimeState
|
|
||||||
|
|
||||||
random_anime = range(1, 15000)
|
random_anime = range(1, 100000)
|
||||||
|
|
||||||
random_anime = random.sample(random_anime, k=50)
|
random_anime = random.sample(random_anime, k=50)
|
||||||
|
|
||||||
anime_data = AniList.search(id_in=list(random_anime))
|
anime_data = AniList.search(id_in=list(random_anime))
|
||||||
|
|
||||||
if anime_data[0]:
|
if anime_data[0]:
|
||||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
if dump_json:
|
||||||
fastanime_runtime_state.anilist_data = anime_data[1]
|
import json
|
||||||
anilist_results_menu(config, fastanime_runtime_state)
|
|
||||||
|
print(json.dumps(anime_data[1]))
|
||||||
|
else:
|
||||||
|
from ...interfaces.anilist_interfaces import anilist_results_menu
|
||||||
|
from ...utils.tools import FastAnimeRuntimeState
|
||||||
|
|
||||||
|
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||||
|
fastanime_runtime_state.anilist_results_data = anime_data[1]
|
||||||
|
anilist_results_menu(config, fastanime_runtime_state)
|
||||||
else:
|
else:
|
||||||
print(anime_data[1])
|
exit(1)
|
||||||
|
|||||||
@@ -5,14 +5,30 @@ import click
|
|||||||
help="Fetch the 15 most recently updated anime from anilist that are currently releasing",
|
help="Fetch the 15 most recently updated anime from anilist that are currently releasing",
|
||||||
short_help="View recently updated anime",
|
short_help="View recently updated anime",
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
"--dump-json",
|
||||||
|
"-d",
|
||||||
|
is_flag=True,
|
||||||
|
help="Only print out the results dont open anilist menu",
|
||||||
|
)
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def recent(config):
|
def recent(config, dump_json):
|
||||||
from ....anilist import AniList
|
from ....anilist import AniList
|
||||||
from ...interfaces.anilist_interfaces import anilist_results_menu
|
|
||||||
from ...utils.tools import FastAnimeRuntimeState
|
|
||||||
|
|
||||||
anime_data = AniList.get_most_recently_updated()
|
anime_data = AniList.get_most_recently_updated()
|
||||||
if anime_data[0]:
|
if anime_data[0]:
|
||||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
if dump_json:
|
||||||
fastanime_runtime_state.anilist_data = anime_data[1]
|
import json
|
||||||
anilist_results_menu(config, fastanime_runtime_state)
|
|
||||||
|
print(json.dumps(anime_data[1]))
|
||||||
|
else:
|
||||||
|
from ...interfaces.anilist_interfaces import anilist_results_menu
|
||||||
|
from ...utils.tools import FastAnimeRuntimeState
|
||||||
|
|
||||||
|
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||||
|
fastanime_runtime_state.anilist_results_data = anime_data[1]
|
||||||
|
anilist_results_menu(config, fastanime_runtime_state)
|
||||||
|
else:
|
||||||
|
from sys import exit
|
||||||
|
|
||||||
|
exit(1)
|
||||||
|
|||||||
@@ -7,26 +7,40 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
@click.command(help="View anime you are rewatching")
|
@click.command(help="View anime you are rewatching")
|
||||||
|
@click.option(
|
||||||
|
"--dump-json",
|
||||||
|
"-d",
|
||||||
|
is_flag=True,
|
||||||
|
help="Only print out the results dont open anilist menu",
|
||||||
|
)
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def rewatching(config: "Config"):
|
def rewatching(config: "Config", dump_json):
|
||||||
|
from sys import exit
|
||||||
|
|
||||||
from ....anilist import AniList
|
from ....anilist import AniList
|
||||||
from ...interfaces import anilist_interfaces
|
|
||||||
from ...utils.tools import FastAnimeRuntimeState, exit_app
|
|
||||||
|
|
||||||
if not config.user:
|
if not config.user:
|
||||||
print("Not authenticated")
|
print("Not authenticated")
|
||||||
print("Please run: fastanime anilist loggin")
|
print("Please run: fastanime anilist loggin")
|
||||||
exit_app()
|
exit(1)
|
||||||
anime_list = AniList.get_anime_list("REPEATING")
|
anime_list = AniList.get_anime_list("REPEATING")
|
||||||
if not anime_list:
|
if not anime_list:
|
||||||
return
|
exit(1)
|
||||||
if not anime_list[0] or not anime_list[1]:
|
if not anime_list[0] or not anime_list[1]:
|
||||||
return
|
exit(1)
|
||||||
media = [
|
media = [
|
||||||
mediaListItem["media"]
|
mediaListItem["media"]
|
||||||
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
|
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
|
||||||
] # pyright:ignore
|
] # pyright:ignore
|
||||||
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
|
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
|
||||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
if dump_json:
|
||||||
fastanime_runtime_state.anilist_data = anime_list[1]
|
import json
|
||||||
anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state)
|
|
||||||
|
print(json.dumps(anime_list[1]))
|
||||||
|
else:
|
||||||
|
from ...interfaces import anilist_interfaces
|
||||||
|
from ...utils.tools import FastAnimeRuntimeState
|
||||||
|
|
||||||
|
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||||
|
fastanime_runtime_state.anilist_results_data = anime_list[1]
|
||||||
|
anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state)
|
||||||
|
|||||||
@@ -4,14 +4,30 @@ import click
|
|||||||
@click.command(
|
@click.command(
|
||||||
help="Fetch the 15 most scored anime", short_help="View most scored anime"
|
help="Fetch the 15 most scored anime", short_help="View most scored anime"
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
"--dump-json",
|
||||||
|
"-d",
|
||||||
|
is_flag=True,
|
||||||
|
help="Only print out the results dont open anilist menu",
|
||||||
|
)
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def scores(config):
|
def scores(config, dump_json):
|
||||||
from ....anilist import AniList
|
from ....anilist import AniList
|
||||||
from ...interfaces.anilist_interfaces import anilist_results_menu
|
|
||||||
from ...utils.tools import FastAnimeRuntimeState
|
|
||||||
|
|
||||||
anime_data = AniList.get_most_scored()
|
anime_data = AniList.get_most_scored()
|
||||||
if anime_data[0]:
|
if anime_data[0]:
|
||||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
if dump_json:
|
||||||
fastanime_runtime_state.data = anime_data[1]
|
import json
|
||||||
anilist_results_menu(config, fastanime_runtime_state)
|
|
||||||
|
print(json.dumps(anime_data[1]))
|
||||||
|
else:
|
||||||
|
from ...interfaces.anilist_interfaces import anilist_results_menu
|
||||||
|
from ...utils.tools import FastAnimeRuntimeState
|
||||||
|
|
||||||
|
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||||
|
fastanime_runtime_state.anilist_results_data = anime_data[1]
|
||||||
|
anilist_results_menu(config, fastanime_runtime_state)
|
||||||
|
else:
|
||||||
|
from sys import exit
|
||||||
|
|
||||||
|
exit(1)
|
||||||
|
|||||||
@@ -2,20 +2,573 @@ import click
|
|||||||
|
|
||||||
from ...completion_functions import anime_titles_shell_complete
|
from ...completion_functions import anime_titles_shell_complete
|
||||||
|
|
||||||
|
tags_available = {
|
||||||
|
"Cast": ["Polyamorous"],
|
||||||
|
"Cast Main Cast": [
|
||||||
|
"Anti-Hero",
|
||||||
|
"Elderly Protagonist",
|
||||||
|
"Ensemble Cast",
|
||||||
|
"Estranged Family",
|
||||||
|
"Female Protagonist",
|
||||||
|
"Male Protagonist",
|
||||||
|
"Primarily Adult Cast",
|
||||||
|
"Primarily Animal Cast",
|
||||||
|
"Primarily Child Cast",
|
||||||
|
"Primarily Female Cast",
|
||||||
|
"Primarily Male Cast",
|
||||||
|
"Primarily Teen Cast",
|
||||||
|
],
|
||||||
|
"Cast Traits": [
|
||||||
|
"Age Regression",
|
||||||
|
"Agender",
|
||||||
|
"Aliens",
|
||||||
|
"Amnesia",
|
||||||
|
"Angels",
|
||||||
|
"Anthropomorphism",
|
||||||
|
"Aromantic",
|
||||||
|
"Arranged Marriage",
|
||||||
|
"Artificial Intelligence",
|
||||||
|
"Asexual",
|
||||||
|
"Butler",
|
||||||
|
"Centaur",
|
||||||
|
"Chimera",
|
||||||
|
"Chuunibyou",
|
||||||
|
"Clone",
|
||||||
|
"Cosplay",
|
||||||
|
"Cowboys",
|
||||||
|
"Crossdressing",
|
||||||
|
"Cyborg",
|
||||||
|
"Delinquents",
|
||||||
|
"Demons",
|
||||||
|
"Detective",
|
||||||
|
"Dinosaurs",
|
||||||
|
"Disability",
|
||||||
|
"Dissociative Identities",
|
||||||
|
"Dragons",
|
||||||
|
"Dullahan",
|
||||||
|
"Elf",
|
||||||
|
"Fairy",
|
||||||
|
"Femboy",
|
||||||
|
"Ghost",
|
||||||
|
"Goblin",
|
||||||
|
"Gods",
|
||||||
|
"Gyaru",
|
||||||
|
"Hikikomori",
|
||||||
|
"Homeless",
|
||||||
|
"Idol",
|
||||||
|
"Kemonomimi",
|
||||||
|
"Kuudere",
|
||||||
|
"Maids",
|
||||||
|
"Mermaid",
|
||||||
|
"Monster Boy",
|
||||||
|
"Monster Girl",
|
||||||
|
"Nekomimi",
|
||||||
|
"Ninja",
|
||||||
|
"Nudity",
|
||||||
|
"Nun",
|
||||||
|
"Office Lady",
|
||||||
|
"Oiran",
|
||||||
|
"Ojou-sama",
|
||||||
|
"Orphan",
|
||||||
|
"Pirates",
|
||||||
|
"Robots",
|
||||||
|
"Samurai",
|
||||||
|
"Shrine Maiden",
|
||||||
|
"Skeleton",
|
||||||
|
"Succubus",
|
||||||
|
"Tanned Skin",
|
||||||
|
"Teacher",
|
||||||
|
"Tomboy",
|
||||||
|
"Transgender",
|
||||||
|
"Tsundere",
|
||||||
|
"Twins",
|
||||||
|
"Vampire",
|
||||||
|
"Veterinarian",
|
||||||
|
"Vikings",
|
||||||
|
"Villainess",
|
||||||
|
"VTuber",
|
||||||
|
"Werewolf",
|
||||||
|
"Witch",
|
||||||
|
"Yandere",
|
||||||
|
"Zombie",
|
||||||
|
],
|
||||||
|
"Demographic": ["Josei", "Kids", "Seinen", "Shoujo", "Shounen"],
|
||||||
|
"Setting": ["Matriarchy"],
|
||||||
|
"Setting Scene": [
|
||||||
|
"Bar",
|
||||||
|
"Boarding School",
|
||||||
|
"Circus",
|
||||||
|
"Coastal",
|
||||||
|
"College",
|
||||||
|
"Desert",
|
||||||
|
"Dungeon",
|
||||||
|
"Foreign",
|
||||||
|
"Inn",
|
||||||
|
"Konbini",
|
||||||
|
"Natural Disaster",
|
||||||
|
"Office",
|
||||||
|
"Outdoor",
|
||||||
|
"Prison",
|
||||||
|
"Restaurant",
|
||||||
|
"Rural",
|
||||||
|
"School",
|
||||||
|
"School Club",
|
||||||
|
"Snowscape",
|
||||||
|
"Urban",
|
||||||
|
"Work",
|
||||||
|
],
|
||||||
|
"Setting Time": [
|
||||||
|
"Achronological Order",
|
||||||
|
"Anachronism",
|
||||||
|
"Ancient China",
|
||||||
|
"Dystopian",
|
||||||
|
"Historical",
|
||||||
|
"Time Skip",
|
||||||
|
],
|
||||||
|
"Setting Universe": [
|
||||||
|
"Afterlife",
|
||||||
|
"Alternate Universe",
|
||||||
|
"Augmented Reality",
|
||||||
|
"Omegaverse",
|
||||||
|
"Post-Apocalyptic",
|
||||||
|
"Space",
|
||||||
|
"Urban Fantasy",
|
||||||
|
"Virtual World",
|
||||||
|
],
|
||||||
|
"Technical": [
|
||||||
|
"4-koma",
|
||||||
|
"Achromatic",
|
||||||
|
"Advertisement",
|
||||||
|
"Anthology",
|
||||||
|
"CGI",
|
||||||
|
"Episodic",
|
||||||
|
"Flash",
|
||||||
|
"Full CGI",
|
||||||
|
"Full Color",
|
||||||
|
"No Dialogue",
|
||||||
|
"Non-fiction",
|
||||||
|
"POV",
|
||||||
|
"Puppetry",
|
||||||
|
"Rotoscoping",
|
||||||
|
"Stop Motion",
|
||||||
|
],
|
||||||
|
"Theme Action": [
|
||||||
|
"Archery",
|
||||||
|
"Battle Royale",
|
||||||
|
"Espionage",
|
||||||
|
"Fugitive",
|
||||||
|
"Guns",
|
||||||
|
"Martial Arts",
|
||||||
|
"Spearplay",
|
||||||
|
"Swordplay",
|
||||||
|
],
|
||||||
|
"Theme Arts": [
|
||||||
|
"Acting",
|
||||||
|
"Calligraphy",
|
||||||
|
"Classic Literature",
|
||||||
|
"Drawing",
|
||||||
|
"Fashion",
|
||||||
|
"Food",
|
||||||
|
"Makeup",
|
||||||
|
"Photography",
|
||||||
|
"Rakugo",
|
||||||
|
"Writing",
|
||||||
|
],
|
||||||
|
"Theme Arts-Music": [
|
||||||
|
"Band",
|
||||||
|
"Classical Music",
|
||||||
|
"Dancing",
|
||||||
|
"Hip-hop Music",
|
||||||
|
"Jazz Music",
|
||||||
|
"Metal Music",
|
||||||
|
"Musical Theater",
|
||||||
|
"Rock Music",
|
||||||
|
],
|
||||||
|
"Theme Comedy": ["Parody", "Satire", "Slapstick", "Surreal Comedy"],
|
||||||
|
"Theme Drama": [
|
||||||
|
"Bullying",
|
||||||
|
"Class Struggle",
|
||||||
|
"Coming of Age",
|
||||||
|
"Conspiracy",
|
||||||
|
"Eco-Horror",
|
||||||
|
"Fake Relationship",
|
||||||
|
"Kingdom Management",
|
||||||
|
"Rehabilitation",
|
||||||
|
"Revenge",
|
||||||
|
"Suicide",
|
||||||
|
"Tragedy",
|
||||||
|
],
|
||||||
|
"Theme Fantasy": [
|
||||||
|
"Alchemy",
|
||||||
|
"Body Swapping",
|
||||||
|
"Cultivation",
|
||||||
|
"Fairy Tale",
|
||||||
|
"Henshin",
|
||||||
|
"Isekai",
|
||||||
|
"Kaiju",
|
||||||
|
"Magic",
|
||||||
|
"Mythology",
|
||||||
|
"Necromancy",
|
||||||
|
"Shapeshifting",
|
||||||
|
"Steampunk",
|
||||||
|
"Super Power",
|
||||||
|
"Superhero",
|
||||||
|
"Wuxia",
|
||||||
|
"Youkai",
|
||||||
|
],
|
||||||
|
"Theme Game": ["Board Game", "E-Sports", "Video Games"],
|
||||||
|
"Theme Game-Card & Board Game": [
|
||||||
|
"Card Battle",
|
||||||
|
"Go",
|
||||||
|
"Karuta",
|
||||||
|
"Mahjong",
|
||||||
|
"Poker",
|
||||||
|
"Shogi",
|
||||||
|
],
|
||||||
|
"Theme Game-Sport": [
|
||||||
|
"Acrobatics",
|
||||||
|
"Airsoft",
|
||||||
|
"American Football",
|
||||||
|
"Athletics",
|
||||||
|
"Badminton",
|
||||||
|
"Baseball",
|
||||||
|
"Basketball",
|
||||||
|
"Bowling",
|
||||||
|
"Boxing",
|
||||||
|
"Cheerleading",
|
||||||
|
"Cycling",
|
||||||
|
"Fencing",
|
||||||
|
"Fishing",
|
||||||
|
"Fitness",
|
||||||
|
"Football",
|
||||||
|
"Golf",
|
||||||
|
"Handball",
|
||||||
|
"Ice Skating",
|
||||||
|
"Judo",
|
||||||
|
"Lacrosse",
|
||||||
|
"Parkour",
|
||||||
|
"Rugby",
|
||||||
|
"Scuba Diving",
|
||||||
|
"Skateboarding",
|
||||||
|
"Sumo",
|
||||||
|
"Surfing",
|
||||||
|
"Swimming",
|
||||||
|
"Table Tennis",
|
||||||
|
"Tennis",
|
||||||
|
"Volleyball",
|
||||||
|
"Wrestling",
|
||||||
|
],
|
||||||
|
"Theme Other": [
|
||||||
|
"Adoption",
|
||||||
|
"Animals",
|
||||||
|
"Astronomy",
|
||||||
|
"Autobiographical",
|
||||||
|
"Biographical",
|
||||||
|
"Body Horror",
|
||||||
|
"Cannibalism",
|
||||||
|
"Chibi",
|
||||||
|
"Cosmic Horror",
|
||||||
|
"Crime",
|
||||||
|
"Crossover",
|
||||||
|
"Death Game",
|
||||||
|
"Denpa",
|
||||||
|
"Drugs",
|
||||||
|
"Economics",
|
||||||
|
"Educational",
|
||||||
|
"Environmental",
|
||||||
|
"Ero Guro",
|
||||||
|
"Filmmaking",
|
||||||
|
"Found Family",
|
||||||
|
"Gambling",
|
||||||
|
"Gender Bending",
|
||||||
|
"Gore",
|
||||||
|
"Language Barrier",
|
||||||
|
"LGBTQ+ Themes",
|
||||||
|
"Lost Civilization",
|
||||||
|
"Marriage",
|
||||||
|
"Medicine",
|
||||||
|
"Memory Manipulation",
|
||||||
|
"Meta",
|
||||||
|
"Mountaineering",
|
||||||
|
"Noir",
|
||||||
|
"Otaku Culture",
|
||||||
|
"Pandemic",
|
||||||
|
"Philosophy",
|
||||||
|
"Politics",
|
||||||
|
"Proxy Battle",
|
||||||
|
"Psychosexual",
|
||||||
|
"Reincarnation",
|
||||||
|
"Religion",
|
||||||
|
"Royal Affairs",
|
||||||
|
"Slavery",
|
||||||
|
"Software Development",
|
||||||
|
"Survival",
|
||||||
|
"Terrorism",
|
||||||
|
"Torture",
|
||||||
|
"Travel",
|
||||||
|
"War",
|
||||||
|
],
|
||||||
|
"Theme Other-Organisations": [
|
||||||
|
"Assassins",
|
||||||
|
"Criminal Organization",
|
||||||
|
"Cult",
|
||||||
|
"Firefighters",
|
||||||
|
"Gangs",
|
||||||
|
"Mafia",
|
||||||
|
"Military",
|
||||||
|
"Police",
|
||||||
|
"Triads",
|
||||||
|
"Yakuza",
|
||||||
|
],
|
||||||
|
"Theme Other-Vehicle": [
|
||||||
|
"Aviation",
|
||||||
|
"Cars",
|
||||||
|
"Mopeds",
|
||||||
|
"Motorcycles",
|
||||||
|
"Ships",
|
||||||
|
"Tanks",
|
||||||
|
"Trains",
|
||||||
|
],
|
||||||
|
"Theme Romance": [
|
||||||
|
"Age Gap",
|
||||||
|
"Bisexual",
|
||||||
|
"Boys' Love",
|
||||||
|
"Female Harem",
|
||||||
|
"Heterosexual",
|
||||||
|
"Love Triangle",
|
||||||
|
"Male Harem",
|
||||||
|
"Matchmaking",
|
||||||
|
"Mixed Gender Harem",
|
||||||
|
"Teens' Love",
|
||||||
|
"Unrequited Love",
|
||||||
|
"Yuri",
|
||||||
|
],
|
||||||
|
"Theme Sci Fi": [
|
||||||
|
"Cyberpunk",
|
||||||
|
"Space Opera",
|
||||||
|
"Time Loop",
|
||||||
|
"Time Manipulation",
|
||||||
|
"Tokusatsu",
|
||||||
|
],
|
||||||
|
"Theme Sci Fi-Mecha": ["Real Robot", "Super Robot"],
|
||||||
|
"Theme Slice of Life": [
|
||||||
|
"Agriculture",
|
||||||
|
"Cute Boys Doing Cute Things",
|
||||||
|
"Cute Girls Doing Cute Things",
|
||||||
|
"Family Life",
|
||||||
|
"Horticulture",
|
||||||
|
"Iyashikei",
|
||||||
|
"Parenthood",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
tags_available_list = []
|
||||||
|
for tag_category, tags_in_category in tags_available.items():
|
||||||
|
tags_available_list.extend(tags_in_category)
|
||||||
|
|
||||||
|
|
||||||
@click.command(
|
@click.command(
|
||||||
help="Search for anime using anilists api and get top ~50 results",
|
help="Search for anime using anilists api and get top ~50 results",
|
||||||
short_help="Search for anime",
|
short_help="Search for anime",
|
||||||
)
|
)
|
||||||
@click.argument("title", shell_complete=anime_titles_shell_complete)
|
@click.option("--title", "-t", shell_complete=anime_titles_shell_complete)
|
||||||
|
@click.option(
|
||||||
|
"--dump-json",
|
||||||
|
"-d",
|
||||||
|
is_flag=True,
|
||||||
|
help="Only print out the results dont open anilist menu",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--season",
|
||||||
|
help="The season the media was released",
|
||||||
|
type=click.Choice(["WINTER", "SPRING", "SUMMER", "FALL"]),
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--status",
|
||||||
|
"-S",
|
||||||
|
help="The media status of the anime",
|
||||||
|
multiple=True,
|
||||||
|
type=click.Choice(
|
||||||
|
["FINISHED", "RELEASING", "NOT_YET_RELEASED", "CANCELLED", "HIATUS"]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--sort",
|
||||||
|
"-s",
|
||||||
|
help="What to sort the search results on",
|
||||||
|
type=click.Choice(
|
||||||
|
[
|
||||||
|
"ID",
|
||||||
|
"ID_DESC",
|
||||||
|
"TITLE_ROMAJI",
|
||||||
|
"TITLE_ROMAJI_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",
|
||||||
|
"POPULARITY",
|
||||||
|
"POPULARITY_DESC",
|
||||||
|
"TRENDING",
|
||||||
|
"TRENDING_DESC",
|
||||||
|
"EPISODES",
|
||||||
|
"EPISODES_DESC",
|
||||||
|
"DURATION",
|
||||||
|
"DURATION_DESC",
|
||||||
|
"STATUS",
|
||||||
|
"STATUS_DESC",
|
||||||
|
"CHAPTERS",
|
||||||
|
"CHAPTERS_DESC",
|
||||||
|
"VOLUMES",
|
||||||
|
"VOLUMES_DESC",
|
||||||
|
"UPDATED_AT",
|
||||||
|
"UPDATED_AT_DESC",
|
||||||
|
"SEARCH_MATCH",
|
||||||
|
"FAVOURITES",
|
||||||
|
"FAVOURITES_DESC",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--genres",
|
||||||
|
"-g",
|
||||||
|
multiple=True,
|
||||||
|
help="the genres to filter by",
|
||||||
|
type=click.Choice(
|
||||||
|
[
|
||||||
|
"Action",
|
||||||
|
"Adventure",
|
||||||
|
"Comedy",
|
||||||
|
"Drama",
|
||||||
|
"Ecchi",
|
||||||
|
"Fantasy",
|
||||||
|
"Horror",
|
||||||
|
"Mahou Shoujo",
|
||||||
|
"Mecha",
|
||||||
|
"Music",
|
||||||
|
"Mystery",
|
||||||
|
"Psychological",
|
||||||
|
"Romance",
|
||||||
|
"Sci-Fi",
|
||||||
|
"Slice of Life",
|
||||||
|
"Sports",
|
||||||
|
"Supernatural",
|
||||||
|
"Thriller",
|
||||||
|
"Hentai",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--tags",
|
||||||
|
"-T",
|
||||||
|
multiple=True,
|
||||||
|
help="the tags to filter by",
|
||||||
|
type=click.Choice(tags_available_list),
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--media-format",
|
||||||
|
"-f",
|
||||||
|
multiple=True,
|
||||||
|
help="Media format",
|
||||||
|
type=click.Choice(
|
||||||
|
["TV", "TV_SHORT", "MOVIE", "SPECIAL", "OVA", "MUSIC", "NOVEL", "ONE_SHOT"]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--year",
|
||||||
|
"-y",
|
||||||
|
type=click.Choice(
|
||||||
|
[
|
||||||
|
"1900",
|
||||||
|
"1910",
|
||||||
|
"1920",
|
||||||
|
"1930",
|
||||||
|
"1940",
|
||||||
|
"1950",
|
||||||
|
"1960",
|
||||||
|
"1970",
|
||||||
|
"1980",
|
||||||
|
"1990",
|
||||||
|
"2000",
|
||||||
|
"2004",
|
||||||
|
"2005",
|
||||||
|
"2006",
|
||||||
|
"2007",
|
||||||
|
"2008",
|
||||||
|
"2009",
|
||||||
|
"2010",
|
||||||
|
"2011",
|
||||||
|
"2012",
|
||||||
|
"2013",
|
||||||
|
"2014",
|
||||||
|
"2015",
|
||||||
|
"2016",
|
||||||
|
"2017",
|
||||||
|
"2018",
|
||||||
|
"2019",
|
||||||
|
"2020",
|
||||||
|
"2021",
|
||||||
|
"2022",
|
||||||
|
"2023",
|
||||||
|
"2024",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
help="the year the media was released",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--on-list/--not-on-list",
|
||||||
|
"-L/-no-L",
|
||||||
|
help="Whether the anime should be in your list or not",
|
||||||
|
type=bool,
|
||||||
|
)
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def search(config, title):
|
def search(
|
||||||
|
config,
|
||||||
|
title,
|
||||||
|
dump_json,
|
||||||
|
season,
|
||||||
|
status,
|
||||||
|
sort,
|
||||||
|
genres,
|
||||||
|
tags,
|
||||||
|
media_format,
|
||||||
|
year,
|
||||||
|
on_list,
|
||||||
|
):
|
||||||
from ....anilist import AniList
|
from ....anilist import AniList
|
||||||
from ...interfaces.anilist_interfaces import anilist_results_menu
|
|
||||||
from ...utils.tools import FastAnimeRuntimeState
|
|
||||||
|
|
||||||
success, search_results = AniList.search(title)
|
success, search_results = AniList.search(
|
||||||
|
query=title,
|
||||||
|
sort=sort,
|
||||||
|
status_in=list(status),
|
||||||
|
genre_in=list(genres),
|
||||||
|
season=season,
|
||||||
|
tag_in=list(tags),
|
||||||
|
seasonYear=year,
|
||||||
|
format_in=list(media_format),
|
||||||
|
on_list=on_list,
|
||||||
|
)
|
||||||
if success:
|
if success:
|
||||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
if dump_json:
|
||||||
fastanime_runtime_state.anilist_data = search_results
|
import json
|
||||||
anilist_results_menu(config, fastanime_runtime_state)
|
|
||||||
|
print(json.dumps(search_results))
|
||||||
|
else:
|
||||||
|
from ...interfaces.anilist_interfaces import anilist_results_menu
|
||||||
|
from ...utils.tools import FastAnimeRuntimeState
|
||||||
|
|
||||||
|
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||||
|
fastanime_runtime_state.anilist_results_data = search_results
|
||||||
|
anilist_results_menu(config, fastanime_runtime_state)
|
||||||
|
else:
|
||||||
|
from sys import exit
|
||||||
|
|
||||||
|
exit(1)
|
||||||
|
|||||||
63
fastanime/cli/commands/anilist/stats.py
Normal file
63
fastanime/cli/commands/anilist/stats.py
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
import click
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ...config import Config
|
||||||
|
|
||||||
|
|
||||||
|
@click.command(help="Print out your anilist stats")
|
||||||
|
@click.pass_obj
|
||||||
|
def stats(
|
||||||
|
config: "Config",
|
||||||
|
):
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
from sys import exit
|
||||||
|
|
||||||
|
from rich.console import Console
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
from rich.markdown import Markdown
|
||||||
|
from rich.panel import Panel
|
||||||
|
|
||||||
|
from ....anilist import AniList
|
||||||
|
|
||||||
|
user_data = AniList.get_user_info()
|
||||||
|
if not user_data[0] or not user_data[1]:
|
||||||
|
print("Failed to get user info")
|
||||||
|
print(user_data[1])
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
KITTEN_EXECUTABLE = shutil.which("kitten")
|
||||||
|
if not KITTEN_EXECUTABLE:
|
||||||
|
print("Kitten not found")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
image_url = user_data[1]["data"]["User"]["avatar"]["medium"]
|
||||||
|
user_name = user_data[1]["data"]["User"]["name"]
|
||||||
|
about = user_data[1]["data"]["User"]["about"] or ""
|
||||||
|
console.clear()
|
||||||
|
image_x = int(console.size.width * 0.1)
|
||||||
|
image_y = int(console.size.height * 0.1)
|
||||||
|
img_w = console.size.width // 3
|
||||||
|
img_h = console.size.height // 3
|
||||||
|
image_process = subprocess.run(
|
||||||
|
[
|
||||||
|
KITTEN_EXECUTABLE,
|
||||||
|
"icat",
|
||||||
|
"--clear",
|
||||||
|
"--place",
|
||||||
|
f"{img_w}x{img_h}@{image_x}x{image_y}",
|
||||||
|
image_url,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
if not image_process.returncode == 0:
|
||||||
|
print("failed to get image from icat")
|
||||||
|
exit(1)
|
||||||
|
console.print(
|
||||||
|
Panel(
|
||||||
|
Markdown(about),
|
||||||
|
title=user_name,
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -5,14 +5,30 @@ import click
|
|||||||
help="Fetch the top 15 anime that are currently trending",
|
help="Fetch the top 15 anime that are currently trending",
|
||||||
short_help="Trending anime 🔥🔥🔥",
|
short_help="Trending anime 🔥🔥🔥",
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
"--dump-json",
|
||||||
|
"-d",
|
||||||
|
is_flag=True,
|
||||||
|
help="Only print out the results dont open anilist menu",
|
||||||
|
)
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def trending(config):
|
def trending(config, dump_json):
|
||||||
from ....anilist import AniList
|
from ....anilist import AniList
|
||||||
from ...interfaces.anilist_interfaces import anilist_results_menu
|
|
||||||
from ...utils.tools import FastAnimeRuntimeState
|
|
||||||
|
|
||||||
success, data = AniList.get_trending()
|
success, data = AniList.get_trending()
|
||||||
if success:
|
if success:
|
||||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
if dump_json:
|
||||||
fastanime_runtime_state.anilist_data = data
|
import json
|
||||||
anilist_results_menu(config, fastanime_runtime_state)
|
|
||||||
|
print(json.dumps(data))
|
||||||
|
else:
|
||||||
|
from ...interfaces.anilist_interfaces import anilist_results_menu
|
||||||
|
from ...utils.tools import FastAnimeRuntimeState
|
||||||
|
|
||||||
|
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||||
|
fastanime_runtime_state.anilist_results_data = data
|
||||||
|
anilist_results_menu(config, fastanime_runtime_state)
|
||||||
|
else:
|
||||||
|
from sys import exit
|
||||||
|
|
||||||
|
exit(1)
|
||||||
|
|||||||
@@ -4,14 +4,30 @@ import click
|
|||||||
@click.command(
|
@click.command(
|
||||||
help="Fetch the 15 most anticipited anime", short_help="View upcoming anime"
|
help="Fetch the 15 most anticipited anime", short_help="View upcoming anime"
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
"--dump-json",
|
||||||
|
"-d",
|
||||||
|
is_flag=True,
|
||||||
|
help="Only print out the results dont open anilist menu",
|
||||||
|
)
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def upcoming(config):
|
def upcoming(config, dump_json):
|
||||||
from ....anilist import AniList
|
from ....anilist import AniList
|
||||||
from ...interfaces.anilist_interfaces import anilist_results_menu
|
|
||||||
from ...utils.tools import FastAnimeRuntimeState
|
|
||||||
|
|
||||||
success, data = AniList.get_upcoming_anime()
|
success, data = AniList.get_upcoming_anime()
|
||||||
if success:
|
if success:
|
||||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
if dump_json:
|
||||||
fastanime_runtime_state.anilist_data = data
|
import json
|
||||||
anilist_results_menu(config, fastanime_runtime_state)
|
|
||||||
|
print(json.dumps(data))
|
||||||
|
else:
|
||||||
|
from ...interfaces.anilist_interfaces import anilist_results_menu
|
||||||
|
from ...utils.tools import FastAnimeRuntimeState
|
||||||
|
|
||||||
|
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||||
|
fastanime_runtime_state.anilist_results_data = data
|
||||||
|
anilist_results_menu(config, fastanime_runtime_state)
|
||||||
|
else:
|
||||||
|
from sys import exit
|
||||||
|
|
||||||
|
exit(1)
|
||||||
|
|||||||
@@ -7,26 +7,40 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
@click.command(help="View anime you are watching")
|
@click.command(help="View anime you are watching")
|
||||||
|
@click.option(
|
||||||
|
"--dump-json",
|
||||||
|
"-d",
|
||||||
|
is_flag=True,
|
||||||
|
help="Only print out the results dont open anilist menu",
|
||||||
|
)
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def watching(config: "Config"):
|
def watching(config: "Config", dump_json):
|
||||||
|
from sys import exit
|
||||||
|
|
||||||
from ....anilist import AniList
|
from ....anilist import AniList
|
||||||
from ...interfaces import anilist_interfaces
|
|
||||||
from ...utils.tools import FastAnimeRuntimeState, exit_app
|
|
||||||
|
|
||||||
if not config.user:
|
if not config.user:
|
||||||
print("Not authenticated")
|
print("Not authenticated")
|
||||||
print("Please run: fastanime anilist loggin")
|
print("Please run: fastanime anilist loggin")
|
||||||
exit_app()
|
exit(1)
|
||||||
anime_list = AniList.get_anime_list("CURRENT")
|
anime_list = AniList.get_anime_list("CURRENT")
|
||||||
if not anime_list:
|
if not anime_list:
|
||||||
return
|
exit(1)
|
||||||
if not anime_list[0] or not anime_list[1]:
|
if not anime_list[0] or not anime_list[1]:
|
||||||
return
|
exit(1)
|
||||||
media = [
|
media = [
|
||||||
mediaListItem["media"]
|
mediaListItem["media"]
|
||||||
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
|
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
|
||||||
] # pyright:ignore
|
] # pyright:ignore
|
||||||
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
|
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
|
||||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
if dump_json:
|
||||||
fastanime_runtime_state.anilist_data = anime_list[1]
|
import json
|
||||||
anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state)
|
|
||||||
|
print(json.dumps(anime_list[1]))
|
||||||
|
else:
|
||||||
|
from ...interfaces import anilist_interfaces
|
||||||
|
from ...utils.tools import FastAnimeRuntimeState
|
||||||
|
|
||||||
|
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||||
|
fastanime_runtime_state.anilist_results_data = anime_list[1]
|
||||||
|
anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
@click.command(
|
@click.command(
|
||||||
help="Opens up your fastanime config in your preferred editor",
|
help="Manage your config with ease",
|
||||||
short_help="Edit your config",
|
short_help="Edit your config",
|
||||||
)
|
)
|
||||||
@click.option("--path", "-p", help="Print the config location and exit", is_flag=True)
|
@click.option("--path", "-p", help="Print the config location and exit", is_flag=True)
|
||||||
@@ -20,8 +20,14 @@ if TYPE_CHECKING:
|
|||||||
help="Configure the desktop entry of fastanime",
|
help="Configure the desktop entry of fastanime",
|
||||||
is_flag=True,
|
is_flag=True,
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
"--update",
|
||||||
|
"-u",
|
||||||
|
help="Persist all the config options passed to fastanime to your config file",
|
||||||
|
is_flag=True,
|
||||||
|
)
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def config(config: "Config", path, view, desktop_entry):
|
def config(user_config: "Config", path, view, desktop_entry, update):
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from rich import print
|
from rich import print
|
||||||
@@ -32,7 +38,7 @@ def config(config: "Config", path, view, desktop_entry):
|
|||||||
if path:
|
if path:
|
||||||
print(USER_CONFIG_PATH)
|
print(USER_CONFIG_PATH)
|
||||||
elif view:
|
elif view:
|
||||||
print(config)
|
print(user_config)
|
||||||
elif desktop_entry:
|
elif desktop_entry:
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
@@ -87,7 +93,9 @@ def config(config: "Config", path, view, desktop_entry):
|
|||||||
with open(desktop_entry_path) as f:
|
with open(desktop_entry_path) as f:
|
||||||
print(f"Successfully wrote \n{f.read()}")
|
print(f"Successfully wrote \n{f.read()}")
|
||||||
exit_app(0)
|
exit_app(0)
|
||||||
|
elif update:
|
||||||
|
with open(USER_CONFIG_PATH, "w",encoding="utf-8") as file:
|
||||||
|
file.write(user_config.__str__())
|
||||||
|
print("update successfull")
|
||||||
else:
|
else:
|
||||||
import click
|
|
||||||
|
|
||||||
click.edit(filename=USER_CONFIG_PATH)
|
click.edit(filename=USER_CONFIG_PATH)
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import time
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import click
|
import click
|
||||||
@@ -28,8 +27,14 @@ if TYPE_CHECKING:
|
|||||||
help="A range of episodes to download (start-end)",
|
help="A range of episodes to download (start-end)",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--force-unknown-ext",
|
"--file",
|
||||||
"-f",
|
"-f",
|
||||||
|
type=click.File(),
|
||||||
|
help="A file to read from all anime to download",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--force-unknown-ext",
|
||||||
|
"-F",
|
||||||
help="This option forces yt-dlp to download extensions its not aware of",
|
help="This option forces yt-dlp to download extensions its not aware of",
|
||||||
is_flag=True,
|
is_flag=True,
|
||||||
)
|
)
|
||||||
@@ -41,15 +46,43 @@ if TYPE_CHECKING:
|
|||||||
default=True,
|
default=True,
|
||||||
)
|
)
|
||||||
@click.option("--verbose", "-v", is_flag=True, help="Download verbosely (everywhere)")
|
@click.option("--verbose", "-v", is_flag=True, help="Download verbosely (everywhere)")
|
||||||
|
@click.option(
|
||||||
|
"--merge", "-m", is_flag=True, help="Merge the subfile with video using ffmpeg"
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--clean",
|
||||||
|
"-c",
|
||||||
|
is_flag=True,
|
||||||
|
help="After merging delete the original files",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--wait-time",
|
||||||
|
"-w",
|
||||||
|
type=int,
|
||||||
|
help="The amount of time to wait after downloading is complete before the screen is completely cleared",
|
||||||
|
default=60,
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--prompt/--no-prompt",
|
||||||
|
help="Whether to prompt for anything instead just do the best thing",
|
||||||
|
default=True,
|
||||||
|
)
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def download(
|
def download(
|
||||||
config: "Config",
|
config: "Config",
|
||||||
anime_titles: list,
|
anime_titles: tuple,
|
||||||
episode_range,
|
episode_range,
|
||||||
|
file,
|
||||||
force_unknown_ext,
|
force_unknown_ext,
|
||||||
silent,
|
silent,
|
||||||
verbose,
|
verbose,
|
||||||
|
merge,
|
||||||
|
clean,
|
||||||
|
wait_time,
|
||||||
|
prompt,
|
||||||
):
|
):
|
||||||
|
import time
|
||||||
|
|
||||||
from rich import print
|
from rich import print
|
||||||
from rich.progress import Progress
|
from rich.progress import Progress
|
||||||
from thefuzz import fuzz
|
from thefuzz import fuzz
|
||||||
@@ -57,6 +90,7 @@ def download(
|
|||||||
from ...AnimeProvider import AnimeProvider
|
from ...AnimeProvider import AnimeProvider
|
||||||
from ...libs.anime_provider.types import Anime
|
from ...libs.anime_provider.types import Anime
|
||||||
from ...libs.fzf import fzf
|
from ...libs.fzf import fzf
|
||||||
|
from ...Utility.data import anime_normalizer
|
||||||
from ...Utility.downloader.downloader import downloader
|
from ...Utility.downloader.downloader import downloader
|
||||||
from ..utils.tools import exit_app
|
from ..utils.tools import exit_app
|
||||||
from ..utils.utils import (
|
from ..utils.utils import (
|
||||||
@@ -66,12 +100,22 @@ def download(
|
|||||||
)
|
)
|
||||||
|
|
||||||
anime_provider = AnimeProvider(config.provider)
|
anime_provider = AnimeProvider(config.provider)
|
||||||
|
anilist_anime_info = None
|
||||||
|
|
||||||
translation_type = config.translation_type
|
translation_type = config.translation_type
|
||||||
download_dir = config.downloads_dir
|
download_dir = config.downloads_dir
|
||||||
|
if file:
|
||||||
|
contents = file.read()
|
||||||
|
anime_titles_from_file = tuple(
|
||||||
|
[title for title in contents.split("\n") if title]
|
||||||
|
)
|
||||||
|
file.close()
|
||||||
|
|
||||||
|
anime_titles = (*anime_titles_from_file, *anime_titles)
|
||||||
print(f"[green bold]Queued:[/] {anime_titles}")
|
print(f"[green bold]Queued:[/] {anime_titles}")
|
||||||
for anime_title in anime_titles:
|
for anime_title in anime_titles:
|
||||||
|
if anime_title == "EOF":
|
||||||
|
break
|
||||||
print(f"[green bold]Now Downloading: [/] {anime_title}")
|
print(f"[green bold]Now Downloading: [/] {anime_title}")
|
||||||
# ---- search for anime ----
|
# ---- search for anime ----
|
||||||
with Progress() as progress:
|
with Progress() as progress:
|
||||||
@@ -83,28 +127,43 @@ def download(
|
|||||||
print("Search results failed")
|
print("Search results failed")
|
||||||
input("Enter to retry")
|
input("Enter to retry")
|
||||||
download(
|
download(
|
||||||
config, anime_title, episode_range, force_unknown_ext, silent, verbose
|
config,
|
||||||
|
anime_title,
|
||||||
|
episode_range,
|
||||||
|
file,
|
||||||
|
force_unknown_ext,
|
||||||
|
silent,
|
||||||
|
verbose,
|
||||||
|
merge,
|
||||||
|
clean,
|
||||||
|
wait_time,
|
||||||
|
prompt,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
search_results = search_results["results"]
|
search_results = search_results["results"]
|
||||||
if not search_results:
|
if not search_results:
|
||||||
print("Nothing muches your search term")
|
print("Nothing muches your search term")
|
||||||
exit_app(1)
|
continue
|
||||||
search_results_ = {
|
search_results_ = {
|
||||||
search_result["title"]: search_result for search_result in search_results
|
search_result["title"]: search_result for search_result in search_results
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.auto_select:
|
if config.auto_select:
|
||||||
search_result = max(
|
selected_anime_title = max(
|
||||||
search_results_.keys(), key=lambda title: fuzz.ratio(title, anime_title)
|
search_results_.keys(),
|
||||||
|
key=lambda title: fuzz.ratio(
|
||||||
|
anime_normalizer.get(title, title), anime_title
|
||||||
|
),
|
||||||
)
|
)
|
||||||
print("[cyan]Auto selecting:[/] ", search_result)
|
print("[cyan]Auto selecting:[/] ", selected_anime_title)
|
||||||
else:
|
else:
|
||||||
choices = list(search_results_.keys())
|
choices = list(search_results_.keys())
|
||||||
if config.use_fzf:
|
if config.use_fzf:
|
||||||
search_result = fzf.run(choices, "Please Select title: ", "FastAnime")
|
selected_anime_title = fzf.run(
|
||||||
|
choices, "Please Select title: ", "FastAnime"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
search_result = fuzzy_inquirer(
|
selected_anime_title = fuzzy_inquirer(
|
||||||
choices,
|
choices,
|
||||||
"Please Select title",
|
"Please Select title",
|
||||||
)
|
)
|
||||||
@@ -113,13 +172,23 @@ def download(
|
|||||||
with Progress() as progress:
|
with Progress() as progress:
|
||||||
progress.add_task("Fetching Anime...", total=None)
|
progress.add_task("Fetching Anime...", total=None)
|
||||||
anime: Anime | None = anime_provider.get_anime(
|
anime: Anime | None = anime_provider.get_anime(
|
||||||
search_results_[search_result]["id"]
|
search_results_[selected_anime_title]["id"]
|
||||||
)
|
)
|
||||||
if not anime:
|
if not anime:
|
||||||
print("Sth went wring anime no found")
|
print("Sth went wring anime no found")
|
||||||
input("Enter to continue...")
|
input("Enter to continue...")
|
||||||
download(
|
download(
|
||||||
config, anime_title, episode_range, force_unknown_ext, silent, verbose
|
config,
|
||||||
|
anime_title,
|
||||||
|
episode_range,
|
||||||
|
file,
|
||||||
|
force_unknown_ext,
|
||||||
|
silent,
|
||||||
|
verbose,
|
||||||
|
merge,
|
||||||
|
clean,
|
||||||
|
wait_time,
|
||||||
|
prompt,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -153,6 +222,11 @@ def download(
|
|||||||
else:
|
else:
|
||||||
episodes_range = sorted(episodes, key=float)
|
episodes_range = sorted(episodes, key=float)
|
||||||
|
|
||||||
|
if config.normalize_titles:
|
||||||
|
from ...libs.common.mini_anilist import get_basic_anime_info_by_title
|
||||||
|
|
||||||
|
anilist_anime_info = get_basic_anime_info_by_title(anime["title"])
|
||||||
|
|
||||||
# lets download em
|
# lets download em
|
||||||
for episode in episodes_range:
|
for episode in episodes_range:
|
||||||
try:
|
try:
|
||||||
@@ -217,13 +291,26 @@ def download(
|
|||||||
|
|
||||||
subtitles = servers[server_name]["subtitles"]
|
subtitles = servers[server_name]["subtitles"]
|
||||||
episode_title = servers[server_name]["episode_title"]
|
episode_title = servers[server_name]["episode_title"]
|
||||||
print(f"[purple]Now Downloading:[/] {search_result} Episode {episode}")
|
|
||||||
|
if anilist_anime_info:
|
||||||
|
selected_anime_title = (
|
||||||
|
anilist_anime_info["title"][config.preferred_language]
|
||||||
|
or anilist_anime_info["title"]["romaji"]
|
||||||
|
or anilist_anime_info["title"]["english"]
|
||||||
|
)
|
||||||
|
import re
|
||||||
|
|
||||||
|
for episode_detail in anilist_anime_info["episodes"]:
|
||||||
|
if re.match(f"Episode {episode} ", episode_detail["title"]):
|
||||||
|
episode_title = episode_detail["title"]
|
||||||
|
break
|
||||||
|
print(f"[purple]Now Downloading:[/] {episode_title}")
|
||||||
subtitles = move_preferred_subtitle_lang_to_top(
|
subtitles = move_preferred_subtitle_lang_to_top(
|
||||||
subtitles, config.sub_lang
|
subtitles, config.sub_lang
|
||||||
)
|
)
|
||||||
downloader._download_file(
|
downloader._download_file(
|
||||||
link,
|
link,
|
||||||
anime["title"],
|
selected_anime_title,
|
||||||
episode_title,
|
episode_title,
|
||||||
download_dir,
|
download_dir,
|
||||||
silent,
|
silent,
|
||||||
@@ -232,10 +319,14 @@ def download(
|
|||||||
verbose,
|
verbose,
|
||||||
headers=provider_headers,
|
headers=provider_headers,
|
||||||
sub=subtitles[0]["url"] if subtitles else "",
|
sub=subtitles[0]["url"] if subtitles else "",
|
||||||
|
merge=merge,
|
||||||
|
clean=clean,
|
||||||
|
prompt=prompt,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
print("Continuing...")
|
print("Continuing...")
|
||||||
print("Done Downloading")
|
print("Done Downloading")
|
||||||
|
time.sleep(wait_time)
|
||||||
exit_app()
|
exit_app()
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ from typing import TYPE_CHECKING
|
|||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
|
from ..completion_functions import downloaded_anime_titles
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ..config import Config
|
from ..config import Config
|
||||||
@@ -12,16 +14,24 @@ if TYPE_CHECKING:
|
|||||||
help="View and watch your downloads using mpv", short_help="Watch downloads"
|
help="View and watch your downloads using mpv", short_help="Watch downloads"
|
||||||
)
|
)
|
||||||
@click.option("--path", "-p", help="print the downloads folder and exit", is_flag=True)
|
@click.option("--path", "-p", help="print the downloads folder and exit", is_flag=True)
|
||||||
|
@click.option(
|
||||||
|
"--title",
|
||||||
|
"-T",
|
||||||
|
shell_complete=downloaded_anime_titles,
|
||||||
|
help="watch a specific title",
|
||||||
|
)
|
||||||
@click.option("--view-episodes", "-v", help="View individual episodes", is_flag=True)
|
@click.option("--view-episodes", "-v", help="View individual episodes", is_flag=True)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--ffmpegthumbnailer-seek-time",
|
"--ffmpegthumbnailer-seek-time",
|
||||||
"--time-to-seek",
|
"--time-to-seek",
|
||||||
"-t",
|
"-t",
|
||||||
type=click.IntRange(-1, 100),
|
type=click.IntRange(-1, 100),
|
||||||
help="ffmpegthumbnailer seek time [0-100]",
|
help="ffmpegthumbnailer seek time",
|
||||||
)
|
)
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def downloads(config: "Config", path: bool, view_episodes, ffmpegthumbnailer_seek_time):
|
def downloads(
|
||||||
|
config: "Config", path: bool, title, view_episodes, ffmpegthumbnailer_seek_time
|
||||||
|
):
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from ...cli.utils.mpv import run_mpv
|
from ...cli.utils.mpv import run_mpv
|
||||||
@@ -239,6 +249,7 @@ def downloads(config: "Config", path: bool, view_episodes, ffmpegthumbnailer_see
|
|||||||
os.listdir(anime_playlist_path), key=sort_by_episode_number
|
os.listdir(anime_playlist_path), key=sort_by_episode_number
|
||||||
)
|
)
|
||||||
downloaded_episodes = [*episodes, "Back"]
|
downloaded_episodes = [*episodes, "Back"]
|
||||||
|
|
||||||
if config.use_fzf:
|
if config.use_fzf:
|
||||||
if not config.preview:
|
if not config.preview:
|
||||||
episode_title = fzf.run(
|
episode_title = fzf.run(
|
||||||
@@ -271,8 +282,12 @@ def downloads(config: "Config", path: bool, view_episodes, ffmpegthumbnailer_see
|
|||||||
run_mpv(episode_path)
|
run_mpv(episode_path)
|
||||||
stream_episode(anime_playlist_path)
|
stream_episode(anime_playlist_path)
|
||||||
|
|
||||||
def stream_anime():
|
def stream_anime(title=None):
|
||||||
if config.use_fzf:
|
if title:
|
||||||
|
from thefuzz import fuzz
|
||||||
|
|
||||||
|
playlist_name = max(anime_downloads, key=lambda t: fuzz.ratio(title, t))
|
||||||
|
elif config.use_fzf:
|
||||||
if not config.preview:
|
if not config.preview:
|
||||||
playlist_name = fzf.run(
|
playlist_name = fzf.run(
|
||||||
anime_downloads,
|
anime_downloads,
|
||||||
@@ -309,4 +324,4 @@ def downloads(config: "Config", path: bool, view_episodes, ffmpegthumbnailer_see
|
|||||||
run_mpv(playlist)
|
run_mpv(playlist)
|
||||||
stream_anime()
|
stream_anime()
|
||||||
|
|
||||||
stream_anime()
|
stream_anime(title)
|
||||||
|
|||||||
@@ -56,26 +56,19 @@ def grab(
|
|||||||
|
|
||||||
from thefuzz import fuzz
|
from thefuzz import fuzz
|
||||||
|
|
||||||
from ...AnimeProvider import AnimeProvider
|
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
|
if config.manga:
|
||||||
|
manga_title = anime_titles[0]
|
||||||
|
from ...MangaProvider import MangaProvider
|
||||||
|
|
||||||
anime_provider = AnimeProvider(config.provider)
|
manga_provider = MangaProvider()
|
||||||
|
search_data = manga_provider.search_for_manga(manga_title)
|
||||||
grabbed_animes = []
|
if not search_data:
|
||||||
for anime_title in anime_titles:
|
|
||||||
# ---- search for anime ----
|
|
||||||
search_results = anime_provider.search_for_anime(
|
|
||||||
anime_title, translation_type=config.translation_type
|
|
||||||
)
|
|
||||||
if not search_results:
|
|
||||||
exit(1)
|
exit(1)
|
||||||
if search_results_only:
|
if search_results_only:
|
||||||
# grab only search results skipping all lines after this
|
print(json.dumps(search_data))
|
||||||
grabbed_animes.append(search_results)
|
exit(0)
|
||||||
continue
|
search_results = search_data["results"]
|
||||||
|
|
||||||
search_results = search_results["results"]
|
|
||||||
if not search_results:
|
if not search_results:
|
||||||
logger.error("no results for your search")
|
logger.error("no results for your search")
|
||||||
exit(1)
|
exit(1)
|
||||||
@@ -83,83 +76,133 @@ def grab(
|
|||||||
search_result["title"]: search_result for search_result in search_results
|
search_result["title"]: search_result for search_result in search_results
|
||||||
}
|
}
|
||||||
|
|
||||||
search_result = max(
|
search_result_anime_title = max(
|
||||||
search_results_.keys(), key=lambda title: fuzz.ratio(title, anime_title)
|
search_results_.keys(), key=lambda title: fuzz.ratio(title, anime_titles[0])
|
||||||
)
|
)
|
||||||
|
manga_info = manga_provider.get_manga(
|
||||||
# ---- fetch anime ----
|
search_results_[search_result_anime_title]["id"]
|
||||||
anime = anime_provider.get_anime(search_results_[search_result]["id"])
|
)
|
||||||
if not anime:
|
if not manga_info:
|
||||||
exit(1)
|
return
|
||||||
if anime_info_only:
|
if anime_info_only:
|
||||||
# grab only the anime data skipping all lines after this
|
print(json.dumps(manga_info))
|
||||||
grabbed_animes.append(anime)
|
exit(0)
|
||||||
continue
|
|
||||||
episodes = sorted(
|
chapter_info = manga_provider.get_chapter_thumbnails(
|
||||||
anime["availableEpisodesDetail"][config.translation_type], key=float
|
manga_info["id"], str(episode_range)
|
||||||
)
|
)
|
||||||
|
if not chapter_info:
|
||||||
|
exit(1)
|
||||||
|
print(json.dumps(chapter_info))
|
||||||
|
|
||||||
# where the magic happens
|
|
||||||
if episode_range:
|
|
||||||
if ":" in episode_range:
|
|
||||||
ep_range_tuple = episode_range.split(":")
|
|
||||||
if len(ep_range_tuple) == 2 and all(ep_range_tuple):
|
|
||||||
episodes_start, episodes_end = ep_range_tuple
|
|
||||||
episodes_range = episodes[int(episodes_start) : int(episodes_end)]
|
|
||||||
elif len(ep_range_tuple) == 3 and all(ep_range_tuple):
|
|
||||||
episodes_start, episodes_end, step = ep_range_tuple
|
|
||||||
episodes_range = episodes[
|
|
||||||
int(episodes_start) : int(episodes_end) : int(step)
|
|
||||||
]
|
|
||||||
else:
|
|
||||||
episodes_start, episodes_end = ep_range_tuple
|
|
||||||
if episodes_start.strip():
|
|
||||||
episodes_range = episodes[int(episodes_start) :]
|
|
||||||
elif episodes_end.strip():
|
|
||||||
episodes_range = episodes[: int(episodes_end)]
|
|
||||||
else:
|
|
||||||
episodes_range = episodes
|
|
||||||
else:
|
|
||||||
episodes_range = episodes[int(episode_range) :]
|
|
||||||
|
|
||||||
else:
|
|
||||||
episodes_range = sorted(episodes, key=float)
|
|
||||||
|
|
||||||
if not episode_streams_only:
|
|
||||||
grabbed_anime = dict(anime)
|
|
||||||
grabbed_anime["requested_episodes"] = episodes_range
|
|
||||||
grabbed_anime["translation_type"] = config.translation_type
|
|
||||||
grabbed_anime["episodes_streams"] = {}
|
|
||||||
else:
|
|
||||||
grabbed_anime = {}
|
|
||||||
|
|
||||||
# lets download em
|
|
||||||
for episode in episodes_range:
|
|
||||||
try:
|
|
||||||
if episode not in episodes:
|
|
||||||
continue
|
|
||||||
streams = anime_provider.get_episode_streams(
|
|
||||||
anime, episode, config.translation_type
|
|
||||||
)
|
|
||||||
if not streams:
|
|
||||||
continue
|
|
||||||
episode_streams = {server["server"]: server for server in streams}
|
|
||||||
|
|
||||||
if episode_streams_only:
|
|
||||||
grabbed_anime[episode] = episode_streams
|
|
||||||
else:
|
|
||||||
grabbed_anime["episodes_streams"][ # pyright:ignore
|
|
||||||
episode
|
|
||||||
] = episode_streams
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(e)
|
|
||||||
|
|
||||||
# grab the full data for single title and appen to final result or episode streams
|
|
||||||
grabbed_animes.append(grabbed_anime)
|
|
||||||
|
|
||||||
# print out the final result either {} or [] depending if more than one title os requested
|
|
||||||
if len(grabbed_animes) == 1:
|
|
||||||
print(json.dumps(grabbed_animes[0]))
|
|
||||||
else:
|
else:
|
||||||
print(json.dumps(grabbed_animes))
|
from ...AnimeProvider import AnimeProvider
|
||||||
|
|
||||||
|
anime_provider = AnimeProvider(config.provider)
|
||||||
|
|
||||||
|
grabbed_animes = []
|
||||||
|
for anime_title in anime_titles:
|
||||||
|
# ---- search for anime ----
|
||||||
|
search_results = anime_provider.search_for_anime(
|
||||||
|
anime_title, translation_type=config.translation_type
|
||||||
|
)
|
||||||
|
if not search_results:
|
||||||
|
exit(1)
|
||||||
|
if search_results_only:
|
||||||
|
# grab only search results skipping all lines after this
|
||||||
|
grabbed_animes.append(search_results)
|
||||||
|
continue
|
||||||
|
|
||||||
|
search_results = search_results["results"]
|
||||||
|
if not search_results:
|
||||||
|
logger.error("no results for your search")
|
||||||
|
exit(1)
|
||||||
|
search_results_ = {
|
||||||
|
search_result["title"]: search_result
|
||||||
|
for search_result in search_results
|
||||||
|
}
|
||||||
|
|
||||||
|
search_result_anime_title = max(
|
||||||
|
search_results_.keys(), key=lambda title: fuzz.ratio(title, anime_title)
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---- fetch anime ----
|
||||||
|
anime = anime_provider.get_anime(
|
||||||
|
search_results_[search_result_anime_title]["id"]
|
||||||
|
)
|
||||||
|
if not anime:
|
||||||
|
exit(1)
|
||||||
|
if anime_info_only:
|
||||||
|
# grab only the anime data skipping all lines after this
|
||||||
|
grabbed_animes.append(anime)
|
||||||
|
continue
|
||||||
|
episodes = sorted(
|
||||||
|
anime["availableEpisodesDetail"][config.translation_type], key=float
|
||||||
|
)
|
||||||
|
|
||||||
|
# where the magic happens
|
||||||
|
if episode_range:
|
||||||
|
if ":" in episode_range:
|
||||||
|
ep_range_tuple = episode_range.split(":")
|
||||||
|
if len(ep_range_tuple) == 2 and all(ep_range_tuple):
|
||||||
|
episodes_start, episodes_end = ep_range_tuple
|
||||||
|
episodes_range = episodes[
|
||||||
|
int(episodes_start) : int(episodes_end)
|
||||||
|
]
|
||||||
|
elif len(ep_range_tuple) == 3 and all(ep_range_tuple):
|
||||||
|
episodes_start, episodes_end, step = ep_range_tuple
|
||||||
|
episodes_range = episodes[
|
||||||
|
int(episodes_start) : int(episodes_end) : int(step)
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
episodes_start, episodes_end = ep_range_tuple
|
||||||
|
if episodes_start.strip():
|
||||||
|
episodes_range = episodes[int(episodes_start) :]
|
||||||
|
elif episodes_end.strip():
|
||||||
|
episodes_range = episodes[: int(episodes_end)]
|
||||||
|
else:
|
||||||
|
episodes_range = episodes
|
||||||
|
else:
|
||||||
|
episodes_range = episodes[int(episode_range) :]
|
||||||
|
|
||||||
|
else:
|
||||||
|
episodes_range = sorted(episodes, key=float)
|
||||||
|
|
||||||
|
if not episode_streams_only:
|
||||||
|
grabbed_anime = dict(anime)
|
||||||
|
grabbed_anime["requested_episodes"] = episodes_range
|
||||||
|
grabbed_anime["translation_type"] = config.translation_type
|
||||||
|
grabbed_anime["episodes_streams"] = {}
|
||||||
|
else:
|
||||||
|
grabbed_anime = {}
|
||||||
|
|
||||||
|
# lets download em
|
||||||
|
for episode in episodes_range:
|
||||||
|
try:
|
||||||
|
if episode not in episodes:
|
||||||
|
continue
|
||||||
|
streams = anime_provider.get_episode_streams(
|
||||||
|
anime, episode, config.translation_type
|
||||||
|
)
|
||||||
|
if not streams:
|
||||||
|
continue
|
||||||
|
episode_streams = {server["server"]: server for server in streams}
|
||||||
|
|
||||||
|
if episode_streams_only:
|
||||||
|
grabbed_anime[episode] = episode_streams
|
||||||
|
else:
|
||||||
|
grabbed_anime["episodes_streams"][ # pyright:ignore
|
||||||
|
episode
|
||||||
|
] = episode_streams
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
|
||||||
|
# grab the full data for single title and appen to final result or episode streams
|
||||||
|
grabbed_animes.append(grabbed_anime)
|
||||||
|
|
||||||
|
# print out the final result either {} or [] depending if more than one title os requested
|
||||||
|
if len(grabbed_animes) == 1:
|
||||||
|
print(json.dumps(grabbed_animes[0]))
|
||||||
|
else:
|
||||||
|
print(json.dumps(grabbed_animes))
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
from ...cli.config import Config
|
|
||||||
from ..completion_functions import anime_titles_shell_complete
|
from ..completion_functions import anime_titles_shell_complete
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ...cli.config import Config
|
||||||
|
|
||||||
|
|
||||||
@click.command(
|
@click.command(
|
||||||
help="This subcommand directly interacts with the provider to enable basic streaming. Useful for binging anime.",
|
help="This subcommand directly interacts with the provider to enable basic streaming. Useful for binging anime.",
|
||||||
@@ -23,215 +27,335 @@ from ..completion_functions import anime_titles_shell_complete
|
|||||||
help="A range of episodes to binge (start-end)",
|
help="A range of episodes to binge (start-end)",
|
||||||
)
|
)
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def search(config: Config, anime_titles: str, episode_range: str):
|
def search(config: "Config", anime_titles: str, episode_range: str):
|
||||||
from click import clear
|
from click import clear
|
||||||
from rich import print
|
from rich import print
|
||||||
from rich.progress import Progress
|
from rich.progress import Progress
|
||||||
from thefuzz import fuzz
|
from thefuzz import fuzz
|
||||||
|
|
||||||
from ...AnimeProvider import AnimeProvider
|
|
||||||
from ...libs.anime_provider.types import Anime
|
|
||||||
from ...libs.fzf import fzf
|
from ...libs.fzf import fzf
|
||||||
from ...libs.rofi import Rofi
|
from ...libs.rofi import Rofi
|
||||||
from ..utils.mpv import run_mpv
|
|
||||||
from ..utils.tools import exit_app
|
from ..utils.tools import exit_app
|
||||||
from ..utils.utils import (
|
from ..utils.utils import fuzzy_inquirer
|
||||||
filter_by_quality,
|
|
||||||
fuzzy_inquirer,
|
|
||||||
move_preferred_subtitle_lang_to_top,
|
|
||||||
)
|
|
||||||
|
|
||||||
anime_provider = AnimeProvider(config.provider)
|
if config.manga:
|
||||||
|
from InquirerPy.prompts.number import NumberPrompt
|
||||||
|
from yt_dlp.utils import sanitize_filename
|
||||||
|
|
||||||
|
from ...MangaProvider import MangaProvider
|
||||||
|
from ..utils.feh import feh_manga_viewer
|
||||||
|
|
||||||
|
manga_title = anime_titles[0]
|
||||||
|
|
||||||
|
manga_provider = MangaProvider()
|
||||||
|
search_data = manga_provider.search_for_manga(manga_title)
|
||||||
|
if not search_data:
|
||||||
|
print("No search results")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
search_results = search_data["results"]
|
||||||
|
|
||||||
print(f"[green bold]Streaming:[/] {anime_titles}")
|
|
||||||
for anime_title in anime_titles:
|
|
||||||
# ---- search for anime ----
|
|
||||||
with Progress() as progress:
|
|
||||||
progress.add_task("Fetching Search Results...", total=None)
|
|
||||||
search_results = anime_provider.search_for_anime(
|
|
||||||
anime_title, config.translation_type
|
|
||||||
)
|
|
||||||
if not search_results:
|
|
||||||
print("Search results not found")
|
|
||||||
input("Enter to retry")
|
|
||||||
search(config, anime_title, episode_range)
|
|
||||||
return
|
|
||||||
search_results = search_results["results"]
|
|
||||||
if not search_results:
|
|
||||||
print("Anime not found :cry:")
|
|
||||||
exit_app()
|
|
||||||
search_results_ = {
|
search_results_ = {
|
||||||
search_result["title"]: search_result for search_result in search_results
|
sanitize_filename(search_result["title"]): search_result
|
||||||
|
for search_result in search_results
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.auto_select:
|
if config.auto_select:
|
||||||
search_result = max(
|
search_result_manga_title = max(
|
||||||
search_results_.keys(), key=lambda title: fuzz.ratio(title, anime_title)
|
search_results_.keys(),
|
||||||
|
key=lambda title: fuzz.ratio(title, manga_title),
|
||||||
)
|
)
|
||||||
print("[cyan]Auto Selecting:[/] ", search_result)
|
print("[cyan]Auto Selecting:[/] ", search_result_manga_title)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
choices = list(search_results_.keys())
|
choices = list(search_results_.keys())
|
||||||
|
preview = None
|
||||||
|
if config.preview:
|
||||||
|
from ..interfaces.utils import get_fzf_manga_preview
|
||||||
|
|
||||||
|
preview = get_fzf_manga_preview(search_results)
|
||||||
if config.use_fzf:
|
if config.use_fzf:
|
||||||
search_result = fzf.run(choices, "Please Select title: ", "FastAnime")
|
search_result_manga_title = fzf.run(
|
||||||
|
choices, "Please Select title: ", preview=preview
|
||||||
|
)
|
||||||
elif config.use_rofi:
|
elif config.use_rofi:
|
||||||
search_result = Rofi.run(choices, "Please Select Title")
|
search_result_manga_title = Rofi.run(choices, "Please Select Title")
|
||||||
else:
|
else:
|
||||||
search_result = fuzzy_inquirer(
|
search_result_manga_title = fuzzy_inquirer(
|
||||||
choices,
|
choices,
|
||||||
"Please Select Title",
|
"Please Select Title",
|
||||||
)
|
)
|
||||||
|
|
||||||
# ---- fetch selected anime ----
|
anilist_id = search_results_[search_result_manga_title]["id"]
|
||||||
with Progress() as progress:
|
manga_info = manga_provider.get_manga(anilist_id)
|
||||||
progress.add_task("Fetching Anime...", total=None)
|
if not manga_info:
|
||||||
anime: Anime | None = anime_provider.get_anime(
|
print("No manga info")
|
||||||
search_results_[search_result]["id"]
|
exit(1)
|
||||||
|
|
||||||
|
anilist_helper = None
|
||||||
|
if config.user:
|
||||||
|
from ...anilist import AniList
|
||||||
|
|
||||||
|
AniList.login_user(config.user["token"])
|
||||||
|
anilist_helper = AniList
|
||||||
|
|
||||||
|
def _manga_viewer():
|
||||||
|
chapter_number = NumberPrompt("Select a chapter number").execute()
|
||||||
|
chapter_info = manga_provider.get_chapter_thumbnails(
|
||||||
|
manga_info["id"], str(chapter_number)
|
||||||
)
|
)
|
||||||
|
|
||||||
if not anime:
|
if not chapter_info:
|
||||||
print("Sth went wring anime no found")
|
print("No chapter info")
|
||||||
input("Enter to continue...")
|
input("Enter to retry...")
|
||||||
search(config, anime_title, episode_range)
|
_manga_viewer()
|
||||||
return
|
return
|
||||||
episodes_range = []
|
print(
|
||||||
episodes: list[str] = sorted(
|
f"[purple bold]Now Reading: [/] {search_result_manga_title} [cyan bold]Chapter:[/] {chapter_info['title']}"
|
||||||
anime["availableEpisodesDetail"][config.translation_type], key=float
|
)
|
||||||
)
|
feh_manga_viewer(chapter_info["thumbnails"], str(chapter_info["title"]))
|
||||||
if episode_range:
|
if anilist_helper:
|
||||||
if ":" in episode_range:
|
anilist_helper.update_anime_list(
|
||||||
ep_range_tuple = episode_range.split(":")
|
{"mediaId": anilist_id, "progress": chapter_number}
|
||||||
if len(ep_range_tuple) == 3 and all(ep_range_tuple):
|
)
|
||||||
episodes_start, episodes_end, step = ep_range_tuple
|
_manga_viewer()
|
||||||
episodes_range = episodes[
|
|
||||||
int(episodes_start) : int(episodes_end) : int(step)
|
_manga_viewer()
|
||||||
]
|
else:
|
||||||
|
from ...AnimeProvider import AnimeProvider
|
||||||
|
from ...libs.anime_provider.types import Anime
|
||||||
|
from ...Utility.data import anime_normalizer
|
||||||
|
from ..utils.mpv import run_mpv
|
||||||
|
from ..utils.utils import filter_by_quality, move_preferred_subtitle_lang_to_top
|
||||||
|
|
||||||
|
anime_provider = AnimeProvider(config.provider)
|
||||||
|
anilist_anime_info = None
|
||||||
|
|
||||||
|
print(f"[green bold]Streaming:[/] {anime_titles}")
|
||||||
|
for anime_title in anime_titles:
|
||||||
|
# ---- search for anime ----
|
||||||
|
with Progress() as progress:
|
||||||
|
progress.add_task("Fetching Search Results...", total=None)
|
||||||
|
search_results = anime_provider.search_for_anime(
|
||||||
|
anime_title, config.translation_type
|
||||||
|
)
|
||||||
|
if not search_results:
|
||||||
|
print("Search results not found")
|
||||||
|
input("Enter to retry")
|
||||||
|
search(config, anime_title, episode_range)
|
||||||
|
return
|
||||||
|
search_results = search_results["results"]
|
||||||
|
if not search_results:
|
||||||
|
print("Anime not found :cry:")
|
||||||
|
exit_app()
|
||||||
|
search_results_ = {
|
||||||
|
search_result["title"]: search_result
|
||||||
|
for search_result in search_results
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.auto_select:
|
||||||
|
search_result_manga_title = max(
|
||||||
|
search_results_.keys(),
|
||||||
|
key=lambda title: fuzz.ratio(
|
||||||
|
anime_normalizer.get(title, title), anime_title
|
||||||
|
),
|
||||||
|
)
|
||||||
|
print("[cyan]Auto Selecting:[/] ", search_result_manga_title)
|
||||||
|
|
||||||
elif len(ep_range_tuple) == 2 and all(ep_range_tuple):
|
|
||||||
episodes_start, episodes_end = ep_range_tuple
|
|
||||||
episodes_range = episodes[int(episodes_start) : int(episodes_end)]
|
|
||||||
else:
|
|
||||||
episodes_start, episodes_end = ep_range_tuple
|
|
||||||
if episodes_start.strip():
|
|
||||||
episodes_range = episodes[int(episodes_start) :]
|
|
||||||
elif episodes_end.strip():
|
|
||||||
episodes_range = episodes[: int(episodes_end)]
|
|
||||||
else:
|
|
||||||
episodes_range = episodes
|
|
||||||
else:
|
else:
|
||||||
episodes_range = episodes[int(episode_range) :]
|
choices = list(search_results_.keys())
|
||||||
|
|
||||||
episodes_range = iter(episodes_range)
|
|
||||||
|
|
||||||
def stream_anime():
|
|
||||||
clear()
|
|
||||||
episode = None
|
|
||||||
|
|
||||||
if episodes_range:
|
|
||||||
try:
|
|
||||||
episode = next(episodes_range) # pyright:ignore
|
|
||||||
print(
|
|
||||||
f"[cyan]Auto selecting:[/] {search_result} [cyan]Episode:[/] {episode}"
|
|
||||||
)
|
|
||||||
except StopIteration:
|
|
||||||
print("[green]Completed binge sequence[/]:smile:")
|
|
||||||
return
|
|
||||||
|
|
||||||
if not episode or episode not in episodes:
|
|
||||||
choices = [*episodes, "end"]
|
|
||||||
if config.use_fzf:
|
if config.use_fzf:
|
||||||
episode = fzf.run(
|
search_result_manga_title = fzf.run(
|
||||||
choices, "Select an episode: ", header=search_result
|
choices, "Please Select title: ", "FastAnime"
|
||||||
)
|
)
|
||||||
elif config.use_rofi:
|
elif config.use_rofi:
|
||||||
episode = Rofi.run(choices, "Select an episode")
|
search_result_manga_title = Rofi.run(choices, "Please Select Title")
|
||||||
else:
|
else:
|
||||||
episode = fuzzy_inquirer(
|
search_result_manga_title = fuzzy_inquirer(
|
||||||
choices,
|
choices,
|
||||||
"Select episode",
|
"Please Select Title",
|
||||||
)
|
)
|
||||||
if episode == "end":
|
|
||||||
return
|
|
||||||
|
|
||||||
# ---- fetch streams ----
|
# ---- fetch selected anime ----
|
||||||
with Progress() as progress:
|
with Progress() as progress:
|
||||||
progress.add_task("Fetching Episode Streams...", total=None)
|
progress.add_task("Fetching Anime...", total=None)
|
||||||
streams = anime_provider.get_episode_streams(
|
anime: Anime | None = anime_provider.get_anime(
|
||||||
anime, episode, config.translation_type
|
search_results_[search_result_manga_title]["id"]
|
||||||
)
|
)
|
||||||
if not streams:
|
|
||||||
print("Failed to get streams")
|
if not anime:
|
||||||
|
print("Sth went wring anime no found")
|
||||||
|
input("Enter to continue...")
|
||||||
|
search(config, anime_title, episode_range)
|
||||||
|
return
|
||||||
|
episodes_range = []
|
||||||
|
episodes: list[str] = sorted(
|
||||||
|
anime["availableEpisodesDetail"][config.translation_type], key=float
|
||||||
|
)
|
||||||
|
if episode_range:
|
||||||
|
if ":" in episode_range:
|
||||||
|
ep_range_tuple = episode_range.split(":")
|
||||||
|
if len(ep_range_tuple) == 3 and all(ep_range_tuple):
|
||||||
|
episodes_start, episodes_end, step = ep_range_tuple
|
||||||
|
episodes_range = episodes[
|
||||||
|
int(episodes_start) : int(episodes_end) : int(step)
|
||||||
|
]
|
||||||
|
|
||||||
|
elif len(ep_range_tuple) == 2 and all(ep_range_tuple):
|
||||||
|
episodes_start, episodes_end = ep_range_tuple
|
||||||
|
episodes_range = episodes[
|
||||||
|
int(episodes_start) : int(episodes_end)
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
episodes_start, episodes_end = ep_range_tuple
|
||||||
|
if episodes_start.strip():
|
||||||
|
episodes_range = episodes[int(episodes_start) :]
|
||||||
|
elif episodes_end.strip():
|
||||||
|
episodes_range = episodes[: int(episodes_end)]
|
||||||
|
else:
|
||||||
|
episodes_range = episodes
|
||||||
|
else:
|
||||||
|
episodes_range = episodes[int(episode_range) :]
|
||||||
|
|
||||||
|
episodes_range = iter(episodes_range)
|
||||||
|
|
||||||
|
if config.normalize_titles:
|
||||||
|
from ...libs.common.mini_anilist import get_basic_anime_info_by_title
|
||||||
|
|
||||||
|
anilist_anime_info = get_basic_anime_info_by_title(anime["title"])
|
||||||
|
|
||||||
|
def stream_anime():
|
||||||
|
clear()
|
||||||
|
episode = None
|
||||||
|
|
||||||
|
if episodes_range:
|
||||||
|
try:
|
||||||
|
episode = next(episodes_range) # pyright:ignore
|
||||||
|
print(
|
||||||
|
f"[cyan]Auto selecting:[/] {search_result_manga_title} [cyan]Episode:[/] {episode}"
|
||||||
|
)
|
||||||
|
except StopIteration:
|
||||||
|
print("[green]Completed binge sequence[/]:smile:")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not episode or episode not in episodes:
|
||||||
|
choices = [*episodes, "end"]
|
||||||
|
if config.use_fzf:
|
||||||
|
episode = fzf.run(
|
||||||
|
choices,
|
||||||
|
"Select an episode: ",
|
||||||
|
header=search_result_manga_title,
|
||||||
|
)
|
||||||
|
elif config.use_rofi:
|
||||||
|
episode = Rofi.run(choices, "Select an episode")
|
||||||
|
else:
|
||||||
|
episode = fuzzy_inquirer(
|
||||||
|
choices,
|
||||||
|
"Select episode",
|
||||||
|
)
|
||||||
|
if episode == "end":
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
# ---- fetch streams ----
|
||||||
# ---- fetch servers ----
|
with Progress() as progress:
|
||||||
if config.server == "top":
|
progress.add_task("Fetching Episode Streams...", total=None)
|
||||||
with Progress() as progress:
|
streams = anime_provider.get_episode_streams(
|
||||||
progress.add_task("Fetching top server...", total=None)
|
anime, episode, config.translation_type
|
||||||
server = next(streams, None)
|
)
|
||||||
if not server:
|
if not streams:
|
||||||
print("Sth went wrong when fetching the episode")
|
print("Failed to get streams")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# ---- fetch servers ----
|
||||||
|
if config.server == "top":
|
||||||
|
with Progress() as progress:
|
||||||
|
progress.add_task("Fetching top server...", total=None)
|
||||||
|
server = next(streams, None)
|
||||||
|
if not server:
|
||||||
|
print("Sth went wrong when fetching the episode")
|
||||||
|
input("Enter to continue")
|
||||||
|
stream_anime()
|
||||||
|
return
|
||||||
|
stream_link = filter_by_quality(config.quality, server["links"])
|
||||||
|
if not stream_link:
|
||||||
|
print("Quality not found")
|
||||||
input("Enter to continue")
|
input("Enter to continue")
|
||||||
stream_anime()
|
stream_anime()
|
||||||
return
|
return
|
||||||
stream_link = filter_by_quality(config.quality, server["links"])
|
link = stream_link["link"]
|
||||||
if not stream_link:
|
subtitles = server["subtitles"]
|
||||||
print("Quality not found")
|
stream_headers = server["headers"]
|
||||||
input("Enter to continue")
|
episode_title = server["episode_title"]
|
||||||
stream_anime()
|
|
||||||
return
|
|
||||||
link = stream_link["link"]
|
|
||||||
subtitles = server["subtitles"]
|
|
||||||
stream_headers = server["headers"]
|
|
||||||
episode_title = server["episode_title"]
|
|
||||||
else:
|
|
||||||
with Progress() as progress:
|
|
||||||
progress.add_task("Fetching servers", total=None)
|
|
||||||
# prompt for server selection
|
|
||||||
servers = {server["server"]: server for server in streams}
|
|
||||||
servers_names = list(servers.keys())
|
|
||||||
if config.server in servers_names:
|
|
||||||
server = config.server
|
|
||||||
else:
|
else:
|
||||||
if config.use_fzf:
|
with Progress() as progress:
|
||||||
server = fzf.run(servers_names, "Select an link: ")
|
progress.add_task("Fetching servers", total=None)
|
||||||
elif config.use_rofi:
|
# prompt for server selection
|
||||||
server = Rofi.run(servers_names, "Select an link")
|
servers = {server["server"]: server for server in streams}
|
||||||
|
servers_names = list(servers.keys())
|
||||||
|
if config.server in servers_names:
|
||||||
|
server = config.server
|
||||||
else:
|
else:
|
||||||
server = fuzzy_inquirer(
|
if config.use_fzf:
|
||||||
servers_names,
|
server = fzf.run(servers_names, "Select an link: ")
|
||||||
"Select link",
|
elif config.use_rofi:
|
||||||
)
|
server = Rofi.run(servers_names, "Select an link")
|
||||||
stream_link = filter_by_quality(
|
else:
|
||||||
config.quality, servers[server]["links"]
|
server = fuzzy_inquirer(
|
||||||
)
|
servers_names,
|
||||||
if not stream_link:
|
"Select link",
|
||||||
print("Quality not found")
|
)
|
||||||
input("Enter to continue")
|
stream_link = filter_by_quality(
|
||||||
stream_anime()
|
config.quality, servers[server]["links"]
|
||||||
return
|
)
|
||||||
link = stream_link["link"]
|
if not stream_link:
|
||||||
stream_headers = servers[server]["headers"]
|
print("Quality not found")
|
||||||
subtitles = servers[server]["subtitles"]
|
input("Enter to continue")
|
||||||
episode_title = servers[server]["episode_title"]
|
stream_anime()
|
||||||
print(f"[purple]Now Playing:[/] {search_result} Episode {episode}")
|
return
|
||||||
|
link = stream_link["link"]
|
||||||
|
stream_headers = servers[server]["headers"]
|
||||||
|
subtitles = servers[server]["subtitles"]
|
||||||
|
episode_title = servers[server]["episode_title"]
|
||||||
|
|
||||||
subtitles = move_preferred_subtitle_lang_to_top(
|
selected_anime_title = search_result_manga_title
|
||||||
subtitles, config.sub_lang
|
if anilist_anime_info:
|
||||||
)
|
selected_anime_title = (
|
||||||
if config.sync_play:
|
anilist_anime_info["title"][config.preferred_language]
|
||||||
from ..utils.syncplay import SyncPlayer
|
or anilist_anime_info["title"]["romaji"]
|
||||||
|
or anilist_anime_info["title"]["english"]
|
||||||
|
)
|
||||||
|
import re
|
||||||
|
|
||||||
SyncPlayer(
|
for episode_detail in anilist_anime_info["episodes"]:
|
||||||
link, episode_title, headers=stream_headers, subtitles=subtitles
|
if re.match(f"Episode {episode} ", episode_detail["title"]):
|
||||||
|
episode_title = episode_detail["title"]
|
||||||
|
break
|
||||||
|
print(
|
||||||
|
f"[purple]Now Playing:[/] {selected_anime_title} Episode {episode}"
|
||||||
)
|
)
|
||||||
else:
|
subtitles = move_preferred_subtitle_lang_to_top(
|
||||||
run_mpv(
|
subtitles, config.sub_lang
|
||||||
link, episode_title, headers=stream_headers, subtitles=subtitles
|
|
||||||
)
|
)
|
||||||
except IndexError as e:
|
if config.sync_play:
|
||||||
print(e)
|
from ..utils.syncplay import SyncPlayer
|
||||||
input("Enter to continue")
|
|
||||||
|
SyncPlayer(
|
||||||
|
link,
|
||||||
|
episode_title,
|
||||||
|
headers=stream_headers,
|
||||||
|
subtitles=subtitles,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
run_mpv(
|
||||||
|
link,
|
||||||
|
episode_title,
|
||||||
|
headers=stream_headers,
|
||||||
|
subtitles=subtitles,
|
||||||
|
)
|
||||||
|
except IndexError as e:
|
||||||
|
print(e)
|
||||||
|
input("Enter to continue")
|
||||||
|
stream_anime()
|
||||||
|
|
||||||
stream_anime()
|
stream_anime()
|
||||||
|
|
||||||
stream_anime()
|
|
||||||
|
|||||||
@@ -6,22 +6,20 @@ ANILIST_ENDPOINT = "https://graphql.anilist.co"
|
|||||||
|
|
||||||
|
|
||||||
anime_title_query = """
|
anime_title_query = """
|
||||||
query($query:String){
|
query ($query: String) {
|
||||||
Page(perPage:50){
|
Page(perPage: 50) {
|
||||||
pageInfo{
|
pageInfo {
|
||||||
total
|
total
|
||||||
currentPage
|
|
||||||
hasNextPage
|
|
||||||
}
|
|
||||||
media(search:$query,type:ANIME){
|
|
||||||
id
|
|
||||||
idMal
|
|
||||||
title{
|
|
||||||
romaji
|
|
||||||
english
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
media(search: $query, type: ANIME) {
|
||||||
|
id
|
||||||
|
idMal
|
||||||
|
title {
|
||||||
|
romaji
|
||||||
|
english
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -46,20 +44,6 @@ def get_anime_titles(query: str, variables: dict = {}):
|
|||||||
)
|
)
|
||||||
anilist_data = response.json()
|
anilist_data = response.json()
|
||||||
|
|
||||||
# ensuring you dont get blocked
|
|
||||||
if (
|
|
||||||
int(response.headers.get("X-RateLimit-Remaining", 0)) < 30
|
|
||||||
and not response.status_code == 500
|
|
||||||
):
|
|
||||||
print("Warning you are exceeding the allowed number of calls per minute")
|
|
||||||
logger.warning(
|
|
||||||
"You are exceeding the allowed number of calls per minute for the AniList api enforcing timeout"
|
|
||||||
)
|
|
||||||
print("Forced timeout will now be initiated")
|
|
||||||
import time
|
|
||||||
|
|
||||||
print("sleeping...")
|
|
||||||
time.sleep(1 * 60)
|
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
eng_titles = [
|
eng_titles = [
|
||||||
anime["title"]["english"]
|
anime["title"]["english"]
|
||||||
@@ -79,5 +63,33 @@ def get_anime_titles(query: str, variables: dict = {}):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def downloaded_anime_titles(ctx, param, incomplete):
|
||||||
|
import os
|
||||||
|
|
||||||
|
from ..constants import USER_VIDEOS_DIR
|
||||||
|
|
||||||
|
try:
|
||||||
|
titles = [
|
||||||
|
title
|
||||||
|
for title in os.listdir(USER_VIDEOS_DIR)
|
||||||
|
if title.lower().startswith(incomplete.lower()) or not incomplete
|
||||||
|
]
|
||||||
|
return titles
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
def anime_titles_shell_complete(ctx, param, incomplete):
|
def anime_titles_shell_complete(ctx, param, incomplete):
|
||||||
return [name for name in get_anime_titles(anime_title_query, {"query": incomplete})]
|
incomplete = incomplete.strip()
|
||||||
|
if not incomplete:
|
||||||
|
incomplete = None
|
||||||
|
variables = {}
|
||||||
|
else:
|
||||||
|
variables = {"query": incomplete}
|
||||||
|
return get_anime_titles(anime_title_query, variables)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
t = input("Enter title")
|
||||||
|
results = get_anime_titles(anime_title_query, {"query": t})
|
||||||
|
print(results)
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import os
|
|||||||
from configparser import ConfigParser
|
from configparser import ConfigParser
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from rich import print
|
|
||||||
|
|
||||||
from ..constants import USER_CONFIG_PATH, USER_DATA_PATH, USER_VIDEOS_DIR
|
from ..constants import USER_CONFIG_PATH, USER_DATA_PATH, USER_VIDEOS_DIR
|
||||||
from ..libs.rofi import Rofi
|
from ..libs.rofi import Rofi
|
||||||
|
|
||||||
@@ -15,46 +13,7 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
class Config(object):
|
class Config(object):
|
||||||
"""class that handles and manages configuration and user data throughout the clis lifespan
|
manga = False
|
||||||
|
|
||||||
Attributes:
|
|
||||||
anime_list: [TODO:attribute]
|
|
||||||
watch_history: [TODO:attribute]
|
|
||||||
fastanime_anilist_app_login_url: [TODO:attribute]
|
|
||||||
anime_provider: [TODO:attribute]
|
|
||||||
user_data: [TODO:attribute]
|
|
||||||
configparser: [TODO:attribute]
|
|
||||||
downloads_dir: [TODO:attribute]
|
|
||||||
provider: [TODO:attribute]
|
|
||||||
use_fzf: [TODO:attribute]
|
|
||||||
use_rofi: [TODO:attribute]
|
|
||||||
skip: [TODO:attribute]
|
|
||||||
icons: [TODO:attribute]
|
|
||||||
preview: [TODO:attribute]
|
|
||||||
translation_type: [TODO:attribute]
|
|
||||||
sort_by: [TODO:attribute]
|
|
||||||
continue_from_history: [TODO:attribute]
|
|
||||||
auto_next: [TODO:attribute]
|
|
||||||
auto_select: [TODO:attribute]
|
|
||||||
use_mpv_mod: [TODO:attribute]
|
|
||||||
quality: [TODO:attribute]
|
|
||||||
notification_duration: [TODO:attribute]
|
|
||||||
error: [TODO:attribute]
|
|
||||||
server: [TODO:attribute]
|
|
||||||
format: [TODO:attribute]
|
|
||||||
force_window: [TODO:attribute]
|
|
||||||
preferred_language: [TODO:attribute]
|
|
||||||
rofi_theme: [TODO:attribute]
|
|
||||||
rofi_theme: [TODO:attribute]
|
|
||||||
rofi_theme_input: [TODO:attribute]
|
|
||||||
rofi_theme_input: [TODO:attribute]
|
|
||||||
rofi_theme_confirm: [TODO:attribute]
|
|
||||||
rofi_theme_confirm: [TODO:attribute]
|
|
||||||
watch_history: [TODO:attribute]
|
|
||||||
anime_list: [TODO:attribute]
|
|
||||||
user: [TODO:attribute]
|
|
||||||
"""
|
|
||||||
|
|
||||||
sync_play = False
|
sync_play = False
|
||||||
anime_list: list
|
anime_list: list
|
||||||
watch_history: dict
|
watch_history: dict
|
||||||
@@ -63,52 +22,50 @@ class Config(object):
|
|||||||
)
|
)
|
||||||
anime_provider: "AnimeProvider"
|
anime_provider: "AnimeProvider"
|
||||||
user_data = {"watch_history": {}, "animelist": [], "user": {}}
|
user_data = {"watch_history": {}, "animelist": [], "user": {}}
|
||||||
|
default_options = {
|
||||||
|
"quality": "1080",
|
||||||
|
"auto_next": "False",
|
||||||
|
"auto_select": "True",
|
||||||
|
"sort_by": "search match",
|
||||||
|
"downloads_dir": USER_VIDEOS_DIR,
|
||||||
|
"translation_type": "sub",
|
||||||
|
"server": "top",
|
||||||
|
"continue_from_history": "True",
|
||||||
|
"preferred_history": "local",
|
||||||
|
"use_python_mpv": "false",
|
||||||
|
"force_window": "immediate",
|
||||||
|
"preferred_language": "english",
|
||||||
|
"use_fzf": "False",
|
||||||
|
"preview": "False",
|
||||||
|
"format": "best[height<=1080]/bestvideo[height<=1080]+bestaudio/best",
|
||||||
|
"provider": "allanime",
|
||||||
|
"error": "3",
|
||||||
|
"icons": "false",
|
||||||
|
"notification_duration": "2",
|
||||||
|
"skip": "false",
|
||||||
|
"use_rofi": "false",
|
||||||
|
"rofi_theme": "",
|
||||||
|
"rofi_theme_input": "",
|
||||||
|
"rofi_theme_confirm": "",
|
||||||
|
"ffmpegthumnailer_seek_time": "-1",
|
||||||
|
"sub_lang": "eng",
|
||||||
|
"normalize_titles": "true",
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.initialize_user_data()
|
self.initialize_user_data()
|
||||||
self.load_config()
|
self.load_config()
|
||||||
|
|
||||||
def load_config(self):
|
def load_config(self):
|
||||||
self.configparser = ConfigParser(
|
self.configparser = ConfigParser(self.default_options)
|
||||||
{
|
|
||||||
"quality": "1080",
|
|
||||||
"auto_next": "False",
|
|
||||||
"auto_select": "True",
|
|
||||||
"sort_by": "search match",
|
|
||||||
"downloads_dir": USER_VIDEOS_DIR,
|
|
||||||
"translation_type": "sub",
|
|
||||||
"server": "top",
|
|
||||||
"continue_from_history": "True",
|
|
||||||
"preferred_history": "local",
|
|
||||||
"use_mpv_mod": "false",
|
|
||||||
"force_window": "immediate",
|
|
||||||
"preferred_language": "english",
|
|
||||||
"use_fzf": "False",
|
|
||||||
"preview": "False",
|
|
||||||
"format": "best[height<=1080]/bestvideo[height<=1080]+bestaudio/best",
|
|
||||||
"provider": "allanime",
|
|
||||||
"error": "3",
|
|
||||||
"icons": "false",
|
|
||||||
"notification_duration": "2",
|
|
||||||
"skip": "false",
|
|
||||||
"use_rofi": "false",
|
|
||||||
"rofi_theme": "",
|
|
||||||
"rofi_theme_input": "",
|
|
||||||
"rofi_theme_confirm": "",
|
|
||||||
"ffmpegthumnailer_seek_time": "-1",
|
|
||||||
"sub_lang": "eng",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
self.configparser.add_section("stream")
|
self.configparser.add_section("stream")
|
||||||
self.configparser.add_section("general")
|
self.configparser.add_section("general")
|
||||||
self.configparser.add_section("anilist")
|
self.configparser.add_section("anilist")
|
||||||
if not os.path.exists(USER_CONFIG_PATH):
|
|
||||||
with open(USER_CONFIG_PATH, "w") as config:
|
|
||||||
self.configparser.write(config)
|
|
||||||
|
|
||||||
self.configparser.read(USER_CONFIG_PATH)
|
|
||||||
|
|
||||||
# --- set config values from file or using defaults ---
|
# --- set config values from file or using defaults ---
|
||||||
|
if os.path.exists(USER_CONFIG_PATH):
|
||||||
|
self.configparser.read(USER_CONFIG_PATH,encoding="utf-8")
|
||||||
|
|
||||||
self.downloads_dir = self.get_downloads_dir()
|
self.downloads_dir = self.get_downloads_dir()
|
||||||
self.sub_lang = self.get_sub_lang()
|
self.sub_lang = self.get_sub_lang()
|
||||||
self.provider = self.get_provider()
|
self.provider = self.get_provider()
|
||||||
@@ -121,8 +78,9 @@ class Config(object):
|
|||||||
self.sort_by = self.get_sort_by()
|
self.sort_by = self.get_sort_by()
|
||||||
self.continue_from_history = self.get_continue_from_history()
|
self.continue_from_history = self.get_continue_from_history()
|
||||||
self.auto_next = self.get_auto_next()
|
self.auto_next = self.get_auto_next()
|
||||||
|
self.normalize_titles = self.get_normalize_titles()
|
||||||
self.auto_select = self.get_auto_select()
|
self.auto_select = self.get_auto_select()
|
||||||
self.use_mpv_mod = self.get_use_mpv_mod()
|
self.use_python_mpv = self.get_use_mpv_mod()
|
||||||
self.quality = self.get_quality()
|
self.quality = self.get_quality()
|
||||||
self.notification_duration = self.get_notification_duration()
|
self.notification_duration = self.get_notification_duration()
|
||||||
self.error = self.get_error()
|
self.error = self.get_error()
|
||||||
@@ -143,6 +101,11 @@ class Config(object):
|
|||||||
self.anime_list: list = self.user_data.get("animelist", [])
|
self.anime_list: list = self.user_data.get("animelist", [])
|
||||||
self.user: dict = self.user_data.get("user", {})
|
self.user: dict = self.user_data.get("user", {})
|
||||||
|
|
||||||
|
os.environ["CURRENT_FASTANIME_PROVIDER"] = self.provider
|
||||||
|
if not os.path.exists(USER_CONFIG_PATH):
|
||||||
|
with open(USER_CONFIG_PATH, "w",encoding="utf-8") as config:
|
||||||
|
config.write(self.__repr__())
|
||||||
|
|
||||||
def update_user(self, user):
|
def update_user(self, user):
|
||||||
self.user = user
|
self.user = user
|
||||||
self.user_data["user"] = user
|
self.user_data["user"] = user
|
||||||
@@ -217,6 +180,9 @@ class Config(object):
|
|||||||
def get_rofi_theme_confirm(self):
|
def get_rofi_theme_confirm(self):
|
||||||
return self.configparser.get("general", "rofi_theme_confirm")
|
return self.configparser.get("general", "rofi_theme_confirm")
|
||||||
|
|
||||||
|
def get_normalize_titles(self):
|
||||||
|
return self.configparser.getboolean("general", "normalize_titles")
|
||||||
|
|
||||||
# --- stream section ---
|
# --- stream section ---
|
||||||
def get_skip(self):
|
def get_skip(self):
|
||||||
return self.configparser.getboolean("stream", "skip")
|
return self.configparser.getboolean("stream", "skip")
|
||||||
@@ -231,7 +197,7 @@ class Config(object):
|
|||||||
return self.configparser.getboolean("stream", "continue_from_history")
|
return self.configparser.getboolean("stream", "continue_from_history")
|
||||||
|
|
||||||
def get_use_mpv_mod(self):
|
def get_use_mpv_mod(self):
|
||||||
return self.configparser.getboolean("stream", "use_mpv_mod")
|
return self.configparser.getboolean("stream", "use_python_mpv")
|
||||||
|
|
||||||
def get_notification_duration(self):
|
def get_notification_duration(self):
|
||||||
return self.configparser.getint("general", "notification_duration")
|
return self.configparser.getint("general", "notification_duration")
|
||||||
@@ -266,108 +232,195 @@ class Config(object):
|
|||||||
self.configparser.write(config)
|
self.configparser.write(config)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
current_config_state = f"""
|
current_config_state = f"""\
|
||||||
[stream]
|
#
|
||||||
# Auto continue from watch history
|
# ███████╗░█████╗░░██████╗████████╗░█████╗░███╗░░██╗██╗███╗░░░███╗███████╗ ░█████╗░░█████╗░███╗░░██╗███████╗██╗░██████╗░
|
||||||
continue_from_history = {self.continue_from_history}
|
# ██╔════╝██╔══██╗██╔════╝╚══██╔══╝██╔══██╗████╗░██║██║████╗░████║██╔════╝ ██╔══██╗██╔══██╗████╗░██║██╔════╝██║██╔════╝░
|
||||||
|
# █████╗░░███████║╚█████╗░░░░██║░░░███████║██╔██╗██║██║██╔████╔██║█████╗░░ ██║░░╚═╝██║░░██║██╔██╗██║█████╗░░██║██║░░██╗░
|
||||||
# which hostory to use [local/remote]
|
# ██╔══╝░░██╔══██║░╚═══██╗░░░██║░░░██╔══██║██║╚████║██║██║╚██╔╝██║██╔══╝░░ ██║░░██╗██║░░██║██║╚████║██╔══╝░░██║██║░░╚██╗
|
||||||
preferred_history = {self.preferred_history}
|
# ██║░░░░░██║░░██║██████╔╝░░░██║░░░██║░░██║██║░╚███║██║██║░╚═╝░██║███████╗ ╚█████╔╝╚█████╔╝██║░╚███║██║░░░░░██║╚██████╔╝
|
||||||
|
# ╚═╝░░░░░╚═╝░░╚═╝╚═════╝░░░░╚═╝░░░╚═╝░░╚═╝╚═╝░░╚══╝╚═╝╚═╝░░░░░╚═╝╚══════╝ ░╚════╝░░╚════╝░╚═╝░░╚══╝╚═╝░░░░░╚═╝░╚═════╝░
|
||||||
|
#
|
||||||
# Preferred language for anime (options: dub, sub)
|
|
||||||
translation_type = {self.translation_type}
|
|
||||||
|
|
||||||
# Default server (options: dropbox, sharepoint, wetransfer.gogoanime, top, wixmp)
|
|
||||||
server = {self.server}
|
|
||||||
|
|
||||||
# Auto-select next episode
|
|
||||||
auto_next = {self.auto_next}
|
|
||||||
|
|
||||||
# Auto select the anime provider results with fuzzy find.
|
|
||||||
# Note this wont always be correct.But 99% of the time will be.
|
|
||||||
auto_select = {self.auto_select}
|
|
||||||
|
|
||||||
# whether to skip the opening and ending theme songs
|
|
||||||
# NOTE: requires ani-skip to be in path
|
|
||||||
skip = {self.skip}
|
|
||||||
|
|
||||||
# the maximum delta time in minutes after which the episode should be considered as completed
|
|
||||||
# used in the continue from time stamp
|
|
||||||
error = {self.error}
|
|
||||||
|
|
||||||
# whether to use python-mpv
|
|
||||||
# to enable superior control over the player
|
|
||||||
# adding more options to it
|
|
||||||
use_mpv_mod = {self.use_mpv_mod}
|
|
||||||
|
|
||||||
# force mpv window
|
|
||||||
# passed directly to mpv so values are same
|
|
||||||
force_window = immediate
|
|
||||||
|
|
||||||
# the format of downloaded anime and trailer
|
|
||||||
# based on yt-dlp format and passed directly to it
|
|
||||||
# learn more by looking it up on their site
|
|
||||||
# only works for downloaded anime if server=gogoanime
|
|
||||||
# since its the only one that offers different formats
|
|
||||||
# the others tend not to
|
|
||||||
format = {self.format}
|
|
||||||
|
|
||||||
[general]
|
[general]
|
||||||
|
# whether to show the icons in the tui [True/False]
|
||||||
|
# more like emojis
|
||||||
|
# by the way if you have any recommendations to which should be used where please
|
||||||
|
# don't hesitate to share your opinion
|
||||||
|
# cause it's a lot of work to look for the right one for each menu option
|
||||||
|
# be sure to also give the replacement emoji
|
||||||
|
icons = {self.icons}
|
||||||
|
|
||||||
# can be [allanime,animepahe]
|
# the quality of the stream [1080,720,480,360]
|
||||||
|
# this option is usually only reliable when:
|
||||||
|
# provider=animepahe
|
||||||
|
# since it provides links that actually point to streams of different qualities
|
||||||
|
# while the rest just point to another link that can provide the anime from the same server
|
||||||
|
quality = {self.quality}
|
||||||
|
|
||||||
|
# whether to normalize provider titles [True/False]
|
||||||
|
# basically takes the provider titles and finds the corresponding anilist title then changes the title to that
|
||||||
|
# useful for uniformity especially when downloading from different providers
|
||||||
|
# this also applies to episode titles
|
||||||
|
normalize_titles = {self.normalize_titles}
|
||||||
|
|
||||||
|
# can be [allanime, animepahe, aniwatch]
|
||||||
|
# allanime is the most realible
|
||||||
|
# animepahe provides different links to streams of different quality so a quality can be selected reliably with --quality option
|
||||||
|
# aniwatch which is now hianime usually provides subs in different languuages and its servers are generally faster
|
||||||
provider = {self.provider}
|
provider = {self.provider}
|
||||||
|
|
||||||
# Display language (options: english, romaji)
|
# Display language [english, romaji]
|
||||||
|
# this is passed to anilist directly and is used to set the language which the anime titles will be in
|
||||||
|
# when using the anilist interface
|
||||||
preferred_language = {self.preferred_language}
|
preferred_language = {self.preferred_language}
|
||||||
|
|
||||||
# Download directory
|
# Download directory
|
||||||
|
# where you will find your videos after downloading them with 'fastanime download' command
|
||||||
downloads_dir = {self.downloads_dir}
|
downloads_dir = {self.downloads_dir}
|
||||||
|
|
||||||
# whether to show a preview window when using fzf or rofi
|
# whether to show a preview window when using fzf or rofi [True/False]
|
||||||
|
# the preview requires you have a commandline image viewer as documented in the README
|
||||||
|
# this is only when usinf fzf
|
||||||
|
# if you dont care about image previews it doesnt matter
|
||||||
|
# though its awesome
|
||||||
|
# try it and you will see
|
||||||
preview = {self.preview}
|
preview = {self.preview}
|
||||||
|
|
||||||
# the time to seek when using ffmpegthumbnailer [-1 to 100]
|
# the time to seek when using ffmpegthumbnailer [-1 to 100]
|
||||||
# -1 means random and is the default
|
# -1 means random and is the default
|
||||||
|
# ffmpegthumbnailer is used to generate previews and you can select at what time in the video to extract an image
|
||||||
|
# random makes things quite exciting cause you never no at what time it will extract the image from
|
||||||
ffmpegthumbnailer_seek_time = {self.ffmpegthumbnailer_seek_time}
|
ffmpegthumbnailer_seek_time = {self.ffmpegthumbnailer_seek_time}
|
||||||
|
|
||||||
# whether to use fzf as the interface for the anilist command and others.
|
# whether to use fzf as the interface for the anilist command and others. [True/False]
|
||||||
use_fzf = {self.use_fzf}
|
use_fzf = {self.use_fzf}
|
||||||
|
|
||||||
# whether to use rofi for the ui
|
# whether to use rofi for the ui [True/False]
|
||||||
|
# it's more useful if you want to create a desktop entry
|
||||||
|
# which can be setup with 'fastanime config --desktop-entry'
|
||||||
|
# though if you want it to be your sole interface even when fastanime is run directly from the terminal
|
||||||
use_rofi = {self.use_rofi}
|
use_rofi = {self.use_rofi}
|
||||||
|
|
||||||
# rofi theme to use
|
# rofi themes to use
|
||||||
|
# the values of this option is the path to the rofi config files to use
|
||||||
|
# i choose to split it into three since it gives the best look and feel
|
||||||
|
# you can refer to the rofi demo on github to see for your self
|
||||||
|
# by the way i recommend getting the rofi themes from this project;
|
||||||
rofi_theme = {self.rofi_theme}
|
rofi_theme = {self.rofi_theme}
|
||||||
|
|
||||||
rofi_theme_input = {self.rofi_theme_input}
|
rofi_theme_input = {self.rofi_theme_input}
|
||||||
|
|
||||||
rofi_theme_confirm = {self.rofi_theme_confirm}
|
rofi_theme_confirm = {self.rofi_theme_confirm}
|
||||||
|
|
||||||
|
|
||||||
# whether to show the icons
|
|
||||||
icons = {self.icons}
|
|
||||||
|
|
||||||
# the duration in minutes a notification will stay in the screen
|
# the duration in minutes a notification will stay in the screen
|
||||||
# used by notifier command
|
# used by notifier command
|
||||||
notification_duration = {self.notification_duration}
|
notification_duration = {self.notification_duration}
|
||||||
"""
|
|
||||||
|
# used when the provider gives subs of different languages
|
||||||
|
# currently its the case for:
|
||||||
|
# aniwatch
|
||||||
|
# the values for this option are the short names for countries
|
||||||
|
# regex is used to determine what you selected
|
||||||
|
sub_lang = {self.sub_lang}
|
||||||
|
|
||||||
|
|
||||||
|
[stream]
|
||||||
|
# Auto continue from watch history [True/False]
|
||||||
|
# this will make fastanime to choose the episode that you last watched to completion
|
||||||
|
# and increment it by one
|
||||||
|
# and use that to auto select the episode you want to watch
|
||||||
|
continue_from_history = {self.continue_from_history}
|
||||||
|
|
||||||
|
# which history to use [local/remote]
|
||||||
|
# local history means it will just use the watch history stored locally in your device
|
||||||
|
# the file that stores it is called watch_history.json and is stored next to your config file
|
||||||
|
# remote means it ignores the last episode stored locally and instead uses the one in your anilist anime list
|
||||||
|
# this config option is useful if you want to overwrite your local history or import history covered from another device or platform
|
||||||
|
# since remote history will take precendence over whats available locally
|
||||||
|
preferred_history = {self.preferred_history}
|
||||||
|
|
||||||
|
# Preferred language for anime [dub/sub]
|
||||||
|
translation_type = {self.translation_type}
|
||||||
|
|
||||||
|
# what server to use for a particular provider
|
||||||
|
# allanime: [dropbox, sharepoint, wetransfer, gogoanime, wixmp]
|
||||||
|
# animepahe: [kwik]
|
||||||
|
# aniwatch: [HD1, HD2, StreamSB, StreamTape]
|
||||||
|
# 'top' can also be used as a value for this option
|
||||||
|
# 'top' will cause fastanime to auto select the first server it sees
|
||||||
|
# this saves on resources and is faster since not all servers are being fetched
|
||||||
|
server = {self.server}
|
||||||
|
|
||||||
|
# Auto select next episode [True/False]
|
||||||
|
# this makes fastanime increment the current episode number
|
||||||
|
# then after using that value to fetch the next episode instead of prompting
|
||||||
|
# this option is useful for binging
|
||||||
|
auto_next = {self.auto_next}
|
||||||
|
|
||||||
|
# Auto select the anime provider results with fuzzy find. [True/False]
|
||||||
|
# Note this won't always be correct
|
||||||
|
# this is because the providers sometime use non-standard names
|
||||||
|
# that are there own preference rather than the official names
|
||||||
|
# But 99% of the time will be accurate
|
||||||
|
# if this happens just turn of auto_select in the menus or from the commandline and manually select the correct anime title
|
||||||
|
# and then please open an issue at <> highlighting the normalized title and the title given by the provider for the anime you wished to watch
|
||||||
|
# or even better edit this file <> and open a pull request
|
||||||
|
auto_select = {self.auto_select}
|
||||||
|
|
||||||
|
# whether to skip the opening and ending theme songs [True/False]
|
||||||
|
# NOTE: requires ani-skip to be in path
|
||||||
|
# for python-mpv users am planning to create this functionality n python without the use of an external script
|
||||||
|
# so its disabled for now
|
||||||
|
skip = {self.skip}
|
||||||
|
|
||||||
|
# the maximum delta time in minutes after which the episode should be considered as completed
|
||||||
|
# used in the continue from time stamp
|
||||||
|
error = {self.error}
|
||||||
|
|
||||||
|
# whether to use python-mpv [True/False]
|
||||||
|
# to enable superior control over the player
|
||||||
|
# adding more options to it
|
||||||
|
# Enable this one and you will be wonder why you did not discover fastanime sooner
|
||||||
|
# Since you basically don't have to close the player window to go to the next or previous episode, switch servers, change translation type or change to a given episode x
|
||||||
|
# so try it if you haven't already
|
||||||
|
# if you have any issues setting it up
|
||||||
|
# don't be afraid to ask
|
||||||
|
# especially on windows
|
||||||
|
# honestly it can be a pain to set it up there
|
||||||
|
# personally it took me quite sometime to figure it out
|
||||||
|
# this is because of how windows handles shared libraries
|
||||||
|
# so just ask when you find yourself stuck
|
||||||
|
# or just switch to arch linux
|
||||||
|
use_python_mpv = {self.use_python_mpv}
|
||||||
|
|
||||||
|
# force mpv window
|
||||||
|
# the default 'immediate' just makes mpv to open the window even if the video has not yet loaded
|
||||||
|
# done for asthetics
|
||||||
|
# passed directly to mpv so values are same
|
||||||
|
force_window = immediate
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
# provider=allanime, server=gogoanime
|
||||||
|
# provider=allanime, server=wixmp
|
||||||
|
# provider=aniwatch
|
||||||
|
# this is because they provider a m3u8 file that contans multiple quality streams
|
||||||
|
format = {self.format}
|
||||||
|
|
||||||
|
# NOTE:
|
||||||
|
# if you have any trouble setting up your config
|
||||||
|
# please don't be afraid to ask in our discord
|
||||||
|
# plus if there are any errors, improvements or suggestions please tell us in the discord
|
||||||
|
# or help us by contributing
|
||||||
|
# we appreciate all the help we can get
|
||||||
|
# since we may not always have the time to immediately implement the changes
|
||||||
|
#
|
||||||
|
# HOPE YOU ENJOY FASTANIME AND BE SURE TO STAR THE PROJECT ON GITHUB
|
||||||
|
#
|
||||||
|
"""
|
||||||
return current_config_state
|
return current_config_state
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.__repr__()
|
return self.__repr__()
|
||||||
|
|
||||||
# WARNING: depracated and will probably be removed
|
|
||||||
def update_anime_list(self, anime_id: int, remove=False):
|
|
||||||
if remove:
|
|
||||||
try:
|
|
||||||
self.anime_list.remove(anime_id)
|
|
||||||
print("Succesfully removed :cry:")
|
|
||||||
except Exception:
|
|
||||||
print(anime_id, "Nothing to remove :confused:")
|
|
||||||
else:
|
|
||||||
self.anime_list.append(anime_id)
|
|
||||||
self.user_data["animelist"] = list(set(self.anime_list))
|
|
||||||
self._update_user_data()
|
|
||||||
print("Succesfully added :smile:")
|
|
||||||
input("Enter to continue...")
|
|
||||||
|
|||||||
@@ -21,7 +21,11 @@ from ...Utility.data import anime_normalizer
|
|||||||
from ...Utility.utils import anime_title_percentage_match
|
from ...Utility.utils import anime_title_percentage_match
|
||||||
from ..utils.mpv import run_mpv
|
from ..utils.mpv import run_mpv
|
||||||
from ..utils.tools import exit_app
|
from ..utils.tools import exit_app
|
||||||
from ..utils.utils import filter_by_quality, fuzzy_inquirer
|
from ..utils.utils import (
|
||||||
|
filter_by_quality,
|
||||||
|
fuzzy_inquirer,
|
||||||
|
move_preferred_subtitle_lang_to_top,
|
||||||
|
)
|
||||||
from .utils import aniskip
|
from .utils import aniskip
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -113,49 +117,49 @@ def media_player_controls(
|
|||||||
current_episode_number,
|
current_episode_number,
|
||||||
):
|
):
|
||||||
custom_args.extend(args)
|
custom_args.extend(args)
|
||||||
subtitles = selected_server["subtitles"]
|
subtitles = move_preferred_subtitle_lang_to_top(
|
||||||
|
selected_server["subtitles"], config.sub_lang
|
||||||
|
)
|
||||||
|
episode_title = selected_server["episode_title"]
|
||||||
|
if config.normalize_titles:
|
||||||
|
import re
|
||||||
|
|
||||||
|
for episode_detail in fastanime_runtime_state.selected_anime_anilist[
|
||||||
|
"streamingEpisodes"
|
||||||
|
]:
|
||||||
|
if re.match(
|
||||||
|
f"Episode {current_episode_number} ", episode_detail["title"]
|
||||||
|
):
|
||||||
|
episode_title = episode_detail["title"]
|
||||||
|
break
|
||||||
if config.sync_play:
|
if config.sync_play:
|
||||||
from ..utils.syncplay import SyncPlayer
|
from ..utils.syncplay import SyncPlayer
|
||||||
|
|
||||||
stop_time, total_time = SyncPlayer(
|
stop_time, total_time = SyncPlayer(
|
||||||
current_episode_stream_link,
|
current_episode_stream_link,
|
||||||
selected_server["episode_title"],
|
episode_title,
|
||||||
headers=selected_server["headers"],
|
headers=selected_server["headers"],
|
||||||
subtitles=subtitles,
|
subtitles=subtitles,
|
||||||
)
|
)
|
||||||
elif config.use_mpv_mod:
|
elif config.use_python_mpv:
|
||||||
from ..utils.player import player
|
from ..utils.player import player
|
||||||
|
|
||||||
mpv = player.create_player(
|
player.create_player(
|
||||||
current_episode_stream_link,
|
current_episode_stream_link,
|
||||||
config.anime_provider,
|
config.anime_provider,
|
||||||
fastanime_runtime_state,
|
fastanime_runtime_state,
|
||||||
config,
|
config,
|
||||||
selected_server["episode_title"],
|
episode_title,
|
||||||
|
start_time,
|
||||||
headers=selected_server["headers"],
|
headers=selected_server["headers"],
|
||||||
|
subtitles=subtitles,
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO: implement custom aniskip
|
|
||||||
if custom_args and None:
|
|
||||||
chapters_file = custom_args[0].split("=", 1)
|
|
||||||
script_opts = custom_args[1].split("=", 1)
|
|
||||||
mpv._set_property("chapters-file", chapters_file[1])
|
|
||||||
mpv._set_property("script-opts", script_opts[1])
|
|
||||||
if not start_time == "0":
|
|
||||||
mpv.start = start_time
|
|
||||||
mpv.wait_until_playing()
|
|
||||||
if subtitles:
|
|
||||||
mpv.sub_add(
|
|
||||||
subtitles[0]["url"], "select", None, subtitles[0]["language"]
|
|
||||||
)
|
|
||||||
mpv.wait_for_shutdown()
|
|
||||||
mpv.terminate()
|
|
||||||
stop_time = player.last_stop_time
|
stop_time = player.last_stop_time
|
||||||
total_time = player.last_total_time
|
total_time = player.last_total_time
|
||||||
else:
|
else:
|
||||||
stop_time, total_time = run_mpv(
|
stop_time, total_time = run_mpv(
|
||||||
current_episode_stream_link,
|
current_episode_stream_link,
|
||||||
selected_server["episode_title"],
|
episode_title,
|
||||||
start_time=start_time,
|
start_time=start_time,
|
||||||
custom_args=custom_args,
|
custom_args=custom_args,
|
||||||
headers=selected_server["headers"],
|
headers=selected_server["headers"],
|
||||||
@@ -217,7 +221,7 @@ def media_player_controls(
|
|||||||
"Are you sure you wish to continue to the next episode, your progress for the current episodes will be erased?",
|
"Are you sure you wish to continue to the next episode, your progress for the current episodes will be erased?",
|
||||||
default=True,
|
default=True,
|
||||||
):
|
):
|
||||||
media_player_controls(config, fastanime_runtime_state)
|
media_actions_menu(config, fastanime_runtime_state)
|
||||||
return
|
return
|
||||||
|
|
||||||
# all checks have passed lets go to the next episode
|
# all checks have passed lets go to the next episode
|
||||||
@@ -366,7 +370,7 @@ def provider_anime_episode_servers_menu(
|
|||||||
anime_id_anilist: int = fastanime_runtime_state.selected_anime_id_anilist
|
anime_id_anilist: int = fastanime_runtime_state.selected_anime_id_anilist
|
||||||
provider_anime: "Anime" = fastanime_runtime_state.provider_anime
|
provider_anime: "Anime" = fastanime_runtime_state.provider_anime
|
||||||
|
|
||||||
server_name = None
|
server_name = ""
|
||||||
# get streams for episode from provider
|
# get streams for episode from provider
|
||||||
with Progress() as progress:
|
with Progress() as progress:
|
||||||
progress.add_task("Fetching Episode Streams...", total=None)
|
progress.add_task("Fetching Episode Streams...", total=None)
|
||||||
@@ -374,7 +378,6 @@ def provider_anime_episode_servers_menu(
|
|||||||
provider_anime,
|
provider_anime,
|
||||||
current_episode_number,
|
current_episode_number,
|
||||||
translation_type,
|
translation_type,
|
||||||
fastanime_runtime_state.selected_anime_anilist,
|
|
||||||
)
|
)
|
||||||
if not episode_streams_generator:
|
if not episode_streams_generator:
|
||||||
if not config.use_rofi:
|
if not config.use_rofi:
|
||||||
@@ -383,7 +386,7 @@ def provider_anime_episode_servers_menu(
|
|||||||
else:
|
else:
|
||||||
if not Rofi.confirm("Sth went wrong!!Enter to continue..."):
|
if not Rofi.confirm("Sth went wrong!!Enter to continue..."):
|
||||||
exit(1)
|
exit(1)
|
||||||
provider_anime_episode_servers_menu(config, fastanime_runtime_state)
|
media_actions_menu(config, fastanime_runtime_state)
|
||||||
return
|
return
|
||||||
|
|
||||||
if config.server == "top":
|
if config.server == "top":
|
||||||
@@ -510,6 +513,8 @@ def provider_anime_episode_servers_menu(
|
|||||||
)
|
)
|
||||||
if start_time != "0" and episode_in_history == current_episode_number:
|
if start_time != "0" and episode_in_history == current_episode_number:
|
||||||
print("[green]Continuing from:[/] ", start_time)
|
print("[green]Continuing from:[/] ", start_time)
|
||||||
|
else:
|
||||||
|
start_time = "0"
|
||||||
custom_args = []
|
custom_args = []
|
||||||
if config.skip:
|
if config.skip:
|
||||||
if args := aniskip(
|
if args := aniskip(
|
||||||
@@ -517,45 +522,45 @@ def provider_anime_episode_servers_menu(
|
|||||||
current_episode_number,
|
current_episode_number,
|
||||||
):
|
):
|
||||||
custom_args.extend(args)
|
custom_args.extend(args)
|
||||||
subtitles = selected_server["subtitles"]
|
subtitles = move_preferred_subtitle_lang_to_top(
|
||||||
|
selected_server["subtitles"], config.sub_lang
|
||||||
|
)
|
||||||
|
episode_title = selected_server["episode_title"]
|
||||||
|
if config.normalize_titles:
|
||||||
|
import re
|
||||||
|
|
||||||
|
for episode_detail in fastanime_runtime_state.selected_anime_anilist[
|
||||||
|
"streamingEpisodes"
|
||||||
|
]:
|
||||||
|
if re.match(f"Episode {current_episode_number} ", episode_detail["title"]):
|
||||||
|
episode_title = episode_detail["title"]
|
||||||
|
break
|
||||||
|
|
||||||
if config.sync_play:
|
if config.sync_play:
|
||||||
from ..utils.syncplay import SyncPlayer
|
from ..utils.syncplay import SyncPlayer
|
||||||
|
|
||||||
stop_time, total_time = SyncPlayer(
|
stop_time, total_time = SyncPlayer(
|
||||||
current_stream_link,
|
current_stream_link,
|
||||||
selected_server["episode_title"],
|
episode_title,
|
||||||
headers=selected_server["headers"],
|
headers=selected_server["headers"],
|
||||||
subtitles=subtitles,
|
subtitles=subtitles,
|
||||||
)
|
)
|
||||||
elif config.use_mpv_mod:
|
elif config.use_python_mpv:
|
||||||
from ..utils.player import player
|
from ..utils.player import player
|
||||||
|
|
||||||
mpv = player.create_player(
|
if start_time == "0" and episode_in_history != current_episode_number:
|
||||||
|
start_time = "0"
|
||||||
|
player.create_player(
|
||||||
current_stream_link,
|
current_stream_link,
|
||||||
anime_provider,
|
anime_provider,
|
||||||
fastanime_runtime_state,
|
fastanime_runtime_state,
|
||||||
config,
|
config,
|
||||||
selected_server["episode_title"],
|
episode_title,
|
||||||
|
start_time,
|
||||||
headers=selected_server["headers"],
|
headers=selected_server["headers"],
|
||||||
|
subtitles=subtitles,
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO: implement custom aniskip intergration
|
|
||||||
if custom_args and None:
|
|
||||||
chapters_file = custom_args[0].split("=", 1)
|
|
||||||
script_opts = custom_args[1].split("=", 1)
|
|
||||||
mpv._set_property("chapters-file", chapters_file[1])
|
|
||||||
mpv._set_property("script-opts", script_opts[1])
|
|
||||||
if not start_time == "0" and episode_in_history == current_episode_number:
|
|
||||||
mpv.start = start_time
|
|
||||||
mpv.wait_until_playing()
|
|
||||||
if subtitles:
|
|
||||||
# subs = ""
|
|
||||||
# for subtitle in subtitles:
|
|
||||||
# subs += f"{subtitle['url']},"
|
|
||||||
mpv.sub_add(subtitles[0]["url"], "select", None, subtitles[0]["language"])
|
|
||||||
# mpv.sub_files = subs
|
|
||||||
mpv.wait_for_shutdown()
|
|
||||||
mpv.terminate()
|
|
||||||
stop_time = player.last_stop_time
|
stop_time = player.last_stop_time
|
||||||
total_time = player.last_total_time
|
total_time = player.last_total_time
|
||||||
current_episode_number = fastanime_runtime_state.provider_current_episode_number
|
current_episode_number = fastanime_runtime_state.provider_current_episode_number
|
||||||
@@ -564,7 +569,7 @@ def provider_anime_episode_servers_menu(
|
|||||||
start_time = "0"
|
start_time = "0"
|
||||||
stop_time, total_time = run_mpv(
|
stop_time, total_time = run_mpv(
|
||||||
current_stream_link,
|
current_stream_link,
|
||||||
selected_server["episode_title"],
|
episode_title,
|
||||||
start_time=start_time,
|
start_time=start_time,
|
||||||
custom_args=custom_args,
|
custom_args=custom_args,
|
||||||
headers=selected_server["headers"],
|
headers=selected_server["headers"],
|
||||||
@@ -576,7 +581,7 @@ def provider_anime_episode_servers_menu(
|
|||||||
# this will try to update the episode to be the next episode if delta has reached a specific threshhold
|
# this will try to update the episode to be the next episode if delta has reached a specific threshhold
|
||||||
# this update will only apply locally
|
# this update will only apply locally
|
||||||
# the remote(anilist) is only updated when its certain you are going to open the player
|
# the remote(anilist) is only updated when its certain you are going to open the player
|
||||||
available_episodes: list = sorted(
|
available_episodes: list[str] = sorted(
|
||||||
fastanime_runtime_state.provider_available_episodes, key=float
|
fastanime_runtime_state.provider_available_episodes, key=float
|
||||||
)
|
)
|
||||||
if stop_time == "0" or total_time == "0":
|
if stop_time == "0" or total_time == "0":
|
||||||
@@ -635,7 +640,9 @@ def provider_anime_episodes_menu(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# prompt for episode number
|
# prompt for episode number
|
||||||
total_episodes = provider_anime["availableEpisodesDetail"][translation_type]
|
total_episodes = sorted(
|
||||||
|
provider_anime["availableEpisodesDetail"][translation_type], key=float
|
||||||
|
)
|
||||||
current_episode_number = ""
|
current_episode_number = ""
|
||||||
|
|
||||||
# auto select episode if continue from history otherwise prompt episode number
|
# auto select episode if continue from history otherwise prompt episode number
|
||||||
@@ -682,11 +689,21 @@ def provider_anime_episodes_menu(
|
|||||||
# prompt for episode number if not set
|
# prompt for episode number if not set
|
||||||
if not current_episode_number or current_episode_number not in total_episodes:
|
if not current_episode_number or current_episode_number not in total_episodes:
|
||||||
choices = [*total_episodes, "Back"]
|
choices = [*total_episodes, "Back"]
|
||||||
|
preview = None
|
||||||
|
if config.preview:
|
||||||
|
from .utils import get_fzf_episode_preview
|
||||||
|
|
||||||
|
e = fastanime_runtime_state.selected_anime_anilist["episodes"]
|
||||||
|
if e:
|
||||||
|
eps = range(0, e + 1)
|
||||||
|
else:
|
||||||
|
eps = total_episodes
|
||||||
|
preview = get_fzf_episode_preview(
|
||||||
|
fastanime_runtime_state.selected_anime_anilist, eps
|
||||||
|
)
|
||||||
if config.use_fzf:
|
if config.use_fzf:
|
||||||
current_episode_number = fzf.run(
|
current_episode_number = fzf.run(
|
||||||
choices,
|
choices, prompt="Select Episode:", header=anime_title, preview=preview
|
||||||
prompt="Select Episode:",
|
|
||||||
header=anime_title,
|
|
||||||
)
|
)
|
||||||
elif config.use_rofi:
|
elif config.use_rofi:
|
||||||
current_episode_number = Rofi.run(choices, "Select Episode")
|
current_episode_number = Rofi.run(choices, "Select Episode")
|
||||||
@@ -699,14 +716,14 @@ def provider_anime_episodes_menu(
|
|||||||
if current_episode_number == "Back":
|
if current_episode_number == "Back":
|
||||||
media_actions_menu(config, fastanime_runtime_state)
|
media_actions_menu(config, fastanime_runtime_state)
|
||||||
return
|
return
|
||||||
|
#
|
||||||
# try to get the start time and if not found default to "0"
|
# # try to get the start time and if not found default to "0"
|
||||||
start_time = user_watch_history.get(str(anime_id_anilist), {}).get(
|
# start_time = user_watch_history.get(str(anime_id_anilist), {}).get(
|
||||||
"start_time", "0"
|
# "start_time", "0"
|
||||||
)
|
# )
|
||||||
config.update_watch_history(
|
# config.update_watch_history(
|
||||||
anime_id_anilist, current_episode_number, start_time=start_time
|
# anime_id_anilist, current_episode_number, start_time=start_time
|
||||||
)
|
# )
|
||||||
|
|
||||||
# update runtime data
|
# update runtime data
|
||||||
fastanime_runtime_state.provider_available_episodes = total_episodes
|
fastanime_runtime_state.provider_available_episodes = total_episodes
|
||||||
@@ -716,7 +733,9 @@ def provider_anime_episodes_menu(
|
|||||||
provider_anime_episode_servers_menu(config, fastanime_runtime_state)
|
provider_anime_episode_servers_menu(config, fastanime_runtime_state)
|
||||||
|
|
||||||
|
|
||||||
def fetch_anime_episode(config, fastanime_runtime_state: "FastAnimeRuntimeState"):
|
def fetch_anime_episode(
|
||||||
|
config: "Config", fastanime_runtime_state: "FastAnimeRuntimeState"
|
||||||
|
):
|
||||||
selected_anime: "SearchResult" = (
|
selected_anime: "SearchResult" = (
|
||||||
fastanime_runtime_state.provider_anime_search_result
|
fastanime_runtime_state.provider_anime_search_result
|
||||||
)
|
)
|
||||||
@@ -724,7 +743,7 @@ def fetch_anime_episode(config, fastanime_runtime_state: "FastAnimeRuntimeState"
|
|||||||
with Progress() as progress:
|
with Progress() as progress:
|
||||||
progress.add_task("Fetching Anime Info...", total=None)
|
progress.add_task("Fetching Anime Info...", total=None)
|
||||||
provider_anime = anime_provider.get_anime(
|
provider_anime = anime_provider.get_anime(
|
||||||
selected_anime["id"], fastanime_runtime_state.selected_anime_anilist
|
selected_anime["id"],
|
||||||
)
|
)
|
||||||
if not provider_anime:
|
if not provider_anime:
|
||||||
print(
|
print(
|
||||||
@@ -735,7 +754,7 @@ def fetch_anime_episode(config, fastanime_runtime_state: "FastAnimeRuntimeState"
|
|||||||
else:
|
else:
|
||||||
if not Rofi.confirm("Sth went wrong!!Enter to continue..."):
|
if not Rofi.confirm("Sth went wrong!!Enter to continue..."):
|
||||||
exit(1)
|
exit(1)
|
||||||
return fetch_anime_episode(config, fastanime_runtime_state)
|
return media_actions_menu(config, fastanime_runtime_state)
|
||||||
|
|
||||||
fastanime_runtime_state.provider_anime = provider_anime
|
fastanime_runtime_state.provider_anime = provider_anime
|
||||||
provider_anime_episodes_menu(config, fastanime_runtime_state)
|
provider_anime_episodes_menu(config, fastanime_runtime_state)
|
||||||
@@ -770,7 +789,6 @@ def anime_provider_search_results_menu(
|
|||||||
provider_search_results = anime_provider.search_for_anime(
|
provider_search_results = anime_provider.search_for_anime(
|
||||||
selected_anime_title,
|
selected_anime_title,
|
||||||
translation_type,
|
translation_type,
|
||||||
selected_anime_anilist,
|
|
||||||
)
|
)
|
||||||
if not provider_search_results:
|
if not provider_search_results:
|
||||||
print(
|
print(
|
||||||
@@ -781,7 +799,7 @@ def anime_provider_search_results_menu(
|
|||||||
else:
|
else:
|
||||||
if not Rofi.confirm("Sth went wrong!!Enter to continue..."):
|
if not Rofi.confirm("Sth went wrong!!Enter to continue..."):
|
||||||
exit(1)
|
exit(1)
|
||||||
return anime_provider_search_results_menu(config, fastanime_runtime_state)
|
return media_actions_menu(config, fastanime_runtime_state)
|
||||||
|
|
||||||
provider_search_results = {
|
provider_search_results = {
|
||||||
anime["title"]: anime for anime in provider_search_results["results"]
|
anime["title"]: anime for anime in provider_search_results["results"]
|
||||||
@@ -954,7 +972,7 @@ def media_actions_menu(
|
|||||||
score = Rofi.ask("Enter Score", is_int=True)
|
score = Rofi.ask("Enter Score", is_int=True)
|
||||||
score = max(100, min(0, score))
|
score = max(100, min(0, score))
|
||||||
else:
|
else:
|
||||||
score = inquirer.number(
|
score = inquirer.number( # pyright:ignore
|
||||||
message="Enter the score:",
|
message="Enter the score:",
|
||||||
min_allowed=0,
|
min_allowed=0,
|
||||||
max_allowed=100,
|
max_allowed=100,
|
||||||
@@ -1027,6 +1045,42 @@ def media_actions_menu(
|
|||||||
|
|
||||||
media_actions_menu(config, fastanime_runtime_state)
|
media_actions_menu(config, fastanime_runtime_state)
|
||||||
|
|
||||||
|
def _change_player(
|
||||||
|
config: "Config", fastanime_runtime_state: "FastAnimeRuntimeState"
|
||||||
|
):
|
||||||
|
"""Change the translation type to use
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config: [TODO:description]
|
||||||
|
fastanime_runtime_state: [TODO:description]
|
||||||
|
"""
|
||||||
|
# prompt for new translation type
|
||||||
|
options = ["syncplay", "mpv-mod", "default"]
|
||||||
|
if config.use_fzf:
|
||||||
|
player = fzf.run(
|
||||||
|
options,
|
||||||
|
prompt="Select Player:",
|
||||||
|
)
|
||||||
|
elif config.use_rofi:
|
||||||
|
player = Rofi.run(options, "Select Player: ")
|
||||||
|
else:
|
||||||
|
player = fuzzy_inquirer(
|
||||||
|
options,
|
||||||
|
"Select Player",
|
||||||
|
)
|
||||||
|
|
||||||
|
# update internal config
|
||||||
|
if player == "syncplay":
|
||||||
|
config.sync_play = True
|
||||||
|
config.use_python_mpv = False
|
||||||
|
else:
|
||||||
|
config.sync_play = False
|
||||||
|
if player == "mpv-mod":
|
||||||
|
config.use_python_mpv = True
|
||||||
|
else:
|
||||||
|
config.use_python_mpv = False
|
||||||
|
media_actions_menu(config, fastanime_runtime_state)
|
||||||
|
|
||||||
def _view_info(config: "Config", fastanime_runtime_state: "FastAnimeRuntimeState"):
|
def _view_info(config: "Config", fastanime_runtime_state: "FastAnimeRuntimeState"):
|
||||||
"""helper function to view info of an anime from terminal
|
"""helper function to view info of an anime from terminal
|
||||||
|
|
||||||
@@ -1140,7 +1194,9 @@ def media_actions_menu(
|
|||||||
config: [TODO:description]
|
config: [TODO:description]
|
||||||
fastanime_runtime_state: [TODO:description]
|
fastanime_runtime_state: [TODO:description]
|
||||||
"""
|
"""
|
||||||
options = ["allanime", "animepahe"]
|
from ...libs.anime_provider import anime_sources
|
||||||
|
|
||||||
|
options = list(anime_sources.keys())
|
||||||
if config.use_fzf:
|
if config.use_fzf:
|
||||||
provider = fzf.run(
|
provider = fzf.run(
|
||||||
options, prompt="Select Translation Type:", header="Language Options"
|
options, prompt="Select Translation Type:", header="Language Options"
|
||||||
@@ -1155,7 +1211,7 @@ def media_actions_menu(
|
|||||||
|
|
||||||
config.provider = provider
|
config.provider = provider
|
||||||
config.anime_provider.provider = provider
|
config.anime_provider.provider = provider
|
||||||
config.anime_provider.lazyload_provider()
|
config.anime_provider.lazyload_provider(provider)
|
||||||
|
|
||||||
media_actions_menu(config, fastanime_runtime_state)
|
media_actions_menu(config, fastanime_runtime_state)
|
||||||
|
|
||||||
@@ -1193,6 +1249,7 @@ def media_actions_menu(
|
|||||||
f"{'📖 ' if icons else ''}View Info": _view_info,
|
f"{'📖 ' if icons else ''}View Info": _view_info,
|
||||||
f"{'🎧 ' if icons else ''}Change Translation Type": _change_translation_type,
|
f"{'🎧 ' if icons else ''}Change Translation Type": _change_translation_type,
|
||||||
f"{'💽 ' if icons else ''}Change Provider": _change_provider,
|
f"{'💽 ' if icons else ''}Change Provider": _change_provider,
|
||||||
|
f"{'💽 ' if icons else ''}Change Player": _change_player,
|
||||||
f"{'🔘 ' if icons else ''}Toggle auto select anime": _toggle_auto_select, # WARN: problematic if you choose an anime that doesnt match id
|
f"{'🔘 ' if icons else ''}Toggle auto select anime": _toggle_auto_select, # WARN: problematic if you choose an anime that doesnt match id
|
||||||
f"{'💠 ' if icons else ''}Toggle auto next episode": _toggle_auto_next,
|
f"{'💠 ' if icons else ''}Toggle auto next episode": _toggle_auto_next,
|
||||||
f"{'🔘 ' if icons else ''}Toggle continue from history": _toggle_continue_from_history,
|
f"{'🔘 ' if icons else ''}Toggle continue from history": _toggle_continue_from_history,
|
||||||
@@ -1224,7 +1281,9 @@ def anilist_results_menu(
|
|||||||
config: [TODO:description]
|
config: [TODO:description]
|
||||||
fastanime_runtime_state: [TODO:description]
|
fastanime_runtime_state: [TODO:description]
|
||||||
"""
|
"""
|
||||||
search_results = fastanime_runtime_state.anilist_data["data"]["Page"]["media"]
|
search_results = fastanime_runtime_state.anilist_results_data["data"]["Page"][
|
||||||
|
"media"
|
||||||
|
]
|
||||||
|
|
||||||
anime_data = {}
|
anime_data = {}
|
||||||
for anime in search_results:
|
for anime in search_results:
|
||||||
@@ -1262,9 +1321,9 @@ def anilist_results_menu(
|
|||||||
choices = [*anime_data.keys(), "Back"]
|
choices = [*anime_data.keys(), "Back"]
|
||||||
if config.use_fzf:
|
if config.use_fzf:
|
||||||
if config.preview:
|
if config.preview:
|
||||||
from .utils import get_fzf_preview
|
from .utils import get_fzf_anime_preview
|
||||||
|
|
||||||
preview = get_fzf_preview(search_results, anime_data.keys())
|
preview = get_fzf_anime_preview(search_results, anime_data.keys())
|
||||||
selected_anime_title = fzf.run(
|
selected_anime_title = fzf.run(
|
||||||
choices,
|
choices,
|
||||||
prompt="Select Anime: ",
|
prompt="Select Anime: ",
|
||||||
@@ -1445,6 +1504,9 @@ def fastanime_main_menu(
|
|||||||
else:
|
else:
|
||||||
config.load_config()
|
config.load_config()
|
||||||
|
|
||||||
|
config.anime_provider.provider = config.provider
|
||||||
|
config.anime_provider.lazyload_provider(config.provider)
|
||||||
|
|
||||||
fastanime_main_menu(config, fastanime_runtime_state)
|
fastanime_main_menu(config, fastanime_runtime_state)
|
||||||
|
|
||||||
icons = config.icons
|
icons = config.icons
|
||||||
@@ -1500,7 +1562,7 @@ def fastanime_main_menu(
|
|||||||
# anilist data is a (bool,data)
|
# anilist data is a (bool,data)
|
||||||
# the bool indicated success
|
# the bool indicated success
|
||||||
if anilist_data[0]:
|
if anilist_data[0]:
|
||||||
fastanime_runtime_state.anilist_data = anilist_data[1]
|
fastanime_runtime_state.anilist_results_data = anilist_data[1]
|
||||||
anilist_results_menu(config, fastanime_runtime_state)
|
anilist_results_menu(config, fastanime_runtime_state)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import textwrap
|
|||||||
from threading import Thread
|
from threading import Thread
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from yt_dlp.utils import clean_html
|
from yt_dlp.utils import clean_html, sanitize_filename
|
||||||
|
|
||||||
from ...constants import APP_CACHE_DIR
|
from ...constants import APP_CACHE_DIR
|
||||||
from ...libs.anilist.types import AnilistBaseMediaDataSchema
|
from ...libs.anilist.types import AnilistBaseMediaDataSchema
|
||||||
@@ -93,7 +93,7 @@ def write_search_results(
|
|||||||
# NOTE: Will probably make this a configuraable option
|
# NOTE: Will probably make this a configuraable option
|
||||||
HEADER_COLOR = 215, 0, 95
|
HEADER_COLOR = 215, 0, 95
|
||||||
SEPARATOR_COLOR = 208, 208, 208
|
SEPARATOR_COLOR = 208, 208, 208
|
||||||
SEPARATOR_WIDTH = 45
|
SEPARATOR_WIDTH = 30
|
||||||
# use concurency to download and write as fast as possible
|
# use concurency to download and write as fast as possible
|
||||||
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
|
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
|
||||||
future_to_task = {}
|
future_to_task = {}
|
||||||
@@ -104,6 +104,11 @@ def write_search_results(
|
|||||||
image_url
|
image_url
|
||||||
)
|
)
|
||||||
|
|
||||||
|
mediaListName = "Not in any of your lists"
|
||||||
|
progress = "UNKNOWN"
|
||||||
|
if anime_list := anime["mediaListEntry"]:
|
||||||
|
mediaListName = anime_list["status"]
|
||||||
|
progress = anime_list["progress"]
|
||||||
# handle the text data
|
# handle the text data
|
||||||
template = f"""
|
template = f"""
|
||||||
{get_true_fg("-"*SEPARATOR_WIDTH,*SEPARATOR_COLOR,bold=False)}
|
{get_true_fg("-"*SEPARATOR_WIDTH,*SEPARATOR_COLOR,bold=False)}
|
||||||
@@ -118,6 +123,9 @@ def write_search_results(
|
|||||||
{get_true_fg('Start Date:',*HEADER_COLOR)} {anilist_data_helper.format_anilist_date_object(anime['startDate'])}
|
{get_true_fg('Start Date:',*HEADER_COLOR)} {anilist_data_helper.format_anilist_date_object(anime['startDate'])}
|
||||||
{get_true_fg('End Date:',*HEADER_COLOR)} {anilist_data_helper.format_anilist_date_object(anime['endDate'])}
|
{get_true_fg('End Date:',*HEADER_COLOR)} {anilist_data_helper.format_anilist_date_object(anime['endDate'])}
|
||||||
{get_true_fg("-"*SEPARATOR_WIDTH,*SEPARATOR_COLOR,bold=False)}
|
{get_true_fg("-"*SEPARATOR_WIDTH,*SEPARATOR_COLOR,bold=False)}
|
||||||
|
{get_true_fg('Media List:',*HEADER_COLOR)} {mediaListName}
|
||||||
|
{get_true_fg('Progress:',*HEADER_COLOR)} {progress}
|
||||||
|
{get_true_fg("-"*SEPARATOR_WIDTH,*SEPARATOR_COLOR,bold=False)}
|
||||||
{get_true_fg('Description:',*HEADER_COLOR)}
|
{get_true_fg('Description:',*HEADER_COLOR)}
|
||||||
"""
|
"""
|
||||||
template = textwrap.dedent(template)
|
template = textwrap.dedent(template)
|
||||||
@@ -168,23 +176,120 @@ def get_rofi_icons(
|
|||||||
logger.error("%r generated an exception: %s" % (url, e))
|
logger.error("%r generated an exception: %s" % (url, e))
|
||||||
|
|
||||||
|
|
||||||
def get_fzf_preview(
|
# get rofi icons
|
||||||
anilist_results: list[AnilistBaseMediaDataSchema], titles, wait=False
|
def get_fzf_manga_preview(manga_results, workers=None, wait=False):
|
||||||
):
|
"""A helper function to make sure that the images are downloaded so they can be used as icons
|
||||||
"""A helper function that constructs data to be used for the fzf preview
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
titles (list[str]): The sanitized titles to use, NOTE: its important that they are sanitized since thay will be used as filenames
|
titles (list[str]): sanitized titles of the anime; NOTE: its important that they are sanitized since they are used as the filenames of the images
|
||||||
wait (bool): whether to block the ui as we wait for preview defaults to false
|
workers ([TODO:parameter]): Number of threads to use to download the images; defaults to as many as possible
|
||||||
anilist_results: the anilist results got from an anilist action
|
anilist_results: the anilist results from an anilist action
|
||||||
|
|
||||||
Returns:
|
|
||||||
THe fzf preview script to use
|
|
||||||
"""
|
"""
|
||||||
# ensure images and info exists
|
|
||||||
|
def _worker():
|
||||||
|
# use concurrency to download the images as fast as possible
|
||||||
|
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
|
||||||
|
# load the jobs
|
||||||
|
future_to_url = {}
|
||||||
|
for manga in manga_results:
|
||||||
|
image_url = manga["poster"]
|
||||||
|
future_to_url[
|
||||||
|
executor.submit(
|
||||||
|
save_image_from_url,
|
||||||
|
image_url,
|
||||||
|
sanitize_filename(manga["title"]),
|
||||||
|
)
|
||||||
|
] = image_url
|
||||||
|
|
||||||
|
# execute the jobs
|
||||||
|
for future in concurrent.futures.as_completed(future_to_url):
|
||||||
|
url = future_to_url[future]
|
||||||
|
try:
|
||||||
|
future.result()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("%r generated an exception: %s" % (url, e))
|
||||||
|
|
||||||
background_worker = Thread(
|
background_worker = Thread(
|
||||||
target=write_search_results, args=(anilist_results, titles)
|
target=_worker,
|
||||||
)
|
)
|
||||||
|
# ensure images and info exists
|
||||||
|
background_worker.daemon = True
|
||||||
|
background_worker.start()
|
||||||
|
|
||||||
|
# the preview script is in bash so making sure fzf doesnt use any other shell lang to process the preview script
|
||||||
|
os.environ["SHELL"] = shutil.which("bash") or "bash"
|
||||||
|
preview = """
|
||||||
|
%s
|
||||||
|
if [ -s %s/{} ]; then fzf-preview %s/{}
|
||||||
|
else echo Loading...
|
||||||
|
fi
|
||||||
|
""" % (
|
||||||
|
fzf_preview,
|
||||||
|
IMAGES_CACHE_DIR,
|
||||||
|
IMAGES_CACHE_DIR,
|
||||||
|
)
|
||||||
|
if wait:
|
||||||
|
background_worker.join()
|
||||||
|
return preview
|
||||||
|
|
||||||
|
|
||||||
|
# get rofi icons
|
||||||
|
def get_fzf_episode_preview(
|
||||||
|
anilist_result: AnilistBaseMediaDataSchema, episodes, workers=None, wait=False
|
||||||
|
):
|
||||||
|
"""A helper function to make sure that the images are downloaded so they can be used as icons
|
||||||
|
|
||||||
|
Args:
|
||||||
|
titles (list[str]): sanitized titles of the anime; NOTE: its important that they are sanitized since they are used as the filenames of the images
|
||||||
|
workers ([TODO:parameter]): Number of threads to use to download the images; defaults to as many as possible
|
||||||
|
anilist_results: the anilist results from an anilist action
|
||||||
|
"""
|
||||||
|
|
||||||
|
HEADER_COLOR = 215, 0, 95
|
||||||
|
import re
|
||||||
|
|
||||||
|
def _worker():
|
||||||
|
# use concurrency to download the images as fast as possible
|
||||||
|
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
|
||||||
|
# load the jobs
|
||||||
|
future_to_url = {}
|
||||||
|
for episode in episodes:
|
||||||
|
episode_title = ""
|
||||||
|
image_url = ""
|
||||||
|
for episode_detail in anilist_result["streamingEpisodes"]:
|
||||||
|
if re.match(f"Episode {episode} ", episode_detail["title"]):
|
||||||
|
episode_title = episode_detail["title"]
|
||||||
|
image_url = episode_detail["thumbnail"]
|
||||||
|
|
||||||
|
if episode_title and image_url:
|
||||||
|
# actual link to download image from
|
||||||
|
if not image_url:
|
||||||
|
continue
|
||||||
|
future_to_url[
|
||||||
|
executor.submit(save_image_from_url, image_url, episode)
|
||||||
|
] = image_url
|
||||||
|
template = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
{get_true_fg('Anime Title:',*HEADER_COLOR)} {anilist_result['title']['romaji'] or anilist_result['title']['english']}
|
||||||
|
{get_true_fg('Episode Title:',*HEADER_COLOR)} {episode_title}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
future_to_url[
|
||||||
|
executor.submit(save_info_from_str, template, episode)
|
||||||
|
] = episode_title
|
||||||
|
|
||||||
|
# execute the jobs
|
||||||
|
for future in concurrent.futures.as_completed(future_to_url):
|
||||||
|
url = future_to_url[future]
|
||||||
|
try:
|
||||||
|
future.result()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("%r generated an exception: %s" % (url, e))
|
||||||
|
|
||||||
|
background_worker = Thread(
|
||||||
|
target=_worker,
|
||||||
|
)
|
||||||
|
# ensure images and info exists
|
||||||
background_worker.daemon = True
|
background_worker.daemon = True
|
||||||
background_worker.start()
|
background_worker.start()
|
||||||
|
|
||||||
@@ -208,3 +313,68 @@ def get_fzf_preview(
|
|||||||
if wait:
|
if wait:
|
||||||
background_worker.join()
|
background_worker.join()
|
||||||
return preview
|
return preview
|
||||||
|
|
||||||
|
|
||||||
|
def get_fzf_anime_preview(
|
||||||
|
anilist_results: list[AnilistBaseMediaDataSchema], titles, wait=False
|
||||||
|
):
|
||||||
|
"""A helper function that constructs data to be used for the fzf preview
|
||||||
|
|
||||||
|
Args:
|
||||||
|
titles (list[str]): The sanitized titles to use, NOTE: its important that they are sanitized since thay will be used as filenames
|
||||||
|
wait (bool): whether to block the ui as we wait for preview defaults to false
|
||||||
|
anilist_results: the anilist results got from an anilist action
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
THe fzf preview script to use
|
||||||
|
"""
|
||||||
|
# ensure images and info exists
|
||||||
|
from ...constants import S_PLATFORM
|
||||||
|
background_worker = Thread(
|
||||||
|
target=write_search_results, args=(anilist_results, titles)
|
||||||
|
)
|
||||||
|
background_worker.daemon = True
|
||||||
|
background_worker.start()
|
||||||
|
|
||||||
|
# the preview script is in bash so making sure fzf doesnt use any other shell lang to process the preview script
|
||||||
|
os.environ["SHELL"] = shutil.which("bash") or "bash"
|
||||||
|
if S_PLATFORM == "win32":
|
||||||
|
preview = """
|
||||||
|
%s
|
||||||
|
title={}
|
||||||
|
dim=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}
|
||||||
|
if [ -s "%s\\\\\\$title" ]; then
|
||||||
|
if command -v chafa >/dev/null;then
|
||||||
|
chafa -f kitty -s $dim "%s\\\\\\$title"
|
||||||
|
fi
|
||||||
|
else echo Loading...
|
||||||
|
fi
|
||||||
|
if [ -s "%s\\\\\\$title" ]; then cat "%s\\\\\\$title"
|
||||||
|
else echo Loading...
|
||||||
|
fi
|
||||||
|
""" % (
|
||||||
|
fzf_preview,
|
||||||
|
IMAGES_CACHE_DIR.replace("\\","\\\\\\"),
|
||||||
|
IMAGES_CACHE_DIR.replace("\\","\\\\\\"),
|
||||||
|
ANIME_INFO_CACHE_DIR.replace("\\","\\\\\\"),
|
||||||
|
ANIME_INFO_CACHE_DIR.replace("\\","\\\\\\"),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
preview = """
|
||||||
|
%s
|
||||||
|
if [ -s %s/{} ]; then fzf-preview %s/{}
|
||||||
|
else echo Loading...
|
||||||
|
fi
|
||||||
|
if [ -s %s/{} ]; then cat %s/{}
|
||||||
|
else echo Loading...
|
||||||
|
fi
|
||||||
|
""" % (
|
||||||
|
fzf_preview,
|
||||||
|
IMAGES_CACHE_DIR,
|
||||||
|
IMAGES_CACHE_DIR,
|
||||||
|
ANIME_INFO_CACHE_DIR,
|
||||||
|
ANIME_INFO_CACHE_DIR,
|
||||||
|
)
|
||||||
|
if wait:
|
||||||
|
background_worker.join()
|
||||||
|
return preview
|
||||||
|
|||||||
12
fastanime/cli/utils/feh.py
Normal file
12
fastanime/cli/utils/feh.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
from sys import exit
|
||||||
|
|
||||||
|
|
||||||
|
def feh_manga_viewer(image_links: list[str], window_title: str):
|
||||||
|
FEH_EXECUTABLE = shutil.which("feh")
|
||||||
|
if not FEH_EXECUTABLE:
|
||||||
|
print("feh not found")
|
||||||
|
exit(1)
|
||||||
|
commands = [FEH_EXECUTABLE, *image_links, "--title", window_title]
|
||||||
|
subprocess.run(commands)
|
||||||
@@ -3,13 +3,14 @@ from typing import TYPE_CHECKING
|
|||||||
import mpv
|
import mpv
|
||||||
|
|
||||||
from ...anilist import AniList
|
from ...anilist import AniList
|
||||||
from .utils import filter_by_quality
|
from .utils import filter_by_quality, move_preferred_subtitle_lang_to_top
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
|
||||||
from ...AnimeProvider import AnimeProvider
|
from ...AnimeProvider import AnimeProvider
|
||||||
from ..config import Config
|
from ..config import Config
|
||||||
|
from .tools import FastAnimeRuntimeState
|
||||||
|
|
||||||
|
|
||||||
def format_time(duration_in_secs: float):
|
def format_time(duration_in_secs: float):
|
||||||
@@ -22,6 +23,7 @@ def format_time(duration_in_secs: float):
|
|||||||
class MpvPlayer(object):
|
class MpvPlayer(object):
|
||||||
anime_provider: "AnimeProvider"
|
anime_provider: "AnimeProvider"
|
||||||
config: "Config"
|
config: "Config"
|
||||||
|
subs = []
|
||||||
mpv_player: "mpv.MPV"
|
mpv_player: "mpv.MPV"
|
||||||
last_stop_time: str = "0"
|
last_stop_time: str = "0"
|
||||||
last_total_time: str = "0"
|
last_total_time: str = "0"
|
||||||
@@ -109,11 +111,10 @@ class MpvPlayer(object):
|
|||||||
provider_anime,
|
provider_anime,
|
||||||
current_episode_number,
|
current_episode_number,
|
||||||
translation_type,
|
translation_type,
|
||||||
fastanime_runtime_state.selected_anime_anilist,
|
|
||||||
)
|
)
|
||||||
if not episode_streams:
|
if not episode_streams:
|
||||||
self.mpv_player.show_text("No streams were found")
|
self.mpv_player.show_text("No streams were found")
|
||||||
return None
|
return
|
||||||
|
|
||||||
# always select the first
|
# always select the first
|
||||||
if server == "top":
|
if server == "top":
|
||||||
@@ -131,8 +132,20 @@ class MpvPlayer(object):
|
|||||||
self.mpv_player.show_text(
|
self.mpv_player.show_text(
|
||||||
f"Invalid server!!; servers available are: {episode_streams_dict.keys()}",
|
f"Invalid server!!; servers available are: {episode_streams_dict.keys()}",
|
||||||
)
|
)
|
||||||
return None
|
return
|
||||||
self.current_media_title = selected_server["episode_title"]
|
self.current_media_title = selected_server["episode_title"]
|
||||||
|
if config.normalize_titles:
|
||||||
|
import re
|
||||||
|
|
||||||
|
for episode_detail in fastanime_runtime_state.selected_anime_anilist[
|
||||||
|
"streamingEpisodes"
|
||||||
|
]:
|
||||||
|
if re.match(
|
||||||
|
f"Episode {current_episode_number} ", episode_detail["title"]
|
||||||
|
):
|
||||||
|
self.current_media_title = episode_detail["title"]
|
||||||
|
break
|
||||||
|
|
||||||
links = selected_server["links"]
|
links = selected_server["links"]
|
||||||
|
|
||||||
stream_link_ = filter_by_quality(quality, links)
|
stream_link_ = filter_by_quality(quality, links)
|
||||||
@@ -142,17 +155,23 @@ class MpvPlayer(object):
|
|||||||
self.mpv_player._set_property("start", "0")
|
self.mpv_player._set_property("start", "0")
|
||||||
stream_link = stream_link_["link"]
|
stream_link = stream_link_["link"]
|
||||||
fastanime_runtime_state.provider_current_episode_stream_link = stream_link
|
fastanime_runtime_state.provider_current_episode_stream_link = stream_link
|
||||||
|
self.subs = move_preferred_subtitle_lang_to_top(
|
||||||
|
selected_server["subtitles"], config.sub_lang
|
||||||
|
)
|
||||||
return stream_link
|
return stream_link
|
||||||
|
|
||||||
def create_player(
|
def create_player(
|
||||||
self,
|
self,
|
||||||
stream_link,
|
stream_link,
|
||||||
anime_provider: "AnimeProvider",
|
anime_provider: "AnimeProvider",
|
||||||
fastanime_runtime_state,
|
fastanime_runtime_state: "FastAnimeRuntimeState",
|
||||||
config: "Config",
|
config: "Config",
|
||||||
title,
|
title,
|
||||||
|
start_time,
|
||||||
headers={},
|
headers={},
|
||||||
|
subtitles=[],
|
||||||
):
|
):
|
||||||
|
self.subs = subtitles
|
||||||
self.anime_provider = anime_provider
|
self.anime_provider = anime_provider
|
||||||
self.fastanime_runtime_state = fastanime_runtime_state
|
self.fastanime_runtime_state = fastanime_runtime_state
|
||||||
self.config = config
|
self.config = config
|
||||||
@@ -171,17 +190,6 @@ class MpvPlayer(object):
|
|||||||
osc=True,
|
osc=True,
|
||||||
ytdl=True,
|
ytdl=True,
|
||||||
)
|
)
|
||||||
mpv_player.force_window = config.force_window
|
|
||||||
# mpv_player.cache = "yes"
|
|
||||||
# mpv_player.cache_pause = "no"
|
|
||||||
mpv_player.title = title
|
|
||||||
mpv_headers = ""
|
|
||||||
if headers:
|
|
||||||
for header_name, header_value in headers.items():
|
|
||||||
mpv_headers += f"{header_name}:{header_value},"
|
|
||||||
mpv_player.http_header_fields = mpv_headers
|
|
||||||
|
|
||||||
mpv_player.play(stream_link)
|
|
||||||
|
|
||||||
# -- events --
|
# -- events --
|
||||||
@mpv_player.event_callback("file-loaded")
|
@mpv_player.event_callback("file-loaded")
|
||||||
@@ -190,6 +198,22 @@ class MpvPlayer(object):
|
|||||||
self.player_fetching = False
|
self.player_fetching = False
|
||||||
if isinstance(d, float):
|
if isinstance(d, float):
|
||||||
self.last_total_time = format_time(d)
|
self.last_total_time = format_time(d)
|
||||||
|
try:
|
||||||
|
if not mpv_player.core_shutdown:
|
||||||
|
if self.subs:
|
||||||
|
for i, subtitle in enumerate(self.subs):
|
||||||
|
if i == 0:
|
||||||
|
flag = "select"
|
||||||
|
else:
|
||||||
|
flag = "auto"
|
||||||
|
mpv_player.sub_add(
|
||||||
|
subtitle["url"], flag, None, subtitle["language"]
|
||||||
|
)
|
||||||
|
self.subs = []
|
||||||
|
except mpv.ShutdownError:
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
@mpv_player.property_observer("time-pos")
|
@mpv_player.property_observer("time-pos")
|
||||||
def handle_time_start_update(*args):
|
def handle_time_start_update(*args):
|
||||||
@@ -218,7 +242,9 @@ class MpvPlayer(object):
|
|||||||
def _next_episode():
|
def _next_episode():
|
||||||
url = self.get_episode("next")
|
url = self.get_episode("next")
|
||||||
if url:
|
if url:
|
||||||
mpv_player.loadfile(url, options=f"title={self.current_media_title}")
|
mpv_player.loadfile(
|
||||||
|
url,
|
||||||
|
)
|
||||||
mpv_player.title = self.current_media_title
|
mpv_player.title = self.current_media_title
|
||||||
|
|
||||||
@mpv_player.on_key_press("shift+p")
|
@mpv_player.on_key_press("shift+p")
|
||||||
@@ -244,7 +270,6 @@ class MpvPlayer(object):
|
|||||||
mpv_player.show_text("Changing translation type...")
|
mpv_player.show_text("Changing translation type...")
|
||||||
anime = anime_provider.get_anime(
|
anime = anime_provider.get_anime(
|
||||||
fastanime_runtime_state.provider_anime_search_result["id"],
|
fastanime_runtime_state.provider_anime_search_result["id"],
|
||||||
fastanime_runtime_state.selected_anime_anilist,
|
|
||||||
)
|
)
|
||||||
if not anime:
|
if not anime:
|
||||||
mpv_player.show_text("Failed to update translation type")
|
mpv_player.show_text("Failed to update translation type")
|
||||||
@@ -327,7 +352,23 @@ class MpvPlayer(object):
|
|||||||
mpv_player.register_message_handler("select-quality", select_quality)
|
mpv_player.register_message_handler("select-quality", select_quality)
|
||||||
|
|
||||||
self.mpv_player = mpv_player
|
self.mpv_player = mpv_player
|
||||||
return mpv_player
|
mpv_player.force_window = config.force_window
|
||||||
|
# mpv_player.cache = "yes"
|
||||||
|
# mpv_player.cache_pause = "no"
|
||||||
|
mpv_player.title = title
|
||||||
|
mpv_headers = ""
|
||||||
|
if headers:
|
||||||
|
for header_name, header_value in headers.items():
|
||||||
|
mpv_headers += f"{header_name}:{header_value},"
|
||||||
|
mpv_player.http_header_fields = mpv_headers
|
||||||
|
|
||||||
|
mpv_player.play(stream_link)
|
||||||
|
|
||||||
|
if not start_time == "0":
|
||||||
|
mpv_player.start = start_time
|
||||||
|
|
||||||
|
mpv_player.wait_for_shutdown()
|
||||||
|
mpv_player.terminate()
|
||||||
|
|
||||||
|
|
||||||
player = MpvPlayer()
|
player = MpvPlayer()
|
||||||
|
|||||||
@@ -9,70 +9,70 @@ fzf_preview = r"""
|
|||||||
# - https://github.com/sharkdp/bat
|
# - https://github.com/sharkdp/bat
|
||||||
# - https://github.com/hpjansson/chafa
|
# - https://github.com/hpjansson/chafa
|
||||||
# - https://iterm2.com/utilities/imgcat
|
# - https://iterm2.com/utilities/imgcat
|
||||||
fzf-preview(){
|
fzf-preview() {
|
||||||
if [[ $# -ne 1 ]]; then
|
if [[ $# -ne 1 ]]; then
|
||||||
>&2 echo "usage: $0 FILENAME"
|
>&2 echo "usage: $0 FILENAME"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
file=${1/#\~\//$HOME/}
|
file=${1/#\~\//$HOME/}
|
||||||
type=$(file --dereference --mime -- "$file")
|
type=$(file --dereference --mime -- "$file")
|
||||||
|
|
||||||
if [[ ! $type =~ image/ ]]; then
|
if [[ ! $type =~ image/ ]]; then
|
||||||
if [[ $type =~ =binary ]]; then
|
if [[ $type =~ =binary ]]; then
|
||||||
file "$1"
|
file "$1"
|
||||||
exit
|
exit
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Sometimes bat is installed as batcat.
|
# Sometimes bat is installed as batcat.
|
||||||
if command -v batcat > /dev/null; then
|
if command -v batcat >/dev/null; then
|
||||||
batname="batcat"
|
batname="batcat"
|
||||||
elif command -v bat > /dev/null; then
|
elif command -v bat >/dev/null; then
|
||||||
batname="bat"
|
batname="bat"
|
||||||
else
|
else
|
||||||
cat "$1"
|
cat "$1"
|
||||||
exit
|
exit
|
||||||
fi
|
fi
|
||||||
|
|
||||||
${batname} --style="${BAT_STYLE:-numbers}" --color=always --pager=never -- "$file"
|
${batname} --style="${BAT_STYLE:-numbers}" --color=always --pager=never -- "$file"
|
||||||
exit
|
exit
|
||||||
fi
|
fi
|
||||||
|
|
||||||
dim=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}
|
dim=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}
|
||||||
if [[ $dim = x ]]; then
|
if [[ $dim = x ]]; then
|
||||||
dim=$(stty size < /dev/tty | awk '{print $2 "x" $1}')
|
dim=$(stty size </dev/tty | awk '{print $2 "x" $1}')
|
||||||
elif ! [[ $KITTY_WINDOW_ID ]] && (( FZF_PREVIEW_TOP + FZF_PREVIEW_LINES == $(stty size < /dev/tty | awk '{print $1}') )); then
|
elif ! [[ $KITTY_WINDOW_ID ]] && ((FZF_PREVIEW_TOP + FZF_PREVIEW_LINES == $(stty size </dev/tty | awk '{print $1}'))); then
|
||||||
# Avoid scrolling issue when the Sixel image touches the bottom of the screen
|
# Avoid scrolling issue when the Sixel image touches the bottom of the screen
|
||||||
# * https://github.com/junegunn/fzf/issues/2544
|
# * https://github.com/junegunn/fzf/issues/2544
|
||||||
dim=${FZF_PREVIEW_COLUMNS}x$((FZF_PREVIEW_LINES - 1))
|
dim=${FZF_PREVIEW_COLUMNS}x$((FZF_PREVIEW_LINES - 1))
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 1. Use kitty icat on kitty terminal
|
# 1. Use kitty icat on kitty terminal
|
||||||
if [[ $KITTY_WINDOW_ID ]]; then
|
if [[ $KITTY_WINDOW_ID ]]; then
|
||||||
# 1. 'memory' is the fastest option but if you want the image to be scrollable,
|
# 1. 'memory' is the fastest option but if you want the image to be scrollable,
|
||||||
# you have to use 'stream'.
|
# you have to use 'stream'.
|
||||||
#
|
#
|
||||||
# 2. The last line of the output is the ANSI reset code without newline.
|
# 2. The last line of the output is the ANSI reset code without newline.
|
||||||
# This confuses fzf and makes it render scroll offset indicator.
|
# This confuses fzf and makes it render scroll offset indicator.
|
||||||
# So we remove the last line and append the reset code to its previous line.
|
# So we remove the last line and append the reset code to its previous line.
|
||||||
kitty icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed '$d' | sed $'$s/$/\e[m/'
|
kitty icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed '$d' | sed $'$s/$/\e[m/'
|
||||||
|
|
||||||
# 2. Use chafa with Sixel output
|
# 2. Use chafa with Sixel output
|
||||||
elif command -v chafa > /dev/null; then
|
elif command -v chafa >/dev/null; then
|
||||||
chafa -f sixel -s "$dim" "$file"
|
chafa -f sixel -s "$dim" "$file"
|
||||||
# Add a new line character so that fzf can display multiple images in the preview window
|
# Add a new line character so that fzf can display multiple images in the preview window
|
||||||
echo
|
echo
|
||||||
|
|
||||||
# 3. If chafa is not found but imgcat is available, use it on iTerm2
|
# 3. If chafa is not found but imgcat is available, use it on iTerm2
|
||||||
elif command -v imgcat > /dev/null; then
|
elif command -v imgcat >/dev/null; then
|
||||||
# NOTE: We should use https://iterm2.com/utilities/it2check to check if the
|
# NOTE: We should use https://iterm2.com/utilities/it2check to check if the
|
||||||
# user is running iTerm2. But for the sake of simplicity, we just assume
|
# user is running iTerm2. But for the sake of simplicity, we just assume
|
||||||
# that's the case here.
|
# that's the case here.
|
||||||
imgcat -W "${dim%%x*}" -H "${dim##*x}" "$file"
|
imgcat -W "${dim%%x*}" -H "${dim##*x}" "$file"
|
||||||
|
|
||||||
# 4. Cannot find any suitable method to preview the image
|
# 4. Cannot find any suitable method to preview the image
|
||||||
else
|
else
|
||||||
file "$file"
|
file "$file"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,39 +1,41 @@
|
|||||||
# TODO: add typing
|
from typing import TYPE_CHECKING
|
||||||
class FastAnimeRuntimeState(dict):
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from ...libs.anilist.types import AnilistBaseMediaDataSchema
|
||||||
|
from ...libs.anime_provider.types import Anime, EpisodeStream, SearchResult, Server
|
||||||
|
|
||||||
|
|
||||||
|
class FastAnimeRuntimeState(object):
|
||||||
"""A class that manages fastanime runtime during anilist command runtime"""
|
"""A class that manages fastanime runtime during anilist command runtime"""
|
||||||
|
|
||||||
def __getattr__(self, attr):
|
provider_current_episode_stream_link: str
|
||||||
try:
|
provider_current_server: "Server"
|
||||||
return self.__getitem__(attr)
|
provider_current_server_name: str
|
||||||
except KeyError:
|
provider_available_episodes: list[str]
|
||||||
raise AttributeError(
|
provider_current_episode_number: str
|
||||||
"%r object has no attribute %r" % (self.__class__.__name__, attr)
|
provider_server_episode_streams: list["EpisodeStream"]
|
||||||
)
|
provider_anime_title: str
|
||||||
|
provider_anime: "Anime"
|
||||||
|
provider_anime_search_result: "SearchResult"
|
||||||
|
|
||||||
def __setattr__(self, attr, value):
|
selected_anime_anilist: "AnilistBaseMediaDataSchema"
|
||||||
self.__setitem__(attr, value)
|
selected_anime_id_anilist: int
|
||||||
|
selected_anime_title_anilist: str
|
||||||
|
# current_anilist_data: "AnilistDataSchema | AnilistMediaList"
|
||||||
|
anilist_results_data: "Any"
|
||||||
|
|
||||||
|
|
||||||
def exit_app(exit_code=0, *args):
|
def exit_app(exit_code=0, *args):
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from rich.console import Console
|
||||||
|
|
||||||
from ...constants import APP_NAME, ICON_PATH, USER_NAME
|
from ...constants import APP_NAME, ICON_PATH, USER_NAME
|
||||||
|
|
||||||
def is_running_in_terminal():
|
console = Console()
|
||||||
try:
|
if not console.is_terminal:
|
||||||
shutil.get_terminal_size()
|
|
||||||
return (
|
|
||||||
sys.stdin
|
|
||||||
and sys.stdin.isatty()
|
|
||||||
and sys.stdout.isatty()
|
|
||||||
and os.getenv("TERM") is not None
|
|
||||||
)
|
|
||||||
except OSError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if not is_running_in_terminal():
|
|
||||||
from plyer import notification
|
from plyer import notification
|
||||||
|
|
||||||
notification.notify(
|
notification.notify(
|
||||||
@@ -43,7 +45,6 @@ def exit_app(exit_code=0, *args):
|
|||||||
title="Shutting down",
|
title="Shutting down",
|
||||||
) # pyright:ignore
|
) # pyright:ignore
|
||||||
else:
|
else:
|
||||||
from rich import print
|
console.clear()
|
||||||
|
console.print("Have a good day :smile:", USER_NAME)
|
||||||
print("Have a good day :smile:", USER_NAME)
|
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|||||||
@@ -19,6 +19,27 @@ BG_GREEN = "\033[48;2;120;233;12;m"
|
|||||||
GREEN = "\033[38;2;45;24;45;m"
|
GREEN = "\033[38;2;45;24;45;m"
|
||||||
|
|
||||||
|
|
||||||
|
def get_requested_quality_or_default_to_first(url, quality):
|
||||||
|
import yt_dlp
|
||||||
|
|
||||||
|
with yt_dlp.YoutubeDL({"quiet": True, "silent": True, "no_warnings": True}) as ydl:
|
||||||
|
m3u8_info = ydl.extract_info(url, False)
|
||||||
|
if not m3u8_info:
|
||||||
|
return
|
||||||
|
|
||||||
|
m3u8_formats = m3u8_info["formats"]
|
||||||
|
quality = int(quality)
|
||||||
|
quality_u = quality - 80
|
||||||
|
quality_l = quality + 80
|
||||||
|
for m3u8_format in m3u8_formats:
|
||||||
|
if m3u8_format["height"] == quality or (
|
||||||
|
m3u8_format["height"] < quality_u and m3u8_format["height"] > quality_l
|
||||||
|
):
|
||||||
|
return m3u8_format["url"]
|
||||||
|
else:
|
||||||
|
return m3u8_formats[0]["url"]
|
||||||
|
|
||||||
|
|
||||||
def move_preferred_subtitle_lang_to_top(sub_list, lang_str):
|
def move_preferred_subtitle_lang_to_top(sub_list, lang_str):
|
||||||
"""Moves the dictionary with the given ID to the front of the list.
|
"""Moves the dictionary with the given ID to the front of the list.
|
||||||
|
|
||||||
@@ -125,7 +146,7 @@ def fuzzy_inquirer(choices: list, prompt: str, **kwargs):
|
|||||||
from click import clear
|
from click import clear
|
||||||
|
|
||||||
clear()
|
clear()
|
||||||
action = inquirer.fuzzy(
|
action = inquirer.fuzzy( # pyright:ignore
|
||||||
prompt,
|
prompt,
|
||||||
choices,
|
choices,
|
||||||
height="100%",
|
height="100%",
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import sys
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from platform import system
|
from platform import system
|
||||||
|
|
||||||
from . import APP_NAME, AUTHOR, __version__
|
import click
|
||||||
|
|
||||||
|
from . import APP_NAME, __version__
|
||||||
|
|
||||||
PLATFORM = system()
|
PLATFORM = system()
|
||||||
|
|
||||||
@@ -17,19 +19,20 @@ if PLATFORM == "Windows":
|
|||||||
ICON_PATH = os.path.join(ASSETS_DIR, "logo.ico")
|
ICON_PATH = os.path.join(ASSETS_DIR, "logo.ico")
|
||||||
else:
|
else:
|
||||||
ICON_PATH = os.path.join(ASSETS_DIR, "logo.png")
|
ICON_PATH = os.path.join(ASSETS_DIR, "logo.png")
|
||||||
PREVIEW_IMAGE = os.path.join(ASSETS_DIR, "preview")
|
# PREVIEW_IMAGE = os.path.join(ASSETS_DIR, "preview")
|
||||||
|
|
||||||
|
|
||||||
# ----- user configs and data -----
|
# ----- user configs and data -----
|
||||||
|
|
||||||
S_PLATFORM = sys.platform
|
S_PLATFORM = sys.platform
|
||||||
|
APP_DATA_DIR = click.get_app_dir(APP_NAME,roaming=False)
|
||||||
if S_PLATFORM == "win32":
|
if S_PLATFORM == "win32":
|
||||||
# app data
|
# app data
|
||||||
app_data_dir_base = os.getenv("LOCALAPPDATA")
|
# app_data_dir_base = os.getenv("LOCALAPPDATA")
|
||||||
if not app_data_dir_base:
|
# if not app_data_dir_base:
|
||||||
raise RuntimeError("Could not determine app data dir please report to devs")
|
# raise RuntimeError("Could not determine app data dir please report to devs")
|
||||||
APP_DATA_DIR = os.path.join(app_data_dir_base, AUTHOR, APP_NAME)
|
# APP_DATA_DIR = os.path.join(app_data_dir_base, AUTHOR, APP_NAME)
|
||||||
|
#
|
||||||
# cache dir
|
# cache dir
|
||||||
APP_CACHE_DIR = os.path.join(APP_DATA_DIR, "cache")
|
APP_CACHE_DIR = os.path.join(APP_DATA_DIR, "cache")
|
||||||
|
|
||||||
@@ -39,9 +42,9 @@ if S_PLATFORM == "win32":
|
|||||||
|
|
||||||
elif S_PLATFORM == "darwin":
|
elif S_PLATFORM == "darwin":
|
||||||
# app data
|
# app data
|
||||||
app_data_dir_base = os.path.expanduser("~/Library/Application Support")
|
# app_data_dir_base = os.path.expanduser("~/Library/Application Support")
|
||||||
APP_DATA_DIR = os.path.join(app_data_dir_base, APP_NAME, __version__)
|
# APP_DATA_DIR = os.path.join(app_data_dir_base, APP_NAME, __version__)
|
||||||
|
#
|
||||||
# cache dir
|
# cache dir
|
||||||
cache_dir_base = os.path.expanduser("~/Library/Caches")
|
cache_dir_base = os.path.expanduser("~/Library/Caches")
|
||||||
APP_CACHE_DIR = os.path.join(cache_dir_base, APP_NAME, __version__)
|
APP_CACHE_DIR = os.path.join(cache_dir_base, APP_NAME, __version__)
|
||||||
@@ -50,12 +53,12 @@ elif S_PLATFORM == "darwin":
|
|||||||
video_dir_base = os.path.expanduser("~/Movies")
|
video_dir_base = os.path.expanduser("~/Movies")
|
||||||
USER_VIDEOS_DIR = os.path.join(video_dir_base, APP_NAME)
|
USER_VIDEOS_DIR = os.path.join(video_dir_base, APP_NAME)
|
||||||
else:
|
else:
|
||||||
# app data
|
# # app data
|
||||||
app_data_dir_base = os.environ.get("XDG_CONFIG_HOME", "")
|
# app_data_dir_base = os.environ.get("XDG_CONFIG_HOME", "")
|
||||||
if not app_data_dir_base.strip():
|
# if not app_data_dir_base.strip():
|
||||||
app_data_dir_base = os.path.expanduser("~/.config")
|
# app_data_dir_base = os.path.expanduser("~/.config")
|
||||||
APP_DATA_DIR = os.path.join(app_data_dir_base, APP_NAME)
|
# APP_DATA_DIR = os.path.join(app_data_dir_base, APP_NAME)
|
||||||
|
#
|
||||||
# cache dir
|
# cache dir
|
||||||
cache_dir_base = os.environ.get("XDG_CACHE_HOME", "")
|
cache_dir_base = os.environ.get("XDG_CACHE_HOME", "")
|
||||||
if not cache_dir_base.strip():
|
if not cache_dir_base.strip():
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from .queries_graphql import (
|
|||||||
delete_list_entry_query,
|
delete_list_entry_query,
|
||||||
get_logged_in_user_query,
|
get_logged_in_user_query,
|
||||||
get_medialist_item_query,
|
get_medialist_item_query,
|
||||||
|
get_user_info,
|
||||||
media_list_mutation,
|
media_list_mutation,
|
||||||
media_list_query,
|
media_list_query,
|
||||||
most_favourite_query,
|
most_favourite_query,
|
||||||
@@ -34,8 +35,9 @@ if TYPE_CHECKING:
|
|||||||
AnilistMediaLists,
|
AnilistMediaLists,
|
||||||
AnilistMediaListStatus,
|
AnilistMediaListStatus,
|
||||||
AnilistNotifications,
|
AnilistNotifications,
|
||||||
AnilistUser,
|
AnilistUser_,
|
||||||
AnilistUserData,
|
AnilistUserData,
|
||||||
|
AnilistViewerData,
|
||||||
)
|
)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
ANILIST_ENDPOINT = "https://graphql.anilist.co"
|
ANILIST_ENDPOINT = "https://graphql.anilist.co"
|
||||||
@@ -77,7 +79,7 @@ class AniListApi:
|
|||||||
return
|
return
|
||||||
if not success or not user:
|
if not success or not user:
|
||||||
return
|
return
|
||||||
user_info: AnilistUser = user["data"]["Viewer"]
|
user_info: "AnilistUser_" = user["data"]["Viewer"]
|
||||||
self.user_id = user_info["id"]
|
self.user_id = user_info["id"]
|
||||||
return user_info
|
return user_info
|
||||||
|
|
||||||
@@ -91,7 +93,7 @@ class AniListApi:
|
|||||||
"""
|
"""
|
||||||
return self._make_authenticated_request(notification_query)
|
return self._make_authenticated_request(notification_query)
|
||||||
|
|
||||||
def update_login_info(self, user: "AnilistUser", token: str):
|
def update_login_info(self, user: "AnilistUser_", token: str):
|
||||||
"""method used to login a user enabling authenticated requests
|
"""method used to login a user enabling authenticated requests
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -103,7 +105,18 @@ class AniListApi:
|
|||||||
self.session.headers.update(self.headers)
|
self.session.headers.update(self.headers)
|
||||||
self.user_id = user["id"]
|
self.user_id = user["id"]
|
||||||
|
|
||||||
def get_logged_in_user(self) -> tuple[bool, "AnilistUserData"] | tuple[bool, None]:
|
def get_user_info(self) -> tuple[bool, "AnilistUserData"] | tuple[bool, None]:
|
||||||
|
"""get the details of the user who is currently logged in
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
an anilist user
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self._make_authenticated_request(get_user_info, {"userId": self.user_id})
|
||||||
|
|
||||||
|
def get_logged_in_user(
|
||||||
|
self,
|
||||||
|
) -> tuple[bool, "AnilistViewerData"] | tuple[bool, None]:
|
||||||
"""get the details of the user who is currently logged in
|
"""get the details of the user who is currently logged in
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -309,9 +322,14 @@ class AniListApi:
|
|||||||
status_not_in: list[str] | None = None,
|
status_not_in: list[str] | None = None,
|
||||||
endDate_greater: int | None = None,
|
endDate_greater: int | None = None,
|
||||||
endDate_lesser: int | None = None,
|
endDate_lesser: int | None = None,
|
||||||
start_greater: int | None = None,
|
startDate_greater: int | None = None,
|
||||||
start_lesser: int | None = None,
|
startDate_lesser: int | None = None,
|
||||||
|
startDate: str | None = None,
|
||||||
|
seasonYear: str | None = None,
|
||||||
page: int | None = None,
|
page: int | None = None,
|
||||||
|
season: str | None = None,
|
||||||
|
format_in: list[str] | None = None,
|
||||||
|
on_list: bool | None = None,
|
||||||
type="ANIME",
|
type="ANIME",
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
@@ -320,7 +338,7 @@ class AniListApi:
|
|||||||
"""
|
"""
|
||||||
variables = {}
|
variables = {}
|
||||||
for key, val in list(locals().items())[1:]:
|
for key, val in list(locals().items())[1:]:
|
||||||
if val is not None and key not in ["variables"]:
|
if (val or val is False) and key not in ["variables"]:
|
||||||
variables[key] = val
|
variables[key] = val
|
||||||
search_results = self.get_data(search_query, variables=variables)
|
search_results = self.get_data(search_query, variables=variables)
|
||||||
return search_results
|
return search_results
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ This module contains all the preset queries for the sake of neatness and convini
|
|||||||
Mostly for internal usage
|
Mostly for internal usage
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# TODO: Format the queries
|
|
||||||
mark_as_read_mutation = """
|
mark_as_read_mutation = """
|
||||||
mutation{
|
mutation{
|
||||||
UpdateUser{
|
UpdateUser{
|
||||||
@@ -17,7 +16,6 @@ query($id:Int){
|
|||||||
pageInfo{
|
pageInfo{
|
||||||
total
|
total
|
||||||
}
|
}
|
||||||
|
|
||||||
reviews(mediaId:$id){
|
reviews(mediaId:$id){
|
||||||
summary
|
summary
|
||||||
user{
|
user{
|
||||||
@@ -35,50 +33,48 @@ query($id:Int){
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
notification_query = """
|
notification_query = """
|
||||||
query{
|
query {
|
||||||
Page(perPage:5){
|
Page(perPage: 5) {
|
||||||
pageInfo {
|
pageInfo {
|
||||||
total
|
total
|
||||||
}
|
|
||||||
notifications(resetNotificationCount:true,type:AIRING) {
|
|
||||||
... on AiringNotification {
|
|
||||||
id
|
|
||||||
type
|
|
||||||
episode
|
|
||||||
contexts
|
|
||||||
createdAt
|
|
||||||
media {
|
|
||||||
id
|
|
||||||
idMal
|
|
||||||
title {
|
|
||||||
romaji
|
|
||||||
english
|
|
||||||
}
|
|
||||||
coverImage{
|
|
||||||
medium
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
notifications(resetNotificationCount: true, type: AIRING) {
|
||||||
|
... on AiringNotification {
|
||||||
|
id
|
||||||
|
type
|
||||||
|
episode
|
||||||
|
contexts
|
||||||
|
createdAt
|
||||||
|
media {
|
||||||
|
id
|
||||||
|
idMal
|
||||||
|
title {
|
||||||
|
romaji
|
||||||
|
english
|
||||||
|
}
|
||||||
|
coverImage {
|
||||||
|
medium
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
get_medialist_item_query = """
|
get_medialist_item_query = """
|
||||||
query($mediaId:Int){
|
query ($mediaId: Int) {
|
||||||
MediaList(mediaId:$mediaId){
|
MediaList(mediaId: $mediaId) {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
delete_list_entry_query = """
|
delete_list_entry_query = """
|
||||||
mutation($id:Int){
|
mutation ($id: Int) {
|
||||||
DeleteMediaListEntry(id:$id){
|
DeleteMediaListEntry(id: $id) {
|
||||||
deleted
|
deleted
|
||||||
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -97,9 +93,85 @@ query{
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
get_user_info = """
|
||||||
|
query ($userId: Int) {
|
||||||
|
User(id: $userId) {
|
||||||
|
name
|
||||||
|
about
|
||||||
|
avatar {
|
||||||
|
large
|
||||||
|
medium
|
||||||
|
}
|
||||||
|
bannerImage
|
||||||
|
statistics {
|
||||||
|
anime {
|
||||||
|
count
|
||||||
|
minutesWatched
|
||||||
|
episodesWatched
|
||||||
|
genres {
|
||||||
|
count
|
||||||
|
meanScore
|
||||||
|
genre
|
||||||
|
}
|
||||||
|
tags {
|
||||||
|
tag {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
count
|
||||||
|
meanScore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
manga {
|
||||||
|
count
|
||||||
|
meanScore
|
||||||
|
chaptersRead
|
||||||
|
volumesRead
|
||||||
|
tags {
|
||||||
|
count
|
||||||
|
meanScore
|
||||||
|
}
|
||||||
|
genres {
|
||||||
|
count
|
||||||
|
meanScore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
favourites {
|
||||||
|
anime {
|
||||||
|
nodes {
|
||||||
|
title {
|
||||||
|
romaji
|
||||||
|
english
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
manga {
|
||||||
|
nodes {
|
||||||
|
title {
|
||||||
|
romaji
|
||||||
|
english
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
media_list_mutation = """
|
media_list_mutation = """
|
||||||
mutation($mediaId:Int,$scoreRaw:Int,$repeat:Int,$progress:Int,$status:MediaListStatus){
|
mutation (
|
||||||
SaveMediaListEntry(mediaId:$mediaId,scoreRaw:$scoreRaw,progress:$progress,repeat:$repeat,status:$status){
|
$mediaId: Int
|
||||||
|
$scoreRaw: Int
|
||||||
|
$repeat: Int
|
||||||
|
$progress: Int
|
||||||
|
$status: MediaListStatus
|
||||||
|
) {
|
||||||
|
SaveMediaListEntry(
|
||||||
|
mediaId: $mediaId
|
||||||
|
scoreRaw: $scoreRaw
|
||||||
|
progress: $progress
|
||||||
|
repeat: $repeat
|
||||||
|
status: $status
|
||||||
|
) {
|
||||||
id
|
id
|
||||||
status
|
status
|
||||||
mediaId
|
mediaId
|
||||||
@@ -116,21 +188,19 @@ mutation($mediaId:Int,$scoreRaw:Int,$repeat:Int,$progress:Int,$status:MediaListS
|
|||||||
month
|
month
|
||||||
day
|
day
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
media_list_query = """
|
media_list_query = """
|
||||||
query ($userId: Int, $status: MediaListStatus,$type:MediaType) {
|
query ($userId: Int, $status: MediaListStatus, $type: MediaType) {
|
||||||
Page {
|
Page {
|
||||||
pageInfo {
|
pageInfo {
|
||||||
currentPage
|
currentPage
|
||||||
total
|
total
|
||||||
}
|
}
|
||||||
mediaList(userId: $userId, status: $status, type: $type) {
|
mediaList(userId: $userId, status: $status, type: $type) {
|
||||||
mediaId
|
mediaId
|
||||||
|
|
||||||
media {
|
media {
|
||||||
id
|
id
|
||||||
idMal
|
idMal
|
||||||
@@ -147,6 +217,10 @@ query ($userId: Int, $status: MediaListStatus,$type:MediaType) {
|
|||||||
id
|
id
|
||||||
}
|
}
|
||||||
popularity
|
popularity
|
||||||
|
streamingEpisodes {
|
||||||
|
title
|
||||||
|
thumbnail
|
||||||
|
}
|
||||||
favourites
|
favourites
|
||||||
averageScore
|
averageScore
|
||||||
episodes
|
episodes
|
||||||
@@ -172,10 +246,10 @@ query ($userId: Int, $status: MediaListStatus,$type:MediaType) {
|
|||||||
}
|
}
|
||||||
status
|
status
|
||||||
description
|
description
|
||||||
mediaListEntry{
|
mediaListEntry {
|
||||||
status
|
status
|
||||||
id
|
id
|
||||||
progress
|
progress
|
||||||
}
|
}
|
||||||
nextAiringEpisode {
|
nextAiringEpisode {
|
||||||
timeUntilAiring
|
timeUntilAiring
|
||||||
@@ -199,7 +273,6 @@ query ($userId: Int, $status: MediaListStatus,$type:MediaType) {
|
|||||||
day
|
day
|
||||||
}
|
}
|
||||||
createdAt
|
createdAt
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -221,72 +294,83 @@ $popularity_greater:Int,\
|
|||||||
$popularity_lesser:Int,\
|
$popularity_lesser:Int,\
|
||||||
$averageScore_greater:Int,\
|
$averageScore_greater:Int,\
|
||||||
$averageScore_lesser:Int,\
|
$averageScore_lesser:Int,\
|
||||||
|
$seasonYear:Int,\
|
||||||
$startDate_greater:FuzzyDateInt,\
|
$startDate_greater:FuzzyDateInt,\
|
||||||
$startDate_lesser:FuzzyDateInt,\
|
$startDate_lesser:FuzzyDateInt,\
|
||||||
|
$startDate:FuzzyDateInt,\
|
||||||
$endDate_greater:FuzzyDateInt,\
|
$endDate_greater:FuzzyDateInt,\
|
||||||
$endDate_lesser:FuzzyDateInt,\
|
$endDate_lesser:FuzzyDateInt,\
|
||||||
|
$format_in:[MediaFormat],\
|
||||||
$type:MediaType\
|
$type:MediaType\
|
||||||
|
$season:MediaSeason\
|
||||||
|
$on_list:Boolean\
|
||||||
"
|
"
|
||||||
# FuzzyDateInt = (yyyymmdd)
|
|
||||||
# MediaStatus = (FINISHED,RELEASING,NOT_YET_RELEASED,CANCELLED,HIATUS)
|
|
||||||
search_query = (
|
search_query = (
|
||||||
"""
|
"""
|
||||||
query($query:String,%s){
|
query($query:String,%s){
|
||||||
Page(perPage:50,page:$page){
|
Page(perPage: 50, page: $page) {
|
||||||
pageInfo{
|
pageInfo {
|
||||||
total
|
total
|
||||||
currentPage
|
currentPage
|
||||||
hasNextPage
|
hasNextPage
|
||||||
}
|
}
|
||||||
media(
|
media(
|
||||||
search:$query,
|
search: $query
|
||||||
id_in:$id_in,
|
id_in: $id_in
|
||||||
genre_in:$genre_in,
|
genre_in: $genre_in
|
||||||
genre_not_in:$genre_not_in,
|
genre_not_in: $genre_not_in
|
||||||
tag_in:$tag_in,
|
tag_in: $tag_in
|
||||||
tag_not_in:$tag_not_in,
|
tag_not_in: $tag_not_in
|
||||||
status_in:$status_in,
|
status_in: $status_in
|
||||||
status:$status,
|
status: $status
|
||||||
status_not_in:$status_not_in,
|
startDate: $startDate
|
||||||
popularity_greater:$popularity_greater,
|
status_not_in: $status_not_in
|
||||||
popularity_lesser:$popularity_lesser,
|
popularity_greater: $popularity_greater
|
||||||
averageScore_greater:$averageScore_greater,
|
popularity_lesser: $popularity_lesser
|
||||||
averageScore_lesser:$averageScore_lesser,
|
averageScore_greater: $averageScore_greater
|
||||||
startDate_greater:$startDate_greater,
|
averageScore_lesser: $averageScore_lesser
|
||||||
startDate_lesser:$startDate_lesser,
|
startDate_greater: $startDate_greater
|
||||||
endDate_greater:$endDate_greater,
|
startDate_lesser: $startDate_lesser
|
||||||
endDate_lesser:$endDate_lesser,
|
endDate_greater: $endDate_greater
|
||||||
sort:$sort,
|
endDate_lesser: $endDate_lesser
|
||||||
type:$type
|
format_in: $format_in
|
||||||
)
|
sort: $sort
|
||||||
{
|
season: $season
|
||||||
|
seasonYear: $seasonYear
|
||||||
|
type: $type
|
||||||
|
onList:$on_list
|
||||||
|
) {
|
||||||
id
|
id
|
||||||
idMal
|
idMal
|
||||||
title{
|
title {
|
||||||
romaji
|
romaji
|
||||||
english
|
english
|
||||||
}
|
}
|
||||||
coverImage{
|
coverImage {
|
||||||
medium
|
medium
|
||||||
large
|
large
|
||||||
}
|
}
|
||||||
trailer {
|
trailer {
|
||||||
site
|
site
|
||||||
id
|
id
|
||||||
|
|
||||||
}
|
}
|
||||||
mediaListEntry{
|
mediaListEntry {
|
||||||
status
|
status
|
||||||
id
|
id
|
||||||
progress
|
progress
|
||||||
}
|
}
|
||||||
popularity
|
popularity
|
||||||
|
streamingEpisodes {
|
||||||
|
title
|
||||||
|
thumbnail
|
||||||
|
}
|
||||||
favourites
|
favourites
|
||||||
averageScore
|
averageScore
|
||||||
episodes
|
episodes
|
||||||
genres
|
genres
|
||||||
studios{
|
studios {
|
||||||
nodes{
|
nodes {
|
||||||
name
|
name
|
||||||
isAnimationStudio
|
isAnimationStudio
|
||||||
}
|
}
|
||||||
@@ -319,17 +403,16 @@ query($query:String,%s){
|
|||||||
)
|
)
|
||||||
|
|
||||||
trending_query = """
|
trending_query = """
|
||||||
query($type:MediaType){
|
query ($type: MediaType) {
|
||||||
Page(perPage:15){
|
Page(perPage: 15) {
|
||||||
|
media(sort: TRENDING_DESC, type: $type, genre_not_in: ["hentai"]) {
|
||||||
media(sort:TRENDING_DESC,type:$type,genre_not_in:["hentai"]){
|
|
||||||
id
|
id
|
||||||
idMal
|
idMal
|
||||||
title{
|
title {
|
||||||
romaji
|
romaji
|
||||||
english
|
english
|
||||||
}
|
}
|
||||||
coverImage{
|
coverImage {
|
||||||
medium
|
medium
|
||||||
large
|
large
|
||||||
}
|
}
|
||||||
@@ -338,6 +421,10 @@ query($type:MediaType){
|
|||||||
id
|
id
|
||||||
}
|
}
|
||||||
popularity
|
popularity
|
||||||
|
streamingEpisodes {
|
||||||
|
title
|
||||||
|
thumbnail
|
||||||
|
}
|
||||||
favourites
|
favourites
|
||||||
averageScore
|
averageScore
|
||||||
genres
|
genres
|
||||||
@@ -350,18 +437,18 @@ query($type:MediaType){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
tags {
|
tags {
|
||||||
name
|
name
|
||||||
}
|
}
|
||||||
startDate {
|
startDate {
|
||||||
year
|
year
|
||||||
month
|
month
|
||||||
day
|
day
|
||||||
}
|
}
|
||||||
mediaListEntry{
|
mediaListEntry {
|
||||||
status
|
status
|
||||||
id
|
id
|
||||||
progress
|
progress
|
||||||
}
|
}
|
||||||
endDate {
|
endDate {
|
||||||
year
|
year
|
||||||
month
|
month
|
||||||
@@ -380,30 +467,37 @@ query($type:MediaType){
|
|||||||
|
|
||||||
# mosts
|
# mosts
|
||||||
most_favourite_query = """
|
most_favourite_query = """
|
||||||
query($type:MediaType){
|
query ($type: MediaType) {
|
||||||
Page(perPage:15){
|
Page(perPage: 15) {
|
||||||
media(sort:FAVOURITES_DESC,type:$type,genre_not_in:["hentai"]){
|
media(sort: FAVOURITES_DESC, type: $type, genre_not_in: ["hentai"]) {
|
||||||
id
|
id
|
||||||
idMal
|
idMal
|
||||||
title{
|
title {
|
||||||
romaji
|
romaji
|
||||||
english
|
english
|
||||||
}
|
}
|
||||||
coverImage{
|
coverImage {
|
||||||
medium
|
medium
|
||||||
large
|
large
|
||||||
}
|
}
|
||||||
trailer {
|
trailer {
|
||||||
site
|
site
|
||||||
id
|
id
|
||||||
|
|
||||||
}
|
}
|
||||||
mediaListEntry{
|
mediaListEntry {
|
||||||
status
|
status
|
||||||
id
|
id
|
||||||
progress
|
progress
|
||||||
}
|
}
|
||||||
popularity
|
popularity
|
||||||
|
streamingEpisodes {
|
||||||
|
title
|
||||||
|
thumbnail
|
||||||
|
}
|
||||||
|
streamingEpisodes {
|
||||||
|
title
|
||||||
|
thumbnail
|
||||||
|
}
|
||||||
favourites
|
favourites
|
||||||
averageScore
|
averageScore
|
||||||
episodes
|
episodes
|
||||||
@@ -416,7 +510,7 @@ query($type:MediaType){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
tags {
|
tags {
|
||||||
name
|
name
|
||||||
}
|
}
|
||||||
startDate {
|
startDate {
|
||||||
year
|
year
|
||||||
@@ -440,30 +534,33 @@ query($type:MediaType){
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
most_scored_query = """
|
most_scored_query = """
|
||||||
query($type:MediaType){
|
query ($type: MediaType) {
|
||||||
Page(perPage:15){
|
Page(perPage: 15) {
|
||||||
media(sort:SCORE_DESC,type:$type,genre_not_in:["hentai"]){
|
media(sort: SCORE_DESC, type: $type, genre_not_in: ["hentai"]) {
|
||||||
id
|
id
|
||||||
idMal
|
idMal
|
||||||
title{
|
title {
|
||||||
romaji
|
romaji
|
||||||
english
|
english
|
||||||
}
|
}
|
||||||
coverImage{
|
coverImage {
|
||||||
medium
|
medium
|
||||||
large
|
large
|
||||||
}
|
}
|
||||||
trailer {
|
trailer {
|
||||||
site
|
site
|
||||||
id
|
id
|
||||||
|
|
||||||
}
|
}
|
||||||
mediaListEntry{
|
mediaListEntry {
|
||||||
status
|
status
|
||||||
id
|
id
|
||||||
progress
|
progress
|
||||||
}
|
}
|
||||||
popularity
|
popularity
|
||||||
|
streamingEpisodes {
|
||||||
|
title
|
||||||
|
thumbnail
|
||||||
|
}
|
||||||
episodes
|
episodes
|
||||||
favourites
|
favourites
|
||||||
averageScore
|
averageScore
|
||||||
@@ -476,7 +573,7 @@ query($type:MediaType){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
tags {
|
tags {
|
||||||
name
|
name
|
||||||
}
|
}
|
||||||
startDate {
|
startDate {
|
||||||
year
|
year
|
||||||
@@ -500,35 +597,38 @@ query($type:MediaType){
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
most_popular_query = """
|
most_popular_query = """
|
||||||
query($type:MediaType){
|
query ($type: MediaType) {
|
||||||
Page(perPage:15){
|
Page(perPage: 15) {
|
||||||
media(sort:POPULARITY_DESC,type:$type,genre_not_in:["hentai"]){
|
media(sort: POPULARITY_DESC, type: $type, genre_not_in: ["hentai"]) {
|
||||||
id
|
id
|
||||||
idMal
|
idMal
|
||||||
title{
|
title {
|
||||||
romaji
|
romaji
|
||||||
english
|
english
|
||||||
}
|
}
|
||||||
coverImage{
|
coverImage {
|
||||||
medium
|
medium
|
||||||
large
|
large
|
||||||
}
|
}
|
||||||
trailer {
|
trailer {
|
||||||
site
|
site
|
||||||
id
|
id
|
||||||
|
|
||||||
}
|
}
|
||||||
popularity
|
popularity
|
||||||
|
streamingEpisodes {
|
||||||
|
title
|
||||||
|
thumbnail
|
||||||
|
}
|
||||||
favourites
|
favourites
|
||||||
averageScore
|
averageScore
|
||||||
description
|
description
|
||||||
episodes
|
episodes
|
||||||
genres
|
genres
|
||||||
mediaListEntry{
|
mediaListEntry {
|
||||||
status
|
status
|
||||||
id
|
id
|
||||||
progress
|
progress
|
||||||
}
|
}
|
||||||
studios {
|
studios {
|
||||||
nodes {
|
nodes {
|
||||||
name
|
name
|
||||||
@@ -536,8 +636,8 @@ query($type:MediaType){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
tags {
|
tags {
|
||||||
name
|
name
|
||||||
}
|
}
|
||||||
startDate {
|
startDate {
|
||||||
year
|
year
|
||||||
month
|
month
|
||||||
@@ -553,36 +653,47 @@ query($type:MediaType){
|
|||||||
timeUntilAiring
|
timeUntilAiring
|
||||||
airingAt
|
airingAt
|
||||||
episode
|
episode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
most_recently_updated_query = """
|
most_recently_updated_query = """
|
||||||
query($type:MediaType){
|
query ($type: MediaType) {
|
||||||
Page(perPage:15){
|
Page(perPage: 15) {
|
||||||
media(sort:UPDATED_AT_DESC,type:$type,averageScore_greater:50,genre_not_in:["hentai"],status:RELEASING){
|
media(
|
||||||
|
sort: UPDATED_AT_DESC
|
||||||
|
type: $type
|
||||||
|
averageScore_greater: 50
|
||||||
|
genre_not_in: ["hentai"]
|
||||||
|
status: RELEASING
|
||||||
|
) {
|
||||||
id
|
id
|
||||||
idMal
|
idMal
|
||||||
title{
|
title {
|
||||||
romaji
|
romaji
|
||||||
english
|
english
|
||||||
}
|
}
|
||||||
coverImage{
|
coverImage {
|
||||||
medium
|
medium
|
||||||
large
|
large
|
||||||
}
|
}
|
||||||
trailer {
|
trailer {
|
||||||
site
|
site
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
mediaListEntry{
|
mediaListEntry {
|
||||||
status
|
status
|
||||||
id
|
id
|
||||||
progress
|
progress
|
||||||
}
|
}
|
||||||
popularity
|
popularity
|
||||||
|
streamingEpisodes {
|
||||||
|
title
|
||||||
|
thumbnail
|
||||||
|
}
|
||||||
|
|
||||||
favourites
|
favourites
|
||||||
averageScore
|
averageScore
|
||||||
description
|
description
|
||||||
@@ -595,7 +706,7 @@ query($type:MediaType){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
tags {
|
tags {
|
||||||
name
|
name
|
||||||
}
|
}
|
||||||
startDate {
|
startDate {
|
||||||
year
|
year
|
||||||
@@ -619,38 +730,41 @@ query($type:MediaType){
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
recommended_query = """
|
recommended_query = """
|
||||||
query($type:MediaType){
|
query ($type: MediaType) {
|
||||||
Page(perPage:15) {
|
Page(perPage: 15) {
|
||||||
media( type: $type,genre_not_in:["hentai"]) {
|
media(type: $type, genre_not_in: ["hentai"]) {
|
||||||
recommendations(sort:RATING_DESC){
|
recommendations(sort: RATING_DESC) {
|
||||||
nodes{
|
nodes {
|
||||||
media{
|
media {
|
||||||
id
|
id
|
||||||
idMal
|
idMal
|
||||||
title{
|
title {
|
||||||
english
|
english
|
||||||
romaji
|
romaji
|
||||||
native
|
native
|
||||||
}
|
}
|
||||||
coverImage{
|
coverImage {
|
||||||
medium
|
medium
|
||||||
large
|
large
|
||||||
}
|
}
|
||||||
mediaListEntry{
|
mediaListEntry {
|
||||||
status
|
status
|
||||||
id
|
id
|
||||||
progress
|
progress
|
||||||
}
|
}
|
||||||
description
|
description
|
||||||
episodes
|
episodes
|
||||||
trailer{
|
trailer {
|
||||||
site
|
site
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
|
|
||||||
genres
|
genres
|
||||||
averageScore
|
averageScore
|
||||||
popularity
|
popularity
|
||||||
|
streamingEpisodes {
|
||||||
|
title
|
||||||
|
thumbnail
|
||||||
|
}
|
||||||
favourites
|
favourites
|
||||||
tags {
|
tags {
|
||||||
name
|
name
|
||||||
@@ -680,9 +794,9 @@ query($type:MediaType){
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
anime_characters_query = """
|
anime_characters_query = """
|
||||||
query($id:Int,$type:MediaType){
|
query ($id: Int, $type: MediaType) {
|
||||||
Page {
|
Page {
|
||||||
media(id:$id, type: $type) {
|
media(id: $id, type: $type) {
|
||||||
characters {
|
characters {
|
||||||
nodes {
|
nodes {
|
||||||
name {
|
name {
|
||||||
@@ -715,13 +829,18 @@ query($id:Int,$type:MediaType){
|
|||||||
|
|
||||||
|
|
||||||
anime_relations_query = """
|
anime_relations_query = """
|
||||||
query ($id: Int,$type:MediaType) {
|
query ($id: Int, $type: MediaType) {
|
||||||
Page(perPage: 20) {
|
Page(perPage: 20) {
|
||||||
media(id: $id, sort: POPULARITY_DESC, type: $type,genre_not_in:["hentai"]) {
|
media(
|
||||||
|
id: $id
|
||||||
|
sort: POPULARITY_DESC
|
||||||
|
type: $type
|
||||||
|
genre_not_in: ["hentai"]
|
||||||
|
) {
|
||||||
relations {
|
relations {
|
||||||
nodes {
|
nodes {
|
||||||
id
|
id
|
||||||
idMal
|
idMal
|
||||||
title {
|
title {
|
||||||
english
|
english
|
||||||
romaji
|
romaji
|
||||||
@@ -731,11 +850,11 @@ query ($id: Int,$type:MediaType) {
|
|||||||
medium
|
medium
|
||||||
large
|
large
|
||||||
}
|
}
|
||||||
mediaListEntry{
|
mediaListEntry {
|
||||||
status
|
status
|
||||||
id
|
id
|
||||||
progress
|
progress
|
||||||
}
|
}
|
||||||
description
|
description
|
||||||
episodes
|
episodes
|
||||||
trailer {
|
trailer {
|
||||||
@@ -745,26 +864,30 @@ query ($id: Int,$type:MediaType) {
|
|||||||
genres
|
genres
|
||||||
averageScore
|
averageScore
|
||||||
popularity
|
popularity
|
||||||
|
streamingEpisodes {
|
||||||
|
title
|
||||||
|
thumbnail
|
||||||
|
}
|
||||||
favourites
|
favourites
|
||||||
tags {
|
tags {
|
||||||
name
|
name
|
||||||
}
|
}
|
||||||
startDate {
|
startDate {
|
||||||
year
|
year
|
||||||
month
|
month
|
||||||
day
|
day
|
||||||
}
|
}
|
||||||
endDate {
|
endDate {
|
||||||
year
|
year
|
||||||
month
|
month
|
||||||
day
|
day
|
||||||
}
|
}
|
||||||
status
|
status
|
||||||
nextAiringEpisode {
|
nextAiringEpisode {
|
||||||
timeUntilAiring
|
timeUntilAiring
|
||||||
airingAt
|
airingAt
|
||||||
episode
|
episode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -790,7 +913,7 @@ query ($id: Int,$type:MediaType) {
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
upcoming_anime_query = """
|
upcoming_anime_query = """
|
||||||
query ($page: Int,$type:MediaType) {
|
query ($page: Int, $type: MediaType) {
|
||||||
Page(page: $page) {
|
Page(page: $page) {
|
||||||
pageInfo {
|
pageInfo {
|
||||||
total
|
total
|
||||||
@@ -798,9 +921,14 @@ query ($page: Int,$type:MediaType) {
|
|||||||
currentPage
|
currentPage
|
||||||
hasNextPage
|
hasNextPage
|
||||||
}
|
}
|
||||||
media(type: $type, status: NOT_YET_RELEASED,sort:POPULARITY_DESC,genre_not_in:["hentai"]) {
|
media(
|
||||||
|
type: $type
|
||||||
|
status: NOT_YET_RELEASED
|
||||||
|
sort: POPULARITY_DESC
|
||||||
|
genre_not_in: ["hentai"]
|
||||||
|
) {
|
||||||
id
|
id
|
||||||
idMal
|
idMal
|
||||||
title {
|
title {
|
||||||
romaji
|
romaji
|
||||||
english
|
english
|
||||||
@@ -813,12 +941,16 @@ query ($page: Int,$type:MediaType) {
|
|||||||
site
|
site
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
mediaListEntry{
|
mediaListEntry {
|
||||||
status
|
status
|
||||||
id
|
id
|
||||||
progress
|
progress
|
||||||
}
|
}
|
||||||
popularity
|
popularity
|
||||||
|
streamingEpisodes {
|
||||||
|
title
|
||||||
|
thumbnail
|
||||||
|
}
|
||||||
favourites
|
favourites
|
||||||
averageScore
|
averageScore
|
||||||
genres
|
genres
|
||||||
@@ -855,20 +987,20 @@ query ($page: Int,$type:MediaType) {
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
anime_query = """
|
anime_query = """
|
||||||
query($id:Int){
|
query ($id: Int) {
|
||||||
Page{
|
Page {
|
||||||
media(id:$id) {
|
media(id: $id) {
|
||||||
id
|
id
|
||||||
idMal
|
idMal
|
||||||
title {
|
title {
|
||||||
romaji
|
romaji
|
||||||
english
|
english
|
||||||
}
|
}
|
||||||
mediaListEntry{
|
mediaListEntry {
|
||||||
status
|
status
|
||||||
id
|
id
|
||||||
progress
|
progress
|
||||||
}
|
}
|
||||||
nextAiringEpisode {
|
nextAiringEpisode {
|
||||||
timeUntilAiring
|
timeUntilAiring
|
||||||
airingAt
|
airingAt
|
||||||
@@ -882,7 +1014,6 @@ query($id:Int){
|
|||||||
node {
|
node {
|
||||||
name {
|
name {
|
||||||
full
|
full
|
||||||
|
|
||||||
}
|
}
|
||||||
gender
|
gender
|
||||||
dateOfBirth {
|
dateOfBirth {
|
||||||
@@ -935,6 +1066,11 @@ query($id:Int){
|
|||||||
countryOfOrigin
|
countryOfOrigin
|
||||||
averageScore
|
averageScore
|
||||||
popularity
|
popularity
|
||||||
|
streamingEpisodes {
|
||||||
|
title
|
||||||
|
thumbnail
|
||||||
|
}
|
||||||
|
|
||||||
favourites
|
favourites
|
||||||
source
|
source
|
||||||
hashtag
|
hashtag
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class AnilistImage(TypedDict):
|
|||||||
large: str
|
large: str
|
||||||
|
|
||||||
|
|
||||||
class AnilistUser(TypedDict):
|
class AnilistUser_(TypedDict):
|
||||||
id: int
|
id: int
|
||||||
name: str
|
name: str
|
||||||
bannerImage: str | None
|
bannerImage: str | None
|
||||||
@@ -28,11 +28,26 @@ class AnilistUser(TypedDict):
|
|||||||
|
|
||||||
|
|
||||||
class AnilistViewer(TypedDict):
|
class AnilistViewer(TypedDict):
|
||||||
Viewer: AnilistUser
|
Viewer: AnilistUser_
|
||||||
|
|
||||||
|
|
||||||
|
class AnilistViewerData(TypedDict):
|
||||||
|
data: AnilistViewer
|
||||||
|
|
||||||
|
|
||||||
|
class AnilistUser(TypedDict):
|
||||||
|
name: str
|
||||||
|
about: str | None
|
||||||
|
avatar: AnilistImage
|
||||||
|
bannerImage: str | None
|
||||||
|
|
||||||
|
|
||||||
|
class AnilistUserInfo(TypedDict):
|
||||||
|
User: AnilistUser
|
||||||
|
|
||||||
|
|
||||||
class AnilistUserData(TypedDict):
|
class AnilistUserData(TypedDict):
|
||||||
data: AnilistViewer
|
data: AnilistUserInfo
|
||||||
|
|
||||||
|
|
||||||
class AnilistMediaTrailer(TypedDict):
|
class AnilistMediaTrailer(TypedDict):
|
||||||
@@ -69,7 +84,7 @@ class AnilistMediaNextAiringEpisode(TypedDict):
|
|||||||
|
|
||||||
class AnilistReview(TypedDict):
|
class AnilistReview(TypedDict):
|
||||||
summary: str
|
summary: str
|
||||||
user: AnilistUser
|
user: AnilistUser_
|
||||||
|
|
||||||
|
|
||||||
class AnilistReviewNodes(TypedDict):
|
class AnilistReviewNodes(TypedDict):
|
||||||
@@ -114,16 +129,17 @@ class AnilistCharactersEdges(TypedDict):
|
|||||||
edges: list[AnilistCharactersEdge]
|
edges: list[AnilistCharactersEdge]
|
||||||
|
|
||||||
|
|
||||||
class AnilistMediaList_(TypedDict):
|
|
||||||
id: int
|
|
||||||
progress: int
|
|
||||||
|
|
||||||
|
|
||||||
AnilistMediaListStatus = Literal[
|
AnilistMediaListStatus = Literal[
|
||||||
"CURRENT", "PLANNING", "COMPLETED", "DROPPED", "PAUSED", "REPEATING"
|
"CURRENT", "PLANNING", "COMPLETED", "DROPPED", "PAUSED", "REPEATING"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class AnilistMediaList_(TypedDict):
|
||||||
|
id: int
|
||||||
|
progress: int
|
||||||
|
status: AnilistMediaListStatus
|
||||||
|
|
||||||
|
|
||||||
class AnilistMediaListProperties(TypedDict):
|
class AnilistMediaListProperties(TypedDict):
|
||||||
status: AnilistMediaListStatus
|
status: AnilistMediaListStatus
|
||||||
score: float
|
score: float
|
||||||
@@ -136,6 +152,11 @@ class AnilistMediaListProperties(TypedDict):
|
|||||||
hiddenFromStatusLists: bool
|
hiddenFromStatusLists: bool
|
||||||
|
|
||||||
|
|
||||||
|
class StreamingEpisode(TypedDict):
|
||||||
|
title: str
|
||||||
|
thumbnail: str
|
||||||
|
|
||||||
|
|
||||||
class AnilistBaseMediaDataSchema(TypedDict):
|
class AnilistBaseMediaDataSchema(TypedDict):
|
||||||
"""
|
"""
|
||||||
This a convenience class is used to type the received Anilist data to enhance dev experience
|
This a convenience class is used to type the received Anilist data to enhance dev experience
|
||||||
@@ -159,6 +180,8 @@ class AnilistBaseMediaDataSchema(TypedDict):
|
|||||||
status: str
|
status: str
|
||||||
nextAiringEpisode: AnilistMediaNextAiringEpisode
|
nextAiringEpisode: AnilistMediaNextAiringEpisode
|
||||||
season: str
|
season: str
|
||||||
|
streamingEpisodes: list[StreamingEpisode]
|
||||||
|
chapters: int
|
||||||
seasonYear: int
|
seasonYear: int
|
||||||
duration: int
|
duration: int
|
||||||
synonyms: list[str]
|
synonyms: list[str]
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
from .allanime import SERVERS_AVAILABLE as ALLANIME_SERVERS
|
from .allanime.constants import SERVERS_AVAILABLE as ALLANIME_SERVERS
|
||||||
from .animepahe import SERVERS_AVAILABLE as ANIMEPAHESERVERS
|
from .animepahe.constants import SERVERS_AVAILABLE as ANIMEPAHESERVERS
|
||||||
from .aniwatch import SERVERS_AVAILABLE as ANIWATCHSERVERS
|
from .aniwatch.constants import SERVERS_AVAILABLE as ANIWATCHSERVERS
|
||||||
|
|
||||||
anime_sources = {
|
anime_sources = {
|
||||||
"allanime": "api.AllAnimeAPI",
|
"allanime": "api.AllAnimeAPI",
|
||||||
"animepahe": "api.AnimePaheApi",
|
"animepahe": "api.AnimePaheApi",
|
||||||
"aniwatch": "api.AniWatchApi",
|
"aniwatch": "api.AniWatchApi",
|
||||||
|
"aniwave": "api.AniWaveApi",
|
||||||
}
|
}
|
||||||
SERVERS_AVAILABLE = [*ALLANIME_SERVERS, *ANIMEPAHESERVERS, *ANIWATCHSERVERS]
|
SERVERS_AVAILABLE = [*ALLANIME_SERVERS, *ANIMEPAHESERVERS, *ANIWATCHSERVERS]
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
SERVERS_AVAILABLE = ["sharepoint", "dropbox", "gogoanime", "weTransfer", "wixmp", "Yt"]
|
|
||||||
|
|||||||
@@ -11,19 +11,11 @@ from requests.exceptions import Timeout
|
|||||||
|
|
||||||
from ...anime_provider.base_provider import AnimeProvider
|
from ...anime_provider.base_provider import AnimeProvider
|
||||||
from ..utils import give_random_quality, one_digit_symmetric_xor
|
from ..utils import give_random_quality, one_digit_symmetric_xor
|
||||||
from .constants import (
|
from .constants import ALLANIME_API_ENDPOINT, ALLANIME_BASE, ALLANIME_REFERER
|
||||||
ALLANIME_API_ENDPOINT,
|
|
||||||
ALLANIME_BASE,
|
|
||||||
ALLANIME_REFERER,
|
|
||||||
USER_AGENT,
|
|
||||||
)
|
|
||||||
from .gql_queries import ALLANIME_EPISODES_GQL, ALLANIME_SEARCH_GQL, ALLANIME_SHOW_GQL
|
from .gql_queries import ALLANIME_EPISODES_GQL, ALLANIME_SEARCH_GQL, ALLANIME_SHOW_GQL
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from typing import Iterator
|
from .types import AllAnimeEpisode
|
||||||
|
|
||||||
from ....libs.anime_provider.allanime.types import AllAnimeEpisode
|
|
||||||
from ....libs.anime_provider.types import Anime, Server
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@@ -36,6 +28,9 @@ class AllAnimeAPI(AnimeProvider):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
api_endpoint = ALLANIME_API_ENDPOINT
|
api_endpoint = ALLANIME_API_ENDPOINT
|
||||||
|
HEADERS = {
|
||||||
|
"Referer": ALLANIME_REFERER,
|
||||||
|
}
|
||||||
|
|
||||||
def _fetch_gql(self, query: str, variables: dict):
|
def _fetch_gql(self, query: str, variables: dict):
|
||||||
"""main abstraction over all requests to the allanime api
|
"""main abstraction over all requests to the allanime api
|
||||||
@@ -54,21 +49,20 @@ class AllAnimeAPI(AnimeProvider):
|
|||||||
"variables": json.dumps(variables),
|
"variables": json.dumps(variables),
|
||||||
"query": query,
|
"query": query,
|
||||||
},
|
},
|
||||||
headers={"Referer": ALLANIME_REFERER, "User-Agent": USER_AGENT},
|
|
||||||
timeout=10,
|
timeout=10,
|
||||||
)
|
)
|
||||||
if response.status_code == 200:
|
if response.ok:
|
||||||
return response.json()["data"]
|
return response.json()["data"]
|
||||||
else:
|
else:
|
||||||
logger.error("allanime(ERROR): ", response.text)
|
logger.error("[ALLANIME-ERROR]: ", response.text)
|
||||||
return {}
|
return {}
|
||||||
except Timeout:
|
except Timeout:
|
||||||
logger.error(
|
logger.error(
|
||||||
"allanime(Error):Timeout exceeded this could mean allanime is down or you have lost internet connection"
|
"[ALLANIME-ERROR]: Timeout exceeded this could mean allanime is down or you have lost internet connection"
|
||||||
)
|
)
|
||||||
return {}
|
return {}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"allanime:Error: {e}")
|
logger.error(f"[ALLANIME-ERROR]: {e}")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def search_for_anime(
|
def search_for_anime(
|
||||||
@@ -123,7 +117,7 @@ class AllAnimeAPI(AnimeProvider):
|
|||||||
return normalized_search_results
|
return normalized_search_results
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"FA(AllAnime): {e}")
|
logger.error(f"[ALLANIME-ERROR]: {e}")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def get_anime(self, allanime_show_id: str):
|
def get_anime(self, allanime_show_id: str):
|
||||||
@@ -150,8 +144,8 @@ class AllAnimeAPI(AnimeProvider):
|
|||||||
}
|
}
|
||||||
return normalized_anime
|
return normalized_anime
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"AllAnime(get_anime): {e}")
|
logger.error(f"[ALLANIME-ERROR]: {e}")
|
||||||
return None
|
return {}
|
||||||
|
|
||||||
def _get_anime_episode(
|
def _get_anime_episode(
|
||||||
self, allanime_show_id: str, episode_string: str, translation_type: str = "sub"
|
self, allanime_show_id: str, episode_string: str, translation_type: str = "sub"
|
||||||
@@ -175,12 +169,10 @@ class AllAnimeAPI(AnimeProvider):
|
|||||||
episode = self._fetch_gql(ALLANIME_EPISODES_GQL, variables)
|
episode = self._fetch_gql(ALLANIME_EPISODES_GQL, variables)
|
||||||
return episode["episode"]
|
return episode["episode"]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"FA(AllAnime): {e}")
|
logger.error(f"[ALLANIME-ERROR]: {e}")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def get_episode_streams(
|
def get_episode_streams(self, anime, episode_number: str, translation_type="sub"):
|
||||||
self, anime: "Anime", episode_number: str, translation_type="sub"
|
|
||||||
) -> "Iterator[Server] | None":
|
|
||||||
"""get the streams of an episode
|
"""get the streams of an episode
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -238,7 +230,7 @@ class AllAnimeAPI(AnimeProvider):
|
|||||||
"quality": "1080",
|
"quality": "1080",
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
} # pyright:ignore
|
}
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# get the stream url for an episode of the defined source names
|
# get the stream url for an episode of the defined source names
|
||||||
@@ -247,14 +239,10 @@ class AllAnimeAPI(AnimeProvider):
|
|||||||
)
|
)
|
||||||
resp = self.session.get(
|
resp = self.session.get(
|
||||||
embed_url,
|
embed_url,
|
||||||
headers={
|
|
||||||
"Referer": ALLANIME_REFERER,
|
|
||||||
"User-Agent": USER_AGENT,
|
|
||||||
},
|
|
||||||
timeout=10,
|
timeout=10,
|
||||||
)
|
)
|
||||||
|
|
||||||
if resp.status_code == 200:
|
if resp.ok:
|
||||||
match embed["sourceName"]:
|
match embed["sourceName"]:
|
||||||
case "Luf-mp4":
|
case "Luf-mp4":
|
||||||
logger.debug("allanime:Found streams from gogoanime")
|
logger.debug("allanime:Found streams from gogoanime")
|
||||||
@@ -267,7 +255,7 @@ class AllAnimeAPI(AnimeProvider):
|
|||||||
)
|
)
|
||||||
+ f"; Episode {episode_number}",
|
+ f"; Episode {episode_number}",
|
||||||
"links": give_random_quality(resp.json()["links"]),
|
"links": give_random_quality(resp.json()["links"]),
|
||||||
} # pyright:ignore
|
}
|
||||||
case "Kir":
|
case "Kir":
|
||||||
logger.debug("allanime:Found streams from wetransfer")
|
logger.debug("allanime:Found streams from wetransfer")
|
||||||
yield {
|
yield {
|
||||||
@@ -279,7 +267,7 @@ class AllAnimeAPI(AnimeProvider):
|
|||||||
)
|
)
|
||||||
+ f"; Episode {episode_number}",
|
+ f"; Episode {episode_number}",
|
||||||
"links": give_random_quality(resp.json()["links"]),
|
"links": give_random_quality(resp.json()["links"]),
|
||||||
} # pyright:ignore
|
}
|
||||||
case "S-mp4":
|
case "S-mp4":
|
||||||
logger.debug("allanime:Found streams from sharepoint")
|
logger.debug("allanime:Found streams from sharepoint")
|
||||||
yield {
|
yield {
|
||||||
@@ -291,7 +279,7 @@ class AllAnimeAPI(AnimeProvider):
|
|||||||
)
|
)
|
||||||
+ f"; Episode {episode_number}",
|
+ f"; Episode {episode_number}",
|
||||||
"links": give_random_quality(resp.json()["links"]),
|
"links": give_random_quality(resp.json()["links"]),
|
||||||
} # pyright:ignore
|
}
|
||||||
case "Sak":
|
case "Sak":
|
||||||
logger.debug("allanime:Found streams from dropbox")
|
logger.debug("allanime:Found streams from dropbox")
|
||||||
yield {
|
yield {
|
||||||
@@ -303,7 +291,7 @@ class AllAnimeAPI(AnimeProvider):
|
|||||||
)
|
)
|
||||||
+ f"; Episode {episode_number}",
|
+ f"; Episode {episode_number}",
|
||||||
"links": give_random_quality(resp.json()["links"]),
|
"links": give_random_quality(resp.json()["links"]),
|
||||||
} # pyright:ignore
|
}
|
||||||
case "Default":
|
case "Default":
|
||||||
logger.debug("allanime:Found streams from wixmp")
|
logger.debug("allanime:Found streams from wixmp")
|
||||||
yield {
|
yield {
|
||||||
@@ -315,98 +303,13 @@ class AllAnimeAPI(AnimeProvider):
|
|||||||
)
|
)
|
||||||
+ f"; Episode {episode_number}",
|
+ f"; Episode {episode_number}",
|
||||||
"links": give_random_quality(resp.json()["links"]),
|
"links": give_random_quality(resp.json()["links"]),
|
||||||
} # pyright:ignore
|
}
|
||||||
|
|
||||||
except Timeout:
|
except Timeout:
|
||||||
logger.error(
|
logger.error(
|
||||||
"Timeout has been exceeded this could mean allanime is down or you have lost internet connection"
|
"[ALLANIME-ERROR]: Timeout has been exceeded this could mean allanime is down or you have lost internet connection"
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"FA(Allanime): {e}")
|
logger.error(f"[ALLANIME-ERROR]: {e}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"FA(Allanime): {e}")
|
logger.error(f"[ALLANIME-ERROR]: {e}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
anime_provider = AllAnimeAPI()
|
|
||||||
# lets see if it works :)
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from InquirerPy import inquirer, validator # pyright:ignore
|
|
||||||
|
|
||||||
anime = input("Enter the anime name: ")
|
|
||||||
translation = input("Enter the translation type: ")
|
|
||||||
|
|
||||||
search_results = anime_provider.search_for_anime(
|
|
||||||
anime, translation_type=translation.strip()
|
|
||||||
)
|
|
||||||
|
|
||||||
if not search_results:
|
|
||||||
raise Exception("No results found")
|
|
||||||
|
|
||||||
search_results = search_results["results"]
|
|
||||||
options = {show["title"]: show for show in search_results}
|
|
||||||
anime = inquirer.fuzzy(
|
|
||||||
"Enter the anime title",
|
|
||||||
list(options.keys()),
|
|
||||||
validate=validator.EmptyInputValidator(),
|
|
||||||
).execute()
|
|
||||||
if anime is None:
|
|
||||||
print("No anime was selected")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
anime_result = options[anime]
|
|
||||||
anime_data = anime_provider.get_anime(anime_result["id"])
|
|
||||||
if not anime_data:
|
|
||||||
raise Exception("Anime not found")
|
|
||||||
availableEpisodesDetail = anime_data["availableEpisodesDetail"]
|
|
||||||
if not availableEpisodesDetail.get(translation.strip()):
|
|
||||||
raise Exception("No episodes found")
|
|
||||||
|
|
||||||
stream_link = True
|
|
||||||
while stream_link != "quit":
|
|
||||||
print("select episode")
|
|
||||||
episode = inquirer.fuzzy(
|
|
||||||
"Choose an episode",
|
|
||||||
availableEpisodesDetail[translation.strip()],
|
|
||||||
validate=validator.EmptyInputValidator(),
|
|
||||||
).execute()
|
|
||||||
if episode is None:
|
|
||||||
print("No episode was selected")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if not anime_data:
|
|
||||||
print("Sth went wrong")
|
|
||||||
break
|
|
||||||
episode_streams_ = anime_provider.get_episode_streams(
|
|
||||||
anime_data, # pyright: ignore
|
|
||||||
episode,
|
|
||||||
translation.strip(),
|
|
||||||
)
|
|
||||||
if episode_streams_ is None:
|
|
||||||
raise Exception("Episode not found")
|
|
||||||
|
|
||||||
episode_streams = list(episode_streams_)
|
|
||||||
stream_links = []
|
|
||||||
for server in episode_streams:
|
|
||||||
stream_links.extend([link["link"] for link in server["links"]])
|
|
||||||
stream_links.append("back")
|
|
||||||
stream_link = inquirer.fuzzy(
|
|
||||||
"Choose a link to stream",
|
|
||||||
stream_links,
|
|
||||||
validate=validator.EmptyInputValidator(),
|
|
||||||
).execute()
|
|
||||||
if stream_link == "quit":
|
|
||||||
print("Have a nice day")
|
|
||||||
sys.exit()
|
|
||||||
if not stream_link:
|
|
||||||
raise Exception("No stream was selected")
|
|
||||||
|
|
||||||
title = episode_streams[0].get(
|
|
||||||
"episode_title", "%s: Episode %s" % (anime_data["title"], episode)
|
|
||||||
)
|
|
||||||
subprocess.run(["mpv", f"--title={title}", stream_link])
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
from yt_dlp.utils.networking import random_user_agent
|
SERVERS_AVAILABLE = ["sharepoint", "dropbox", "gogoanime", "weTransfer", "wixmp", "Yt"]
|
||||||
|
|
||||||
ALLANIME_BASE = "allanime.day"
|
ALLANIME_BASE = "allanime.day"
|
||||||
ALLANIME_REFERER = "https://allanime.to/"
|
ALLANIME_REFERER = "https://allanime.to/"
|
||||||
ALLANIME_API_ENDPOINT = "https://api.{}/api/".format(ALLANIME_BASE)
|
ALLANIME_API_ENDPOINT = "https://api.{}/api/".format(ALLANIME_BASE)
|
||||||
USER_AGENT = random_user_agent()
|
|
||||||
|
|||||||
@@ -1,56 +1,56 @@
|
|||||||
ALLANIME_SEARCH_GQL = """
|
ALLANIME_SEARCH_GQL = """
|
||||||
query(
|
query (
|
||||||
$search: SearchInput
|
$search: SearchInput
|
||||||
$limit: Int
|
$limit: Int
|
||||||
$page: Int
|
$page: Int
|
||||||
$translationType: VaildTranslationTypeEnumType
|
$translationType: VaildTranslationTypeEnumType
|
||||||
$countryOrigin: VaildCountryOriginEnumType
|
$countryOrigin: VaildCountryOriginEnumType
|
||||||
) {
|
) {
|
||||||
shows(
|
shows(
|
||||||
search: $search
|
search: $search
|
||||||
limit: $limit
|
limit: $limit
|
||||||
page: $page
|
page: $page
|
||||||
translationType: $translationType
|
translationType: $translationType
|
||||||
countryOrigin: $countryOrigin
|
countryOrigin: $countryOrigin
|
||||||
) {
|
) {
|
||||||
pageInfo {
|
pageInfo {
|
||||||
total
|
total
|
||||||
}
|
|
||||||
edges {
|
|
||||||
_id
|
|
||||||
name
|
|
||||||
availableEpisodes
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
edges {
|
||||||
|
_id
|
||||||
|
name
|
||||||
|
availableEpisodes
|
||||||
|
__typename
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
ALLANIME_EPISODES_GQL = """\
|
ALLANIME_EPISODES_GQL = """\
|
||||||
query ($showId: String!, $translationType: VaildTranslationTypeEnumType!, $episodeString: String!) {
|
query (
|
||||||
episode(
|
$showId: String!
|
||||||
showId: $showId
|
$translationType: VaildTranslationTypeEnumType!
|
||||||
translationType: $translationType
|
$episodeString: String!
|
||||||
episodeString: $episodeString
|
) {
|
||||||
) {
|
episode(
|
||||||
|
showId: $showId
|
||||||
episodeString
|
translationType: $translationType
|
||||||
sourceUrls
|
episodeString: $episodeString
|
||||||
notes
|
) {
|
||||||
}
|
episodeString
|
||||||
}"""
|
sourceUrls
|
||||||
|
notes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
ALLANIME_SHOW_GQL = """
|
ALLANIME_SHOW_GQL = """
|
||||||
query ($showId: String!) {
|
query ($showId: String!) {
|
||||||
show(
|
show(_id: $showId) {
|
||||||
_id: $showId
|
_id
|
||||||
) {
|
name
|
||||||
|
availableEpisodesDetail
|
||||||
_id
|
}
|
||||||
name
|
|
||||||
availableEpisodesDetail
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
SERVERS_AVAILABLE = ["kwik"]
|
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ from .constants import (
|
|||||||
from .utils import process_animepahe_embed_page
|
from .utils import process_animepahe_embed_page
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ..types import Anime
|
|
||||||
from .types import AnimePaheAnimePage, AnimePaheSearchPage, AnimeSearchResult
|
from .types import AnimePaheAnimePage, AnimePaheSearchPage, AnimeSearchResult
|
||||||
JUICY_STREAM_REGEX = re.compile(r"source='(.*)';")
|
JUICY_STREAM_REGEX = re.compile(r"source='(.*)';")
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -32,13 +31,15 @@ KWIK_RE = re.compile(r"Player\|(.+?)'")
|
|||||||
class AnimePaheApi(AnimeProvider):
|
class AnimePaheApi(AnimeProvider):
|
||||||
search_page: "AnimePaheSearchPage"
|
search_page: "AnimePaheSearchPage"
|
||||||
anime: "AnimePaheAnimePage"
|
anime: "AnimePaheAnimePage"
|
||||||
|
HEADERS = REQUEST_HEADERS
|
||||||
|
|
||||||
def search_for_anime(self, user_query: str, *args):
|
def search_for_anime(self, user_query: str, *args):
|
||||||
try:
|
try:
|
||||||
url = f"{ANIMEPAHE_ENDPOINT}m=search&q={user_query}"
|
url = f"{ANIMEPAHE_ENDPOINT}m=search&q={user_query}"
|
||||||
headers = {**REQUEST_HEADERS}
|
response = self.session.get(
|
||||||
response = self.session.get(url, headers=headers)
|
url,
|
||||||
if not response.status_code == 200:
|
)
|
||||||
|
if not response.ok:
|
||||||
return
|
return
|
||||||
data: "AnimePaheSearchPage" = response.json()
|
data: "AnimePaheSearchPage" = response.json()
|
||||||
self.search_page = data
|
self.search_page = data
|
||||||
@@ -66,7 +67,7 @@ class AnimePaheApi(AnimeProvider):
|
|||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"AnimePahe(search): {e}")
|
logger.error(f"[ANIMEPAHE-ERROR]: {e}")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def get_anime(self, session_id: str, *args):
|
def get_anime(self, session_id: str, *args):
|
||||||
@@ -85,8 +86,10 @@ class AnimePaheApi(AnimeProvider):
|
|||||||
url,
|
url,
|
||||||
page,
|
page,
|
||||||
):
|
):
|
||||||
response = self.session.get(url, headers=REQUEST_HEADERS)
|
response = self.session.get(
|
||||||
if response.status_code == 200:
|
url,
|
||||||
|
)
|
||||||
|
if response.ok:
|
||||||
if not data:
|
if not data:
|
||||||
data.update(response.json())
|
data.update(response.json())
|
||||||
else:
|
else:
|
||||||
@@ -147,12 +150,10 @@ class AnimePaheApi(AnimeProvider):
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"AnimePahe(anime): {e}")
|
logger.error(f"[ANIMEPAHE-ERROR]: {e}")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def get_episode_streams(
|
def get_episode_streams(self, anime, episode_number: str, translation_type, *args):
|
||||||
self, anime: "Anime", episode_number: str, translation_type, *args
|
|
||||||
):
|
|
||||||
try:
|
try:
|
||||||
# extract episode details from memory
|
# extract episode details from memory
|
||||||
episode = [
|
episode = [
|
||||||
@@ -163,7 +164,7 @@ class AnimePaheApi(AnimeProvider):
|
|||||||
|
|
||||||
if not episode:
|
if not episode:
|
||||||
logger.error(
|
logger.error(
|
||||||
f"AnimePahe(streams): episode {episode_number} doesn't exist"
|
f"[ANIMEPAHE-ERROR]: episode {episode_number} doesn't exist"
|
||||||
)
|
)
|
||||||
return []
|
return []
|
||||||
episode = episode[0]
|
episode = episode[0]
|
||||||
@@ -171,7 +172,7 @@ class AnimePaheApi(AnimeProvider):
|
|||||||
anime_id = anime["id"]
|
anime_id = anime["id"]
|
||||||
# fetch the episode page
|
# fetch the episode page
|
||||||
url = f"{ANIMEPAHE_BASE}/play/{anime_id}/{episode['session']}"
|
url = f"{ANIMEPAHE_BASE}/play/{anime_id}/{episode['session']}"
|
||||||
response = self.session.get(url, headers=REQUEST_HEADERS)
|
response = self.session.get(url)
|
||||||
# get the element containing links to juicy streams
|
# get the element containing links to juicy streams
|
||||||
c = get_element_by_id("resolutionMenu", response.text)
|
c = get_element_by_id("resolutionMenu", response.text)
|
||||||
resolutionMenuItems = get_elements_html_by_class("dropdown-item", c)
|
resolutionMenuItems = get_elements_html_by_class("dropdown-item", c)
|
||||||
@@ -203,20 +204,24 @@ class AnimePaheApi(AnimeProvider):
|
|||||||
|
|
||||||
if not embed_url:
|
if not embed_url:
|
||||||
logger.warn(
|
logger.warn(
|
||||||
"AnimePahe: embed url not found please report to the developers"
|
"[ANIMEPAHE-WARN]: embed url not found please report to the developers"
|
||||||
)
|
)
|
||||||
return []
|
return []
|
||||||
# get embed page
|
# get embed page
|
||||||
embed_response = self.session.get(embed_url, headers=SERVER_HEADERS)
|
embed_response = self.session.get(
|
||||||
|
embed_url, headers={"User-Agent": self.USER_AGENT, **SERVER_HEADERS}
|
||||||
|
)
|
||||||
|
if not response.ok:
|
||||||
|
continue
|
||||||
embed_page = embed_response.text
|
embed_page = embed_response.text
|
||||||
|
|
||||||
decoded_js = process_animepahe_embed_page(embed_page)
|
decoded_js = process_animepahe_embed_page(embed_page)
|
||||||
if not decoded_js:
|
if not decoded_js:
|
||||||
logger.error("Animepahe: failed to decode embed page")
|
logger.error("[ANIMEPAHE-ERROR]: failed to decode embed page")
|
||||||
return
|
return
|
||||||
juicy_stream = JUICY_STREAM_REGEX.search(decoded_js)
|
juicy_stream = JUICY_STREAM_REGEX.search(decoded_js)
|
||||||
if not juicy_stream:
|
if not juicy_stream:
|
||||||
logger.error("Animepahe: failed to find juicy stream")
|
logger.error("[ANIMEPAHE-ERROR]: failed to find juicy stream")
|
||||||
return
|
return
|
||||||
juicy_stream = juicy_stream.group(1)
|
juicy_stream = juicy_stream.group(1)
|
||||||
# add the link
|
# add the link
|
||||||
@@ -229,4 +234,4 @@ class AnimePaheApi(AnimeProvider):
|
|||||||
)
|
)
|
||||||
yield streams
|
yield streams
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Animepahe: {e}")
|
logger.error(f"[ANIMEPAHE-ERROR]: {e}")
|
||||||
|
|||||||
@@ -1,18 +1,14 @@
|
|||||||
from yt_dlp.utils.networking import random_user_agent
|
|
||||||
|
|
||||||
USER_AGENT = random_user_agent()
|
|
||||||
ANIMEPAHE = "animepahe.ru"
|
ANIMEPAHE = "animepahe.ru"
|
||||||
ANIMEPAHE_BASE = f"https://{ANIMEPAHE}"
|
ANIMEPAHE_BASE = f"https://{ANIMEPAHE}"
|
||||||
ANIMEPAHE_ENDPOINT = f"{ANIMEPAHE_BASE}/api?"
|
ANIMEPAHE_ENDPOINT = f"{ANIMEPAHE_BASE}/api?"
|
||||||
|
|
||||||
|
SERVERS_AVAILABLE = ["kwik"]
|
||||||
REQUEST_HEADERS = {
|
REQUEST_HEADERS = {
|
||||||
"Cookie": "__ddgid_=VvX0ebHrH2DsFZo4; __ddgmark_=3savRpSVFhvZcn5x; __ddg2_=buBJ3c4pNBYKFZNp; __ddg1_=rbVADKr9URtt55zoIGFa; SERVERID=janna; XSRF-TOKEN=eyJpdiI6IjV5bFNtd0phUHgvWGJxc25wL0VJSUE9PSIsInZhbHVlIjoicEJTZktlR2hxR2JZTWhnL0JzazlvZU5TQTR2bjBWZ2dDb0RwUXVUUWNSclhQWUhLRStYSmJmWmUxWkpiYkFRYU12RjFWejlSWHorME1wZG5qQ1U0TnFlNnBFR2laQjN1MjdyNjc5TjVPdXdJb2o5VkU1bEduRW9pRHNDTHh6Sy8iLCJtYWMiOiI0OTc0ZmNjY2UwMGJkOWY2MWNkM2NlMjk2ZGMyZGJmMWE0NTdjZTdkNGI2Y2IwNTIzZmFiZWU5ZTE2OTk0YmU4IiwidGFnIjoiIn0%3D; laravel_session=eyJpdiI6ImxvdlpqREFnTjdaeFJubUlXQWlJVWc9PSIsInZhbHVlIjoiQnE4R3VHdjZ4M1NDdEVWM1ZqMUxtNnVERnJCcmtCUHZKNzRPR2RFbzNFcStTL29xdnVTbWhsNVRBUXEybVZWNU1UYVlTazFqYlN5UjJva1k4czNGaXBTbkJJK01oTUd3VHRYVHBoc3dGUWxHYnFlS2NJVVNFbTFqMVBWdFpuVUgiLCJtYWMiOiI1NDdjZTVkYmNhNjUwZTMxZmRlZmVmMmRlMGNiYjAwYjlmYjFjY2U0MDc1YTQzZThiMTIxMjJlYTg1NTA4YjBmIiwidGFnIjoiIn0%3D; latest=5592 ",
|
"Cookie": "__ddgid_=VvX0ebHrH2DsFZo4; __ddgmark_=3savRpSVFhvZcn5x; __ddg2_=buBJ3c4pNBYKFZNp; __ddg1_=rbVADKr9URtt55zoIGFa; SERVERID=janna; XSRF-TOKEN=eyJpdiI6IjV5bFNtd0phUHgvWGJxc25wL0VJSUE9PSIsInZhbHVlIjoicEJTZktlR2hxR2JZTWhnL0JzazlvZU5TQTR2bjBWZ2dDb0RwUXVUUWNSclhQWUhLRStYSmJmWmUxWkpiYkFRYU12RjFWejlSWHorME1wZG5qQ1U0TnFlNnBFR2laQjN1MjdyNjc5TjVPdXdJb2o5VkU1bEduRW9pRHNDTHh6Sy8iLCJtYWMiOiI0OTc0ZmNjY2UwMGJkOWY2MWNkM2NlMjk2ZGMyZGJmMWE0NTdjZTdkNGI2Y2IwNTIzZmFiZWU5ZTE2OTk0YmU4IiwidGFnIjoiIn0%3D; laravel_session=eyJpdiI6ImxvdlpqREFnTjdaeFJubUlXQWlJVWc9PSIsInZhbHVlIjoiQnE4R3VHdjZ4M1NDdEVWM1ZqMUxtNnVERnJCcmtCUHZKNzRPR2RFbzNFcStTL29xdnVTbWhsNVRBUXEybVZWNU1UYVlTazFqYlN5UjJva1k4czNGaXBTbkJJK01oTUd3VHRYVHBoc3dGUWxHYnFlS2NJVVNFbTFqMVBWdFpuVUgiLCJtYWMiOiI1NDdjZTVkYmNhNjUwZTMxZmRlZmVmMmRlMGNiYjAwYjlmYjFjY2U0MDc1YTQzZThiMTIxMjJlYTg1NTA4YjBmIiwidGFnIjoiIn0%3D; latest=5592 ",
|
||||||
"Host": ANIMEPAHE,
|
"Host": ANIMEPAHE,
|
||||||
"User-Agent": USER_AGENT,
|
|
||||||
"Accept": "application , text/javascript, */*; q=0.01",
|
"Accept": "application , text/javascript, */*; q=0.01",
|
||||||
"Accept-Encoding": "gzip, deflate, br, zstd",
|
"Accept-Encoding": "Utf-8",
|
||||||
"Referer": ANIMEPAHE_BASE,
|
"Referer": ANIMEPAHE_BASE,
|
||||||
"X-Requested-With": "XMLHttpRequest",
|
|
||||||
"DNT": "1",
|
"DNT": "1",
|
||||||
"Connection": "keep-alive",
|
"Connection": "keep-alive",
|
||||||
"Sec-Fetch-Dest": "empty",
|
"Sec-Fetch-Dest": "empty",
|
||||||
@@ -21,19 +17,17 @@ REQUEST_HEADERS = {
|
|||||||
"TE": "trailers",
|
"TE": "trailers",
|
||||||
}
|
}
|
||||||
SERVER_HEADERS = {
|
SERVER_HEADERS = {
|
||||||
"User-Agent": USER_AGENT,
|
"Host": "kwik.si",
|
||||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/png,image/svg+xml,*/*;q=0.8",
|
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/png,image/svg+xml,*/*;q=0.8",
|
||||||
"Accept-Language": "en-US,en;q=0.5",
|
"Accept-Language": "en-US,en;q=0.5",
|
||||||
"Accept-Encoding": "gzip, deflate, br, zstd",
|
"Accept-Encoding": "Utf-8",
|
||||||
"DNT": "1",
|
"DNT": "1",
|
||||||
"Alt-Used": "kwik.si",
|
|
||||||
"Connection": "keep-alive",
|
"Connection": "keep-alive",
|
||||||
"Referer": ANIMEPAHE_BASE,
|
"Referer": "https://animepahe.ru/",
|
||||||
"Cookie": "kwik_session=eyJpdiI6IlZ5UDd0c0lKTDB1NXlhTHZPeWxFc2c9PSIsInZhbHVlIjoieDJZbGhZUG1QZDNaeWtqR3lwWFNnREdhaHBxNVZRMWNDOHVucGpiMHRJOVdhVmpBc3lpTko1VExRMTFWcE1yUVJtVitoTWdOOU5ObTQ0Q0dHU0MzZU0yRUVvNmtWcUdmY3R4UWx4YklJTmpUL0ZodjhtVEpjWU96cEZoUUhUbVYiLCJtYWMiOiI2OGY2YThkOGU0MTgwOThmYzcyZThmNzFlZjlhMzQzMDgwNjlmMTc4NTIzMzc2YjE3YjNmMWQyNTk4NzczMmZiIiwidGFnIjoiIn0%3D; srv=s0; cf_clearance=QMoZtUpZrX0Mh4XJiFmFSSmoWndISPne5FcsGmKKvTQ-1723297585-1.0.1.1-6tVUnP.aef9XeNj0CnN.19D1el_r53t.lhqddX.J88gohH9UnsPWKeJ4yT0pTbcaGRbPuXTLOS.U72.wdy.gMg",
|
|
||||||
"Upgrade-Insecure-Requests": "1",
|
"Upgrade-Insecure-Requests": "1",
|
||||||
"Sec-Fetch-Dest": "iframe",
|
"Sec-Fetch-Dest": "iframe",
|
||||||
"Sec-Fetch-Mode": "navigate",
|
"Sec-Fetch-Mode": "navigate",
|
||||||
"Sec-Fetch-Site": "cross-site",
|
"Sec-Fetch-Site": "cross-site",
|
||||||
"Sec-Fetch-User": "?1",
|
|
||||||
"Priority": "u=4",
|
"Priority": "u=4",
|
||||||
|
"TE": "trailers",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,12 +21,12 @@ def animepahe_embed_decoder(
|
|||||||
encoded_js_p: str,
|
encoded_js_p: str,
|
||||||
base_a: int,
|
base_a: int,
|
||||||
no_of_keys_c: int,
|
no_of_keys_c: int,
|
||||||
key_values_k: list,
|
values_to_replace_with_k: list,
|
||||||
decode_mapper_d: dict = {},
|
|
||||||
):
|
):
|
||||||
|
decode_mapper_d: dict = {}
|
||||||
for i in range(no_of_keys_c):
|
for i in range(no_of_keys_c):
|
||||||
key = animepahe_key_creator(i, base_a)
|
key = animepahe_key_creator(i, base_a)
|
||||||
val = key_values_k[i] or key
|
val = values_to_replace_with_k[i] or key
|
||||||
decode_mapper_d[key] = val
|
decode_mapper_d[key] = val
|
||||||
return re.sub(
|
return re.sub(
|
||||||
r"\b\w+\b", lambda match: decode_mapper_d[match.group(0)], encoded_js_p
|
r"\b\w+\b", lambda match: decode_mapper_d[match.group(0)], encoded_js_p
|
||||||
@@ -64,18 +64,12 @@ def process_animepahe_embed_page(embed_page: str):
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
data = """<script>eval(function(p,a,c,k,e,d){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--){d[e(c)]=k[c]||e(c)}k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--){if(k[c]){p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c])}}return p}('f $7={H:a(2){4 B(9.7.h(y z("(?:(?:^|.*;)\\\\s*"+d(2).h(/[\\-\\.\\+\\*]/g,"\\\\$&")+"\\\\s*\\\\=\\\\s*([^;]*).*$)|^.*$"),"$1"))||G},E:a(2,q,3,6,5,t){k(!2||/^(?:8|r\\-v|o|m|p)$/i.D(2)){4 w}f b="";k(3){F(3.J){j K:b=3===P?"; 8=O, I N Q M:u:u A":"; r-v="+3;n;j L:b="; 8="+3;n;j S:b="; 8="+3.Z();n}}9.7=d(2)+"="+d(q)+b+(5?"; m="+5:"")+(6?"; o="+6:"")+(t?"; p":"");4 x},Y:a(2,6,5){k(!2||!11.C(2)){4 w}9.7=d(2)+"=; 8=12, R 10 W l:l:l A"+(5?"; m="+5:"")+(6?"; o="+6:"");4 x},C:a(2){4(y z("(?:^|;\\\\s*)"+d(2).h(/[\\-\\.\\+\\*]/g,"\\\\$&")+"\\\\s*\\\\=")).D(9.7)},X:a(){f c=9.7.h(/((?:^|\\s*;)[^\\=]+)(?=;|$)|^\\s*|\\s*(?:\\=[^;]*)?(?:\\1|$)/g,"").T(/\\s*(?:\\=[^;]*)?;\\s*/);U(f e=0;e<c.V;e++){c[e]=B(c[e])}4 c}};',62,65,'||sKey|vEnd|return|sDomain|sPath|cookie|expires|document|function|sExpires|aKeys|encodeURIComponent|nIdx|var||replace||case|if|00|domain|break|path|secure|sValue|max||bSecure|59|age|false|true|new|RegExp|GMT|decodeURIComponent|hasItem|test|setItem|switch|null|getItem|31|constructor|Number|String|23|Dec|Fri|Infinity|9999|01|Date|split|for|length|1970|keys|removeItem|toUTCString|Jan|this|Thu'.split('|'),0,{}));eval(function(p,a,c,k,e,d){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--){d[e(c)]=k[c]||e(c)}k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--){if(k[c]){p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c])}}return p}('h o=\'1D://1C-E.1B.1A.1z/1y/E/1x/1w/1v.1u\';h d=s.r(\'d\');h 0=B 1t(d,{\'1s\':{\'1r\':i},\'1q\':\'16:9\',\'D\':1,\'1p\':5,\'1o\':{\'1n\':\'1m\'},1l:[\'7-1k\',\'7\',\'1j\',\'1i-1h\',\'1g\',\'1f-1e\',\'1d\',\'D\',\'1c\',\'1b\',\'1a\',\'19\',\'C\',\'18\'],\'C\':{\'17\':i}});8(!A.15()){d.14=o}x{j z={13:12,11:10,Z:Y,X:i,W:i};h c=B A(z);c.V(o);c.U(d);g.c=c}0.3("T",6=>{g.S.R.Q("P")});0.O=1;k v(b,n,m){8(b.y){b.y(n,m,N)}x 8(b.w){b.w(\'3\'+n,m)}}j 4=k(l){g.M.L(l,\'*\')};v(g,\'l\',k(e){j a=e.a;8(a===\'7\')0.7();8(a===\'f\')0.f();8(a===\'u\')0.u()});0.3(\'t\',6=>{4(\'t\')});0.3(\'7\',6=>{4(\'7\')});0.3(\'f\',6=>{4(\'f\')});0.3(\'K\',6=>{4(0.q);s.r(\'.J-I\').H=G(0.q.F(2))});0.3(\'p\',6=>{4(\'p\')});',62,102,'player|||on|sendMessage||event|play|if||data|element|hls|video||pause|window|const|true|var|function|message|eventHandler|eventName|source|ended|currentTime|querySelector|document|ready|stop|bindEvent|attachEvent|else|addEventListener|config|Hls|new|fullscreen|volume|01|toFixed|String|innerHTML|timestamp|ss|timeupdate|postMessage|parent|false|speed|landscape|lock|orientation|screen|enterfullscreen|attachMedia|loadSource|lowLatencyMode|enableWorker|Infinity|backBufferLength|600|maxMaxBufferLength|180|maxBufferLength|src|isSupported||iosNative|capture|airplay|pip|settings|captions|mute|time|current|progress|forward|fast|rewind|large|controls|kwik|key|storage|seekTime|ratio|global|keyboard|Plyr|m3u8|uwu|b92a392054c041a3f9c6eecabeb0e127183f44e547828447b10bca8d77523e6f|03|stream|org|nextcdn|files|eu|https'.split('|'),0,{}))"""
|
# Testing time
|
||||||
a = 62
|
filepath = input("Enter file name: ")
|
||||||
c = 102
|
if filepath:
|
||||||
k = "player|||on|sendMessage||event|play|if||data|element|hls|video||pause|window|const|true|var|function|message|eventHandler|eventName|source|ended|currentTime|querySelector|document|ready|stop|bindEvent|attachEvent|else|addEventListener|config|Hls|new|fullscreen|volume|toFixed|String|innerHTML|timestamp|ss|timeupdate|postMessage|parent|false|speed|landscape|lock|orientation|screen|enterfullscreen|attachMedia|loadSource|lowLatencyMode|enableWorker|Infinity|backBufferLength|600|maxMaxBufferLength||180|maxBufferLength|src|isSupported||iosNative|capture|airplay|pip|settings|captions|mute|time|current|progress|forward|fast|rewind|large|controls|kwik|key|storage|seekTime|ratio|global|keyboard|Plyr|m3u8|uwu|cda74eaebce25a12f5e548f7c220bb5dc245700b0280bdb45ff98b2fe4803d2b|06|stream|org|nextcdn|files|eu|https".split(
|
with open(filepath, "r") as file:
|
||||||
"|"
|
data = file.read()
|
||||||
)
|
else:
|
||||||
|
data = """<script>eval(function(p,a,c,k,e,d){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--){d[e(c)]=k[c]||e(c)}k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--){if(k[c]){p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c])}}return p}('f $7={H:a(2){4 B(9.7.h(y z("(?:(?:^|.*;)\\\\s*"+d(2).h(/[\\-\\.\\+\\*]/g,"\\\\$&")+"\\\\s*\\\\=\\\\s*([^;]*).*$)|^.*$"),"$1"))||G},E:a(2,q,3,6,5,t){k(!2||/^(?:8|r\\-v|o|m|p)$/i.D(2)){4 w}f b="";k(3){F(3.J){j K:b=3===P?"; 8=O, I N Q M:u:u A":"; r-v="+3;n;j L:b="; 8="+3;n;j S:b="; 8="+3.Z();n}}9.7=d(2)+"="+d(q)+b+(5?"; m="+5:"")+(6?"; o="+6:"")+(t?"; p":"");4 x},Y:a(2,6,5){k(!2||!11.C(2)){4 w}9.7=d(2)+"=; 8=12, R 10 W l:l:l A"+(5?"; m="+5:"")+(6?"; o="+6:"");4 x},C:a(2){4(y z("(?:^|;\\\\s*)"+d(2).h(/[\\-\\.\\+\\*]/g,"\\\\$&")+"\\\\s*\\\\=")).D(9.7)},X:a(){f c=9.7.h(/((?:^|\\s*;)[^\\=]+)(?=;|$)|^\\s*|\\s*(?:\\=[^;]*)?(?:\\1|$)/g,"").T(/\\s*(?:\\=[^;]*)?;\\s*/);U(f e=0;e<c.V;e++){c[e]=B(c[e])}4 c}};',62,65,'||sKey|vEnd|return|sDomain|sPath|cookie|expires|document|function|sExpires|aKeys|encodeURIComponent|nIdx|var||replace||case|if|00|domain|break|path|secure|sValue|max||bSecure|59|age|false|true|new|RegExp|GMT|decodeURIComponent|hasItem|test|setItem|switch|null|getItem|31|constructor|Number|String|23|Dec|Fri|Infinity|9999|01|Date|split|for|length|1970|keys|removeItem|toUTCString|Jan|this|Thu'.split('|'),0,{}));eval(function(p,a,c,k,e,d){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--){d[e(c)]=k[c]||e(c)}k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--){if(k[c]){p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c])}}return p}('h o=\'1D://1C-E.1B.1A.1z/1y/E/1x/1w/1v.1u\';h d=s.r(\'d\');h 0=B 1t(d,{\'1s\':{\'1r\':i},\'1q\':\'16:9\',\'D\':1,\'1p\':5,\'1o\':{\'1n\':\'1m\'},1l:[\'7-1k\',\'7\',\'1j\',\'1i-1h\',\'1g\',\'1f-1e\',\'1d\',\'D\',\'1c\',\'1b\',\'1a\',\'19\',\'C\',\'18\'],\'C\':{\'17\':i}});8(!A.15()){d.14=o}x{j z={13:12,11:10,Z:Y,X:i,W:i};h c=B A(z);c.V(o);c.U(d);g.c=c}0.3("T",6=>{g.S.R.Q("P")});0.O=1;k v(b,n,m){8(b.y){b.y(n,m,N)}x 8(b.w){b.w(\'3\'+n,m)}}j 4=k(l){g.M.L(l,\'*\')};v(g,\'l\',k(e){j a=e.a;8(a===\'7\')0.7();8(a===\'f\')0.f();8(a===\'u\')0.u()});0.3(\'t\',6=>{4(\'t\')});0.3(\'7\',6=>{4(\'7\')});0.3(\'f\',6=>{4(\'f\')});0.3(\'K\',6=>{4(0.q);s.r(\'.J-I\').H=G(0.q.F(2))});0.3(\'p\',6=>{4(\'p\')});',62,102,'player|||on|sendMessage||event|play|if||data|element|hls|video||pause|window|const|true|var|function|message|eventHandler|eventName|source|ended|currentTime|querySelector|document|ready|stop|bindEvent|attachEvent|else|addEventListener|config|Hls|new|fullscreen|volume|01|toFixed|String|innerHTML|timestamp|ss|timeupdate|postMessage|parent|false|speed|landscape|lock|orientation|screen|enterfullscreen|attachMedia|loadSource|lowLatencyMode|enableWorker|Infinity|backBufferLength|600|maxMaxBufferLength|180|maxBufferLength|src|isSupported||iosNative|capture|airplay|pip|settings|captions|mute|time|current|progress|forward|fast|rewind|large|controls|kwik|key|storage|seekTime|ratio|global|keyboard|Plyr|m3u8|uwu|b92a392054c041a3f9c6eecabeb0e127183f44e547828447b10bca8d77523e6f|03|stream|org|nextcdn|files|eu|https'.split('|'),0,{}))</script>"""
|
||||||
|
|
||||||
p = "h o='1D://1C-11.1B.1A.1z/1y/11/1x/1w/1v.1u';h d=s.r('d');h 0=B 1t(d,{'1s':{'1r':i},'1q':'16:9','D':1,'1p':5,'1o':{'1n':'1m'},1l:['7-1k','7','1j','1i-1h','1g','1f-1e','1d','D','1c','1b','1a','19','C','18'],'C':{'17':i}});8(!A.15()){d.14=o}x{j z={13:12,10:Z,Y:X,W:i,V:i};h c=B A(z);c.U(o);c.T(d);g.c=c}0.3(\"S\",6=>{g.R.Q.P(\"O\")});0.N=1;k v(b,n,m){8(b.y){b.y(n,m,M)}x 8(b.w){b.w('3'+n,m)}}j 4=k(l){g.L.K(l,'*')};v(g,'l',k(e){j a=e.a;8(a==='7')0.7();8(a==='f')0.f();8(a==='u')0.u()});0.3('t',6=>{4('t')});0.3('7',6=>{4('7')});0.3('f',6=>{4('f')});0.3('J',6=>{4(0.q);s.r('.I-H').G=F(0.q.E(2))});0.3('p',6=>{4('p')});"
|
print(process_animepahe_embed_page(data))
|
||||||
result = animepahe_embed_decoder(
|
|
||||||
p,
|
|
||||||
a,
|
|
||||||
c,
|
|
||||||
k,
|
|
||||||
)
|
|
||||||
print(result) # Output: j player = B A();
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
SERVERS_AVAILABLE = ["HD1", "HD2", "StreamSB", "StreamTape"]
|
|
||||||
|
|||||||
@@ -1,42 +1,105 @@
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
from html.parser import HTMLParser
|
||||||
from itertools import cycle
|
from itertools import cycle
|
||||||
|
from urllib.parse import quote_plus
|
||||||
|
|
||||||
from yt_dlp.utils import (
|
from yt_dlp.utils import (
|
||||||
|
clean_html,
|
||||||
extract_attributes,
|
extract_attributes,
|
||||||
|
get_element_by_class,
|
||||||
get_element_html_by_class,
|
get_element_html_by_class,
|
||||||
|
get_elements_by_class,
|
||||||
get_elements_html_by_class,
|
get_elements_html_by_class,
|
||||||
)
|
)
|
||||||
|
|
||||||
from ..base_provider import AnimeProvider
|
from ..base_provider import AnimeProvider
|
||||||
from ..common import fetch_anime_info_from_bal
|
|
||||||
from ..mini_anilist import search_for_anime_with_anilist
|
|
||||||
from ..utils import give_random_quality
|
from ..utils import give_random_quality
|
||||||
from . import SERVERS_AVAILABLE
|
from .constants import SERVERS_AVAILABLE
|
||||||
from .types import AniWatchStream
|
from .types import AniWatchStream
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
LINK_TO_STREAMS_REGEX = re.compile(r".*://(.*)/embed-(2|4|6)/e-([0-9])/(.*)\?.*")
|
LINK_TO_STREAMS_REGEX = re.compile(r".*://(.*)/embed-(2|4|6)/e-([0-9])/(.*)\?.*")
|
||||||
|
IMAGE_HTML_ELEMENT_REGEX = re.compile(r"<img.*?>")
|
||||||
|
|
||||||
|
|
||||||
|
class ParseAnchorAndImgTag(HTMLParser):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.img_tag = None
|
||||||
|
self.a_tag = None
|
||||||
|
|
||||||
|
def handle_starttag(self, tag, attrs):
|
||||||
|
if tag == "img":
|
||||||
|
self.img_tag = {attr[0]: attr[1] for attr in attrs}
|
||||||
|
if tag == "a":
|
||||||
|
self.a_tag = {attr[0]: attr[1] for attr in attrs}
|
||||||
|
|
||||||
|
|
||||||
class AniWatchApi(AnimeProvider):
|
class AniWatchApi(AnimeProvider):
|
||||||
|
# HEADERS = {"Referer": "https://hianime.to/home"}
|
||||||
|
|
||||||
def search_for_anime(self, anime_title: str, *args):
|
def search_for_anime(self, anime_title: str, *args):
|
||||||
try:
|
try:
|
||||||
return search_for_anime_with_anilist(anime_title)
|
query = quote_plus(anime_title)
|
||||||
except Exception as e:
|
url = f"https://hianime.to/search?keyword={query}"
|
||||||
logger.error(e)
|
response = self.session.get(url)
|
||||||
|
if not response.ok:
|
||||||
def get_anime(self, anilist_id, *args):
|
|
||||||
try:
|
|
||||||
bal_results = fetch_anime_info_from_bal(anilist_id)
|
|
||||||
if not bal_results:
|
|
||||||
return
|
return
|
||||||
ZORO = bal_results["Sites"]["Zoro"]
|
search_page = response.text
|
||||||
aniwatch_id = list(ZORO.keys())[0]
|
search_results_html_items = get_elements_by_class("flw-item", search_page)
|
||||||
|
results = []
|
||||||
|
for search_results_html_item in search_results_html_items:
|
||||||
|
film_poster_html = get_element_by_class(
|
||||||
|
"film-poster", search_results_html_item
|
||||||
|
)
|
||||||
|
|
||||||
|
if not film_poster_html:
|
||||||
|
continue
|
||||||
|
# get availableEpisodes
|
||||||
|
episodes_html = get_element_html_by_class("tick-sub", film_poster_html)
|
||||||
|
episodes = clean_html(episodes_html) or 12
|
||||||
|
|
||||||
|
# get anime id and poster image url
|
||||||
|
parser = ParseAnchorAndImgTag()
|
||||||
|
parser.feed(film_poster_html)
|
||||||
|
image_data = parser.img_tag
|
||||||
|
anime_link_data = parser.a_tag
|
||||||
|
if not image_data or not anime_link_data:
|
||||||
|
continue
|
||||||
|
|
||||||
|
episodes = int(episodes)
|
||||||
|
|
||||||
|
# finally!!
|
||||||
|
image_link = image_data["data-src"]
|
||||||
|
anime_id = anime_link_data["data-id"]
|
||||||
|
title = anime_link_data["title"]
|
||||||
|
|
||||||
|
results.append(
|
||||||
|
{
|
||||||
|
"availableEpisodes": list(range(1, episodes)),
|
||||||
|
"id": anime_id,
|
||||||
|
"title": title,
|
||||||
|
"poster": image_link,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.search_results = results
|
||||||
|
return {"pageInfo": {}, "results": results}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[ANIWATCH-ERROR]: {e}")
|
||||||
|
|
||||||
|
def get_anime(self, aniwatch_id, *args):
|
||||||
|
try:
|
||||||
|
anime_result = {}
|
||||||
|
for anime in self.search_results:
|
||||||
|
if anime["id"] == aniwatch_id:
|
||||||
|
anime_result = anime
|
||||||
|
break
|
||||||
anime_url = f"https://hianime.to/ajax/v2/episode/list/{aniwatch_id}"
|
anime_url = f"https://hianime.to/ajax/v2/episode/list/{aniwatch_id}"
|
||||||
response = self.session.get(anime_url, timeout=10)
|
response = self.session.get(anime_url, timeout=10)
|
||||||
if response.status_code == 200:
|
if response.ok:
|
||||||
response_json = response.json()
|
response_json = response.json()
|
||||||
aniwatch_anime_page = response_json["html"]
|
aniwatch_anime_page = response_json["html"]
|
||||||
episodes_info_container_html = get_element_html_by_class(
|
episodes_info_container_html = get_element_html_by_class(
|
||||||
@@ -54,7 +117,13 @@ class AniWatchApi(AnimeProvider):
|
|||||||
self.episodes_info = [
|
self.episodes_info = [
|
||||||
{
|
{
|
||||||
"id": episode["data-id"],
|
"id": episode["data-id"],
|
||||||
"title": f"{episode['title'] or ZORO['title']}; Episode {episode['data-number']}",
|
"title": (
|
||||||
|
(episode["title"] or "").replace(
|
||||||
|
f"Episode {episode['data-number']}", ""
|
||||||
|
)
|
||||||
|
or anime_result["title"]
|
||||||
|
)
|
||||||
|
+ f"; Episode {episode['data-number']}",
|
||||||
"episode": episode["data-number"],
|
"episode": episode["data-number"],
|
||||||
}
|
}
|
||||||
for episode in episodes_info_dicts
|
for episode in episodes_info_dicts
|
||||||
@@ -66,12 +135,12 @@ class AniWatchApi(AnimeProvider):
|
|||||||
"sub": episodes,
|
"sub": episodes,
|
||||||
"raw": episodes,
|
"raw": episodes,
|
||||||
},
|
},
|
||||||
"poster": ZORO[aniwatch_id]["image"],
|
"poster": anime_result["poster"],
|
||||||
"title": ZORO[aniwatch_id]["title"],
|
"title": anime_result["title"],
|
||||||
"episodes_info": self.episodes_info,
|
"episodes_info": self.episodes_info,
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(e)
|
logger.error(f"[ANIWACTCH-ERROR]: {e}")
|
||||||
|
|
||||||
def get_episode_streams(self, anime, episode, translation_type, *args):
|
def get_episode_streams(self, anime, episode, translation_type, *args):
|
||||||
try:
|
try:
|
||||||
@@ -85,7 +154,7 @@ class AniWatchApi(AnimeProvider):
|
|||||||
episode_details = episode_details[0]
|
episode_details = episode_details[0]
|
||||||
episode_url = f"https://hianime.to/ajax/v2/episode/servers?episodeId={episode_details['id']}"
|
episode_url = f"https://hianime.to/ajax/v2/episode/servers?episodeId={episode_details['id']}"
|
||||||
response = self.session.get(episode_url)
|
response = self.session.get(episode_url)
|
||||||
if response.status_code == 200:
|
if response.ok:
|
||||||
response_json = response.json()
|
response_json = response.json()
|
||||||
episode_page_html = response_json["html"]
|
episode_page_html = response_json["html"]
|
||||||
servers_containers_html = get_elements_html_by_class(
|
servers_containers_html = get_elements_html_by_class(
|
||||||
@@ -125,7 +194,7 @@ class AniWatchApi(AnimeProvider):
|
|||||||
servers_info = extract_attributes(server_html)
|
servers_info = extract_attributes(server_html)
|
||||||
embed_url = f"https://hianime.to/ajax/v2/episode/sources?id={servers_info['data-id']}"
|
embed_url = f"https://hianime.to/ajax/v2/episode/sources?id={servers_info['data-id']}"
|
||||||
embed_response = self.session.get(embed_url)
|
embed_response = self.session.get(embed_url)
|
||||||
if embed_response.status_code == 200:
|
if embed_response.ok:
|
||||||
embed_json = embed_response.json()
|
embed_json = embed_response.json()
|
||||||
raw_link_to_streams = embed_json["link"]
|
raw_link_to_streams = embed_json["link"]
|
||||||
match = LINK_TO_STREAMS_REGEX.match(raw_link_to_streams)
|
match = LINK_TO_STREAMS_REGEX.match(raw_link_to_streams)
|
||||||
@@ -138,7 +207,7 @@ class AniWatchApi(AnimeProvider):
|
|||||||
|
|
||||||
link_to_streams = f"https://{provider_domain}/embed-{embed_type}/ajax/e-{episode_number}/getSources?id={source_id}"
|
link_to_streams = f"https://{provider_domain}/embed-{embed_type}/ajax/e-{episode_number}/getSources?id={source_id}"
|
||||||
link_to_streams_response = self.session.get(link_to_streams)
|
link_to_streams_response = self.session.get(link_to_streams)
|
||||||
if link_to_streams_response.status_code == 200:
|
if link_to_streams_response.ok:
|
||||||
juicy_streams_json: "AniWatchStream" = (
|
juicy_streams_json: "AniWatchStream" = (
|
||||||
link_to_streams_response.json()
|
link_to_streams_response.json()
|
||||||
)
|
)
|
||||||
@@ -162,6 +231,6 @@ class AniWatchApi(AnimeProvider):
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(e)
|
logger.error(f"[ANIWATCH_ERROR]: {e}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(e)
|
logger.error(f"[ANIWATCH_ERROR]: {e}")
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
SERVERS_AVAILABLE = ["HD1", "HD2", "StreamSB", "StreamTape"]
|
||||||
|
|||||||
65
fastanime/libs/anime_provider/aniwave/api.py
Normal file
65
fastanime/libs/anime_provider/aniwave/api.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
from html.parser import HTMLParser
|
||||||
|
|
||||||
|
from yt_dlp.utils import clean_html, get_element_by_class, get_elements_by_class
|
||||||
|
|
||||||
|
from ..base_provider import AnimeProvider
|
||||||
|
from .constants import ANIWAVE_BASE, SEARCH_HEADERS
|
||||||
|
|
||||||
|
|
||||||
|
class ParseAnchorAndImgTag(HTMLParser):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.img_tag = None
|
||||||
|
self.a_tag = None
|
||||||
|
|
||||||
|
def handle_starttag(self, tag, attrs):
|
||||||
|
if tag == "img":
|
||||||
|
self.img_tag = {attr[0]: attr[1] for attr in attrs}
|
||||||
|
if tag == "a":
|
||||||
|
self.a_tag = {attr[0]: attr[1] for attr in attrs}
|
||||||
|
|
||||||
|
|
||||||
|
class AniWaveApi(AnimeProvider):
|
||||||
|
def search_for_anime(self, anime_title, *args):
|
||||||
|
self.session.headers.update(SEARCH_HEADERS)
|
||||||
|
search_url = f"{ANIWAVE_BASE}/filter"
|
||||||
|
params = {"keyword": anime_title}
|
||||||
|
res = self.session.get(search_url, params=params)
|
||||||
|
search_page = res.text
|
||||||
|
search_results_html_list = get_elements_by_class("item", search_page)
|
||||||
|
results = []
|
||||||
|
for result_html in search_results_html_list:
|
||||||
|
aniposter_html = get_element_by_class("poster", result_html)
|
||||||
|
episode_html = get_element_by_class("sub", aniposter_html)
|
||||||
|
episodes = clean_html(episode_html) or 12
|
||||||
|
if not aniposter_html:
|
||||||
|
return
|
||||||
|
parser = ParseAnchorAndImgTag()
|
||||||
|
parser.feed(aniposter_html)
|
||||||
|
image_data = parser.img_tag
|
||||||
|
anime_link_data = parser.a_tag
|
||||||
|
if not image_data or not anime_link_data:
|
||||||
|
continue
|
||||||
|
|
||||||
|
episodes = int(episodes)
|
||||||
|
|
||||||
|
# finally!!
|
||||||
|
image_link = image_data["src"]
|
||||||
|
title = image_data["alt"]
|
||||||
|
anime_id = anime_link_data["href"]
|
||||||
|
|
||||||
|
results.append(
|
||||||
|
{
|
||||||
|
"availableEpisodes": list(range(1, episodes)),
|
||||||
|
"id": anime_id,
|
||||||
|
"title": title,
|
||||||
|
"poster": image_link,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.search_results = results
|
||||||
|
return {"pageInfo": {}, "results": results}
|
||||||
|
|
||||||
|
def get_anime(self, anime_id, *args):
|
||||||
|
anime_page_url = f"{ANIWAVE_BASE}{anime_id}"
|
||||||
|
self.session.get(anime_page_url)
|
||||||
|
# TODO: to be continued; mostly js so very difficult
|
||||||
20
fastanime/libs/anime_provider/aniwave/constants.py
Normal file
20
fastanime/libs/anime_provider/aniwave/constants.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
ANIWAVE_BASE = "https://aniwave.to"
|
||||||
|
|
||||||
|
SEARCH_HEADERS = {
|
||||||
|
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/png,image/svg+xml,*/*;q=0.8",
|
||||||
|
"Accept-Language": "en-US,en;q=0.5",
|
||||||
|
# 'Accept-Encoding': 'Utf-8',
|
||||||
|
"Referer": "https://aniwave.to/filter",
|
||||||
|
"DNT": "1",
|
||||||
|
"Upgrade-Insecure-Requests": "1",
|
||||||
|
"Sec-Fetch-Dest": "document",
|
||||||
|
"Sec-Fetch-Mode": "navigate",
|
||||||
|
"Sec-Fetch-Site": "same-origin",
|
||||||
|
"Sec-Fetch-User": "?1",
|
||||||
|
"Connection": "keep-alive",
|
||||||
|
"Alt-Used": "aniwave.to",
|
||||||
|
# 'Cookie': '__pf=1; usertype=guest; session=BElk9DJdO3sFdDmLiGxuNiM9eGYO1TjktGsmdwjV',
|
||||||
|
"Priority": "u=0, i",
|
||||||
|
# Requests doesn't support trailers
|
||||||
|
# 'TE': 'trailers',
|
||||||
|
}
|
||||||
@@ -1,8 +1,13 @@
|
|||||||
import requests
|
import requests
|
||||||
|
from yt_dlp.utils.networking import random_user_agent
|
||||||
|
|
||||||
|
|
||||||
class AnimeProvider:
|
class AnimeProvider:
|
||||||
session: requests.Session
|
session: requests.Session
|
||||||
|
|
||||||
|
USER_AGENT = random_user_agent()
|
||||||
|
HEADERS = {}
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.session = requests.session()
|
self.session = requests.session()
|
||||||
|
self.session.headers.update({"User-Agent": self.USER_AGENT, **self.HEADERS})
|
||||||
|
|||||||
@@ -1,153 +0,0 @@
|
|||||||
import logging
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
from requests import post
|
|
||||||
from thefuzz import fuzz
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from ..anilist.types import AnilistDataSchema
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
ANILIST_ENDPOINT = "https://graphql.anilist.co"
|
|
||||||
"""
|
|
||||||
query($query:String){
|
|
||||||
Page(perPage:50){
|
|
||||||
pageInfo{
|
|
||||||
total
|
|
||||||
currentPage
|
|
||||||
hasNextPage
|
|
||||||
}
|
|
||||||
media(search:$query,type:ANIME){
|
|
||||||
id
|
|
||||||
idMal
|
|
||||||
title{
|
|
||||||
romaji
|
|
||||||
english
|
|
||||||
}
|
|
||||||
episodes
|
|
||||||
status
|
|
||||||
nextAiringEpisode {
|
|
||||||
timeUntilAiring
|
|
||||||
airingAt
|
|
||||||
episode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
def search_for_anime_with_anilist(anime_title: str):
|
|
||||||
query = """
|
|
||||||
query($query:String){
|
|
||||||
Page(perPage:50){
|
|
||||||
pageInfo{
|
|
||||||
total
|
|
||||||
currentPage
|
|
||||||
hasNextPage
|
|
||||||
}
|
|
||||||
media(search:$query,type:ANIME){
|
|
||||||
id
|
|
||||||
idMal
|
|
||||||
title{
|
|
||||||
romaji
|
|
||||||
english
|
|
||||||
}
|
|
||||||
episodes
|
|
||||||
status
|
|
||||||
nextAiringEpisode {
|
|
||||||
timeUntilAiring
|
|
||||||
airingAt
|
|
||||||
episode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
response = post(
|
|
||||||
ANILIST_ENDPOINT,
|
|
||||||
json={"query": query, "variables": {"query": anime_title}},
|
|
||||||
timeout=10,
|
|
||||||
)
|
|
||||||
if response.status_code == 200:
|
|
||||||
anilist_data: "AnilistDataSchema" = response.json()
|
|
||||||
return {
|
|
||||||
"pageInfo": anilist_data["data"]["Page"]["pageInfo"],
|
|
||||||
"results": [
|
|
||||||
{
|
|
||||||
"id": anime_result["id"],
|
|
||||||
"title": anime_result["title"]["romaji"]
|
|
||||||
or anime_result["title"]["english"],
|
|
||||||
"type": "anime",
|
|
||||||
"availableEpisodes": list(
|
|
||||||
range(
|
|
||||||
1,
|
|
||||||
(
|
|
||||||
anime_result["episodes"]
|
|
||||||
if not anime_result["status"] == "RELEASING"
|
|
||||||
and anime_result["episodes"]
|
|
||||||
else (
|
|
||||||
anime_result["nextAiringEpisode"]["episode"] - 1
|
|
||||||
if anime_result["nextAiringEpisode"]
|
|
||||||
else 0
|
|
||||||
)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
}
|
|
||||||
for anime_result in anilist_data["data"]["Page"]["media"]
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def get_mal_id_and_anilist_id(anime_title: str) -> "dict[str,int] | None":
|
|
||||||
"""the abstraction over all none authenticated requests and that returns data of a similar type
|
|
||||||
|
|
||||||
Args:
|
|
||||||
query: the anilist query
|
|
||||||
variables: the anilist api variables
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
a boolean indicating success and none or an anilist object depending on success
|
|
||||||
"""
|
|
||||||
query = """
|
|
||||||
query($query:String){
|
|
||||||
Page(perPage:50){
|
|
||||||
pageInfo{
|
|
||||||
total
|
|
||||||
currentPage
|
|
||||||
hasNextPage
|
|
||||||
}
|
|
||||||
media(search:$query,type:ANIME){
|
|
||||||
id
|
|
||||||
idMal
|
|
||||||
title{
|
|
||||||
romaji
|
|
||||||
english
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
variables = {"query": anime_title}
|
|
||||||
response = post(
|
|
||||||
ANILIST_ENDPOINT,
|
|
||||||
json={"query": query, "variables": variables},
|
|
||||||
timeout=10,
|
|
||||||
)
|
|
||||||
anilist_data: "AnilistDataSchema" = response.json()
|
|
||||||
if response.status_code == 200:
|
|
||||||
anime = max(
|
|
||||||
anilist_data["data"]["Page"]["media"],
|
|
||||||
key=lambda anime: max(
|
|
||||||
(
|
|
||||||
fuzz.ratio(anime, str(anime["title"]["romaji"])),
|
|
||||||
fuzz.ratio(anime_title, str(anime["title"]["english"])),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return {"id_anilist": anime["id"], "id_mal": anime["idMal"]}
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Something unexpected occured {e}")
|
|
||||||
15
fastanime/libs/common/common.py
Normal file
15
fastanime/libs/common/common.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from requests import get
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_anime_info_from_bal(anilist_id):
|
||||||
|
try:
|
||||||
|
url = f"https://raw.githubusercontent.com/bal-mackup/mal-backup/master/anilist/anime/{anilist_id}.json"
|
||||||
|
response = get(url, timeout=11)
|
||||||
|
if response.status_code == 200:
|
||||||
|
return response.json()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
286
fastanime/libs/common/mini_anilist.py
Normal file
286
fastanime/libs/common/mini_anilist.py
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
import logging
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from requests import post
|
||||||
|
from thefuzz import fuzz
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ..anilist.types import AnilistDataSchema
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
ANILIST_ENDPOINT = "https://graphql.anilist.co"
|
||||||
|
"""
|
||||||
|
query ($query: String) {
|
||||||
|
Page(perPage: 50) {
|
||||||
|
pageInfo {
|
||||||
|
total
|
||||||
|
currentPage
|
||||||
|
hasNextPage
|
||||||
|
}
|
||||||
|
media(search: $query, type: ANIME) {
|
||||||
|
id
|
||||||
|
idMal
|
||||||
|
title {
|
||||||
|
romaji
|
||||||
|
english
|
||||||
|
}
|
||||||
|
episodes
|
||||||
|
status
|
||||||
|
nextAiringEpisode {
|
||||||
|
timeUntilAiring
|
||||||
|
airingAt
|
||||||
|
episode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def search_for_manga_with_anilist(manga_title: str):
|
||||||
|
query = """
|
||||||
|
query ($query: String) {
|
||||||
|
Page(perPage: 50) {
|
||||||
|
pageInfo {
|
||||||
|
currentPage
|
||||||
|
}
|
||||||
|
media(search: $query, type: MANGA) {
|
||||||
|
id
|
||||||
|
idMal
|
||||||
|
title {
|
||||||
|
romaji
|
||||||
|
english
|
||||||
|
}
|
||||||
|
chapters
|
||||||
|
status
|
||||||
|
coverImage {
|
||||||
|
medium
|
||||||
|
large
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
response = post(
|
||||||
|
ANILIST_ENDPOINT,
|
||||||
|
json={"query": query, "variables": {"query": manga_title}},
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
if response.status_code == 200:
|
||||||
|
anilist_data: "AnilistDataSchema" = response.json()
|
||||||
|
return {
|
||||||
|
"pageInfo": anilist_data["data"]["Page"]["pageInfo"],
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": anime_result["id"],
|
||||||
|
"poster": anime_result["coverImage"]["large"],
|
||||||
|
"title": (
|
||||||
|
anime_result["title"]["romaji"]
|
||||||
|
or anime_result["title"]["english"]
|
||||||
|
)
|
||||||
|
+ f" [Chapters: {anime_result['chapters']}]",
|
||||||
|
"type": "manga",
|
||||||
|
"availableChapters": list(
|
||||||
|
range(
|
||||||
|
1,
|
||||||
|
(
|
||||||
|
anime_result["chapters"]
|
||||||
|
if anime_result["chapters"]
|
||||||
|
else 0
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
for anime_result in anilist_data["data"]["Page"]["media"]
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def search_for_anime_with_anilist(anime_title: str):
|
||||||
|
query = """
|
||||||
|
query ($query: String) {
|
||||||
|
Page(perPage: 50) {
|
||||||
|
pageInfo {
|
||||||
|
total
|
||||||
|
currentPage
|
||||||
|
hasNextPage
|
||||||
|
}
|
||||||
|
media(search: $query, type: ANIME) {
|
||||||
|
id
|
||||||
|
idMal
|
||||||
|
title {
|
||||||
|
romaji
|
||||||
|
english
|
||||||
|
}
|
||||||
|
episodes
|
||||||
|
status
|
||||||
|
nextAiringEpisode {
|
||||||
|
timeUntilAiring
|
||||||
|
airingAt
|
||||||
|
episode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
response = post(
|
||||||
|
ANILIST_ENDPOINT,
|
||||||
|
json={"query": query, "variables": {"query": anime_title}},
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
if response.status_code == 200:
|
||||||
|
anilist_data: "AnilistDataSchema" = response.json()
|
||||||
|
return {
|
||||||
|
"pageInfo": anilist_data["data"]["Page"]["pageInfo"],
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": anime_result["id"],
|
||||||
|
"title": anime_result["title"]["romaji"]
|
||||||
|
or anime_result["title"]["english"],
|
||||||
|
"type": "anime",
|
||||||
|
"availableEpisodes": list(
|
||||||
|
range(
|
||||||
|
1,
|
||||||
|
(
|
||||||
|
anime_result["episodes"]
|
||||||
|
if not anime_result["status"] == "RELEASING"
|
||||||
|
and anime_result["episodes"]
|
||||||
|
else (
|
||||||
|
anime_result["nextAiringEpisode"]["episode"] - 1
|
||||||
|
if anime_result["nextAiringEpisode"]
|
||||||
|
else 0
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
for anime_result in anilist_data["data"]["Page"]["media"]
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_mal_id_and_anilist_id(anime_title: str) -> "dict[str,int] | None":
|
||||||
|
"""the abstraction over all none authenticated requests and that returns data of a similar type
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: the anilist query
|
||||||
|
variables: the anilist api variables
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
a boolean indicating success and none or an anilist object depending on success
|
||||||
|
"""
|
||||||
|
query = """
|
||||||
|
query ($query: String) {
|
||||||
|
Page(perPage: 50) {
|
||||||
|
pageInfo {
|
||||||
|
total
|
||||||
|
currentPage
|
||||||
|
hasNextPage
|
||||||
|
}
|
||||||
|
media(search: $query, type: ANIME) {
|
||||||
|
id
|
||||||
|
idMal
|
||||||
|
title {
|
||||||
|
romaji
|
||||||
|
english
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
variables = {"query": anime_title}
|
||||||
|
response = post(
|
||||||
|
ANILIST_ENDPOINT,
|
||||||
|
json={"query": query, "variables": variables},
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
anilist_data: "AnilistDataSchema" = response.json()
|
||||||
|
if response.status_code == 200:
|
||||||
|
anime = max(
|
||||||
|
anilist_data["data"]["Page"]["media"],
|
||||||
|
key=lambda anime: max(
|
||||||
|
(
|
||||||
|
fuzz.ratio(anime, str(anime["title"]["romaji"])),
|
||||||
|
fuzz.ratio(anime_title, str(anime["title"]["english"])),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return {"id_anilist": anime["id"], "id_mal": anime["idMal"]}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Something unexpected occured {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def get_basic_anime_info_by_title(anime_title: str):
|
||||||
|
"""the abstraction over all none authenticated requests and that returns data of a similar type
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: the anilist query
|
||||||
|
variables: the anilist api variables
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
a boolean indicating success and none or an anilist object depending on success
|
||||||
|
"""
|
||||||
|
query = """
|
||||||
|
query ($query: String) {
|
||||||
|
Page(perPage: 50) {
|
||||||
|
pageInfo {
|
||||||
|
total
|
||||||
|
}
|
||||||
|
media(search: $query, type: ANIME) {
|
||||||
|
id
|
||||||
|
idMal
|
||||||
|
title {
|
||||||
|
romaji
|
||||||
|
english
|
||||||
|
}
|
||||||
|
streamingEpisodes {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
from ...Utility.data import anime_normalizer
|
||||||
|
|
||||||
|
# normalize the title
|
||||||
|
anime_title = anime_normalizer.get(anime_title, anime_title)
|
||||||
|
try:
|
||||||
|
variables = {"query": anime_title}
|
||||||
|
response = post(
|
||||||
|
ANILIST_ENDPOINT,
|
||||||
|
json={"query": query, "variables": variables},
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
anilist_data: "AnilistDataSchema" = response.json()
|
||||||
|
if response.status_code == 200:
|
||||||
|
anime = max(
|
||||||
|
anilist_data["data"]["Page"]["media"],
|
||||||
|
key=lambda anime: max(
|
||||||
|
(
|
||||||
|
fuzz.ratio(
|
||||||
|
anime_title.lower(), str(anime["title"]["romaji"]).lower()
|
||||||
|
),
|
||||||
|
fuzz.ratio(
|
||||||
|
anime_title.lower(), str(anime["title"]["english"]).lower()
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"idAnilist": anime["id"],
|
||||||
|
"idMal": anime["idMal"],
|
||||||
|
"title": {
|
||||||
|
"english": anime["title"]["english"],
|
||||||
|
"romaji": anime["title"]["romaji"],
|
||||||
|
},
|
||||||
|
"episodes": [
|
||||||
|
{"title": episode["title"]}
|
||||||
|
for episode in anime["streamingEpisodes"]
|
||||||
|
if episode
|
||||||
|
],
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Something unexpected occured {e}")
|
||||||
@@ -5,7 +5,6 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
from typing import Callable, List
|
from typing import Callable, List
|
||||||
|
|
||||||
# TODO: will probably scrap art not to useful
|
|
||||||
from click import clear
|
from click import clear
|
||||||
from rich import print
|
from rich import print
|
||||||
|
|
||||||
@@ -123,7 +122,9 @@ class FZF:
|
|||||||
[self.FZF_EXECUTABLE, *commands],
|
[self.FZF_EXECUTABLE, *commands],
|
||||||
input=fzf_input,
|
input=fzf_input,
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
|
universal_newlines=True,
|
||||||
text=True,
|
text=True,
|
||||||
|
encoding="utf-8"
|
||||||
)
|
)
|
||||||
if not result or result.returncode != 0 or not result.stdout:
|
if not result or result.returncode != 0 or not result.stdout:
|
||||||
print("sth went wrong:confused:")
|
print("sth went wrong:confused:")
|
||||||
|
|||||||
1
fastanime/libs/manga_provider/__init__.py
Normal file
1
fastanime/libs/manga_provider/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
manga_sources = {"mangadex": "api.MangaDexApi"}
|
||||||
13
fastanime/libs/manga_provider/base_provider.py
Normal file
13
fastanime/libs/manga_provider/base_provider.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import requests
|
||||||
|
from yt_dlp.utils.networking import random_user_agent
|
||||||
|
|
||||||
|
|
||||||
|
class MangaProvider:
|
||||||
|
session: requests.Session
|
||||||
|
|
||||||
|
USER_AGENT = random_user_agent()
|
||||||
|
HEADERS = {}
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.session = requests.session()
|
||||||
|
self.session.headers.update({"User-Agent": self.USER_AGENT, **self.HEADERS})
|
||||||
15
fastanime/libs/manga_provider/common.py
Normal file
15
fastanime/libs/manga_provider/common.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from requests import get
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_manga_info_from_bal(anilist_id):
|
||||||
|
try:
|
||||||
|
url = f"https://raw.githubusercontent.com/bal-mackup/mal-backup/master/anilist/manga/{anilist_id}.json"
|
||||||
|
response = get(url, timeout=11)
|
||||||
|
if response.ok:
|
||||||
|
return response.json()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
0
fastanime/libs/manga_provider/mangadex/__init__.py
Normal file
0
fastanime/libs/manga_provider/mangadex/__init__.py
Normal file
51
fastanime/libs/manga_provider/mangadex/api.py
Normal file
51
fastanime/libs/manga_provider/mangadex/api.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from ...common.mini_anilist import search_for_manga_with_anilist
|
||||||
|
from ..base_provider import MangaProvider
|
||||||
|
from ..common import fetch_manga_info_from_bal
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class MangaDexApi(MangaProvider):
|
||||||
|
def search_for_manga(self, title: str, *args):
|
||||||
|
try:
|
||||||
|
search_results = search_for_manga_with_anilist(title)
|
||||||
|
return search_results
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[MANGADEX-ERROR]: {e}")
|
||||||
|
|
||||||
|
def get_manga(self, anilist_manga_id: str):
|
||||||
|
bal_data = fetch_manga_info_from_bal(anilist_manga_id)
|
||||||
|
if not bal_data:
|
||||||
|
return
|
||||||
|
manga_id, MangaDexManga = next(iter(bal_data["Sites"]["Mangadex"].items()))
|
||||||
|
return {
|
||||||
|
"id": manga_id,
|
||||||
|
"title": MangaDexManga["title"],
|
||||||
|
"poster": MangaDexManga["image"],
|
||||||
|
"availableChapters": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_chapter_thumbnails(self, manga_id, chapter):
|
||||||
|
chapter_info_url = f"https://api.mangadex.org/chapter?manga={manga_id}&translatedLanguage[]=en&chapter={chapter}&includeEmptyPages=0"
|
||||||
|
chapter_info_response = self.session.get(chapter_info_url)
|
||||||
|
if not chapter_info_response.ok:
|
||||||
|
return
|
||||||
|
chapter_info = next(iter(chapter_info_response.json()["data"]))
|
||||||
|
chapters_thumbnails_url = (
|
||||||
|
f"https://api.mangadex.org/at-home/server/{chapter_info['id']}"
|
||||||
|
)
|
||||||
|
chapter_thumbnails_response = self.session.get(chapters_thumbnails_url)
|
||||||
|
if not chapter_thumbnails_response.ok:
|
||||||
|
return
|
||||||
|
chapter_thumbnails_info = chapter_thumbnails_response.json()
|
||||||
|
base_url = chapter_thumbnails_info["baseUrl"]
|
||||||
|
hash = chapter_thumbnails_info["chapter"]["hash"]
|
||||||
|
return {
|
||||||
|
"thumbnails": [
|
||||||
|
f"{base_url}/data/{hash}/{chapter_thumbnail}"
|
||||||
|
for chapter_thumbnail in chapter_thumbnails_info["chapter"]["data"]
|
||||||
|
],
|
||||||
|
"title": chapter_info["attributes"]["title"],
|
||||||
|
}
|
||||||
172
poetry.lock
generated
172
poetry.lock
generated
@@ -194,13 +194,13 @@ cffi = ">=1.0.0"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cachetools"
|
name = "cachetools"
|
||||||
version = "5.4.0"
|
version = "5.5.0"
|
||||||
description = "Extensible memoizing collections and decorators"
|
description = "Extensible memoizing collections and decorators"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
files = [
|
files = [
|
||||||
{file = "cachetools-5.4.0-py3-none-any.whl", hash = "sha256:3ae3b49a3d5e28a77a0be2b37dbcb89005058959cb2323858c2657c4a8cab474"},
|
{file = "cachetools-5.5.0-py3-none-any.whl", hash = "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292"},
|
||||||
{file = "cachetools-5.4.0.tar.gz", hash = "sha256:b8adc2e7c07f105ced7bc56dbb6dfbe7c4a00acce20e2227b3f355be89bc6827"},
|
{file = "cachetools-5.5.0.tar.gz", hash = "sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -845,13 +845,13 @@ testing = ["covdefaults (>=2.3)", "pytest (>=8.2.2)", "pytest-cov (>=5)", "pytes
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyright"
|
name = "pyright"
|
||||||
version = "1.1.376"
|
version = "1.1.377"
|
||||||
description = "Command line wrapper for pyright"
|
description = "Command line wrapper for pyright"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
files = [
|
files = [
|
||||||
{file = "pyright-1.1.376-py3-none-any.whl", hash = "sha256:0f2473b12c15c46b3207f0eec224c3cea2bdc07cd45dd4a037687cbbca0fbeff"},
|
{file = "pyright-1.1.377-py3-none-any.whl", hash = "sha256:af0dd2b6b636c383a6569a083f8c5a8748ae4dcde5df7914b3f3f267e14dd162"},
|
||||||
{file = "pyright-1.1.376.tar.gz", hash = "sha256:bffd63b197cd0810395bb3245c06b01f95a85ddf6bfa0e5644ed69c841e954dd"},
|
{file = "pyright-1.1.377.tar.gz", hash = "sha256:aabc30fedce0ded34baa0c49b24f10e68f4bfc8f68ae7f3d175c4b0f256b4fcf"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -1243,83 +1243,97 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "websockets"
|
name = "websockets"
|
||||||
version = "12.0"
|
version = "13.0"
|
||||||
description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)"
|
description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "websockets-12.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d554236b2a2006e0ce16315c16eaa0d628dab009c33b63ea03f41c6107958374"},
|
{file = "websockets-13.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ad4fa707ff9e2ffee019e946257b5300a45137a58f41fbd9a4db8e684ab61528"},
|
||||||
{file = "websockets-12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2d225bb6886591b1746b17c0573e29804619c8f755b5598d875bb4235ea639be"},
|
{file = "websockets-13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6fd757f313c13c34dae9f126d3ba4cf97175859c719e57c6a614b781c86b617e"},
|
||||||
{file = "websockets-12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eb809e816916a3b210bed3c82fb88eaf16e8afcf9c115ebb2bacede1797d2547"},
|
{file = "websockets-13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cbac2eb7ce0fac755fb983c9247c4a60c4019bcde4c0e4d167aeb17520cc7ef1"},
|
||||||
{file = "websockets-12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c588f6abc13f78a67044c6b1273a99e1cf31038ad51815b3b016ce699f0d75c2"},
|
{file = "websockets-13.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4b83cf7354cbbc058e97b3e545dceb75b8d9cf17fd5a19db419c319ddbaaf7a"},
|
||||||
{file = "websockets-12.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5aa9348186d79a5f232115ed3fa9020eab66d6c3437d72f9d2c8ac0c6858c558"},
|
{file = "websockets-13.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9202c0010c78fad1041e1c5285232b6508d3633f92825687549540a70e9e5901"},
|
||||||
{file = "websockets-12.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6350b14a40c95ddd53e775dbdbbbc59b124a5c8ecd6fbb09c2e52029f7a9f480"},
|
{file = "websockets-13.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6566e79c8c7cbea75ec450f6e1828945fc5c9a4769ceb1c7b6e22470539712"},
|
||||||
{file = "websockets-12.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:70ec754cc2a769bcd218ed8d7209055667b30860ffecb8633a834dde27d6307c"},
|
{file = "websockets-13.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e7fcad070dcd9ad37a09d89a4cbc2a5e3e45080b88977c0da87b3090f9f55ead"},
|
||||||
{file = "websockets-12.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6e96f5ed1b83a8ddb07909b45bd94833b0710f738115751cdaa9da1fb0cb66e8"},
|
{file = "websockets-13.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a8f7d65358a25172db00c69bcc7df834155ee24229f560d035758fd6613111a"},
|
||||||
{file = "websockets-12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4d87be612cbef86f994178d5186add3d94e9f31cc3cb499a0482b866ec477603"},
|
{file = "websockets-13.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:63b702fb31e3f058f946ccdfa551f4d57a06f7729c369e8815eb18643099db37"},
|
||||||
{file = "websockets-12.0-cp310-cp310-win32.whl", hash = "sha256:befe90632d66caaf72e8b2ed4d7f02b348913813c8b0a32fae1cc5fe3730902f"},
|
{file = "websockets-13.0-cp310-cp310-win32.whl", hash = "sha256:3a20cf14ba7b482c4a1924b5e061729afb89c890ca9ed44ac4127c6c5986e424"},
|
||||||
{file = "websockets-12.0-cp310-cp310-win_amd64.whl", hash = "sha256:363f57ca8bc8576195d0540c648aa58ac18cf85b76ad5202b9f976918f4219cf"},
|
{file = "websockets-13.0-cp310-cp310-win_amd64.whl", hash = "sha256:587245f0704d0bb675f919898d7473e8827a6d578e5a122a21756ca44b811ec8"},
|
||||||
{file = "websockets-12.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5d873c7de42dea355d73f170be0f23788cf3fa9f7bed718fd2830eefedce01b4"},
|
{file = "websockets-13.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:06df8306c241c235075d2ae77367038e701e53bc8c1bb4f6644f4f53aa6dedd0"},
|
||||||
{file = "websockets-12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3f61726cae9f65b872502ff3c1496abc93ffbe31b278455c418492016e2afc8f"},
|
{file = "websockets-13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:85a1f92a02f0b8c1bf02699731a70a8a74402bb3f82bee36e7768b19a8ed9709"},
|
||||||
{file = "websockets-12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed2fcf7a07334c77fc8a230755c2209223a7cc44fc27597729b8ef5425aa61a3"},
|
{file = "websockets-13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9ed02c604349068d46d87ef4c2012c112c791f2bec08671903a6bb2bd9c06784"},
|
||||||
{file = "websockets-12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e332c210b14b57904869ca9f9bf4ca32f5427a03eeb625da9b616c85a3a506c"},
|
{file = "websockets-13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b89849171b590107f6724a7b0790736daead40926ddf47eadf998b4ff51d6414"},
|
||||||
{file = "websockets-12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5693ef74233122f8ebab026817b1b37fe25c411ecfca084b29bc7d6efc548f45"},
|
{file = "websockets-13.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:939a16849d71203628157a5e4a495da63967c744e1e32018e9b9e2689aca64d4"},
|
||||||
{file = "websockets-12.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e9e7db18b4539a29cc5ad8c8b252738a30e2b13f033c2d6e9d0549b45841c04"},
|
{file = "websockets-13.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad818cdac37c0ad4c58e51cb4964eae4f18b43c4a83cb37170b0d90c31bd80cf"},
|
||||||
{file = "websockets-12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6e2df67b8014767d0f785baa98393725739287684b9f8d8a1001eb2839031447"},
|
{file = "websockets-13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cbfe82a07596a044de78bb7a62519e71690c5812c26c5f1d4b877e64e4f46309"},
|
||||||
{file = "websockets-12.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bea88d71630c5900690fcb03161ab18f8f244805c59e2e0dc4ffadae0a7ee0ca"},
|
{file = "websockets-13.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e07e76c49f39c5b45cbd7362b94f001ae209a3ea4905ae9a09cfd53b3c76373d"},
|
||||||
{file = "websockets-12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dff6cdf35e31d1315790149fee351f9e52978130cef6c87c4b6c9b3baf78bc53"},
|
{file = "websockets-13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:372f46a0096cfda23c88f7e42349a33f8375e10912f712e6b496d3a9a557290f"},
|
||||||
{file = "websockets-12.0-cp311-cp311-win32.whl", hash = "sha256:3e3aa8c468af01d70332a382350ee95f6986db479ce7af14d5e81ec52aa2b402"},
|
{file = "websockets-13.0-cp311-cp311-win32.whl", hash = "sha256:376a43a4fd96725f13450d3d2e98f4f36c3525c562ab53d9a98dd2950dca9a8a"},
|
||||||
{file = "websockets-12.0-cp311-cp311-win_amd64.whl", hash = "sha256:25eb766c8ad27da0f79420b2af4b85d29914ba0edf69f547cc4f06ca6f1d403b"},
|
{file = "websockets-13.0-cp311-cp311-win_amd64.whl", hash = "sha256:2be1382a4daa61e2f3e2be3b3c86932a8db9d1f85297feb6e9df22f391f94452"},
|
||||||
{file = "websockets-12.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0e6e2711d5a8e6e482cacb927a49a3d432345dfe7dea8ace7b5790df5932e4df"},
|
{file = "websockets-13.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b5407c34776b9b77bd89a5f95eb0a34aaf91889e3f911c63f13035220eb50107"},
|
||||||
{file = "websockets-12.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:dbcf72a37f0b3316e993e13ecf32f10c0e1259c28ffd0a85cee26e8549595fbc"},
|
{file = "websockets-13.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4782ec789f059f888c1e8fdf94383d0e64b531cffebbf26dd55afd53ab487ca4"},
|
||||||
{file = "websockets-12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12743ab88ab2af1d17dd4acb4645677cb7063ef4db93abffbf164218a5d54c6b"},
|
{file = "websockets-13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c8feb8e19ef65c9994e652c5b0324abd657bedd0abeb946fb4f5163012c1e730"},
|
||||||
{file = "websockets-12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b645f491f3c48d3f8a00d1fce07445fab7347fec54a3e65f0725d730d5b99cb"},
|
{file = "websockets-13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f3d2e20c442b58dbac593cb1e02bc02d149a86056cc4126d977ad902472e3b"},
|
||||||
{file = "websockets-12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9893d1aa45a7f8b3bc4510f6ccf8db8c3b62120917af15e3de247f0780294b92"},
|
{file = "websockets-13.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e39d393e0ab5b8bd01717cc26f2922026050188947ff54fe6a49dc489f7750b7"},
|
||||||
{file = "websockets-12.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f38a7b376117ef7aff996e737583172bdf535932c9ca021746573bce40165ed"},
|
{file = "websockets-13.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f661a4205741bdc88ac9c2b2ec003c72cee97e4acd156eb733662ff004ba429"},
|
||||||
{file = "websockets-12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f764ba54e33daf20e167915edc443b6f88956f37fb606449b4a5b10ba42235a5"},
|
{file = "websockets-13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:384129ad0490e06bab2b98c1da9b488acb35bb11e2464c728376c6f55f0d45f3"},
|
||||||
{file = "websockets-12.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:1e4b3f8ea6a9cfa8be8484c9221ec0257508e3a1ec43c36acdefb2a9c3b00aa2"},
|
{file = "websockets-13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:df5c0eff91f61b8205a6c9f7b255ff390cdb77b61c7b41f79ca10afcbb22b6cb"},
|
||||||
{file = "websockets-12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9fdf06fd06c32205a07e47328ab49c40fc1407cdec801d698a7c41167ea45113"},
|
{file = "websockets-13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:02cc9bb1a887dac0e08bf657c5d00aa3fac0d03215d35a599130c2034ae6663a"},
|
||||||
{file = "websockets-12.0-cp312-cp312-win32.whl", hash = "sha256:baa386875b70cbd81798fa9f71be689c1bf484f65fd6fb08d051a0ee4e79924d"},
|
{file = "websockets-13.0-cp312-cp312-win32.whl", hash = "sha256:d9726d2c9bd6aed8cb994d89b3910ca0079406edce3670886ec828a73e7bdd53"},
|
||||||
{file = "websockets-12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae0a5da8f35a5be197f328d4727dbcfafa53d1824fac3d96cdd3a642fe09394f"},
|
{file = "websockets-13.0-cp312-cp312-win_amd64.whl", hash = "sha256:fa0839f35322f7b038d8adcf679e2698c3a483688cc92e3bd15ee4fb06669e9a"},
|
||||||
{file = "websockets-12.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5f6ffe2c6598f7f7207eef9a1228b6f5c818f9f4d53ee920aacd35cec8110438"},
|
{file = "websockets-13.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:da7e501e59857e8e3e9d10586139dc196b80445a591451ca9998aafba1af5278"},
|
||||||
{file = "websockets-12.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9edf3fc590cc2ec20dc9d7a45108b5bbaf21c0d89f9fd3fd1685e223771dc0b2"},
|
{file = "websockets-13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a00e1e587c655749afb5b135d8d3edcfe84ec6db864201e40a882e64168610b3"},
|
||||||
{file = "websockets-12.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8572132c7be52632201a35f5e08348137f658e5ffd21f51f94572ca6c05ea81d"},
|
{file = "websockets-13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a7fbf2a8fe7556a8f4e68cb3e736884af7bf93653e79f6219f17ebb75e97d8f0"},
|
||||||
{file = "websockets-12.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:604428d1b87edbf02b233e2c207d7d528460fa978f9e391bd8aaf9c8311de137"},
|
{file = "websockets-13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ea9c9c7443a97ea4d84d3e4d42d0e8c4235834edae652993abcd2aff94affd7"},
|
||||||
{file = "websockets-12.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a9d160fd080c6285e202327aba140fc9a0d910b09e423afff4ae5cbbf1c7205"},
|
{file = "websockets-13.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:35c2221b539b360203f3f9ad168e527bf16d903e385068ae842c186efb13d0ea"},
|
||||||
{file = "websockets-12.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87b4aafed34653e465eb77b7c93ef058516cb5acf3eb21e42f33928616172def"},
|
{file = "websockets-13.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:358d37c5c431dd050ffb06b4b075505aae3f4f795d7fff9794e5ed96ce99b998"},
|
||||||
{file = "websockets-12.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b2ee7288b85959797970114deae81ab41b731f19ebcd3bd499ae9ca0e3f1d2c8"},
|
{file = "websockets-13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:038e7a0f1bfafc7bf52915ab3506b7a03d1e06381e9f60440c856e8918138151"},
|
||||||
{file = "websockets-12.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:7fa3d25e81bfe6a89718e9791128398a50dec6d57faf23770787ff441d851967"},
|
{file = "websockets-13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fd038bc9e2c134847f1e0ce3191797fad110756e690c2fdd9702ed34e7a43abb"},
|
||||||
{file = "websockets-12.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a571f035a47212288e3b3519944f6bf4ac7bc7553243e41eac50dd48552b6df7"},
|
{file = "websockets-13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93b8c2008f372379fb6e5d2b3f7c9ec32f7b80316543fd3a5ace6610c5cde1b0"},
|
||||||
{file = "websockets-12.0-cp38-cp38-win32.whl", hash = "sha256:3c6cc1360c10c17463aadd29dd3af332d4a1adaa8796f6b0e9f9df1fdb0bad62"},
|
{file = "websockets-13.0-cp313-cp313-win32.whl", hash = "sha256:851fd0afb3bc0b73f7c5b5858975d42769a5fdde5314f4ef2c106aec63100687"},
|
||||||
{file = "websockets-12.0-cp38-cp38-win_amd64.whl", hash = "sha256:1bf386089178ea69d720f8db6199a0504a406209a0fc23e603b27b300fdd6892"},
|
{file = "websockets-13.0-cp313-cp313-win_amd64.whl", hash = "sha256:7d14901fdcf212804970c30ab9ee8f3f0212e620c7ea93079d6534863444fb4e"},
|
||||||
{file = "websockets-12.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ab3d732ad50a4fbd04a4490ef08acd0517b6ae6b77eb967251f4c263011a990d"},
|
{file = "websockets-13.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ae7a519a56a714f64c3445cabde9fc2fc927e7eae44f413eae187cddd9e54178"},
|
||||||
{file = "websockets-12.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1d9697f3337a89691e3bd8dc56dea45a6f6d975f92e7d5f773bc715c15dde28"},
|
{file = "websockets-13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5575031472ca87302aeb2ce2c2349f4c6ea978c86a9d1289bc5d16058ad4c10a"},
|
||||||
{file = "websockets-12.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1df2fbd2c8a98d38a66f5238484405b8d1d16f929bb7a33ed73e4801222a6f53"},
|
{file = "websockets-13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9895df6cd0bfe79d09bcd1dbdc03862846f26fbd93797153de954306620c1d00"},
|
||||||
{file = "websockets-12.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23509452b3bc38e3a057382c2e941d5ac2e01e251acce7adc74011d7d8de434c"},
|
{file = "websockets-13.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4de299c947a54fca9ce1c5fd4a08eb92ffce91961becb13bd9195f7c6e71b47"},
|
||||||
{file = "websockets-12.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e5fc14ec6ea568200ea4ef46545073da81900a2b67b3e666f04adf53ad452ec"},
|
{file = "websockets-13.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:05c25f7b849702950b6fd0e233989bb73a0d2bc83faa3b7233313ca395205f6d"},
|
||||||
{file = "websockets-12.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46e71dbbd12850224243f5d2aeec90f0aaa0f2dde5aeeb8fc8df21e04d99eff9"},
|
{file = "websockets-13.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ede95125a30602b1691a4b1da88946bf27dae283cf30f22cd2cb8ca4b2e0d119"},
|
||||||
{file = "websockets-12.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b81f90dcc6c85a9b7f29873beb56c94c85d6f0dac2ea8b60d995bd18bf3e2aae"},
|
{file = "websockets-13.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:addf0a16e4983280efed272d8cb3b2e05f0051755372461e7d966b80a6554e16"},
|
||||||
{file = "websockets-12.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a02413bc474feda2849c59ed2dfb2cddb4cd3d2f03a2fedec51d6e959d9b608b"},
|
{file = "websockets-13.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:06b3186e97bf9a33921fa60734d5ed90f2a9b407cce8d23c7333a0984049ef61"},
|
||||||
{file = "websockets-12.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bbe6013f9f791944ed31ca08b077e26249309639313fff132bfbf3ba105673b9"},
|
{file = "websockets-13.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:eae368cac85adc4c7dc3b0d5f84ffcca609d658db6447387300478e44db70796"},
|
||||||
{file = "websockets-12.0-cp39-cp39-win32.whl", hash = "sha256:cbe83a6bbdf207ff0541de01e11904827540aa069293696dd528a6640bd6a5f6"},
|
{file = "websockets-13.0-cp38-cp38-win32.whl", hash = "sha256:337837ac788d955728b1ab01876d72b73da59819a3388e1c5e8e05c3999f1afa"},
|
||||||
{file = "websockets-12.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc4e7fa5414512b481a2483775a8e8be7803a35b30ca805afa4998a84f9fd9e8"},
|
{file = "websockets-13.0-cp38-cp38-win_amd64.whl", hash = "sha256:f66e00e42f25ca7e91076366303e11c82572ca87cc5aae51e6e9c094f315ab41"},
|
||||||
{file = "websockets-12.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:248d8e2446e13c1d4326e0a6a4e9629cb13a11195051a73acf414812700badbd"},
|
{file = "websockets-13.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:94c1c02721139fe9940b38d28fb15b4b782981d800d5f40f9966264fbf23dcc8"},
|
||||||
{file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f44069528d45a933997a6fef143030d8ca8042f0dfaad753e2906398290e2870"},
|
{file = "websockets-13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bd4ba86513430513e2aa25a441bb538f6f83734dc368a2c5d18afdd39097aa33"},
|
||||||
{file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c4e37d36f0d19f0a4413d3e18c0d03d0c268ada2061868c1e6f5ab1a6d575077"},
|
{file = "websockets-13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a1ab8f0e0cadc5be5f3f9fa11a663957fecbf483d434762c8dfb8aa44948944a"},
|
||||||
{file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d829f975fc2e527a3ef2f9c8f25e553eb7bc779c6665e8e1d52aa22800bb38b"},
|
{file = "websockets-13.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3670def5d3dfd5af6f6e2b3b243ea8f1f72d8da1ef927322f0703f85c90d9603"},
|
||||||
{file = "websockets-12.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2c71bd45a777433dd9113847af751aae36e448bc6b8c361a566cb043eda6ec30"},
|
{file = "websockets-13.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6058b6be92743358885ad6dcdecb378fde4a4c74d4dd16a089d07580c75a0e80"},
|
||||||
{file = "websockets-12.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0bee75f400895aef54157b36ed6d3b308fcab62e5260703add87f44cee9c82a6"},
|
{file = "websockets-13.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:516062a0a8ef5ecbfa4acbaec14b199fc070577834f9fe3d40800a99f92523ca"},
|
||||||
{file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:423fc1ed29f7512fceb727e2d2aecb952c46aa34895e9ed96071821309951123"},
|
{file = "websockets-13.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:da7e918d82e7bdfc6f66d31febe1b2e28a1ca3387315f918de26f5e367f61572"},
|
||||||
{file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27a5e9964ef509016759f2ef3f2c1e13f403725a5e6a1775555994966a66e931"},
|
{file = "websockets-13.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:9cc7f35dcb49a4e32db82a849fcc0714c4d4acc9d2273aded2d61f87d7f660b7"},
|
||||||
{file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3181df4583c4d3994d31fb235dc681d2aaad744fbdbf94c4802485ececdecf2"},
|
{file = "websockets-13.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f5737c53eb2c8ed8f64b50d3dafd3c1dae739f78aa495a288421ac1b3de82717"},
|
||||||
{file = "websockets-12.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:b067cb952ce8bf40115f6c19f478dc71c5e719b7fbaa511359795dfd9d1a6468"},
|
{file = "websockets-13.0-cp39-cp39-win32.whl", hash = "sha256:265e1f0d3f788ce8ef99dca591a1aec5263b26083ca0934467ad9a1d1181067c"},
|
||||||
{file = "websockets-12.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:00700340c6c7ab788f176d118775202aadea7602c5cc6be6ae127761c16d6b0b"},
|
{file = "websockets-13.0-cp39-cp39-win_amd64.whl", hash = "sha256:4d70c89e3d3b347a7c4d3c33f8d323f0584c9ceb69b82c2ef8a174ca84ea3d4a"},
|
||||||
{file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e469d01137942849cff40517c97a30a93ae79917752b34029f0ec72df6b46399"},
|
{file = "websockets-13.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:602cbd010d8c21c8475f1798b705bb18567eb189c533ab5ef568bc3033fdf417"},
|
||||||
{file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffefa1374cd508d633646d51a8e9277763a9b78ae71324183693959cf94635a7"},
|
{file = "websockets-13.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:bf8eb5dca4f484a60f5327b044e842e0d7f7cdbf02ea6dc4a4f811259f1f1f0b"},
|
||||||
{file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba0cab91b3956dfa9f512147860783a1829a8d905ee218a9837c18f683239611"},
|
{file = "websockets-13.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89d795c1802d99a643bf689b277e8604c14b5af1bc0a31dade2cd7a678087212"},
|
||||||
{file = "websockets-12.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2cb388a5bfb56df4d9a406783b7f9dbefb888c09b71629351cc6b036e9259370"},
|
{file = "websockets-13.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:788bc841d250beccff67a20a5a53a15657a60111ef9c0c0a97fbdd614fae0fe2"},
|
||||||
{file = "websockets-12.0-py3-none-any.whl", hash = "sha256:dc284bbc8d7c78a6c69e0c7325ab46ee5e40bb4d50e494d8131a07ef47500e9e"},
|
{file = "websockets-13.0-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7334752052532c156d28b8eaf3558137e115c7871ea82adff69b6d94a7bee273"},
|
||||||
{file = "websockets-12.0.tar.gz", hash = "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b"},
|
{file = "websockets-13.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e7a1963302947332c3039e3f66209ec73b1626f8a0191649e0713c391e9f5b0d"},
|
||||||
|
{file = "websockets-13.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2e1cf4e1eb84b4fd74a47688e8b0940c89a04ad9f6937afa43d468e71128cd68"},
|
||||||
|
{file = "websockets-13.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:c026ee729c4ce55708a14b839ba35086dfae265fc12813b62d34ce33f4980c1c"},
|
||||||
|
{file = "websockets-13.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5f9d23fbbf96eefde836d9692670bfc89e2d159f456d499c5efcf6a6281c1af"},
|
||||||
|
{file = "websockets-13.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ad684cb7efce227d756bae3e8484f2e56aa128398753b54245efdfbd1108f2c"},
|
||||||
|
{file = "websockets-13.0-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1e10b3fbed7be4a59831d3a939900e50fcd34d93716e433d4193a4d0d1d335d"},
|
||||||
|
{file = "websockets-13.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:d42a818e634f789350cd8fb413a3f5eec1cf0400a53d02062534c41519f5125c"},
|
||||||
|
{file = "websockets-13.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e5ba5e9b332267d0f2c33ede390061850f1ac3ee6cd1bdcf4c5ea33ead971966"},
|
||||||
|
{file = "websockets-13.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:f9af457ed593e35f467140d8b61d425495b127744a9d65d45a366f8678449a23"},
|
||||||
|
{file = "websockets-13.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcea3eb58c09c3a31cc83b45c06d5907f02ddaf10920aaa6443975310f699b95"},
|
||||||
|
{file = "websockets-13.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c210d1460dc8d326ffdef9703c2f83269b7539a1690ad11ae04162bc1878d33d"},
|
||||||
|
{file = "websockets-13.0-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b32f38bc81170fd56d0482d505b556e52bf9078b36819a8ba52624bd6667e39e"},
|
||||||
|
{file = "websockets-13.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:81a11a1ddd5320429db47c04d35119c3e674d215173d87aaeb06ae80f6e9031f"},
|
||||||
|
{file = "websockets-13.0-py3-none-any.whl", hash = "sha256:dbbac01e80aee253d44c4f098ab3cc17c822518519e869b284cfbb8cd16cc9de"},
|
||||||
|
{file = "websockets-13.0.tar.gz", hash = "sha256:b7bf950234a482b7461afdb2ec99eee3548ec4d53f418c7990bb79c620476602"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "fastanime"
|
name = "fastanime"
|
||||||
version = "2.3.1"
|
version = "2.5.1"
|
||||||
description = "A browser anime site experience from the terminal"
|
description = "A browser anime site experience from the terminal"
|
||||||
authors = ["Benextempest <benextempest@gmail.com>"]
|
authors = ["Benextempest <benextempest@gmail.com>"]
|
||||||
license = "UNLICENSE"
|
license = "UNLICENSE"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
"typeCheckingMode": "standard",
|
"venvPath": ".",
|
||||||
"reportPrivateImportUsage": false
|
"venv": ".venv",
|
||||||
|
"pythonVersion": "3.10"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
# TODO: Write tests to make sure all click commands work
|
|
||||||
import pytest
|
import pytest
|
||||||
from click.testing import CliRunner
|
from click.testing import CliRunner
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user