Compare commits

..

27 Commits

Author SHA1 Message Date
BeneX254
3ef7c5248c Update README.md 2024-09-15 13:46:50 +03:00
Benex254
8bebc401fd fix: rename use_mpv_mod to use_python_mpv in config 2024-09-15 13:40:20 +03:00
Benex254
215b28457b docs: update readme 2024-09-15 13:39:58 +03:00
Benex254
dfd2bfc857 docs: update readme 2024-09-15 13:29:32 +03:00
Benex254
f991292e94 chore: bump version 2024-09-15 13:29:20 +03:00
Benex254
d837457f80 feat: improve config file docs 2024-09-15 13:22:14 +03:00
Benex254
343bdba31b feat: add the --update option to the config command which causes all config options passed to fastanime to be persisted to your config file 2024-09-15 13:22:14 +03:00
benex
1c1c2457e8 feat: improve the preview with a workaround 2024-09-15 10:05:20 +03:00
benex
b083bfb074 fix: previews not working on windows 2024-09-15 09:36:15 +03:00
benex
ea1abcb2ae feat: dont use roaming folder for the config file 2024-09-15 08:54:02 +03:00
benex
001030ba2b fix: unicode error when running fzf on wndows 2024-09-15 08:53:22 +03:00
BeneX254
eda8984781 Update README.md 2024-09-13 21:57:15 +03:00
Benex254
d8dc6f0a34 chore: bump version 2024-09-10 19:15:43 +03:00
Benex254
2d711a7a7f docs: update readme 2024-09-10 19:15:25 +03:00
Benex254
30ca25626a feat: add --titles option to downloads 2024-09-10 19:11:52 +03:00
Benex254
b1f5a558c8 feat: improve animepahe utils 2024-09-10 19:11:19 +03:00
Benex254
8062c8dc83 feat: stat command ?? 2024-08-23 20:51:53 +03:00
Benex254
cb7eed46bc docs: update readme 2024-08-23 17:44:23 +03:00
Benex254
4626eca89e feat: improvements on media list intergration 2024-08-23 17:44:10 +03:00
Benex254
0d549c5915 docs: update readme 2024-08-23 17:18:49 +03:00
Benex254
33c518ed4c chore: cleanup codebase 2024-08-23 17:18:36 +03:00
Benex254
8e155dcc74 chore: bump version 2024-08-23 16:05:45 +03:00
Benex254
7743b0423e chore: clean up codebase 2024-08-23 16:05:26 +03:00
Benex254
6346ea7343 docs: update readme 2024-08-23 11:40:08 +03:00
Benex254
32de01047f chore:bump version 2024-08-23 11:39:57 +03:00
Benex254
35c7f81afb fix: no chapter title 2024-08-23 11:39:45 +03:00
Benex254
2dbbb1c4df feat: add experimental manga support 2024-08-23 11:19:25 +03:00
55 changed files with 2042 additions and 1288 deletions

254
README.md
View File

@@ -25,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-->
@@ -53,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)
@@ -177,6 +178,7 @@ 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
@@ -239,6 +241,7 @@ Available options for the fastanime include:
- `--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 - `--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
@@ -252,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:
@@ -291,10 +297,11 @@ It offers the following options:
- `--title <anime-title>` or `-t <anime-title>` - `--title <anime-title>` or `-t <anime-title>`
- `--tags <tag>` or `-T <tag>` can be specified multiple times for different tags to filter by. - `--tags <tag>` or `-T <tag>` can be specified multiple times for different tags to filter by.
- `--year <year>` or `-y <year>` - `--year <year>` or `-y <year>`
- `--status <MediaStatus>` or `-S <MediaStatus>` - `--status <MediaStatus>` or `-S <MediaStatus>` can be specified multiple times
- `--media-format <MediaFormat>` or `-f <MediaFormat>` - `--media-format <MediaFormat>` or `-f <MediaFormat>`
- `--season <MediaSeason>` - `--season <MediaSeason>`
- `--genres <genre>` or `-g <genre>` can be specified multiple times. - `--genres <genre>` or `-g <genre>` can be specified multiple times.
- `--on-list/--not-on-list`
Example: Example:
@@ -303,7 +310,9 @@ Example:
fastanime anilist search -T isekai fastanime anilist search -T isekai
# get anime of 2024 and sort by popularity # get anime of 2024 and sort by popularity
fastanime anilist search -y 2024 -s POPULARITY_DESC # 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 # get anime of 2024 season WINTER
fastanime anilist search -y 2024 --season WINTER fastanime anilist search -y 2024 --season WINTER
@@ -529,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
@@ -651,78 +664,205 @@ The default interface uses inquirerPy which is customizable. Read here to findou
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
# whether to use python mpv for enhanced experience # whether to use python-mpv [True/False]
use_mpv_mod=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
normalize_titles = true # since we may not always have the time to immediately implement the changes
#
downloads_dir = <Default-videos-dir>/FastAnime # Download directory # HOPE YOU ENJOY FASTANIME AND BE SURE TO STAR THE PROJECT ON GITHUB
#
preview=false # whether to show a preview window when using fzf or rofi
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

View File

@@ -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__)
@@ -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:
logger.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:
logger.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:
logger.error(e) logger.error(f"[ANIMEPROVIDER-ERROR]: {e}")
results = None results = None
return results # pyright:ignore return results

105
fastanime/MangaProvider.py Normal file
View 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

View File

@@ -11,7 +11,7 @@ anime_normalizer_raw = {
'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", '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"}, "aniwatch": {"My Star": "Oshi no Ko"},
"animepahe": {}, "animepahe": {"Azumanga Daiou The Animation": "Azumanga Daioh"},
} }

View File

@@ -31,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,

View File

@@ -6,7 +6,7 @@ if sys.version_info < (3, 10):
) # noqa: F541 ) # noqa: F541
__version__ = "v2.4.2" __version__ = "v2.5.0"
APP_NAME = "FastAnime" APP_NAME = "FastAnime"
AUTHOR = "Benex254" AUTHOR = "Benex254"

View File

@@ -40,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)
@@ -144,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,
@@ -177,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
@@ -220,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:
@@ -254,10 +260,10 @@ 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 downloads_dir: if downloads_dir:
ctx.obj.downloads_dir = downloads_dir ctx.obj.downloads_dir = downloads_dir
if translation_type: if translation_type:

View File

@@ -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",
} }

View File

@@ -42,5 +42,5 @@ def completed(config: "Config", dump_json):
from ...interfaces import anilist_interfaces from ...interfaces import anilist_interfaces
fastanime_runtime_state = FastAnimeRuntimeState() fastanime_runtime_state = FastAnimeRuntimeState()
fastanime_runtime_state.anilist_data = anime_list[1] fastanime_runtime_state.anilist_results_data = anime_list[1]
anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state) anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state)

View File

@@ -42,5 +42,5 @@ def dropped(config: "Config", dump_json):
from ...utils.tools import FastAnimeRuntimeState from ...utils.tools import FastAnimeRuntimeState
fastanime_runtime_state = FastAnimeRuntimeState() fastanime_runtime_state = FastAnimeRuntimeState()
fastanime_runtime_state.anilist_data = anime_list[1] fastanime_runtime_state.anilist_results_data = anime_list[1]
anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state) anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state)

View File

@@ -26,7 +26,7 @@ def favourites(config, dump_json):
from ...utils.tools import FastAnimeRuntimeState from ...utils.tools import FastAnimeRuntimeState
fastanime_runtime_state = FastAnimeRuntimeState() fastanime_runtime_state = FastAnimeRuntimeState()
fastanime_runtime_state.anilist_data = anime_data[1] fastanime_runtime_state.anilist_results_data = anime_data[1]
anilist_results_menu(config, fastanime_runtime_state) anilist_results_menu(config, fastanime_runtime_state)
else: else:
from sys import exit from sys import exit

View File

@@ -42,5 +42,5 @@ def paused(config: "Config", dump_json):
from ...utils.tools import FastAnimeRuntimeState from ...utils.tools import FastAnimeRuntimeState
anilist_config = FastAnimeRuntimeState() anilist_config = FastAnimeRuntimeState()
anilist_config.data = anime_list[1] anilist_config.anilist_results_data = anime_list[1]
anilist_interfaces.anilist_results_menu(config, anilist_config) anilist_interfaces.anilist_results_menu(config, anilist_config)

View File

@@ -42,5 +42,5 @@ def planning(config: "Config", dump_json):
from ...utils.tools import FastAnimeRuntimeState from ...utils.tools import FastAnimeRuntimeState
fastanime_runtime_state = FastAnimeRuntimeState() fastanime_runtime_state = FastAnimeRuntimeState()
fastanime_runtime_state.anilist_data = anime_list[1] fastanime_runtime_state.anilist_results_data = anime_list[1]
anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state) anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state)

View File

@@ -25,7 +25,7 @@ def popular(config, dump_json):
from ...utils.tools import FastAnimeRuntimeState from ...utils.tools import FastAnimeRuntimeState
fastanime_runtime_state = FastAnimeRuntimeState() fastanime_runtime_state = FastAnimeRuntimeState()
fastanime_runtime_state.anilist_data = anime_data[1] fastanime_runtime_state.anilist_results_data = anime_data[1]
anilist_results_menu(config, fastanime_runtime_state) anilist_results_menu(config, fastanime_runtime_state)
else: else:
from sys import exit from sys import exit

View File

@@ -33,7 +33,7 @@ def random_anime(config, dump_json):
from ...utils.tools import FastAnimeRuntimeState from ...utils.tools import FastAnimeRuntimeState
fastanime_runtime_state = FastAnimeRuntimeState() fastanime_runtime_state = FastAnimeRuntimeState()
fastanime_runtime_state.anilist_data = anime_data[1] fastanime_runtime_state.anilist_results_data = anime_data[1]
anilist_results_menu(config, fastanime_runtime_state) anilist_results_menu(config, fastanime_runtime_state)
else: else:
exit(1) exit(1)

View File

@@ -26,7 +26,7 @@ def recent(config, dump_json):
from ...utils.tools import FastAnimeRuntimeState from ...utils.tools import FastAnimeRuntimeState
fastanime_runtime_state = FastAnimeRuntimeState() fastanime_runtime_state = FastAnimeRuntimeState()
fastanime_runtime_state.anilist_data = anime_data[1] fastanime_runtime_state.anilist_results_data = anime_data[1]
anilist_results_menu(config, fastanime_runtime_state) anilist_results_menu(config, fastanime_runtime_state)
else: else:
from sys import exit from sys import exit

View File

@@ -42,5 +42,5 @@ def rewatching(config: "Config", dump_json):
from ...utils.tools import FastAnimeRuntimeState from ...utils.tools import FastAnimeRuntimeState
fastanime_runtime_state = FastAnimeRuntimeState() fastanime_runtime_state = FastAnimeRuntimeState()
fastanime_runtime_state.anilist_data = anime_list[1] fastanime_runtime_state.anilist_results_data = anime_list[1]
anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state) anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state)

View File

@@ -25,7 +25,7 @@ def scores(config, dump_json):
from ...utils.tools import FastAnimeRuntimeState from ...utils.tools import FastAnimeRuntimeState
fastanime_runtime_state = FastAnimeRuntimeState() fastanime_runtime_state = FastAnimeRuntimeState()
fastanime_runtime_state.data = anime_data[1] fastanime_runtime_state.anilist_results_data = anime_data[1]
anilist_results_menu(config, fastanime_runtime_state) anilist_results_menu(config, fastanime_runtime_state)
else: else:
from sys import exit from sys import exit

View File

@@ -4,7 +4,7 @@ from ...completion_functions import anime_titles_shell_complete
tags_available = { tags_available = {
"Cast": ["Polyamorous"], "Cast": ["Polyamorous"],
"Cast / Main Cast": [ "Cast Main Cast": [
"Anti-Hero", "Anti-Hero",
"Elderly Protagonist", "Elderly Protagonist",
"Ensemble Cast", "Ensemble Cast",
@@ -18,7 +18,7 @@ tags_available = {
"Primarily Male Cast", "Primarily Male Cast",
"Primarily Teen Cast", "Primarily Teen Cast",
], ],
"Cast / Traits": [ "Cast Traits": [
"Age Regression", "Age Regression",
"Agender", "Agender",
"Aliens", "Aliens",
@@ -94,7 +94,7 @@ tags_available = {
], ],
"Demographic": ["Josei", "Kids", "Seinen", "Shoujo", "Shounen"], "Demographic": ["Josei", "Kids", "Seinen", "Shoujo", "Shounen"],
"Setting": ["Matriarchy"], "Setting": ["Matriarchy"],
"Setting / Scene": [ "Setting Scene": [
"Bar", "Bar",
"Boarding School", "Boarding School",
"Circus", "Circus",
@@ -117,7 +117,7 @@ tags_available = {
"Urban", "Urban",
"Work", "Work",
], ],
"Setting / Time": [ "Setting Time": [
"Achronological Order", "Achronological Order",
"Anachronism", "Anachronism",
"Ancient China", "Ancient China",
@@ -125,7 +125,7 @@ tags_available = {
"Historical", "Historical",
"Time Skip", "Time Skip",
], ],
"Setting / Universe": [ "Setting Universe": [
"Afterlife", "Afterlife",
"Alternate Universe", "Alternate Universe",
"Augmented Reality", "Augmented Reality",
@@ -152,7 +152,7 @@ tags_available = {
"Rotoscoping", "Rotoscoping",
"Stop Motion", "Stop Motion",
], ],
"Theme / Action": [ "Theme Action": [
"Archery", "Archery",
"Battle Royale", "Battle Royale",
"Espionage", "Espionage",
@@ -162,7 +162,7 @@ tags_available = {
"Spearplay", "Spearplay",
"Swordplay", "Swordplay",
], ],
"Theme / Arts": [ "Theme Arts": [
"Acting", "Acting",
"Calligraphy", "Calligraphy",
"Classic Literature", "Classic Literature",
@@ -174,7 +174,7 @@ tags_available = {
"Rakugo", "Rakugo",
"Writing", "Writing",
], ],
"Theme / Arts-Music": [ "Theme Arts-Music": [
"Band", "Band",
"Classical Music", "Classical Music",
"Dancing", "Dancing",
@@ -184,8 +184,8 @@ tags_available = {
"Musical Theater", "Musical Theater",
"Rock Music", "Rock Music",
], ],
"Theme / Comedy": ["Parody", "Satire", "Slapstick", "Surreal Comedy"], "Theme Comedy": ["Parody", "Satire", "Slapstick", "Surreal Comedy"],
"Theme / Drama": [ "Theme Drama": [
"Bullying", "Bullying",
"Class Struggle", "Class Struggle",
"Coming of Age", "Coming of Age",
@@ -198,7 +198,7 @@ tags_available = {
"Suicide", "Suicide",
"Tragedy", "Tragedy",
], ],
"Theme / Fantasy": [ "Theme Fantasy": [
"Alchemy", "Alchemy",
"Body Swapping", "Body Swapping",
"Cultivation", "Cultivation",
@@ -216,8 +216,8 @@ tags_available = {
"Wuxia", "Wuxia",
"Youkai", "Youkai",
], ],
"Theme / Game": ["Board Game", "E-Sports", "Video Games"], "Theme Game": ["Board Game", "E-Sports", "Video Games"],
"Theme / Game-Card & Board Game": [ "Theme Game-Card & Board Game": [
"Card Battle", "Card Battle",
"Go", "Go",
"Karuta", "Karuta",
@@ -225,7 +225,7 @@ tags_available = {
"Poker", "Poker",
"Shogi", "Shogi",
], ],
"Theme / Game-Sport": [ "Theme Game-Sport": [
"Acrobatics", "Acrobatics",
"Airsoft", "Airsoft",
"American Football", "American Football",
@@ -258,7 +258,7 @@ tags_available = {
"Volleyball", "Volleyball",
"Wrestling", "Wrestling",
], ],
"Theme / Other": [ "Theme Other": [
"Adoption", "Adoption",
"Animals", "Animals",
"Astronomy", "Astronomy",
@@ -308,7 +308,7 @@ tags_available = {
"Travel", "Travel",
"War", "War",
], ],
"Theme / Other-Organisations": [ "Theme Other-Organisations": [
"Assassins", "Assassins",
"Criminal Organization", "Criminal Organization",
"Cult", "Cult",
@@ -320,7 +320,7 @@ tags_available = {
"Triads", "Triads",
"Yakuza", "Yakuza",
], ],
"Theme / Other-Vehicle": [ "Theme Other-Vehicle": [
"Aviation", "Aviation",
"Cars", "Cars",
"Mopeds", "Mopeds",
@@ -329,7 +329,7 @@ tags_available = {
"Tanks", "Tanks",
"Trains", "Trains",
], ],
"Theme / Romance": [ "Theme Romance": [
"Age Gap", "Age Gap",
"Bisexual", "Bisexual",
"Boys' Love", "Boys' Love",
@@ -343,15 +343,15 @@ tags_available = {
"Unrequited Love", "Unrequited Love",
"Yuri", "Yuri",
], ],
"Theme / Sci Fi": [ "Theme Sci Fi": [
"Cyberpunk", "Cyberpunk",
"Space Opera", "Space Opera",
"Time Loop", "Time Loop",
"Time Manipulation", "Time Manipulation",
"Tokusatsu", "Tokusatsu",
], ],
"Theme / Sci Fi-Mecha": ["Real Robot", "Super Robot"], "Theme Sci Fi-Mecha": ["Real Robot", "Super Robot"],
"Theme / Slice of Life": [ "Theme Slice of Life": [
"Agriculture", "Agriculture",
"Cute Boys Doing Cute Things", "Cute Boys Doing Cute Things",
"Cute Girls Doing Cute Things", "Cute Girls Doing Cute Things",
@@ -386,6 +386,7 @@ for tag_category, tags_in_category in tags_available.items():
"--status", "--status",
"-S", "-S",
help="The media status of the anime", help="The media status of the anime",
multiple=True,
type=click.Choice( type=click.Choice(
["FINISHED", "RELEASING", "NOT_YET_RELEASED", "CANCELLED", "HIATUS"] ["FINISHED", "RELEASING", "NOT_YET_RELEASED", "CANCELLED", "HIATUS"]
), ),
@@ -486,57 +487,74 @@ for tag_category, tags_in_category in tags_available.items():
"-y", "-y",
type=click.Choice( type=click.Choice(
[ [
"2024",
"2023",
"2022",
"2021",
"2020",
"2019",
"2018",
"2017",
"2016",
"2015",
"2014",
"2013",
"2012",
"2011",
"2010",
"2009",
"2008",
"2007",
"2006",
"2005",
"2004",
"2000",
"1990",
"1980",
"1970",
"1960",
"1950",
"1940",
"1930",
"1920",
"1910",
"1900", "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", 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( def search(
config, title, dump_json, season, status, sort, genres, tags, media_format, year config,
title,
dump_json,
season,
status,
sort,
genres,
tags,
media_format,
year,
on_list,
): ):
from ....anilist import AniList from ....anilist import AniList
success, search_results = AniList.search( success, search_results = AniList.search(
query=title, query=title,
sort=sort, sort=sort,
status=status, status_in=list(status),
genre_in=list(genres), genre_in=list(genres),
season=season, season=season,
tag_in=list(tags), tag_in=list(tags),
seasonYear=year, seasonYear=year,
format_in=list(media_format), format_in=list(media_format),
on_list=on_list,
) )
if success: if success:
if dump_json: if dump_json:
@@ -548,7 +566,7 @@ def search(
from ...utils.tools import FastAnimeRuntimeState from ...utils.tools import FastAnimeRuntimeState
fastanime_runtime_state = FastAnimeRuntimeState() fastanime_runtime_state = FastAnimeRuntimeState()
fastanime_runtime_state.anilist_data = search_results fastanime_runtime_state.anilist_results_data = search_results
anilist_results_menu(config, fastanime_runtime_state) anilist_results_menu(config, fastanime_runtime_state)
else: else:
from sys import exit from sys import exit

View 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,
)
)

View File

@@ -26,7 +26,7 @@ def trending(config, dump_json):
from ...utils.tools import FastAnimeRuntimeState from ...utils.tools import FastAnimeRuntimeState
fastanime_runtime_state = FastAnimeRuntimeState() fastanime_runtime_state = FastAnimeRuntimeState()
fastanime_runtime_state.anilist_data = data fastanime_runtime_state.anilist_results_data = data
anilist_results_menu(config, fastanime_runtime_state) anilist_results_menu(config, fastanime_runtime_state)
else: else:
from sys import exit from sys import exit

View File

@@ -25,7 +25,7 @@ def upcoming(config, dump_json):
from ...utils.tools import FastAnimeRuntimeState from ...utils.tools import FastAnimeRuntimeState
fastanime_runtime_state = FastAnimeRuntimeState() fastanime_runtime_state = FastAnimeRuntimeState()
fastanime_runtime_state.anilist_data = data fastanime_runtime_state.anilist_results_data = data
anilist_results_menu(config, fastanime_runtime_state) anilist_results_menu(config, fastanime_runtime_state)
else: else:
from sys import exit from sys import exit

View File

@@ -42,5 +42,5 @@ def watching(config: "Config", dump_json):
from ...utils.tools import FastAnimeRuntimeState from ...utils.tools import FastAnimeRuntimeState
fastanime_runtime_state = FastAnimeRuntimeState() fastanime_runtime_state = FastAnimeRuntimeState()
fastanime_runtime_state.anilist_data = anime_list[1] fastanime_runtime_state.anilist_results_data = anime_list[1]
anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state) anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state)

View File

@@ -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") as file:
file.write(user_config.__repr__())
print("update successfull")
else: else:
import click
click.edit(filename=USER_CONFIG_PATH) click.edit(filename=USER_CONFIG_PATH)

View File

@@ -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)

View File

@@ -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))

View File

@@ -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,240 +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 ...Utility.data import anime_normalizer
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:
anilist_anime_info = None 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(), search_results_.keys(),
key=lambda title: fuzz.ratio( key=lambda title: fuzz.ratio(title, manga_title),
anime_normalizer.get(title, title), anime_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)
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} [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()
return
link = stream_link["link"]
stream_headers = servers[server]["headers"]
subtitles = servers[server]["subtitles"]
episode_title = servers[server]["episode_title"]
selected_anime_title = search_result selected_anime_title = search_result_manga_title
if anilist_anime_info: if anilist_anime_info:
selected_anime_title = ( selected_anime_title = (
anilist_anime_info["title"][config.preferred_language] anilist_anime_info["title"][config.preferred_language]
or anilist_anime_info["title"]["romaji"] or anilist_anime_info["title"]["romaji"]
or anilist_anime_info["title"]["english"] or anilist_anime_info["title"]["english"]
) )
import re import re
for episode_detail in anilist_anime_info["episodes"]: for episode_detail in anilist_anime_info["episodes"]:
if re.match(f"Episode {episode} ", episode_detail["title"]): if re.match(f"Episode {episode} ", episode_detail["title"]):
episode_title = episode_detail["title"] episode_title = episode_detail["title"]
break break
print( print(
f"[purple]Now Playing:[/] {selected_anime_title} Episode {episode}" f"[purple]Now Playing:[/] {selected_anime_title} Episode {episode}"
) )
subtitles = move_preferred_subtitle_lang_to_top( subtitles = move_preferred_subtitle_lang_to_top(
subtitles, config.sub_lang subtitles, config.sub_lang
) )
if config.sync_play: if config.sync_play:
from ..utils.syncplay import SyncPlayer from ..utils.syncplay import SyncPlayer
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()
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()

View File

@@ -6,20 +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
}
media(search:$query,type:ANIME){
id
idMal
title{
romaji
english
}
}
} }
media(search: $query, type: ANIME) {
id
idMal
title {
romaji
english
}
}
}
} }
""" """
@@ -63,6 +63,22 @@ 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):
incomplete = incomplete.strip() incomplete = incomplete.strip()
if not incomplete: if not incomplete:

View File

@@ -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,53 +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",
"normalize_titles": "true",
}
)
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)
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()
@@ -124,7 +80,7 @@ class Config(object):
self.auto_next = self.get_auto_next() self.auto_next = self.get_auto_next()
self.normalize_titles = self.get_normalize_titles() 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()
@@ -146,6 +102,9 @@ class Config(object):
self.user: dict = self.user_data.get("user", {}) self.user: dict = self.user_data.get("user", {})
os.environ["CURRENT_FASTANIME_PROVIDER"] = self.provider os.environ["CURRENT_FASTANIME_PROVIDER"] = self.provider
if not os.path.exists(USER_CONFIG_PATH):
with open(USER_CONFIG_PATH, "w") as config:
config.write(self.__repr__())
def update_user(self, user): def update_user(self, user):
self.user = user self.user = user
@@ -238,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")
@@ -273,111 +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}
# whether to normalize provider titles # 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} normalize_titles = {self.normalize_titles}
# can be [allanime,animepahe] # 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...")

View File

@@ -141,7 +141,7 @@ def media_player_controls(
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
player.create_player( player.create_player(
@@ -221,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
@@ -370,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)
@@ -378,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:
@@ -387,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":
@@ -546,7 +545,7 @@ def provider_anime_episode_servers_menu(
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
if start_time == "0" and episode_in_history != current_episode_number: if start_time == "0" and episode_in_history != current_episode_number:
@@ -582,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":
@@ -641,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
@@ -732,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
) )
@@ -740,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(
@@ -751,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)
@@ -786,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(
@@ -797,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"]
@@ -1070,13 +1072,13 @@ def media_actions_menu(
# update internal config # update internal config
if player == "syncplay": if player == "syncplay":
config.sync_play = True config.sync_play = True
config.use_mpv_mod = False config.use_python_mpv = False
else: else:
config.sync_play = False config.sync_play = False
if player == "mpv-mod": if player == "mpv-mod":
config.use_mpv_mod = True config.use_python_mpv = True
else: else:
config.use_mpv_mod = False config.use_python_mpv = False
media_actions_menu(config, fastanime_runtime_state) 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"):
@@ -1279,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:
@@ -1558,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:

View File

@@ -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,6 +176,63 @@ def get_rofi_icons(
logger.error("%r generated an exception: %s" % (url, e)) logger.error("%r generated an exception: %s" % (url, e))
# get rofi icons
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
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
"""
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(
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 # get rofi icons
def get_fzf_episode_preview( def get_fzf_episode_preview(
anilist_result: AnilistBaseMediaDataSchema, episodes, workers=None, wait=False anilist_result: AnilistBaseMediaDataSchema, episodes, workers=None, wait=False
@@ -264,6 +329,7 @@ def get_fzf_anime_preview(
THe fzf preview script to use THe fzf preview script to use
""" """
# ensure images and info exists # ensure images and info exists
from ...constants import S_PLATFORM
background_worker = Thread( background_worker = Thread(
target=write_search_results, args=(anilist_results, titles) target=write_search_results, args=(anilist_results, titles)
) )
@@ -272,21 +338,43 @@ def get_fzf_anime_preview(
# the preview script is in bash so making sure fzf doesnt use any other shell lang to process the preview script # 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" os.environ["SHELL"] = shutil.which("bash") or "bash"
preview = """ if S_PLATFORM == "win32":
%s preview = """
if [ -s %s/{} ]; then fzf-preview %s/{} %s
else echo Loading... title={}
fi dim=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}
if [ -s %s/{} ]; then cat %s/{} if [ -s "%s\\\\\\$title" ]; then
else echo Loading... if command -v chafa >/dev/null;then
fi chafa -f kitty -s $dim "%s\\\\\\$title"
""" % ( fi
fzf_preview, else echo Loading...
IMAGES_CACHE_DIR, fi
IMAGES_CACHE_DIR, if [ -s "%s\\\\\\$title" ]; then cat "%s\\\\\\$title"
ANIME_INFO_CACHE_DIR, else echo Loading...
ANIME_INFO_CACHE_DIR, 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: if wait:
background_worker.join() background_worker.join()
return preview return preview

View 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)

View File

@@ -10,6 +10,7 @@ if TYPE_CHECKING:
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):
@@ -110,7 +111,6 @@ 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")
@@ -164,7 +164,7 @@ class MpvPlayer(object):
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, start_time,
@@ -270,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")

View File

@@ -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
} }
""" """

View File

@@ -1,17 +1,30 @@
# 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):

View File

@@ -25,7 +25,7 @@ else:
# ----- 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) 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")

View File

@@ -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:
@@ -316,6 +329,7 @@ class AniListApi:
page: int | None = None, page: int | None = None,
season: str | None = None, season: str | None = None,
format_in: list[str] | None = None, format_in: list[str] | None = None,
on_list: bool | None = None,
type="ANIME", type="ANIME",
**kwargs, **kwargs,
): ):
@@ -324,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 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

View File

@@ -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,11 +217,10 @@ query ($userId: Int, $status: MediaListStatus,$type:MediaType) {
id id
} }
popularity popularity
streamingEpisodes{ streamingEpisodes {
title title
thumbnail thumbnail
} }
favourites favourites
averageScore averageScore
episodes episodes
@@ -177,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
@@ -204,7 +273,6 @@ query ($userId: Int, $status: MediaListStatus,$type:MediaType) {
day day
} }
createdAt createdAt
} }
} }
} }
@@ -235,76 +303,74 @@ $endDate_lesser:FuzzyDateInt,\
$format_in:[MediaFormat],\ $format_in:[MediaFormat],\
$type:MediaType\ $type:MediaType\
$season:MediaSeason\ $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
startDate:$startDate, startDate: $startDate
status_not_in:$status_not_in, status_not_in: $status_not_in
popularity_greater:$popularity_greater, popularity_greater: $popularity_greater
popularity_lesser:$popularity_lesser, popularity_lesser: $popularity_lesser
averageScore_greater:$averageScore_greater, averageScore_greater: $averageScore_greater
averageScore_lesser:$averageScore_lesser, averageScore_lesser: $averageScore_lesser
startDate_greater:$startDate_greater, startDate_greater: $startDate_greater
startDate_lesser:$startDate_lesser, startDate_lesser: $startDate_lesser
endDate_greater:$endDate_greater, endDate_greater: $endDate_greater
endDate_lesser:$endDate_lesser, endDate_lesser: $endDate_lesser
format_in:$format_in, format_in: $format_in
sort:$sort, sort: $sort
season:$season, season: $season
seasonYear:$seasonYear, seasonYear: $seasonYear
type:$type 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{ streamingEpisodes {
title title
thumbnail thumbnail
} }
favourites favourites
averageScore averageScore
episodes episodes
genres genres
studios{ studios {
nodes{ nodes {
name name
isAnimationStudio isAnimationStudio
} }
@@ -337,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
} }
@@ -356,11 +421,10 @@ query($type:MediaType){
id id
} }
popularity popularity
streamingEpisodes{ streamingEpisodes {
title title
thumbnail thumbnail
} }
favourites favourites
averageScore averageScore
genres genres
@@ -373,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
@@ -403,39 +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{ streamingEpisodes {
title title
thumbnail thumbnail
} }
streamingEpisodes {
streamingEpisodes{
title title
thumbnail thumbnail
} }
favourites favourites
averageScore averageScore
episodes episodes
@@ -448,7 +510,7 @@ query($type:MediaType){
} }
} }
tags { tags {
name name
} }
startDate { startDate {
year year
@@ -472,35 +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{ streamingEpisodes {
title title
thumbnail thumbnail
} }
episodes episodes
favourites favourites
averageScore averageScore
@@ -513,7 +573,7 @@ query($type:MediaType){
} }
} }
tags { tags {
name name
} }
startDate { startDate {
year year
@@ -537,40 +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{ streamingEpisodes {
title title
thumbnail 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
@@ -578,8 +636,8 @@ query($type:MediaType){
} }
} }
tags { tags {
name name
} }
startDate { startDate {
year year
month month
@@ -595,40 +653,46 @@ 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{ streamingEpisodes {
title title
thumbnail thumbnail
} }
favourites favourites
averageScore averageScore
@@ -642,7 +706,7 @@ query($type:MediaType){
} }
} }
tags { tags {
name name
} }
startDate { startDate {
year year
@@ -666,43 +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{ streamingEpisodes {
title title
thumbnail thumbnail
} }
favourites favourites
tags { tags {
name name
@@ -732,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 {
@@ -767,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
@@ -783,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 {
@@ -797,31 +864,30 @@ query ($id: Int,$type:MediaType) {
genres genres
averageScore averageScore
popularity popularity
streamingEpisodes{ streamingEpisodes {
title title
thumbnail 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
} }
} }
} }
} }
@@ -847,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
@@ -855,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
@@ -870,17 +941,16 @@ query ($page: Int,$type:MediaType) {
site site
id id
} }
mediaListEntry{ mediaListEntry {
status status
id id
progress progress
} }
popularity popularity
streamingEpisodes{ streamingEpisodes {
title title
thumbnail thumbnail
} }
favourites favourites
averageScore averageScore
genres genres
@@ -917,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
@@ -944,7 +1014,6 @@ query($id:Int){
node { node {
name { name {
full full
} }
gender gender
dateOfBirth { dateOfBirth {
@@ -997,10 +1066,10 @@ query($id:Int){
countryOfOrigin countryOfOrigin
averageScore averageScore
popularity popularity
streamingEpisodes{ streamingEpisodes {
title title
thumbnail thumbnail
} }
favourites favourites
source source

View File

@@ -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
@@ -165,6 +181,7 @@ class AnilistBaseMediaDataSchema(TypedDict):
nextAiringEpisode: AnilistMediaNextAiringEpisode nextAiringEpisode: AnilistMediaNextAiringEpisode
season: str season: str
streamingEpisodes: list[StreamingEpisode] streamingEpisodes: list[StreamingEpisode]
chapters: int
seasonYear: int seasonYear: int
duration: int duration: int
synonyms: list[str] synonyms: list[str]

View File

@@ -15,10 +15,7 @@ from .constants import ALLANIME_API_ENDPOINT, ALLANIME_BASE, ALLANIME_REFERER
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__)
@@ -54,18 +51,18 @@ class AllAnimeAPI(AnimeProvider):
}, },
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(
@@ -120,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):
@@ -147,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"
@@ -172,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:
@@ -235,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,7 +242,7 @@ class AllAnimeAPI(AnimeProvider):
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")
@@ -260,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 {
@@ -272,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 {
@@ -284,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 {
@@ -296,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 {
@@ -308,16 +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 []

View File

@@ -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
}
} }
""" """

View File

@@ -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__)
@@ -40,7 +39,7 @@ class AnimePaheApi(AnimeProvider):
response = self.session.get( response = self.session.get(
url, 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
@@ -68,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):
@@ -90,7 +89,7 @@ class AnimePaheApi(AnimeProvider):
response = self.session.get( response = self.session.get(
url, url,
) )
if response.status_code == 200: if response.ok:
if not data: if not data:
data.update(response.json()) data.update(response.json())
else: else:
@@ -151,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 = [
@@ -167,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]
@@ -207,24 +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_response = self.session.get(
embed_url, headers={"User-Agent": self.USER_AGENT, **SERVER_HEADERS} embed_url, headers={"User-Agent": self.USER_AGENT, **SERVER_HEADERS}
) )
if not response.status_code == 200: if not response.ok:
continue 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
@@ -237,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}")

View File

@@ -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();

View File

@@ -45,7 +45,7 @@ class AniWatchApi(AnimeProvider):
query = quote_plus(anime_title) query = quote_plus(anime_title)
url = f"https://hianime.to/search?keyword={query}" url = f"https://hianime.to/search?keyword={query}"
response = self.session.get(url) response = self.session.get(url)
if response.status_code != 200: if not response.ok:
return return
search_page = response.text search_page = response.text
search_results_html_items = get_elements_by_class("flw-item", search_page) search_results_html_items = get_elements_by_class("flw-item", search_page)
@@ -88,7 +88,7 @@ class AniWatchApi(AnimeProvider):
return {"pageInfo": {}, "results": results} return {"pageInfo": {}, "results": results}
except Exception as e: except Exception as e:
logger.error(e) logger.error(f"[ANIWATCH-ERROR]: {e}")
def get_anime(self, aniwatch_id, *args): def get_anime(self, aniwatch_id, *args):
try: try:
@@ -99,7 +99,7 @@ class AniWatchApi(AnimeProvider):
break 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(
@@ -140,7 +140,7 @@ class AniWatchApi(AnimeProvider):
"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:
@@ -154,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(
@@ -194,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)
@@ -207,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()
) )
@@ -231,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}")

View File

@@ -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}")

View 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)

View File

@@ -10,59 +10,118 @@ logger = logging.getLogger(__name__)
ANILIST_ENDPOINT = "https://graphql.anilist.co" ANILIST_ENDPOINT = "https://graphql.anilist.co"
""" """
query($query:String){ query ($query: String) {
Page(perPage:50){ Page(perPage: 50) {
pageInfo{ pageInfo {
total total
currentPage currentPage
hasNextPage hasNextPage
} }
media(search:$query,type:ANIME){ media(search: $query, type: ANIME) {
id id
idMal idMal
title{ title {
romaji romaji
english english
} }
episodes episodes
status status
nextAiringEpisode { nextAiringEpisode {
timeUntilAiring timeUntilAiring
airingAt airingAt
episode episode
} }
} }
} }
} }
""" """
def search_foranime_with_anilist(anime_title: str): def search_for_manga_with_anilist(manga_title: str):
query = """ query = """
query($query:String){ query ($query: String) {
Page(perPage:50){ Page(perPage: 50) {
pageInfo{ pageInfo {
total currentPage
currentPage }
hasNextPage media(search: $query, type: MANGA) {
id
idMal
title {
romaji
english
}
chapters
status
coverImage {
medium
large
} }
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": 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( response = post(
ANILIST_ENDPOINT, ANILIST_ENDPOINT,
@@ -111,23 +170,23 @@ def get_mal_id_and_anilist_id(anime_title: str) -> "dict[str,int] | None":
a boolean indicating success and none or an anilist object depending on success a boolean indicating success and none or an anilist object depending on success
""" """
query = """ query = """
query($query:String){ query ($query: String) {
Page(perPage:50){ Page(perPage: 50) {
pageInfo{ pageInfo {
total total
currentPage currentPage
hasNextPage hasNextPage
}
media(search: $query, type: ANIME) {
id
idMal
title {
romaji
english
} }
media(search:$query,type:ANIME){
id
idMal
title{
romaji
english
}
} }
} }
} }
""" """
try: try:
@@ -164,24 +223,24 @@ def get_basic_anime_info_by_title(anime_title: str):
a boolean indicating success and none or an anilist object depending on success a boolean indicating success and none or an anilist object depending on success
""" """
query = """ query = """
query($query:String){ query ($query: String) {
Page(perPage:50){ Page(perPage: 50) {
pageInfo{ pageInfo {
total total
} }
media(search:$query,type:ANIME){ media(search: $query, type: ANIME) {
id id
idMal idMal
title{ title {
romaji romaji
english english
} }
streamingEpisodes{ streamingEpisodes {
title title
} }
} }
} }
} }
""" """
from ...Utility.data import anime_normalizer from ...Utility.data import anime_normalizer

View File

@@ -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:")

View File

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

View 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})

View 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)

View 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"],
}

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "fastanime" name = "fastanime"
version = "2.4.2" version = "2.5.0"
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"

View File

@@ -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