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>
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-->
@@ -53,6 +53,7 @@ Heavily inspired by [animdl](https://github.com/justfoolingaround/animdl), [magi
- [MPV specific commands](#mpv-specific-commands)
- [Key Bindings](#key-bindings)
- [Script Messages](#script-messages)
- [styling the default interface](#styling-the-default-interface)
- [Configuration](#configuration)
- [Contributing](#contributing)
- [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
- [ffmpegthumbnailer](https://github.com/dirkvdb/ffmpegthumbnailer) used for local previews of downloaded anime
- [syncplay](https://syncplay.pl/) to enable watch together.
- [feh]() used in manga mode
## 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
- `--sub-lang <en/or any other common shortform for country>` regex is used to determine the appropriate. Only works when provider is aniwatch.
- `--normalize-titles/--no-normalize-titles` whether to normalize provider titles
- `--manga` toggle experimental manga mode
Example usage of the above options
@@ -252,13 +255,16 @@ fastanime --sync-play --server sharepoint search -t <anime-title>
fastanime --sync-play --server sharepoint anilist
# downloading dubbed anime
fastanime --dub download <anime>
fastanime --dub download -t <anime>
# use icons and fzf for a more elegant ui with preview
fastanime --icons --preview --fzf anilist
# use icons with default ui
fastanime --icons --default anilist
# viewing manga
fastanime --manga search -t <manga-title>
```
#### The anilist command :fire: :fire: :fire:
@@ -291,10 +297,11 @@ It offers the following options:
- `--title <anime-title>` or `-t <anime-title>`
- `--tags <tag>` or `-T <tag>` can be specified multiple times for different tags to filter by.
- `--year <year>` or `-y <year>`
- `--status <MediaStatus>` or `-S <MediaStatus>`
- `--status <MediaStatus>` or `-S <MediaStatus>` can be specified multiple times
- `--media-format <MediaFormat>` or `-f <MediaFormat>`
- `--season <MediaSeason>`
- `--genres <genre>` or `-g <genre>` can be specified multiple times.
- `--on-list/--not-on-list`
Example:
@@ -303,7 +310,9 @@ Example:
fastanime anilist search -T isekai
# 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
fastanime anilist search -y 2024 --season WINTER
@@ -529,6 +538,10 @@ fastanime downloads --time-to-seek <intRange(-1,100)>
# --- or ---
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
fastanime downloads --path
# 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`.
> [!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
#
# ███████╗░█████╗░░██████╗████████╗░█████╗░███╗░░██╗██╗███╗░░░███╗███████╗ ░█████╗░░█████╗░███╗░░██╗███████╗██╗░██████╗░
# ██╔════╝██╔══██╗██╔════╝╚══██╔══╝██╔══██╗████╗░██║██║████╗░████║██╔════╝ ██╔══██╗██╔══██╗████╗░██║██╔════╝██║██╔════╝░
# █████╗░░███████║╚█████╗░░░░██║░░░███████║██╔██╗██║██║██╔████╔██║█████╗░░ ██║░░╚═╝██║░░██║██╔██╗██║█████╗░░██║██║░░██╗░
# ██╔══╝░░██╔══██║░╚═══██╗░░░██║░░░██╔══██║██║╚████║██║██║╚██╔╝██║██╔══╝░░ ██║░░██╗██║░░██║██║╚████║██╔══╝░░██║██║░░╚██╗
# ██║░░░░░██║░░██║██████╔╝░░░██║░░░██║░░██║██║░╚███║██║██║░╚═╝░██║███████╗ ╚█████╔╝╚█████╔╝██║░╚███║██║░░░░░██║╚██████╔╝
# ╚═╝░░░░░╚═╝░░╚═╝╚═════╝░░░░╚═╝░░░╚═╝░░╚═╝╚═╝░░╚══╝╚═╝╚═╝░░░░░╚═╝╚══════╝ ░╚════╝░░╚════╝░╚═╝░░╚══╝╚═╝░░░░░╚═╝░╚═════╝░
#
[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]
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]
# 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
# force mpv window
# passed directly to mpv so values are same
force_window = immediate
# Preferred language for anime [dub/sub]
translation_type = sub
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.
# Note this wont always be correct.But 99% of the time will be.
auto_select=True
# whether to skip the opening and ending theme songs
# note requires ani-skip to be in path
skip=false
# 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 = False
# the maximum delta time in minutes after which the episode should be considered as completed
# used in the continue from time stamp
error=3
error = 3
# whether to use python mpv for enhanced experience
use_mpv_mod=False
# whether to use python-mpv [True/False]
# to enable superior control over the player
# adding more options to it
# Enable this one and you will be wonder why you did not discover fastanime sooner
# Since you basically don't have to close the player window to go to the next or previous episode, switch servers, change translation type or
change to a given episode x
# so try it if you haven't already
# if you have any issues setting it up
# don't be afraid to ask
# especially on windows
# honestly it can be a pain to set it up there
# personally it took me quite sometime to figure it out
# this is because of how windows handles shared libraries
# so just ask when you find yourself stuck
# or just switch to arch linux
use_python_mpv = False
# force mpv window
# the default 'immediate' just makes mpv to open the window even if the video has not yet loaded
# done for asthetics
# passed directly to mpv so values are same
force_window = immediate
# the format of downloaded anime and trailer
# based on yt-dlp format and passed directly to it
# learn more by looking it up on their site
# only works for downloaded anime if server=gogoanime
# since its the only one that offers different formats
# the others tend not to
format=best[height<=1080]/bestvideo[height<=1080]+bestaudio/best # default
# 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 = best[height<=1080]/bestvideo[height<=1080]+bestaudio/best
[general]
# can be [allanime,animepahe]
provider = allanime
preferred_language = romaji # Display language (options: english, romaji)
normalize_titles = true
downloads_dir = <Default-videos-dir>/FastAnime # Download directory
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
# 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
#
```
## Contributing

View File

@@ -12,7 +12,6 @@ from .libs.anime_provider import anime_sources
if TYPE_CHECKING:
from typing import Iterator
from .libs.anilist.types import AnilistBaseMediaDataSchema
from .libs.anime_provider.types import Anime, SearchResults, Server
logger = logging.getLogger(__name__)
@@ -51,7 +50,6 @@ class AnimeProvider:
self,
user_query,
translation_type,
anilist_obj: "AnilistBaseMediaDataSchema | None" = None,
nsfw=True,
unknown=True,
) -> "SearchResults | None":
@@ -73,14 +71,14 @@ class AnimeProvider:
user_query, translation_type, nsfw, unknown
)
except Exception as e:
logger.error(e)
logger.error(f"[ANIMEPROVIDER-ERROR]: {e}")
results = None
return results
def get_anime(
self,
anime_id: str,
anilist_obj: "AnilistBaseMediaDataSchema | None" = None,
) -> "Anime | None":
"""core abstraction over getting info of an anime from all providers
@@ -95,7 +93,8 @@ class AnimeProvider:
try:
results = anime_provider.get_anime(anime_id)
except Exception as e:
logger.error(e)
logger.error(f"[ANIMEPROVIDER-ERROR]: {e}")
results = None
return results
@@ -104,7 +103,6 @@ class AnimeProvider:
anime,
episode: str,
translation_type: str,
anilist_obj: "AnilistBaseMediaDataSchema|None" = None,
) -> "Iterator[Server] | None":
"""core abstractions for getting juicy streams from all providers
@@ -123,6 +121,7 @@ class AnimeProvider:
anime, episode, translation_type
)
except Exception as e:
logger.error(e)
logger.error(f"[ANIMEPROVIDER-ERROR]: {e}")
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",
},
"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.start()
# Function to download the file
# TODO: untpack the title to its actual values episode_title and anime_title
def _download_file(
self,
url: str,

View File

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

View File

@@ -40,6 +40,7 @@ signal.signal(signal.SIGINT, handle_exit)
short_help="Stream Anime",
)
@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-file", help="Allow logging to a file", 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(),
)
@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.pass_context
def run_cli(
ctx: click.Context,
manga,
log,
log_file,
rich_traceback,
@@ -177,12 +179,13 @@ def run_cli(
rofi_theme,
rofi_theme_confirm,
rofi_theme_input,
use_mpv_mod,
use_python_mpv,
sync_play,
):
from .config import Config
ctx.obj = Config()
ctx.obj.manga = manga
if log:
import logging
@@ -220,7 +223,10 @@ def run_cli(
if sync_play:
ctx.obj.sync_play = sync_play
if provider:
import os
ctx.obj.provider = provider
os.environ["CURRENT_FASTANIME_PROVIDER"] = provider
if server:
ctx.obj.server = server
if format:
@@ -254,10 +260,10 @@ def run_cli(
):
ctx.obj.auto_select = auto_select
if (
ctx.get_parameter_source("use_mpv_mod")
ctx.get_parameter_source("use_python_mpv")
== click.core.ParameterSource.COMMANDLINE
):
ctx.obj.use_mpv_mod = use_mpv_mod
ctx.obj.use_python_mpv = use_python_mpv
if downloads_dir:
ctx.obj.downloads_dir = downloads_dir
if translation_type:

View File

@@ -20,6 +20,7 @@ commands = {
"completed": "completed.completed",
"planning": "planning.planning",
"notifier": "notifier.notifier",
"stats": "stats.stats",
}

View File

@@ -42,5 +42,5 @@ def completed(config: "Config", dump_json):
from ...interfaces import anilist_interfaces
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)

View File

@@ -42,5 +42,5 @@ def dropped(config: "Config", dump_json):
from ...utils.tools import 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)

View File

@@ -26,7 +26,7 @@ def favourites(config, dump_json):
from ...utils.tools import 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)
else:
from sys import exit

View File

@@ -42,5 +42,5 @@ def paused(config: "Config", dump_json):
from ...utils.tools import 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)

View File

@@ -42,5 +42,5 @@ def planning(config: "Config", dump_json):
from ...utils.tools import 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)

View File

@@ -25,7 +25,7 @@ def popular(config, dump_json):
from ...utils.tools import 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)
else:
from sys import exit

View File

@@ -33,7 +33,7 @@ def random_anime(config, dump_json):
from ...utils.tools import 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)
else:
exit(1)

View File

@@ -26,7 +26,7 @@ def recent(config, dump_json):
from ...utils.tools import 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)
else:
from sys import exit

View File

@@ -42,5 +42,5 @@ def rewatching(config: "Config", dump_json):
from ...utils.tools import 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)

View File

@@ -25,7 +25,7 @@ def scores(config, dump_json):
from ...utils.tools import 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)
else:
from sys import exit

View File

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

View File

@@ -25,7 +25,7 @@ def upcoming(config, dump_json):
from ...utils.tools import 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)
else:
from sys import exit

View File

@@ -42,5 +42,5 @@ def watching(config: "Config", dump_json):
from ...utils.tools import 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)

View File

@@ -7,7 +7,7 @@ if TYPE_CHECKING:
@click.command(
help="Opens up your fastanime config in your preferred editor",
help="Manage your config with ease",
short_help="Edit your config",
)
@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",
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
def config(config: "Config", path, view, desktop_entry):
def config(user_config: "Config", path, view, desktop_entry, update):
import sys
from rich import print
@@ -32,7 +38,7 @@ def config(config: "Config", path, view, desktop_entry):
if path:
print(USER_CONFIG_PATH)
elif view:
print(config)
print(user_config)
elif desktop_entry:
import os
import shutil
@@ -87,7 +93,9 @@ def config(config: "Config", path, view, desktop_entry):
with open(desktop_entry_path) as f:
print(f"Successfully wrote \n{f.read()}")
exit_app(0)
elif update:
with open(USER_CONFIG_PATH, "w") as file:
file.write(user_config.__repr__())
print("update successfull")
else:
import click
click.edit(filename=USER_CONFIG_PATH)

View File

@@ -3,6 +3,8 @@ from typing import TYPE_CHECKING
import click
from ..completion_functions import downloaded_anime_titles
logger = logging.getLogger(__name__)
if TYPE_CHECKING:
from ..config import Config
@@ -12,16 +14,24 @@ if TYPE_CHECKING:
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(
"--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(
"--ffmpegthumbnailer-seek-time",
"--time-to-seek",
"-t",
type=click.IntRange(-1, 100),
help="ffmpegthumbnailer seek time [0-100]",
help="ffmpegthumbnailer seek time",
)
@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
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
)
downloaded_episodes = [*episodes, "Back"]
if config.use_fzf:
if not config.preview:
episode_title = fzf.run(
@@ -271,8 +282,12 @@ def downloads(config: "Config", path: bool, view_episodes, ffmpegthumbnailer_see
run_mpv(episode_path)
stream_episode(anime_playlist_path)
def stream_anime():
if config.use_fzf:
def stream_anime(title=None):
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:
playlist_name = fzf.run(
anime_downloads,
@@ -309,4 +324,4 @@ def downloads(config: "Config", path: bool, view_episodes, ffmpegthumbnailer_see
run_mpv(playlist)
stream_anime()
stream_anime()
stream_anime(title)

View File

@@ -56,26 +56,19 @@ def grab(
from thefuzz import fuzz
from ...AnimeProvider import AnimeProvider
logger = getLogger(__name__)
if config.manga:
manga_title = anime_titles[0]
from ...MangaProvider import MangaProvider
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:
manga_provider = MangaProvider()
search_data = manga_provider.search_for_manga(manga_title)
if not search_data:
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"]
print(json.dumps(search_data))
exit(0)
search_results = search_data["results"]
if not search_results:
logger.error("no results for your search")
exit(1)
@@ -83,83 +76,133 @@ def grab(
search_result["title"]: search_result for search_result in search_results
}
search_result = max(
search_results_.keys(), key=lambda title: fuzz.ratio(title, anime_title)
search_result_anime_title = max(
search_results_.keys(), key=lambda title: fuzz.ratio(title, anime_titles[0])
)
# ---- fetch anime ----
anime = anime_provider.get_anime(search_results_[search_result]["id"])
if not anime:
exit(1)
manga_info = manga_provider.get_manga(
search_results_[search_result_anime_title]["id"]
)
if not manga_info:
return
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
print(json.dumps(manga_info))
exit(0)
chapter_info = manga_provider.get_chapter_thumbnails(
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:
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
from ...cli.config import Config
from ..completion_functions import anime_titles_shell_complete
if TYPE_CHECKING:
from ...cli.config import Config
@click.command(
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)",
)
@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 rich import print
from rich.progress import Progress
from thefuzz import fuzz
from ...AnimeProvider import AnimeProvider
from ...libs.anime_provider.types import Anime
from ...libs.fzf import fzf
from ...libs.rofi import Rofi
from ...Utility.data import anime_normalizer
from ..utils.mpv import run_mpv
from ..utils.tools import exit_app
from ..utils.utils import (
filter_by_quality,
fuzzy_inquirer,
move_preferred_subtitle_lang_to_top,
)
from ..utils.utils import fuzzy_inquirer
anime_provider = AnimeProvider(config.provider)
anilist_anime_info = None
if config.manga:
from InquirerPy.prompts.number import NumberPrompt
from yt_dlp.utils import sanitize_filename
from ...MangaProvider import MangaProvider
from ..utils.feh import feh_manga_viewer
manga_title = anime_titles[0]
manga_provider = MangaProvider()
search_data = manga_provider.search_for_manga(manga_title)
if not search_data:
print("No search results")
exit(1)
search_results = search_data["results"]
print(f"[green bold]Streaming:[/] {anime_titles}")
for anime_title in anime_titles:
# ---- search for anime ----
with Progress() as progress:
progress.add_task("Fetching Search Results...", total=None)
search_results = anime_provider.search_for_anime(
anime_title, config.translation_type
)
if not search_results:
print("Search results not found")
input("Enter to retry")
search(config, anime_title, episode_range)
return
search_results = search_results["results"]
if not search_results:
print("Anime not found :cry:")
exit_app()
search_results_ = {
search_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:
search_result = max(
search_result_manga_title = max(
search_results_.keys(),
key=lambda title: fuzz.ratio(
anime_normalizer.get(title, title), anime_title
),
key=lambda title: fuzz.ratio(title, manga_title),
)
print("[cyan]Auto Selecting:[/] ", search_result)
print("[cyan]Auto Selecting:[/] ", search_result_manga_title)
else:
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:
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:
search_result = Rofi.run(choices, "Please Select Title")
search_result_manga_title = Rofi.run(choices, "Please Select Title")
else:
search_result = fuzzy_inquirer(
search_result_manga_title = fuzzy_inquirer(
choices,
"Please Select Title",
)
# ---- fetch selected anime ----
with Progress() as progress:
progress.add_task("Fetching Anime...", total=None)
anime: Anime | None = anime_provider.get_anime(
search_results_[search_result]["id"]
anilist_id = search_results_[search_result_manga_title]["id"]
manga_info = manga_provider.get_manga(anilist_id)
if not manga_info:
print("No manga info")
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:
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)
]
if not chapter_info:
print("No chapter info")
input("Enter to retry...")
_manga_viewer()
return
print(
f"[purple bold]Now Reading: [/] {search_result_manga_title} [cyan bold]Chapter:[/] {chapter_info['title']}"
)
feh_manga_viewer(chapter_info["thumbnails"], str(chapter_info["title"]))
if anilist_helper:
anilist_helper.update_anime_list(
{"mediaId": anilist_id, "progress": chapter_number}
)
_manga_viewer()
_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:
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} [cyan]Episode:[/] {episode}"
)
except StopIteration:
print("[green]Completed binge sequence[/]:smile:")
return
if not episode or episode not in episodes:
choices = [*episodes, "end"]
choices = list(search_results_.keys())
if config.use_fzf:
episode = fzf.run(
choices, "Select an episode: ", header=search_result
search_result_manga_title = fzf.run(
choices, "Please Select title: ", "FastAnime"
)
elif config.use_rofi:
episode = Rofi.run(choices, "Select an episode")
search_result_manga_title = Rofi.run(choices, "Please Select Title")
else:
episode = fuzzy_inquirer(
search_result_manga_title = fuzzy_inquirer(
choices,
"Select episode",
"Please Select Title",
)
if episode == "end":
return
# ---- fetch streams ----
# ---- fetch selected anime ----
with Progress() as progress:
progress.add_task("Fetching Episode Streams...", total=None)
streams = anime_provider.get_episode_streams(
anime, episode, config.translation_type
progress.add_task("Fetching Anime...", total=None)
anime: Anime | None = anime_provider.get_anime(
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
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")
# ---- fetch streams ----
with Progress() as progress:
progress.add_task("Fetching Episode Streams...", total=None)
streams = anime_provider.get_episode_streams(
anime, episode, config.translation_type
)
if not streams:
print("Failed to get streams")
return
try:
# ---- fetch servers ----
if config.server == "top":
with Progress() as progress:
progress.add_task("Fetching top server...", total=None)
server = next(streams, None)
if not server:
print("Sth went wrong when fetching the episode")
input("Enter to continue")
stream_anime()
return
stream_link = filter_by_quality(config.quality, server["links"])
if not stream_link:
print("Quality not found")
input("Enter to continue")
stream_anime()
return
stream_link = filter_by_quality(config.quality, server["links"])
if not stream_link:
print("Quality not found")
input("Enter to continue")
stream_anime()
return
link = stream_link["link"]
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
link = stream_link["link"]
subtitles = server["subtitles"]
stream_headers = server["headers"]
episode_title = server["episode_title"]
else:
if config.use_fzf:
server = fzf.run(servers_names, "Select an link: ")
elif config.use_rofi:
server = Rofi.run(servers_names, "Select an link")
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:
server = fuzzy_inquirer(
servers_names,
"Select link",
)
stream_link = filter_by_quality(
config.quality, servers[server]["links"]
)
if not stream_link:
print("Quality not found")
input("Enter to continue")
stream_anime()
return
link = stream_link["link"]
stream_headers = servers[server]["headers"]
subtitles = servers[server]["subtitles"]
episode_title = servers[server]["episode_title"]
if config.use_fzf:
server = fzf.run(servers_names, "Select an link: ")
elif config.use_rofi:
server = Rofi.run(servers_names, "Select an link")
else:
server = fuzzy_inquirer(
servers_names,
"Select link",
)
stream_link = filter_by_quality(
config.quality, servers[server]["links"]
)
if not stream_link:
print("Quality not found")
input("Enter to continue")
stream_anime()
return
link = stream_link["link"]
stream_headers = servers[server]["headers"]
subtitles = servers[server]["subtitles"]
episode_title = servers[server]["episode_title"]
selected_anime_title = search_result
if anilist_anime_info:
selected_anime_title = (
anilist_anime_info["title"][config.preferred_language]
or anilist_anime_info["title"]["romaji"]
or anilist_anime_info["title"]["english"]
)
import re
selected_anime_title = search_result_manga_title
if anilist_anime_info:
selected_anime_title = (
anilist_anime_info["title"][config.preferred_language]
or anilist_anime_info["title"]["romaji"]
or anilist_anime_info["title"]["english"]
)
import re
for episode_detail in anilist_anime_info["episodes"]:
if re.match(f"Episode {episode} ", episode_detail["title"]):
episode_title = episode_detail["title"]
break
print(
f"[purple]Now Playing:[/] {selected_anime_title} Episode {episode}"
)
subtitles = move_preferred_subtitle_lang_to_top(
subtitles, config.sub_lang
)
if config.sync_play:
from ..utils.syncplay import SyncPlayer
for episode_detail in anilist_anime_info["episodes"]:
if re.match(f"Episode {episode} ", episode_detail["title"]):
episode_title = episode_detail["title"]
break
print(
f"[purple]Now Playing:[/] {selected_anime_title} Episode {episode}"
)
subtitles = move_preferred_subtitle_lang_to_top(
subtitles, config.sub_lang
)
if config.sync_play:
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()

View File

@@ -6,20 +6,20 @@ ANILIST_ENDPOINT = "https://graphql.anilist.co"
anime_title_query = """
query($query:String){
Page(perPage:50){
pageInfo{
total
}
media(search:$query,type:ANIME){
id
idMal
title{
romaji
english
}
}
query ($query: String) {
Page(perPage: 50) {
pageInfo {
total
}
media(search: $query, type: ANIME) {
id
idMal
title {
romaji
english
}
}
}
}
"""
@@ -63,6 +63,22 @@ def get_anime_titles(query: str, variables: dict = {}):
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):
incomplete = incomplete.strip()
if not incomplete:

View File

@@ -4,8 +4,6 @@ import os
from configparser import ConfigParser
from typing import TYPE_CHECKING
from rich import print
from ..constants import USER_CONFIG_PATH, USER_DATA_PATH, USER_VIDEOS_DIR
from ..libs.rofi import Rofi
@@ -15,46 +13,7 @@ if TYPE_CHECKING:
class Config(object):
"""class that handles and manages configuration and user data throughout the clis lifespan
Attributes:
anime_list: [TODO:attribute]
watch_history: [TODO:attribute]
fastanime_anilist_app_login_url: [TODO:attribute]
anime_provider: [TODO:attribute]
user_data: [TODO:attribute]
configparser: [TODO:attribute]
downloads_dir: [TODO:attribute]
provider: [TODO:attribute]
use_fzf: [TODO:attribute]
use_rofi: [TODO:attribute]
skip: [TODO:attribute]
icons: [TODO:attribute]
preview: [TODO:attribute]
translation_type: [TODO:attribute]
sort_by: [TODO:attribute]
continue_from_history: [TODO:attribute]
auto_next: [TODO:attribute]
auto_select: [TODO:attribute]
use_mpv_mod: [TODO:attribute]
quality: [TODO:attribute]
notification_duration: [TODO:attribute]
error: [TODO:attribute]
server: [TODO:attribute]
format: [TODO:attribute]
force_window: [TODO:attribute]
preferred_language: [TODO:attribute]
rofi_theme: [TODO:attribute]
rofi_theme: [TODO:attribute]
rofi_theme_input: [TODO:attribute]
rofi_theme_input: [TODO:attribute]
rofi_theme_confirm: [TODO:attribute]
rofi_theme_confirm: [TODO:attribute]
watch_history: [TODO:attribute]
anime_list: [TODO:attribute]
user: [TODO:attribute]
"""
manga = False
sync_play = False
anime_list: list
watch_history: dict
@@ -63,53 +22,50 @@ class Config(object):
)
anime_provider: "AnimeProvider"
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:
self.initialize_user_data()
self.load_config()
def load_config(self):
self.configparser = ConfigParser(
{
"quality": "1080",
"auto_next": "False",
"auto_select": "True",
"sort_by": "search match",
"downloads_dir": USER_VIDEOS_DIR,
"translation_type": "sub",
"server": "top",
"continue_from_history": "True",
"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 = ConfigParser(self.default_options)
self.configparser.add_section("stream")
self.configparser.add_section("general")
self.configparser.add_section("anilist")
if not os.path.exists(USER_CONFIG_PATH):
with open(USER_CONFIG_PATH, "w") as config:
self.configparser.write(config)
self.configparser.read(USER_CONFIG_PATH)
# --- set config values from file or using defaults ---
if os.path.exists(USER_CONFIG_PATH):
self.configparser.read(USER_CONFIG_PATH)
self.downloads_dir = self.get_downloads_dir()
self.sub_lang = self.get_sub_lang()
self.provider = self.get_provider()
@@ -124,7 +80,7 @@ class Config(object):
self.auto_next = self.get_auto_next()
self.normalize_titles = self.get_normalize_titles()
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.notification_duration = self.get_notification_duration()
self.error = self.get_error()
@@ -146,6 +102,9 @@ class Config(object):
self.user: dict = self.user_data.get("user", {})
os.environ["CURRENT_FASTANIME_PROVIDER"] = self.provider
if not os.path.exists(USER_CONFIG_PATH):
with open(USER_CONFIG_PATH, "w") as config:
config.write(self.__repr__())
def update_user(self, user):
self.user = user
@@ -238,7 +197,7 @@ class Config(object):
return self.configparser.getboolean("stream", "continue_from_history")
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):
return self.configparser.getint("general", "notification_duration")
@@ -273,111 +232,195 @@ class Config(object):
self.configparser.write(config)
def __repr__(self):
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}
current_config_state = f"""\
#
# ███████╗░█████╗░░██████╗████████╗░█████╗░███╗░░██╗██╗███╗░░░███╗███████╗ ░█████╗░░█████╗░███╗░░██╗███████╗██╗░██████╗░
# ██╔════╝██╔══██╗██╔════╝╚══██╔══╝██╔══██╗████╗░██║██║████╗░████║██╔════╝ ██╔══██╗██╔══██╗████╗░██║██╔════╝██║██╔════╝░
# █████╗░░███████║╚█████╗░░░░██║░░░███████║██╔██╗██║██║██╔████╔██║█████╗░░ ██║░░╚═╝██║░░██║██╔██╗██║█████╗░░██║██║░░██╗░
# ██╔══╝░░██╔══██║░╚═══██╗░░░██║░░░██╔══██║██║╚████║██║██║╚██╔╝██║██╔══╝░░ ██║░░██╗██║░░██║██║╚████║██╔══╝░░██║██║░░╚██╗
# ██║░░░░░██║░░██║██████╔╝░░░██║░░░██║░░██║██║░╚███║██║██║░╚═╝░██║███████╗ ╚█████╔╝╚█████╔╝██║░╚███║██║░░░░░██║╚██████╔╝
# ╚═╝░░░░░╚═╝░░╚═╝╚═════╝░░░░╚═╝░░░╚═╝░░╚═╝╚═╝░░╚══╝╚═╝╚═╝░░░░░╚═╝╚══════╝ ░╚════╝░░╚════╝░╚═╝░░╚══╝╚═╝░░░░░╚═╝░╚═════╝░
#
[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}
# 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}
# 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}
# Download directory
# where you will find your videos after downloading them with 'fastanime download' command
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}
# 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 = {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}
# 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}
# 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_input = {self.rofi_theme_input}
rofi_theme_confirm = {self.rofi_theme_confirm}
# whether to show the icons
icons = {self.icons}
# the duration in minutes a notification will stay in the screen
# used by notifier command
notification_duration = {self.notification_duration}
"""
# 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
def __str__(self):
return self.__repr__()
# WARNING: depracated and will probably be removed
def update_anime_list(self, anime_id: int, remove=False):
if remove:
try:
self.anime_list.remove(anime_id)
print("Succesfully removed :cry:")
except Exception:
print(anime_id, "Nothing to remove :confused:")
else:
self.anime_list.append(anime_id)
self.user_data["animelist"] = list(set(self.anime_list))
self._update_user_data()
print("Succesfully added :smile:")
input("Enter to continue...")

View File

@@ -141,7 +141,7 @@ def media_player_controls(
headers=selected_server["headers"],
subtitles=subtitles,
)
elif config.use_mpv_mod:
elif config.use_python_mpv:
from ..utils.player import 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?",
default=True,
):
media_player_controls(config, fastanime_runtime_state)
media_actions_menu(config, fastanime_runtime_state)
return
# 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
provider_anime: "Anime" = fastanime_runtime_state.provider_anime
server_name = None
server_name = ""
# get streams for episode from provider
with Progress() as progress:
progress.add_task("Fetching Episode Streams...", total=None)
@@ -378,7 +378,6 @@ def provider_anime_episode_servers_menu(
provider_anime,
current_episode_number,
translation_type,
fastanime_runtime_state.selected_anime_anilist,
)
if not episode_streams_generator:
if not config.use_rofi:
@@ -387,7 +386,7 @@ def provider_anime_episode_servers_menu(
else:
if not Rofi.confirm("Sth went wrong!!Enter to continue..."):
exit(1)
provider_anime_episode_servers_menu(config, fastanime_runtime_state)
media_actions_menu(config, fastanime_runtime_state)
return
if config.server == "top":
@@ -546,7 +545,7 @@ def provider_anime_episode_servers_menu(
headers=selected_server["headers"],
subtitles=subtitles,
)
elif config.use_mpv_mod:
elif config.use_python_mpv:
from ..utils.player import player
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 update will only apply locally
# 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
)
if stop_time == "0" or total_time == "0":
@@ -641,7 +640,9 @@ def provider_anime_episodes_menu(
)
# prompt for episode number
total_episodes = provider_anime["availableEpisodesDetail"][translation_type]
total_episodes = sorted(
provider_anime["availableEpisodesDetail"][translation_type], key=float
)
current_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)
def fetch_anime_episode(config, fastanime_runtime_state: "FastAnimeRuntimeState"):
def fetch_anime_episode(
config: "Config", fastanime_runtime_state: "FastAnimeRuntimeState"
):
selected_anime: "SearchResult" = (
fastanime_runtime_state.provider_anime_search_result
)
@@ -740,7 +743,7 @@ def fetch_anime_episode(config, fastanime_runtime_state: "FastAnimeRuntimeState"
with Progress() as progress:
progress.add_task("Fetching Anime Info...", total=None)
provider_anime = anime_provider.get_anime(
selected_anime["id"], fastanime_runtime_state.selected_anime_anilist
selected_anime["id"],
)
if not provider_anime:
print(
@@ -751,7 +754,7 @@ def fetch_anime_episode(config, fastanime_runtime_state: "FastAnimeRuntimeState"
else:
if not Rofi.confirm("Sth went wrong!!Enter to continue..."):
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
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(
selected_anime_title,
translation_type,
selected_anime_anilist,
)
if not provider_search_results:
print(
@@ -797,7 +799,7 @@ def anime_provider_search_results_menu(
else:
if not Rofi.confirm("Sth went wrong!!Enter to continue..."):
exit(1)
return anime_provider_search_results_menu(config, fastanime_runtime_state)
return media_actions_menu(config, fastanime_runtime_state)
provider_search_results = {
anime["title"]: anime for anime in provider_search_results["results"]
@@ -1070,13 +1072,13 @@ def media_actions_menu(
# update internal config
if player == "syncplay":
config.sync_play = True
config.use_mpv_mod = False
config.use_python_mpv = False
else:
config.sync_play = False
if player == "mpv-mod":
config.use_mpv_mod = True
config.use_python_mpv = True
else:
config.use_mpv_mod = False
config.use_python_mpv = False
media_actions_menu(config, fastanime_runtime_state)
def _view_info(config: "Config", fastanime_runtime_state: "FastAnimeRuntimeState"):
@@ -1279,7 +1281,9 @@ def anilist_results_menu(
config: [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 = {}
for anime in search_results:
@@ -1558,7 +1562,7 @@ def fastanime_main_menu(
# anilist data is a (bool,data)
# the bool indicated success
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)
else:

View File

@@ -7,7 +7,7 @@ import textwrap
from threading import Thread
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 ...libs.anilist.types import AnilistBaseMediaDataSchema
@@ -93,7 +93,7 @@ def write_search_results(
# NOTE: Will probably make this a configuraable option
HEADER_COLOR = 215, 0, 95
SEPARATOR_COLOR = 208, 208, 208
SEPARATOR_WIDTH = 45
SEPARATOR_WIDTH = 30
# use concurency to download and write as fast as possible
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
future_to_task = {}
@@ -104,6 +104,11 @@ def write_search_results(
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
template = f"""
{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('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('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)}
"""
template = textwrap.dedent(template)
@@ -168,6 +176,63 @@ def get_rofi_icons(
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
def get_fzf_episode_preview(
anilist_result: AnilistBaseMediaDataSchema, episodes, workers=None, wait=False
@@ -264,6 +329,7 @@ def get_fzf_anime_preview(
THe fzf preview script to use
"""
# ensure images and info exists
from ...constants import S_PLATFORM
background_worker = Thread(
target=write_search_results, args=(anilist_results, titles)
)
@@ -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
os.environ["SHELL"] = shutil.which("bash") or "bash"
preview = """
%s
if [ -s %s/{} ]; then fzf-preview %s/{}
else echo Loading...
fi
if [ -s %s/{} ]; then cat %s/{}
else echo Loading...
fi
""" % (
fzf_preview,
IMAGES_CACHE_DIR,
IMAGES_CACHE_DIR,
ANIME_INFO_CACHE_DIR,
ANIME_INFO_CACHE_DIR,
)
if S_PLATFORM == "win32":
preview = """
%s
title={}
dim=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}
if [ -s "%s\\\\\\$title" ]; then
if command -v chafa >/dev/null;then
chafa -f kitty -s $dim "%s\\\\\\$title"
fi
else echo Loading...
fi
if [ -s "%s\\\\\\$title" ]; then cat "%s\\\\\\$title"
else echo Loading...
fi
""" % (
fzf_preview,
IMAGES_CACHE_DIR.replace("\\","\\\\\\"),
IMAGES_CACHE_DIR.replace("\\","\\\\\\"),
ANIME_INFO_CACHE_DIR.replace("\\","\\\\\\"),
ANIME_INFO_CACHE_DIR.replace("\\","\\\\\\"),
)
else:
preview = """
%s
if [ -s %s/{} ]; then fzf-preview %s/{}
else echo Loading...
fi
if [ -s %s/{} ]; then cat %s/{}
else echo Loading...
fi
""" % (
fzf_preview,
IMAGES_CACHE_DIR,
IMAGES_CACHE_DIR,
ANIME_INFO_CACHE_DIR,
ANIME_INFO_CACHE_DIR,
)
if wait:
background_worker.join()
return preview

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 ..config import Config
from .tools import FastAnimeRuntimeState
def format_time(duration_in_secs: float):
@@ -110,7 +111,6 @@ class MpvPlayer(object):
provider_anime,
current_episode_number,
translation_type,
fastanime_runtime_state.selected_anime_anilist,
)
if not episode_streams:
self.mpv_player.show_text("No streams were found")
@@ -164,7 +164,7 @@ class MpvPlayer(object):
self,
stream_link,
anime_provider: "AnimeProvider",
fastanime_runtime_state,
fastanime_runtime_state: "FastAnimeRuntimeState",
config: "Config",
title,
start_time,
@@ -270,7 +270,6 @@ class MpvPlayer(object):
mpv_player.show_text("Changing translation type...")
anime = anime_provider.get_anime(
fastanime_runtime_state.provider_anime_search_result["id"],
fastanime_runtime_state.selected_anime_anilist,
)
if not anime:
mpv_player.show_text("Failed to update translation type")

View File

@@ -9,70 +9,70 @@ fzf_preview = r"""
# - https://github.com/sharkdp/bat
# - https://github.com/hpjansson/chafa
# - https://iterm2.com/utilities/imgcat
fzf-preview(){
if [[ $# -ne 1 ]]; then
>&2 echo "usage: $0 FILENAME"
exit 1
fi
fzf-preview() {
if [[ $# -ne 1 ]]; then
>&2 echo "usage: $0 FILENAME"
exit 1
fi
file=${1/#\~\//$HOME/}
type=$(file --dereference --mime -- "$file")
file=${1/#\~\//$HOME/}
type=$(file --dereference --mime -- "$file")
if [[ ! $type =~ image/ ]]; then
if [[ $type =~ =binary ]]; then
file "$1"
exit
fi
if [[ ! $type =~ image/ ]]; then
if [[ $type =~ =binary ]]; then
file "$1"
exit
fi
# Sometimes bat is installed as batcat.
if command -v batcat > /dev/null; then
batname="batcat"
elif command -v bat > /dev/null; then
batname="bat"
else
cat "$1"
exit
fi
# Sometimes bat is installed as batcat.
if command -v batcat >/dev/null; then
batname="batcat"
elif command -v bat >/dev/null; then
batname="bat"
else
cat "$1"
exit
fi
${batname} --style="${BAT_STYLE:-numbers}" --color=always --pager=never -- "$file"
exit
fi
${batname} --style="${BAT_STYLE:-numbers}" --color=always --pager=never -- "$file"
exit
fi
dim=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}
if [[ $dim = x ]]; then
dim=$(stty size < /dev/tty | awk '{print $2 "x" $1}')
elif ! [[ $KITTY_WINDOW_ID ]] && (( FZF_PREVIEW_TOP + FZF_PREVIEW_LINES == $(stty size < /dev/tty | awk '{print $1}') )); then
# Avoid scrolling issue when the Sixel image touches the bottom of the screen
# * https://github.com/junegunn/fzf/issues/2544
dim=${FZF_PREVIEW_COLUMNS}x$((FZF_PREVIEW_LINES - 1))
fi
dim=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}
if [[ $dim = x ]]; then
dim=$(stty size </dev/tty | awk '{print $2 "x" $1}')
elif ! [[ $KITTY_WINDOW_ID ]] && ((FZF_PREVIEW_TOP + FZF_PREVIEW_LINES == $(stty size </dev/tty | awk '{print $1}'))); then
# Avoid scrolling issue when the Sixel image touches the bottom of the screen
# * https://github.com/junegunn/fzf/issues/2544
dim=${FZF_PREVIEW_COLUMNS}x$((FZF_PREVIEW_LINES - 1))
fi
# 1. Use kitty icat on kitty terminal
if [[ $KITTY_WINDOW_ID ]]; then
# 1. 'memory' is the fastest option but if you want the image to be scrollable,
# you have to use 'stream'.
#
# 2. The last line of the output is the ANSI reset code without newline.
# This confuses fzf and makes it render scroll offset indicator.
# So we remove the last line and append the reset code to its previous line.
kitty icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed '$d' | sed $'$s/$/\e[m/'
# 1. Use kitty icat on kitty terminal
if [[ $KITTY_WINDOW_ID ]]; then
# 1. 'memory' is the fastest option but if you want the image to be scrollable,
# you have to use 'stream'.
#
# 2. The last line of the output is the ANSI reset code without newline.
# This confuses fzf and makes it render scroll offset indicator.
# So we remove the last line and append the reset code to its previous line.
kitty icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed '$d' | sed $'$s/$/\e[m/'
# 2. Use chafa with Sixel output
elif command -v chafa > /dev/null; then
chafa -f sixel -s "$dim" "$file"
# Add a new line character so that fzf can display multiple images in the preview window
echo
# 2. Use chafa with Sixel output
elif command -v chafa >/dev/null; then
chafa -f sixel -s "$dim" "$file"
# Add a new line character so that fzf can display multiple images in the preview window
echo
# 3. If chafa is not found but imgcat is available, use it on iTerm2
elif command -v imgcat > /dev/null; then
# NOTE: We should use https://iterm2.com/utilities/it2check to check if the
# user is running iTerm2. But for the sake of simplicity, we just assume
# that's the case here.
imgcat -W "${dim%%x*}" -H "${dim##*x}" "$file"
# 3. If chafa is not found but imgcat is available, use it on iTerm2
elif command -v imgcat >/dev/null; then
# NOTE: We should use https://iterm2.com/utilities/it2check to check if the
# user is running iTerm2. But for the sake of simplicity, we just assume
# that's the case here.
imgcat -W "${dim%%x*}" -H "${dim##*x}" "$file"
# 4. Cannot find any suitable method to preview the image
else
file "$file"
fi
# 4. Cannot find any suitable method to preview the image
else
file "$file"
fi
}
"""

View File

@@ -1,17 +1,30 @@
# TODO: add typing
class FastAnimeRuntimeState(dict):
from typing import TYPE_CHECKING
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"""
def __getattr__(self, attr):
try:
return self.__getitem__(attr)
except KeyError:
raise AttributeError(
"%r object has no attribute %r" % (self.__class__.__name__, attr)
)
provider_current_episode_stream_link: str
provider_current_server: "Server"
provider_current_server_name: str
provider_available_episodes: list[str]
provider_current_episode_number: str
provider_server_episode_streams: list["EpisodeStream"]
provider_anime_title: str
provider_anime: "Anime"
provider_anime_search_result: "SearchResult"
def __setattr__(self, attr, value):
self.__setitem__(attr, value)
selected_anime_anilist: "AnilistBaseMediaDataSchema"
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):

View File

@@ -25,7 +25,7 @@ else:
# ----- user configs and data -----
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":
# app data
# app_data_dir_base = os.getenv("LOCALAPPDATA")

View File

@@ -15,6 +15,7 @@ from .queries_graphql import (
delete_list_entry_query,
get_logged_in_user_query,
get_medialist_item_query,
get_user_info,
media_list_mutation,
media_list_query,
most_favourite_query,
@@ -34,8 +35,9 @@ if TYPE_CHECKING:
AnilistMediaLists,
AnilistMediaListStatus,
AnilistNotifications,
AnilistUser,
AnilistUser_,
AnilistUserData,
AnilistViewerData,
)
logger = logging.getLogger(__name__)
ANILIST_ENDPOINT = "https://graphql.anilist.co"
@@ -77,7 +79,7 @@ class AniListApi:
return
if not success or not user:
return
user_info: AnilistUser = user["data"]["Viewer"]
user_info: "AnilistUser_" = user["data"]["Viewer"]
self.user_id = user_info["id"]
return user_info
@@ -91,7 +93,7 @@ class AniListApi:
"""
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
Args:
@@ -103,7 +105,18 @@ class AniListApi:
self.session.headers.update(self.headers)
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
Returns:
@@ -316,6 +329,7 @@ class AniListApi:
page: int | None = None,
season: str | None = None,
format_in: list[str] | None = None,
on_list: bool | None = None,
type="ANIME",
**kwargs,
):
@@ -324,7 +338,7 @@ class AniListApi:
"""
variables = {}
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
search_results = self.get_data(search_query, variables=variables)
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
"""
# TODO: Format the queries
mark_as_read_mutation = """
mutation{
UpdateUser{
@@ -17,7 +16,6 @@ query($id:Int){
pageInfo{
total
}
reviews(mediaId:$id){
summary
user{
@@ -35,50 +33,48 @@ query($id:Int){
"""
notification_query = """
query{
Page(perPage:5){
pageInfo {
total
}
notifications(resetNotificationCount:true,type:AIRING) {
... on AiringNotification {
id
type
episode
contexts
createdAt
media {
id
idMal
title {
romaji
english
}
coverImage{
medium
}
}
}
}
query {
Page(perPage: 5) {
pageInfo {
total
}
notifications(resetNotificationCount: true, type: AIRING) {
... on AiringNotification {
id
type
episode
contexts
createdAt
media {
id
idMal
title {
romaji
english
}
coverImage {
medium
}
}
}
}
}
}
"""
get_medialist_item_query = """
query($mediaId:Int){
MediaList(mediaId:$mediaId){
id
}
query ($mediaId: Int) {
MediaList(mediaId: $mediaId) {
id
}
}
"""
delete_list_entry_query = """
mutation($id:Int){
DeleteMediaListEntry(id:$id){
deleted
}
mutation ($id: Int) {
DeleteMediaListEntry(id: $id) {
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 = """
mutation($mediaId:Int,$scoreRaw:Int,$repeat:Int,$progress:Int,$status:MediaListStatus){
SaveMediaListEntry(mediaId:$mediaId,scoreRaw:$scoreRaw,progress:$progress,repeat:$repeat,status:$status){
mutation (
$mediaId: Int
$scoreRaw: Int
$repeat: Int
$progress: Int
$status: MediaListStatus
) {
SaveMediaListEntry(
mediaId: $mediaId
scoreRaw: $scoreRaw
progress: $progress
repeat: $repeat
status: $status
) {
id
status
mediaId
@@ -116,21 +188,19 @@ mutation($mediaId:Int,$scoreRaw:Int,$repeat:Int,$progress:Int,$status:MediaListS
month
day
}
}
}
"""
media_list_query = """
query ($userId: Int, $status: MediaListStatus,$type:MediaType) {
query ($userId: Int, $status: MediaListStatus, $type: MediaType) {
Page {
pageInfo {
currentPage
total
currentPage
total
}
mediaList(userId: $userId, status: $status, type: $type) {
mediaId
media {
id
idMal
@@ -147,11 +217,10 @@ query ($userId: Int, $status: MediaListStatus,$type:MediaType) {
id
}
popularity
streamingEpisodes{
title
thumbnail
}
streamingEpisodes {
title
thumbnail
}
favourites
averageScore
episodes
@@ -177,10 +246,10 @@ query ($userId: Int, $status: MediaListStatus,$type:MediaType) {
}
status
description
mediaListEntry{
status
id
progress
mediaListEntry {
status
id
progress
}
nextAiringEpisode {
timeUntilAiring
@@ -204,7 +273,6 @@ query ($userId: Int, $status: MediaListStatus,$type:MediaType) {
day
}
createdAt
}
}
}
@@ -235,76 +303,74 @@ $endDate_lesser:FuzzyDateInt,\
$format_in:[MediaFormat],\
$type:MediaType\
$season:MediaSeason\
$on_list:Boolean\
"
# FuzzyDateInt = (yyyymmdd)
# MediaStatus = (FINISHED,RELEASING,NOT_YET_RELEASED,CANCELLED,HIATUS)
search_query = (
"""
query($query:String,%s){
Page(perPage:50,page:$page){
pageInfo{
Page(perPage: 50, page: $page) {
pageInfo {
total
currentPage
hasNextPage
}
media(
search:$query,
id_in:$id_in,
genre_in:$genre_in,
genre_not_in:$genre_not_in,
tag_in:$tag_in,
tag_not_in:$tag_not_in,
status_in:$status_in,
status:$status,
startDate:$startDate,
status_not_in:$status_not_in,
popularity_greater:$popularity_greater,
popularity_lesser:$popularity_lesser,
averageScore_greater:$averageScore_greater,
averageScore_lesser:$averageScore_lesser,
startDate_greater:$startDate_greater,
startDate_lesser:$startDate_lesser,
endDate_greater:$endDate_greater,
endDate_lesser:$endDate_lesser,
format_in:$format_in,
sort:$sort,
season:$season,
seasonYear:$seasonYear,
type:$type
)
{
search: $query
id_in: $id_in
genre_in: $genre_in
genre_not_in: $genre_not_in
tag_in: $tag_in
tag_not_in: $tag_not_in
status_in: $status_in
status: $status
startDate: $startDate
status_not_in: $status_not_in
popularity_greater: $popularity_greater
popularity_lesser: $popularity_lesser
averageScore_greater: $averageScore_greater
averageScore_lesser: $averageScore_lesser
startDate_greater: $startDate_greater
startDate_lesser: $startDate_lesser
endDate_greater: $endDate_greater
endDate_lesser: $endDate_lesser
format_in: $format_in
sort: $sort
season: $season
seasonYear: $seasonYear
type: $type
onList:$on_list
) {
id
idMal
title{
idMal
title {
romaji
english
}
coverImage{
coverImage {
medium
large
}
trailer {
site
id
}
mediaListEntry{
status
mediaListEntry {
status
id
progress
}
}
popularity
streamingEpisodes{
streamingEpisodes {
title
thumbnail
}
}
favourites
averageScore
episodes
genres
studios{
nodes{
studios {
nodes {
name
isAnimationStudio
}
@@ -337,17 +403,16 @@ query($query:String,%s){
)
trending_query = """
query($type:MediaType){
Page(perPage:15){
media(sort:TRENDING_DESC,type:$type,genre_not_in:["hentai"]){
query ($type: MediaType) {
Page(perPage: 15) {
media(sort: TRENDING_DESC, type: $type, genre_not_in: ["hentai"]) {
id
idMal
title{
idMal
title {
romaji
english
}
coverImage{
coverImage {
medium
large
}
@@ -356,11 +421,10 @@ query($type:MediaType){
id
}
popularity
streamingEpisodes{
streamingEpisodes {
title
thumbnail
}
}
favourites
averageScore
genres
@@ -373,18 +437,18 @@ query($type:MediaType){
}
}
tags {
name
name
}
startDate {
year
month
day
}
mediaListEntry{
mediaListEntry {
status
id
progress
}
}
endDate {
year
month
@@ -403,39 +467,37 @@ query($type:MediaType){
# mosts
most_favourite_query = """
query($type:MediaType){
Page(perPage:15){
media(sort:FAVOURITES_DESC,type:$type,genre_not_in:["hentai"]){
query ($type: MediaType) {
Page(perPage: 15) {
media(sort: FAVOURITES_DESC, type: $type, genre_not_in: ["hentai"]) {
id
idMal
title{
idMal
title {
romaji
english
}
coverImage{
coverImage {
medium
large
}
trailer {
site
id
}
mediaListEntry{
mediaListEntry {
status
id
progress
}
}
popularity
streamingEpisodes{
streamingEpisodes {
title
thumbnail
}
streamingEpisodes{
}
streamingEpisodes {
title
thumbnail
}
}
favourites
averageScore
episodes
@@ -448,7 +510,7 @@ query($type:MediaType){
}
}
tags {
name
name
}
startDate {
year
@@ -472,35 +534,33 @@ query($type:MediaType){
"""
most_scored_query = """
query($type:MediaType){
Page(perPage:15){
media(sort:SCORE_DESC,type:$type,genre_not_in:["hentai"]){
query ($type: MediaType) {
Page(perPage: 15) {
media(sort: SCORE_DESC, type: $type, genre_not_in: ["hentai"]) {
id
idMal
title{
idMal
title {
romaji
english
}
coverImage{
coverImage {
medium
large
}
trailer {
site
id
}
mediaListEntry{
mediaListEntry {
status
id
progress
}
}
popularity
streamingEpisodes{
streamingEpisodes {
title
thumbnail
}
}
episodes
favourites
averageScore
@@ -513,7 +573,7 @@ query($type:MediaType){
}
}
tags {
name
name
}
startDate {
year
@@ -537,40 +597,38 @@ query($type:MediaType){
"""
most_popular_query = """
query($type:MediaType){
Page(perPage:15){
media(sort:POPULARITY_DESC,type:$type,genre_not_in:["hentai"]){
query ($type: MediaType) {
Page(perPage: 15) {
media(sort: POPULARITY_DESC, type: $type, genre_not_in: ["hentai"]) {
id
idMal
title{
idMal
title {
romaji
english
}
coverImage{
coverImage {
medium
large
}
trailer {
site
id
}
popularity
streamingEpisodes{
streamingEpisodes {
title
thumbnail
}
}
favourites
averageScore
description
episodes
genres
mediaListEntry{
mediaListEntry {
status
id
progress
}
}
studios {
nodes {
name
@@ -578,8 +636,8 @@ query($type:MediaType){
}
}
tags {
name
}
name
}
startDate {
year
month
@@ -595,40 +653,46 @@ query($type:MediaType){
timeUntilAiring
airingAt
episode
}
}
}
}
}
"""
most_recently_updated_query = """
query($type:MediaType){
Page(perPage:15){
media(sort:UPDATED_AT_DESC,type:$type,averageScore_greater:50,genre_not_in:["hentai"],status:RELEASING){
query ($type: MediaType) {
Page(perPage: 15) {
media(
sort: UPDATED_AT_DESC
type: $type
averageScore_greater: 50
genre_not_in: ["hentai"]
status: RELEASING
) {
id
idMal
title{
idMal
title {
romaji
english
}
coverImage{
coverImage {
medium
large
}
trailer {
site
id
id
}
mediaListEntry{
mediaListEntry {
status
id
progress
}
}
popularity
streamingEpisodes{
streamingEpisodes {
title
thumbnail
}
}
favourites
averageScore
@@ -642,7 +706,7 @@ query($type:MediaType){
}
}
tags {
name
name
}
startDate {
year
@@ -666,43 +730,41 @@ query($type:MediaType){
"""
recommended_query = """
query($type:MediaType){
Page(perPage:15) {
media( type: $type,genre_not_in:["hentai"]) {
recommendations(sort:RATING_DESC){
nodes{
media{
query ($type: MediaType) {
Page(perPage: 15) {
media(type: $type, genre_not_in: ["hentai"]) {
recommendations(sort: RATING_DESC) {
nodes {
media {
id
idMal
title{
idMal
title {
english
romaji
native
}
coverImage{
coverImage {
medium
large
}
mediaListEntry{
status
id
progress
mediaListEntry {
status
id
progress
}
description
episodes
trailer{
trailer {
site
id
}
genres
averageScore
popularity
streamingEpisodes{
title
thumbnail
}
streamingEpisodes {
title
thumbnail
}
favourites
tags {
name
@@ -732,9 +794,9 @@ query($type:MediaType){
"""
anime_characters_query = """
query($id:Int,$type:MediaType){
query ($id: Int, $type: MediaType) {
Page {
media(id:$id, type: $type) {
media(id: $id, type: $type) {
characters {
nodes {
name {
@@ -767,13 +829,18 @@ query($id:Int,$type:MediaType){
anime_relations_query = """
query ($id: Int,$type:MediaType) {
query ($id: Int, $type: MediaType) {
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 {
nodes {
id
idMal
idMal
title {
english
romaji
@@ -783,11 +850,11 @@ query ($id: Int,$type:MediaType) {
medium
large
}
mediaListEntry{
status
id
progress
}
mediaListEntry {
status
id
progress
}
description
episodes
trailer {
@@ -797,31 +864,30 @@ query ($id: Int,$type:MediaType) {
genres
averageScore
popularity
streamingEpisodes{
title
thumbnail
}
streamingEpisodes {
title
thumbnail
}
favourites
tags {
name
}
startDate {
year
month
day
}
endDate {
year
month
day
}
status
nextAiringEpisode {
timeUntilAiring
airingAt
episode
}
year
month
day
}
endDate {
year
month
day
}
status
nextAiringEpisode {
timeUntilAiring
airingAt
episode
}
}
}
}
@@ -847,7 +913,7 @@ query ($id: Int,$type:MediaType) {
"""
upcoming_anime_query = """
query ($page: Int,$type:MediaType) {
query ($page: Int, $type: MediaType) {
Page(page: $page) {
pageInfo {
total
@@ -855,9 +921,14 @@ query ($page: Int,$type:MediaType) {
currentPage
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
idMal
idMal
title {
romaji
english
@@ -870,17 +941,16 @@ query ($page: Int,$type:MediaType) {
site
id
}
mediaListEntry{
status
mediaListEntry {
status
id
progress
}
}
popularity
streamingEpisodes{
streamingEpisodes {
title
thumbnail
}
}
favourites
averageScore
genres
@@ -917,20 +987,20 @@ query ($page: Int,$type:MediaType) {
"""
anime_query = """
query($id:Int){
Page{
media(id:$id) {
query ($id: Int) {
Page {
media(id: $id) {
id
idMal
idMal
title {
romaji
english
}
mediaListEntry{
status
mediaListEntry {
status
id
progress
}
}
nextAiringEpisode {
timeUntilAiring
airingAt
@@ -944,7 +1014,6 @@ query($id:Int){
node {
name {
full
}
gender
dateOfBirth {
@@ -997,10 +1066,10 @@ query($id:Int){
countryOfOrigin
averageScore
popularity
streamingEpisodes{
streamingEpisodes {
title
thumbnail
}
}
favourites
source

View File

@@ -19,7 +19,7 @@ class AnilistImage(TypedDict):
large: str
class AnilistUser(TypedDict):
class AnilistUser_(TypedDict):
id: int
name: str
bannerImage: str | None
@@ -28,11 +28,26 @@ class AnilistUser(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):
data: AnilistViewer
data: AnilistUserInfo
class AnilistMediaTrailer(TypedDict):
@@ -69,7 +84,7 @@ class AnilistMediaNextAiringEpisode(TypedDict):
class AnilistReview(TypedDict):
summary: str
user: AnilistUser
user: AnilistUser_
class AnilistReviewNodes(TypedDict):
@@ -114,16 +129,17 @@ class AnilistCharactersEdges(TypedDict):
edges: list[AnilistCharactersEdge]
class AnilistMediaList_(TypedDict):
id: int
progress: int
AnilistMediaListStatus = Literal[
"CURRENT", "PLANNING", "COMPLETED", "DROPPED", "PAUSED", "REPEATING"
]
class AnilistMediaList_(TypedDict):
id: int
progress: int
status: AnilistMediaListStatus
class AnilistMediaListProperties(TypedDict):
status: AnilistMediaListStatus
score: float
@@ -165,6 +181,7 @@ class AnilistBaseMediaDataSchema(TypedDict):
nextAiringEpisode: AnilistMediaNextAiringEpisode
season: str
streamingEpisodes: list[StreamingEpisode]
chapters: int
seasonYear: int
duration: int
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
if TYPE_CHECKING:
from typing import Iterator
from ....libs.anime_provider.allanime.types import AllAnimeEpisode
from ....libs.anime_provider.types import Anime, Server
from .types import AllAnimeEpisode
logger = logging.getLogger(__name__)
@@ -54,18 +51,18 @@ class AllAnimeAPI(AnimeProvider):
},
timeout=10,
)
if response.status_code == 200:
if response.ok:
return response.json()["data"]
else:
logger.error("allanime(ERROR): ", response.text)
logger.error("[ALLANIME-ERROR]: ", response.text)
return {}
except Timeout:
logger.error(
"allanime(Error):Timeout exceeded this could mean allanime is down or you have lost internet connection"
"[ALLANIME-ERROR]: Timeout exceeded this could mean allanime is down or you have lost internet connection"
)
return {}
except Exception as e:
logger.error(f"allanime:Error: {e}")
logger.error(f"[ALLANIME-ERROR]: {e}")
return {}
def search_for_anime(
@@ -120,7 +117,7 @@ class AllAnimeAPI(AnimeProvider):
return normalized_search_results
except Exception as e:
logger.error(f"FA(AllAnime): {e}")
logger.error(f"[ALLANIME-ERROR]: {e}")
return {}
def get_anime(self, allanime_show_id: str):
@@ -147,8 +144,8 @@ class AllAnimeAPI(AnimeProvider):
}
return normalized_anime
except Exception as e:
logger.error(f"AllAnime(get_anime): {e}")
return None
logger.error(f"[ALLANIME-ERROR]: {e}")
return {}
def _get_anime_episode(
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)
return episode["episode"]
except Exception as e:
logger.error(f"FA(AllAnime): {e}")
logger.error(f"[ALLANIME-ERROR]: {e}")
return {}
def get_episode_streams(
self, anime: "Anime", episode_number: str, translation_type="sub"
) -> "Iterator[Server] | None":
def get_episode_streams(self, anime, episode_number: str, translation_type="sub"):
"""get the streams of an episode
Args:
@@ -235,7 +230,7 @@ class AllAnimeAPI(AnimeProvider):
"quality": "1080",
}
],
} # pyright:ignore
}
continue
# get the stream url for an episode of the defined source names
@@ -247,7 +242,7 @@ class AllAnimeAPI(AnimeProvider):
timeout=10,
)
if resp.status_code == 200:
if resp.ok:
match embed["sourceName"]:
case "Luf-mp4":
logger.debug("allanime:Found streams from gogoanime")
@@ -260,7 +255,7 @@ class AllAnimeAPI(AnimeProvider):
)
+ f"; Episode {episode_number}",
"links": give_random_quality(resp.json()["links"]),
} # pyright:ignore
}
case "Kir":
logger.debug("allanime:Found streams from wetransfer")
yield {
@@ -272,7 +267,7 @@ class AllAnimeAPI(AnimeProvider):
)
+ f"; Episode {episode_number}",
"links": give_random_quality(resp.json()["links"]),
} # pyright:ignore
}
case "S-mp4":
logger.debug("allanime:Found streams from sharepoint")
yield {
@@ -284,7 +279,7 @@ class AllAnimeAPI(AnimeProvider):
)
+ f"; Episode {episode_number}",
"links": give_random_quality(resp.json()["links"]),
} # pyright:ignore
}
case "Sak":
logger.debug("allanime:Found streams from dropbox")
yield {
@@ -296,7 +291,7 @@ class AllAnimeAPI(AnimeProvider):
)
+ f"; Episode {episode_number}",
"links": give_random_quality(resp.json()["links"]),
} # pyright:ignore
}
case "Default":
logger.debug("allanime:Found streams from wixmp")
yield {
@@ -308,16 +303,13 @@ class AllAnimeAPI(AnimeProvider):
)
+ f"; Episode {episode_number}",
"links": give_random_quality(resp.json()["links"]),
} # pyright:ignore
}
except Timeout:
logger.error(
"Timeout has been exceeded this could mean allanime is down or you have lost internet connection"
"[ALLANIME-ERROR]: Timeout has been exceeded this could mean allanime is down or you have lost internet connection"
)
except Exception as e:
logger.error(f"FA(Allanime): {e}")
logger.error(f"[ALLANIME-ERROR]: {e}")
except Exception as e:
logger.error(f"FA(Allanime): {e}")
logger.error(f"[ALLANIME-ERROR]: {e}")
return []

View File

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

View File

@@ -20,7 +20,6 @@ from .constants import (
from .utils import process_animepahe_embed_page
if TYPE_CHECKING:
from ..types import Anime
from .types import AnimePaheAnimePage, AnimePaheSearchPage, AnimeSearchResult
JUICY_STREAM_REGEX = re.compile(r"source='(.*)';")
logger = logging.getLogger(__name__)
@@ -40,7 +39,7 @@ class AnimePaheApi(AnimeProvider):
response = self.session.get(
url,
)
if not response.status_code == 200:
if not response.ok:
return
data: "AnimePaheSearchPage" = response.json()
self.search_page = data
@@ -68,7 +67,7 @@ class AnimePaheApi(AnimeProvider):
}
except Exception as e:
logger.error(f"AnimePahe(search): {e}")
logger.error(f"[ANIMEPAHE-ERROR]: {e}")
return {}
def get_anime(self, session_id: str, *args):
@@ -90,7 +89,7 @@ class AnimePaheApi(AnimeProvider):
response = self.session.get(
url,
)
if response.status_code == 200:
if response.ok:
if not data:
data.update(response.json())
else:
@@ -151,12 +150,10 @@ class AnimePaheApi(AnimeProvider):
],
}
except Exception as e:
logger.error(f"AnimePahe(anime): {e}")
logger.error(f"[ANIMEPAHE-ERROR]: {e}")
return {}
def get_episode_streams(
self, anime: "Anime", episode_number: str, translation_type, *args
):
def get_episode_streams(self, anime, episode_number: str, translation_type, *args):
try:
# extract episode details from memory
episode = [
@@ -167,7 +164,7 @@ class AnimePaheApi(AnimeProvider):
if not episode:
logger.error(
f"AnimePahe(streams): episode {episode_number} doesn't exist"
f"[ANIMEPAHE-ERROR]: episode {episode_number} doesn't exist"
)
return []
episode = episode[0]
@@ -207,24 +204,24 @@ class AnimePaheApi(AnimeProvider):
if not embed_url:
logger.warn(
"AnimePahe: embed url not found please report to the developers"
"[ANIMEPAHE-WARN]: embed url not found please report to the developers"
)
return []
# get embed page
embed_response = self.session.get(
embed_url, headers={"User-Agent": self.USER_AGENT, **SERVER_HEADERS}
)
if not response.status_code == 200:
if not response.ok:
continue
embed_page = embed_response.text
decoded_js = process_animepahe_embed_page(embed_page)
if not decoded_js:
logger.error("Animepahe: failed to decode embed page")
logger.error("[ANIMEPAHE-ERROR]: failed to decode embed page")
return
juicy_stream = JUICY_STREAM_REGEX.search(decoded_js)
if not juicy_stream:
logger.error("Animepahe: failed to find juicy stream")
logger.error("[ANIMEPAHE-ERROR]: failed to find juicy stream")
return
juicy_stream = juicy_stream.group(1)
# add the link
@@ -237,4 +234,4 @@ class AnimePaheApi(AnimeProvider):
)
yield streams
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,
base_a: int,
no_of_keys_c: int,
key_values_k: list,
decode_mapper_d: dict = {},
values_to_replace_with_k: list,
):
decode_mapper_d: dict = {}
for i in range(no_of_keys_c):
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
return re.sub(
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__":
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,{}))"""
a = 62
c = 102
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(
"|"
)
# Testing time
filepath = input("Enter file name: ")
if filepath:
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')});"
result = animepahe_embed_decoder(
p,
a,
c,
k,
)
print(result) # Output: j player = B A();
print(process_animepahe_embed_page(data))

View File

@@ -45,7 +45,7 @@ class AniWatchApi(AnimeProvider):
query = quote_plus(anime_title)
url = f"https://hianime.to/search?keyword={query}"
response = self.session.get(url)
if response.status_code != 200:
if not response.ok:
return
search_page = response.text
search_results_html_items = get_elements_by_class("flw-item", search_page)
@@ -88,7 +88,7 @@ class AniWatchApi(AnimeProvider):
return {"pageInfo": {}, "results": results}
except Exception as e:
logger.error(e)
logger.error(f"[ANIWATCH-ERROR]: {e}")
def get_anime(self, aniwatch_id, *args):
try:
@@ -99,7 +99,7 @@ class AniWatchApi(AnimeProvider):
break
anime_url = f"https://hianime.to/ajax/v2/episode/list/{aniwatch_id}"
response = self.session.get(anime_url, timeout=10)
if response.status_code == 200:
if response.ok:
response_json = response.json()
aniwatch_anime_page = response_json["html"]
episodes_info_container_html = get_element_html_by_class(
@@ -140,7 +140,7 @@ class AniWatchApi(AnimeProvider):
"episodes_info": self.episodes_info,
}
except Exception as e:
logger.error(e)
logger.error(f"[ANIWACTCH-ERROR]: {e}")
def get_episode_streams(self, anime, episode, translation_type, *args):
try:
@@ -154,7 +154,7 @@ class AniWatchApi(AnimeProvider):
episode_details = episode_details[0]
episode_url = f"https://hianime.to/ajax/v2/episode/servers?episodeId={episode_details['id']}"
response = self.session.get(episode_url)
if response.status_code == 200:
if response.ok:
response_json = response.json()
episode_page_html = response_json["html"]
servers_containers_html = get_elements_html_by_class(
@@ -194,7 +194,7 @@ class AniWatchApi(AnimeProvider):
servers_info = extract_attributes(server_html)
embed_url = f"https://hianime.to/ajax/v2/episode/sources?id={servers_info['data-id']}"
embed_response = self.session.get(embed_url)
if embed_response.status_code == 200:
if embed_response.ok:
embed_json = embed_response.json()
raw_link_to_streams = embed_json["link"]
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_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" = (
link_to_streams_response.json()
)
@@ -231,6 +231,6 @@ class AniWatchApi(AnimeProvider):
),
}
except Exception as e:
logger.error(e)
logger.error(f"[ANIWATCH_ERROR]: {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"
"""
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
}
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_foranime_with_anilist(anime_title: str):
def search_for_manga_with_anilist(manga_title: str):
query = """
query($query:String){
Page(perPage:50){
pageInfo{
total
currentPage
hasNextPage
query ($query: String) {
Page(perPage: 50) {
pageInfo {
currentPage
}
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(
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
"""
query = """
query($query:String){
Page(perPage:50){
pageInfo{
total
currentPage
hasNextPage
query ($query: String) {
Page(perPage: 50) {
pageInfo {
total
currentPage
hasNextPage
}
media(search: $query, type: ANIME) {
id
idMal
title {
romaji
english
}
media(search:$query,type:ANIME){
id
idMal
title{
romaji
english
}
}
}
}
}
"""
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
"""
query = """
query($query:String){
Page(perPage:50){
pageInfo{
total
query ($query: String) {
Page(perPage: 50) {
pageInfo {
total
}
media(search:$query,type:ANIME){
id
idMal
title{
romaji
english
}
streamingEpisodes{
media(search: $query, type: ANIME) {
id
idMal
title {
romaji
english
}
streamingEpisodes {
title
}
}
}
}
}
}
"""
from ...Utility.data import anime_normalizer

View File

@@ -5,7 +5,6 @@ import subprocess
import sys
from typing import Callable, List
# TODO: will probably scrap art not to useful
from click import clear
from rich import print
@@ -123,7 +122,9 @@ class FZF:
[self.FZF_EXECUTABLE, *commands],
input=fzf_input,
stdout=subprocess.PIPE,
universal_newlines=True,
text=True,
encoding="utf-8"
)
if not result or result.returncode != 0 or not result.stdout:
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]
name = "fastanime"
version = "2.4.2"
version = "2.5.0"
description = "A browser anime site experience from the terminal"
authors = ["Benextempest <benextempest@gmail.com>"]
license = "UNLICENSE"

View File

@@ -1,4 +1,3 @@
# TODO: Write tests to make sure all click commands work
import pytest
from click.testing import CliRunner