Compare commits

...

22 Commits

Author SHA1 Message Date
Benex254
8f3834453c chore: bump version 2024-08-19 15:28:04 +03:00
Benex254
7ad8b8a0e3 fix: return values 2024-08-19 15:25:36 +03:00
Benex254
80b41f06da feat:add new ui command 2024-08-19 15:25:05 +03:00
Benex254
e79321ed50 chore: bump version 2024-08-19 13:05:03 +03:00
Benex254
f7b5898dfa fix: some stuff 2024-08-19 13:04:30 +03:00
Benex254
144bf53081 chore: bump version 2024-08-19 11:01:13 +03:00
Benex254
16dded9724 fix: inability to properly detect terminal 2024-08-19 10:51:39 +03:00
Benex254
c47b158bff fix: logging issue 2024-08-19 10:51:11 +03:00
Benex254
9a36e15d9d feat: intergrate subs to python-mpv based player 2024-08-19 10:37:04 +03:00
Benex254
d6b2bd7761 fix: ep title 2024-08-19 10:36:20 +03:00
Benex254
2346552dc4 fix: logging issue 2024-08-19 00:38:51 +03:00
Benex254
ba275055db fix: logging issue 2024-08-19 00:38:29 +03:00
Benex254
de4ddf2f3a chore: bump version 2024-08-19 00:21:48 +03:00
Benex254
9c94d824d1 fix: rearrange servers available 2024-08-19 00:21:16 +03:00
Benex254
495f3cfbf6 chore: bump version 2024-08-18 23:59:30 +03:00
Benex254
b56c9ae3dd docs: update reamde 2024-08-18 23:59:16 +03:00
Benex254
5e9ef87526 feat: improve provider api 2024-08-18 23:55:29 +03:00
Benex254
b68d6d6fe9 feat: accomodate subtitle streams 2024-08-18 23:54:59 +03:00
Benex254
5870cc6640 feat: accomodate subtitle streams 2024-08-18 23:54:36 +03:00
Benex254
7a43d58d82 fix: command order 2024-08-18 23:54:16 +03:00
Benex254
fc7efebc8d feat: accomodate subtitle streams 2024-08-18 23:53:36 +03:00
Benex254
528be74194 feat(aniwatch): init 2024-08-18 23:52:18 +03:00
30 changed files with 657 additions and 159 deletions

View File

@@ -51,7 +51,6 @@ Heavily inspired by [animdl](https://github.com/justfoolingaround/animdl), [magi
- [Key Bindings](#key-bindings) - [Key Bindings](#key-bindings)
- [Script Messages](#script-messages) - [Script Messages](#script-messages)
- [Configuration](#configuration) - [Configuration](#configuration)
- [The python api](#the-python-api)
- [Contributing](#contributing) - [Contributing](#contributing)
- [Receiving Support](#receiving-support) - [Receiving Support](#receiving-support)
- [Supporting the Project](#supporting-the-project) - [Supporting the Project](#supporting-the-project)
@@ -59,7 +58,7 @@ Heavily inspired by [animdl](https://github.com/justfoolingaround/animdl), [magi
> [!IMPORTANT] > [!IMPORTANT]
> >
> This project currently scrapes allanime and animepahe. The site is in the public domain and can be accessed by any one with a browser. > This project currently scrapes allanime, aniwatch and animepahe. The site is in the public domain and can be accessed by any one with a browser.
## Installation ## Installation
@@ -234,6 +233,7 @@ Available options for the fastanime include:
- `--use-mpv-mod/--use-default-player` whether to use python-mpv - `--use-mpv-mod/--use-default-player` whether to use python-mpv
- `--provider <allanime/animepahe>` anime site of choice to scrape from - `--provider <allanime/animepahe>` anime site of choice to scrape from
- `--sync-play` or `-sp` use syncplay for streaming anime so you can watch with your friends - `--sync-play` or `-sp` use syncplay for streaming anime so you can watch with your friends
- `--sub_lang <en/or any other common shortform for country>` regex is used to determine the appropriate. Only works when provider is aniwatch.
Example usage of the above options Example usage of the above options
@@ -654,28 +654,6 @@ notification_duration=2
# Not implemented yet # Not implemented yet
``` ```
## The python api
The project offers a python api that can be used in other python programs.
```python
from fastanime.AnimeProvider import AnimeProvider
# all output is typed, so will be easy to work with
# providers include [allanime, animepahe]
provider = AnimeProvider(provider="allanime")
# to search for anime
provider.search_for_anime()
# to get anime info
provider.get_anime()
# to get streams of an episode
provider.get_episode_streams()
```
## Contributing ## Contributing
We welcome your issues and feature requests. However, due to time constraints, we currently do not plan to add another provider. We welcome your issues and feature requests. However, due to time constraints, we currently do not plan to add another provider.

View File

@@ -37,12 +37,12 @@ class AnimeProvider:
self.provider = provider self.provider = provider
self.dynamic = dynamic self.dynamic = dynamic
self.retries = retries self.retries = retries
self.lazyload_provider() self.lazyload_provider(self.provider)
def lazyload_provider(self): def lazyload_provider(self, provider):
"""updates the current provider being used""" """updates the current provider being used"""
_, anime_provider_cls_name = anime_sources[self.provider].split(".", 1) _, anime_provider_cls_name = anime_sources[provider].split(".", 1)
package = f"fastanime.libs.anime_provider.{self.provider}" package = f"fastanime.libs.anime_provider.{provider}"
provider_api = importlib.import_module(".api", package) provider_api = importlib.import_module(".api", package)
anime_provider = getattr(provider_api, anime_provider_cls_name) anime_provider = getattr(provider_api, anime_provider_cls_name)
self.anime_provider = anime_provider() self.anime_provider = anime_provider()
@@ -73,7 +73,7 @@ class AnimeProvider:
user_query, translation_type, nsfw, unknown user_query, translation_type, nsfw, unknown
) )
except Exception as e: except Exception as e:
logging.error(e) logger.error(e)
results = None results = None
return results return results
@@ -95,7 +95,7 @@ class AnimeProvider:
try: try:
results = anime_provider.get_anime(anime_id) results = anime_provider.get_anime(anime_id)
except Exception as e: except Exception as e:
logging.error(e) logger.error(e)
results = None results = None
return results return results
@@ -123,6 +123,6 @@ class AnimeProvider:
anime, episode, translation_type anime, episode, translation_type
) )
except Exception as e: except Exception as e:
logging.error(e) logger.error(e)
results = None results = None
return results # pyright:ignore return results # pyright:ignore

View File

@@ -38,6 +38,7 @@ class YtDLPDownloader:
force_unknown_ext=False, force_unknown_ext=False,
verbose=False, verbose=False,
headers={}, headers={},
sub="",
): ):
"""Helper function that downloads anime given url and path details """Helper function that downloads anime given url and path details
@@ -60,9 +61,11 @@ class YtDLPDownloader:
"format": vid_format, "format": vid_format,
"compat_opts": ("allow-unsafe-ext",) if force_unknown_ext else tuple(), "compat_opts": ("allow-unsafe-ext",) if force_unknown_ext else tuple(),
} }
urls = [url]
if sub:
urls.append(sub)
with yt_dlp.YoutubeDL(ydl_opts) as ydl: with yt_dlp.YoutubeDL(ydl_opts) as ydl:
ydl.download([url]) ydl.download(urls)
# WARN: May remove this legacy functionality # WARN: May remove this legacy functionality
def download_file(self, url: str, title, silent=True): def download_file(self, url: str, title, silent=True):

View File

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

View File

@@ -98,6 +98,11 @@ signal.signal(signal.SIGINT, handle_exit)
type=click.Choice(["dub", "sub"]), type=click.Choice(["dub", "sub"]),
help="Anime language[dub/sub]", help="Anime language[dub/sub]",
) )
@click.option(
"-sl",
"--sub-lang",
help="Set the preferred language for subs",
)
@click.option( @click.option(
"-A/-no-A", "-A/-no-A",
"--auto-next/--no-auto-next", "--auto-next/--no-auto-next",
@@ -156,6 +161,7 @@ def run_cli(
local_history, local_history,
skip, skip,
translation_type, translation_type,
sub_lang,
quality, quality,
auto_next, auto_next,
auto_select, auto_select,
@@ -186,7 +192,7 @@ def run_cli(
FORMAT = "%(message)s" FORMAT = "%(message)s"
logging.basicConfig( logging.basicConfig(
level="NOTSET", format=FORMAT, datefmt="[%X]", handlers=[RichHandler()] level="debug", format=FORMAT, datefmt="[%X]", handlers=[RichHandler()]
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logger.info("logging has been initialized") logger.info("logging has been initialized")
@@ -203,6 +209,10 @@ def run_cli(
datefmt="[%d/%m/%Y@%H:%M:%S]", datefmt="[%d/%m/%Y@%H:%M:%S]",
filemode="w", filemode="w",
) )
else:
import logging
logging.basicConfig(level=logging.CRITICAL)
if rich_traceback: if rich_traceback:
from rich.traceback import install from rich.traceback import install
@@ -216,6 +226,8 @@ def run_cli(
ctx.obj.server = server ctx.obj.server = server
if format: if format:
ctx.obj.format = format ctx.obj.format = format
if sub_lang:
ctx.obj.sub_lang = sub_lang
if ctx.get_parameter_source("continue_") == click.core.ParameterSource.COMMANDLINE: if ctx.get_parameter_source("continue_") == click.core.ParameterSource.COMMANDLINE:
ctx.obj.continue_from_history = continue_ ctx.obj.continue_from_history = continue_
if ctx.get_parameter_source("skip") == click.core.ParameterSource.COMMANDLINE: if ctx.get_parameter_source("skip") == click.core.ParameterSource.COMMANDLINE:

View File

@@ -59,7 +59,11 @@ def download(
from ...libs.fzf import fzf from ...libs.fzf import fzf
from ...Utility.downloader.downloader import downloader from ...Utility.downloader.downloader import downloader
from ..utils.tools import exit_app from ..utils.tools import exit_app
from ..utils.utils import filter_by_quality, fuzzy_inquirer from ..utils.utils import (
filter_by_quality,
fuzzy_inquirer,
move_preferred_subtitle_lang_to_top,
)
anime_provider = AnimeProvider(config.provider) anime_provider = AnimeProvider(config.provider)
@@ -168,37 +172,12 @@ def download(
if config.server == "top": if config.server == "top":
with Progress() as progress: with Progress() as progress:
progress.add_task("Fetching top server...", total=None) progress.add_task("Fetching top server...", total=None)
server = next(streams, None) server_name = next(streams, None)
if not server: if not server_name:
print("Sth went wrong when fetching the server") print("Sth went wrong when fetching the server")
continue continue
stream_link = filter_by_quality(config.quality, server["links"])
if not stream_link:
print("[yellow bold]WARNING:[/] No streams found")
time.sleep(1)
print("Continuing...")
continue
link = stream_link["link"]
provider_headers = server["headers"]
episode_title = server["episode_title"]
else:
with Progress() as progress:
progress.add_task("Fetching servers", total=None)
# prompt for server selection
servers = {server["server"]: server for server in streams}
servers_names = list(servers.keys())
if config.server in servers_names:
server = config.server
else:
if config.use_fzf:
server = fzf.run(servers_names, "Select an link: ")
else:
server = fuzzy_inquirer(
servers_names,
"Select link",
)
stream_link = filter_by_quality( stream_link = filter_by_quality(
config.quality, servers[server]["links"] config.quality, server_name["links"]
) )
if not stream_link: if not stream_link:
print("[yellow bold]WARNING:[/] No streams found") print("[yellow bold]WARNING:[/] No streams found")
@@ -206,11 +185,42 @@ def download(
print("Continuing...") print("Continuing...")
continue continue
link = stream_link["link"] link = stream_link["link"]
provider_headers = servers[server]["headers"] provider_headers = server_name["headers"]
episode_title = server_name["episode_title"]
subtitles = server_name["subtitles"]
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_name = config.server
else:
if config.use_fzf:
server_name = fzf.run(servers_names, "Select an link: ")
else:
server_name = fuzzy_inquirer(
servers_names,
"Select link",
)
stream_link = filter_by_quality(
config.quality, servers[server_name]["links"]
)
if not stream_link:
print("[yellow bold]WARNING:[/] No streams found")
time.sleep(1)
print("Continuing...")
continue
link = stream_link["link"]
provider_headers = servers[server_name]["headers"]
episode_title = servers[server]["episode_title"] subtitles = servers[server_name]["subtitles"]
episode_title = servers[server_name]["episode_title"]
print(f"[purple]Now Downloading:[/] {search_result} Episode {episode}") print(f"[purple]Now Downloading:[/] {search_result} Episode {episode}")
subtitles = move_preferred_subtitle_lang_to_top(
subtitles, config.sub_lang
)
downloader._download_file( downloader._download_file(
link, link,
anime["title"], anime["title"],
@@ -221,6 +231,7 @@ def download(
force_unknown_ext, force_unknown_ext,
verbose, verbose,
headers=provider_headers, headers=provider_headers,
sub=subtitles[0]["url"] if subtitles else "",
) )
except Exception as e: except Exception as e:
print(e) print(e)

View File

@@ -91,13 +91,13 @@ def grab(
anime = anime_provider.get_anime(search_results_[search_result]["id"]) anime = anime_provider.get_anime(search_results_[search_result]["id"])
if not anime: if not anime:
exit(1) exit(1)
episodes = sorted(
anime["availableEpisodesDetail"][config.translation_type], key=float
)
if anime_info_only: if anime_info_only:
# grab only the anime data skipping all lines after this # grab only the anime data skipping all lines after this
grabbed_animes.append(anime) grabbed_animes.append(anime)
continue continue
episodes = sorted(
anime["availableEpisodesDetail"][config.translation_type], key=float
)
# where the magic happens # where the magic happens
if episode_range: if episode_range:

View File

@@ -35,7 +35,11 @@ def search(config: Config, anime_titles: str, episode_range: str):
from ...libs.rofi import Rofi from ...libs.rofi import Rofi
from ..utils.mpv import run_mpv from ..utils.mpv import run_mpv
from ..utils.tools import exit_app from ..utils.tools import exit_app
from ..utils.utils import filter_by_quality, fuzzy_inquirer from ..utils.utils import (
filter_by_quality,
fuzzy_inquirer,
move_preferred_subtitle_lang_to_top,
)
anime_provider = AnimeProvider(config.provider) anime_provider = AnimeProvider(config.provider)
@@ -177,6 +181,7 @@ def search(config: Config, anime_titles: str, episode_range: str):
stream_anime() stream_anime()
return return
link = stream_link["link"] link = stream_link["link"]
subtitles = server["subtitles"]
stream_headers = server["headers"] stream_headers = server["headers"]
episode_title = server["episode_title"] episode_title = server["episode_title"]
else: else:
@@ -207,15 +212,23 @@ def search(config: Config, anime_titles: str, episode_range: str):
return return
link = stream_link["link"] link = stream_link["link"]
stream_headers = servers[server]["headers"] stream_headers = servers[server]["headers"]
subtitles = servers[server]["subtitles"]
episode_title = servers[server]["episode_title"] episode_title = servers[server]["episode_title"]
print(f"[purple]Now Playing:[/] {search_result} Episode {episode}") print(f"[purple]Now Playing:[/] {search_result} Episode {episode}")
subtitles = move_preferred_subtitle_lang_to_top(
subtitles, config.sub_lang
)
if config.sync_play: if config.sync_play:
from ..utils.syncplay import SyncPlayer from ..utils.syncplay import SyncPlayer
SyncPlayer(link, episode_title, headers=stream_headers) SyncPlayer(
link, episode_title, headers=stream_headers, subtitles=subtitles
)
else: else:
run_mpv(link, episode_title, headers=stream_headers) run_mpv(
link, episode_title, headers=stream_headers, subtitles=subtitles
)
except IndexError as e: except IndexError as e:
print(e) print(e)
input("Enter to continue") input("Enter to continue")

View File

@@ -96,6 +96,7 @@ class Config(object):
"rofi_theme_input": "", "rofi_theme_input": "",
"rofi_theme_confirm": "", "rofi_theme_confirm": "",
"ffmpegthumnailer_seek_time": "-1", "ffmpegthumnailer_seek_time": "-1",
"sub_lang": "eng",
} }
) )
self.configparser.add_section("stream") self.configparser.add_section("stream")
@@ -109,6 +110,7 @@ class Config(object):
# --- set config values from file or using defaults --- # --- set config values from file or using defaults ---
self.downloads_dir = self.get_downloads_dir() self.downloads_dir = self.get_downloads_dir()
self.sub_lang = self.get_sub_lang()
self.provider = self.get_provider() self.provider = self.get_provider()
self.use_fzf = self.get_use_fzf() self.use_fzf = self.get_use_fzf()
self.use_rofi = self.get_use_rofi() self.use_rofi = self.get_use_rofi()
@@ -187,6 +189,9 @@ class Config(object):
def get_preferred_language(self): def get_preferred_language(self):
return self.configparser.get("general", "preferred_language") return self.configparser.get("general", "preferred_language")
def get_sub_lang(self):
return self.configparser.get("general", "sub_lang")
def get_downloads_dir(self): def get_downloads_dir(self):
return self.configparser.get("general", "downloads_dir") return self.configparser.get("general", "downloads_dir")

View File

@@ -21,7 +21,11 @@ from ...Utility.data import anime_normalizer
from ...Utility.utils import anime_title_percentage_match from ...Utility.utils import anime_title_percentage_match
from ..utils.mpv import run_mpv from ..utils.mpv import run_mpv
from ..utils.tools import exit_app from ..utils.tools import exit_app
from ..utils.utils import filter_by_quality, fuzzy_inquirer from ..utils.utils import (
filter_by_quality,
fuzzy_inquirer,
move_preferred_subtitle_lang_to_top,
)
from .utils import aniskip from .utils import aniskip
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -113,6 +117,9 @@ def media_player_controls(
current_episode_number, current_episode_number,
): ):
custom_args.extend(args) custom_args.extend(args)
subtitles = move_preferred_subtitle_lang_to_top(
selected_server["subtitles"], config.sub_lang
)
if config.sync_play: if config.sync_play:
from ..utils.syncplay import SyncPlayer from ..utils.syncplay import SyncPlayer
@@ -120,29 +127,21 @@ def media_player_controls(
current_episode_stream_link, current_episode_stream_link,
selected_server["episode_title"], selected_server["episode_title"],
headers=selected_server["headers"], headers=selected_server["headers"],
subtitles=subtitles,
) )
elif config.use_mpv_mod: elif config.use_mpv_mod:
from ..utils.player import player from ..utils.player import player
mpv = player.create_player( player.create_player(
current_episode_stream_link, current_episode_stream_link,
config.anime_provider, config.anime_provider,
fastanime_runtime_state, fastanime_runtime_state,
config, config,
selected_server["episode_title"], selected_server["episode_title"],
start_time,
headers=selected_server["headers"], headers=selected_server["headers"],
subtitles=subtitles,
) )
# TODO: implement custom aniskip
if custom_args and None:
chapters_file = custom_args[0].split("=", 1)
script_opts = custom_args[1].split("=", 1)
mpv._set_property("chapters-file", chapters_file[1])
mpv._set_property("script-opts", script_opts[1])
if not start_time == "0":
mpv.start = start_time
mpv.wait_for_shutdown()
mpv.terminate()
stop_time = player.last_stop_time stop_time = player.last_stop_time
total_time = player.last_total_time total_time = player.last_total_time
else: else:
@@ -152,6 +151,7 @@ def media_player_controls(
start_time=start_time, start_time=start_time,
custom_args=custom_args, custom_args=custom_args,
headers=selected_server["headers"], headers=selected_server["headers"],
subtitles=subtitles,
) )
# either update the watch history to the next episode or current depending on progress # either update the watch history to the next episode or current depending on progress
@@ -502,6 +502,8 @@ def provider_anime_episode_servers_menu(
) )
if start_time != "0" and episode_in_history == current_episode_number: if start_time != "0" and episode_in_history == current_episode_number:
print("[green]Continuing from:[/] ", start_time) print("[green]Continuing from:[/] ", start_time)
else:
start_time = "0"
custom_args = [] custom_args = []
if config.skip: if config.skip:
if args := aniskip( if args := aniskip(
@@ -509,6 +511,9 @@ def provider_anime_episode_servers_menu(
current_episode_number, current_episode_number,
): ):
custom_args.extend(args) custom_args.extend(args)
subtitles = move_preferred_subtitle_lang_to_top(
selected_server["subtitles"], config.sub_lang
)
if config.sync_play: if config.sync_play:
from ..utils.syncplay import SyncPlayer from ..utils.syncplay import SyncPlayer
@@ -516,29 +521,24 @@ def provider_anime_episode_servers_menu(
current_stream_link, current_stream_link,
selected_server["episode_title"], selected_server["episode_title"],
headers=selected_server["headers"], headers=selected_server["headers"],
subtitles=subtitles,
) )
elif config.use_mpv_mod: elif config.use_mpv_mod:
from ..utils.player import player from ..utils.player import player
mpv = player.create_player( if start_time == "0" and episode_in_history != current_episode_number:
start_time = "0"
player.create_player(
current_stream_link, current_stream_link,
anime_provider, anime_provider,
fastanime_runtime_state, fastanime_runtime_state,
config, config,
selected_server["episode_title"], selected_server["episode_title"],
start_time,
headers=selected_server["headers"], headers=selected_server["headers"],
subtitles=subtitles,
) )
# TODO: implement custom aniskip intergration
if custom_args and None:
chapters_file = custom_args[0].split("=", 1)
script_opts = custom_args[1].split("=", 1)
mpv._set_property("chapters-file", chapters_file[1])
mpv._set_property("script-opts", script_opts[1])
if not start_time == "0" and episode_in_history == current_episode_number:
mpv.start = start_time
mpv.wait_for_shutdown()
mpv.terminate()
stop_time = player.last_stop_time stop_time = player.last_stop_time
total_time = player.last_total_time total_time = player.last_total_time
current_episode_number = fastanime_runtime_state.provider_current_episode_number current_episode_number = fastanime_runtime_state.provider_current_episode_number
@@ -551,6 +551,7 @@ def provider_anime_episode_servers_menu(
start_time=start_time, start_time=start_time,
custom_args=custom_args, custom_args=custom_args,
headers=selected_server["headers"], headers=selected_server["headers"],
subtitles=subtitles,
) )
print("Finished at: ", stop_time) print("Finished at: ", stop_time)
@@ -681,14 +682,14 @@ def provider_anime_episodes_menu(
if current_episode_number == "Back": if current_episode_number == "Back":
media_actions_menu(config, fastanime_runtime_state) media_actions_menu(config, fastanime_runtime_state)
return return
#
# try to get the start time and if not found default to "0" # # try to get the start time and if not found default to "0"
start_time = user_watch_history.get(str(anime_id_anilist), {}).get( # start_time = user_watch_history.get(str(anime_id_anilist), {}).get(
"start_time", "0" # "start_time", "0"
) # )
config.update_watch_history( # config.update_watch_history(
anime_id_anilist, current_episode_number, start_time=start_time # anime_id_anilist, current_episode_number, start_time=start_time
) # )
# update runtime data # update runtime data
fastanime_runtime_state.provider_available_episodes = total_episodes fastanime_runtime_state.provider_available_episodes = total_episodes
@@ -1009,6 +1010,42 @@ def media_actions_menu(
media_actions_menu(config, fastanime_runtime_state) media_actions_menu(config, fastanime_runtime_state)
def _change_player(
config: "Config", fastanime_runtime_state: "FastAnimeRuntimeState"
):
"""Change the translation type to use
Args:
config: [TODO:description]
fastanime_runtime_state: [TODO:description]
"""
# prompt for new translation type
options = ["syncplay", "mpv-mod", "default"]
if config.use_fzf:
player = fzf.run(
options,
prompt="Select Player:",
)
elif config.use_rofi:
player = Rofi.run(options, "Select Player: ")
else:
player = fuzzy_inquirer(
options,
"Select Player",
)
# update internal config
if player == "syncplay":
config.sync_play = True
config.use_mpv_mod = False
else:
config.sync_play = False
if player == "mpv-mod":
config.use_mpv_mod = True
else:
config.use_mpv_mod = False
media_actions_menu(config, fastanime_runtime_state)
def _view_info(config: "Config", fastanime_runtime_state: "FastAnimeRuntimeState"): def _view_info(config: "Config", fastanime_runtime_state: "FastAnimeRuntimeState"):
"""helper function to view info of an anime from terminal """helper function to view info of an anime from terminal
@@ -1122,7 +1159,9 @@ def media_actions_menu(
config: [TODO:description] config: [TODO:description]
fastanime_runtime_state: [TODO:description] fastanime_runtime_state: [TODO:description]
""" """
options = ["allanime", "animepahe"] from ...libs.anime_provider import anime_sources
options = list(anime_sources.keys())
if config.use_fzf: if config.use_fzf:
provider = fzf.run( provider = fzf.run(
options, prompt="Select Translation Type:", header="Language Options" options, prompt="Select Translation Type:", header="Language Options"
@@ -1137,7 +1176,7 @@ def media_actions_menu(
config.provider = provider config.provider = provider
config.anime_provider.provider = provider config.anime_provider.provider = provider
config.anime_provider.lazyload_provider() config.anime_provider.lazyload_provider(provider)
media_actions_menu(config, fastanime_runtime_state) media_actions_menu(config, fastanime_runtime_state)
@@ -1175,6 +1214,7 @@ def media_actions_menu(
f"{'📖 ' if icons else ''}View Info": _view_info, f"{'📖 ' if icons else ''}View Info": _view_info,
f"{'🎧 ' if icons else ''}Change Translation Type": _change_translation_type, f"{'🎧 ' if icons else ''}Change Translation Type": _change_translation_type,
f"{'💽 ' if icons else ''}Change Provider": _change_provider, f"{'💽 ' if icons else ''}Change Provider": _change_provider,
f"{'💽 ' if icons else ''}Change Player": _change_player,
f"{'🔘 ' if icons else ''}Toggle auto select anime": _toggle_auto_select, # WARN: problematic if you choose an anime that doesnt match id f"{'🔘 ' if icons else ''}Toggle auto select anime": _toggle_auto_select, # WARN: problematic if you choose an anime that doesnt match id
f"{'💠 ' if icons else ''}Toggle auto next episode": _toggle_auto_next, f"{'💠 ' if icons else ''}Toggle auto next episode": _toggle_auto_next,
f"{'🔘 ' if icons else ''}Toggle continue from history": _toggle_continue_from_history, f"{'🔘 ' if icons else ''}Toggle continue from history": _toggle_continue_from_history,
@@ -1427,6 +1467,9 @@ def fastanime_main_menu(
else: else:
config.load_config() config.load_config()
config.anime_provider.provider = config.provider
config.anime_provider.lazyload_provider(config.provider)
fastanime_main_menu(config, fastanime_runtime_state) fastanime_main_menu(config, fastanime_runtime_state)
icons = config.icons icons = config.icons

View File

@@ -55,6 +55,7 @@ def run_mpv(
ytdl_format="", ytdl_format="",
custom_args=[], custom_args=[],
headers={}, headers={},
subtitles=[],
): ):
# Determine if mpv is available # Determine if mpv is available
MPV = shutil.which("mpv") MPV = shutil.which("mpv")
@@ -108,6 +109,8 @@ def run_mpv(
for header_name, header_value in headers.items(): for header_name, header_value in headers.items():
mpv_headers += f"{header_name}:{header_value}," mpv_headers += f"{header_name}:{header_value},"
mpv_args.append(mpv_headers) mpv_args.append(mpv_headers)
for subtitle in subtitles:
mpv_args.append(f"--sub-file={subtitle['url']}")
if start_time != "0": if start_time != "0":
mpv_args.append(f"--start={start_time}") mpv_args.append(f"--start={start_time}")
if title: if title:

View File

@@ -3,7 +3,7 @@ from typing import TYPE_CHECKING
import mpv import mpv
from ...anilist import AniList from ...anilist import AniList
from .utils import filter_by_quality from .utils import filter_by_quality, move_preferred_subtitle_lang_to_top
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Literal from typing import Literal
@@ -22,6 +22,7 @@ def format_time(duration_in_secs: float):
class MpvPlayer(object): class MpvPlayer(object):
anime_provider: "AnimeProvider" anime_provider: "AnimeProvider"
config: "Config" config: "Config"
subs = []
mpv_player: "mpv.MPV" mpv_player: "mpv.MPV"
last_stop_time: str = "0" last_stop_time: str = "0"
last_total_time: str = "0" last_total_time: str = "0"
@@ -113,7 +114,7 @@ class MpvPlayer(object):
) )
if not episode_streams: if not episode_streams:
self.mpv_player.show_text("No streams were found") self.mpv_player.show_text("No streams were found")
return None return
# always select the first # always select the first
if server == "top": if server == "top":
@@ -131,7 +132,7 @@ class MpvPlayer(object):
self.mpv_player.show_text( self.mpv_player.show_text(
f"Invalid server!!; servers available are: {episode_streams_dict.keys()}", f"Invalid server!!; servers available are: {episode_streams_dict.keys()}",
) )
return None return
self.current_media_title = selected_server["episode_title"] self.current_media_title = selected_server["episode_title"]
links = selected_server["links"] links = selected_server["links"]
@@ -142,6 +143,9 @@ class MpvPlayer(object):
self.mpv_player._set_property("start", "0") self.mpv_player._set_property("start", "0")
stream_link = stream_link_["link"] stream_link = stream_link_["link"]
fastanime_runtime_state.provider_current_episode_stream_link = stream_link fastanime_runtime_state.provider_current_episode_stream_link = stream_link
self.subs = move_preferred_subtitle_lang_to_top(
selected_server["subtitles"], config.sub_lang
)
return stream_link return stream_link
def create_player( def create_player(
@@ -151,8 +155,11 @@ class MpvPlayer(object):
fastanime_runtime_state, fastanime_runtime_state,
config: "Config", config: "Config",
title, title,
start_time,
headers={}, headers={},
subtitles=[],
): ):
self.subs = subtitles
self.anime_provider = anime_provider self.anime_provider = anime_provider
self.fastanime_runtime_state = fastanime_runtime_state self.fastanime_runtime_state = fastanime_runtime_state
self.config = config self.config = config
@@ -171,17 +178,6 @@ class MpvPlayer(object):
osc=True, osc=True,
ytdl=True, ytdl=True,
) )
mpv_player.force_window = config.force_window
# mpv_player.cache = "yes"
# mpv_player.cache_pause = "no"
mpv_player.title = title
mpv_headers = ""
if headers:
for header_name, header_value in headers.items():
mpv_headers += f"{header_name}:{header_value},"
mpv_player.http_header_fields = mpv_headers
mpv_player.play(stream_link)
# -- events -- # -- events --
@mpv_player.event_callback("file-loaded") @mpv_player.event_callback("file-loaded")
@@ -190,6 +186,22 @@ class MpvPlayer(object):
self.player_fetching = False self.player_fetching = False
if isinstance(d, float): if isinstance(d, float):
self.last_total_time = format_time(d) self.last_total_time = format_time(d)
try:
if not mpv_player.core_shutdown:
if self.subs:
for i, subtitle in enumerate(self.subs):
if i == 0:
flag = "select"
else:
flag = "auto"
mpv_player.sub_add(
subtitle["url"], flag, None, subtitle["language"]
)
self.subs = []
except mpv.ShutdownError:
pass
except Exception:
pass
@mpv_player.property_observer("time-pos") @mpv_player.property_observer("time-pos")
def handle_time_start_update(*args): def handle_time_start_update(*args):
@@ -218,7 +230,9 @@ class MpvPlayer(object):
def _next_episode(): def _next_episode():
url = self.get_episode("next") url = self.get_episode("next")
if url: if url:
mpv_player.loadfile(url, options=f"title={self.current_media_title}") mpv_player.loadfile(
url,
)
mpv_player.title = self.current_media_title mpv_player.title = self.current_media_title
@mpv_player.on_key_press("shift+p") @mpv_player.on_key_press("shift+p")
@@ -327,7 +341,23 @@ class MpvPlayer(object):
mpv_player.register_message_handler("select-quality", select_quality) mpv_player.register_message_handler("select-quality", select_quality)
self.mpv_player = mpv_player self.mpv_player = mpv_player
return mpv_player mpv_player.force_window = config.force_window
# mpv_player.cache = "yes"
# mpv_player.cache_pause = "no"
mpv_player.title = title
mpv_headers = ""
if headers:
for header_name, header_value in headers.items():
mpv_headers += f"{header_name}:{header_value},"
mpv_player.http_header_fields = mpv_headers
mpv_player.play(stream_link)
if not start_time == "0":
mpv_player.start = start_time
mpv_player.wait_for_shutdown()
mpv_player.terminate()
player = MpvPlayer() player = MpvPlayer()

View File

@@ -4,7 +4,7 @@ import subprocess
from .tools import exit_app from .tools import exit_app
def SyncPlayer(url: str, anime_title=None, headers={}, *args): def SyncPlayer(url: str, anime_title=None, headers={}, subtitles=[], *args):
# TODO: handle m3u8 multi quality streams # TODO: handle m3u8 multi quality streams
# #
# check for SyncPlay # check for SyncPlay
@@ -20,6 +20,8 @@ def SyncPlayer(url: str, anime_title=None, headers={}, *args):
for header_name, header_value in headers.items(): for header_name, header_value in headers.items():
mpv_headers += f"{header_name}:{header_value}," mpv_headers += f"{header_name}:{header_value},"
mpv_args.append(mpv_headers) mpv_args.append(mpv_headers)
for subtitle in subtitles:
mpv_args.append(f"--sub-file={subtitle['url']}")
if not anime_title: if not anime_title:
subprocess.run( subprocess.run(
[ [

View File

@@ -15,25 +15,14 @@ class FastAnimeRuntimeState(dict):
def exit_app(exit_code=0, *args): def exit_app(exit_code=0, *args):
import os
import shutil
import sys import sys
from rich.console import Console
from ...constants import APP_NAME, ICON_PATH, USER_NAME from ...constants import APP_NAME, ICON_PATH, USER_NAME
def is_running_in_terminal(): console = Console()
try: if not console.is_terminal:
shutil.get_terminal_size()
return (
sys.stdin
and sys.stdin.isatty()
and sys.stdout.isatty()
and os.getenv("TERM") is not None
)
except OSError:
return False
if not is_running_in_terminal():
from plyer import notification from plyer import notification
notification.notify( notification.notify(
@@ -43,7 +32,6 @@ def exit_app(exit_code=0, *args):
title="Shutting down", title="Shutting down",
) # pyright:ignore ) # pyright:ignore
else: else:
from rich import print console.clear()
console.print("Have a good day :smile:", USER_NAME)
print("Have a good day :smile:", USER_NAME)
sys.exit(exit_code) sys.exit(exit_code)

View File

@@ -19,6 +19,25 @@ BG_GREEN = "\033[48;2;120;233;12;m"
GREEN = "\033[38;2;45;24;45;m" GREEN = "\033[38;2;45;24;45;m"
def move_preferred_subtitle_lang_to_top(sub_list, lang_str):
"""Moves the dictionary with the given ID to the front of the list.
Args:
sub_list: list of subs
lang_str: the sub lang pref
Returns:
The modified list.
"""
import re
for i, d in enumerate(sub_list):
if re.search(lang_str, d["language"], re.IGNORECASE):
sub_list.insert(0, sub_list.pop(i))
break
return sub_list
def filter_by_quality(quality: str, stream_links: "list[EpisodeStream]", default=True): def filter_by_quality(quality: str, stream_links: "list[EpisodeStream]", default=True):
"""Helper function used to filter a list of EpisodeStream objects to one that has a corresponding quality """Helper function used to filter a list of EpisodeStream objects to one that has a corresponding quality

View File

@@ -1,13 +1,10 @@
from .allanime import SERVERS_AVAILABLE as ALLANIME_SERVERS
from .animepahe import SERVERS_AVAILABLE as ANIMEPAHESERVERS
from .aniwatch import SERVERS_AVAILABLE as ANIWATCHSERVERS
anime_sources = { anime_sources = {
"allanime": "api.AllAnimeAPI", "allanime": "api.AllAnimeAPI",
"animepahe": "api.AnimePaheApi", "animepahe": "api.AnimePaheApi",
"aniwatch": "api.AniWatchApi",
} }
SERVERS_AVAILABLE = [ SERVERS_AVAILABLE = [*ALLANIME_SERVERS, *ANIMEPAHESERVERS, *ANIWATCHSERVERS]
"sharepoint",
"dropbox",
"gogoanime",
"weTransfer",
"wixmp",
"kwik",
"Yt",
]

View File

@@ -0,0 +1 @@
SERVERS_AVAILABLE = ["sharepoint", "dropbox", "gogoanime", "weTransfer", "wixmp", "Yt"]

View File

@@ -231,6 +231,7 @@ class AllAnimeAPI(AnimeProvider):
"server": "Yt", "server": "Yt",
"episode_title": f'{anime["title"]}; Episode {episode_number}', "episode_title": f'{anime["title"]}; Episode {episode_number}',
"headers": {"Referer": f"https://{ALLANIME_BASE}/"}, "headers": {"Referer": f"https://{ALLANIME_BASE}/"},
"subtitles": [],
"links": [ "links": [
{ {
"link": url, "link": url,
@@ -260,6 +261,7 @@ class AllAnimeAPI(AnimeProvider):
yield { yield {
"server": "gogoanime", "server": "gogoanime",
"headers": {}, "headers": {},
"subtitles": [],
"episode_title": ( "episode_title": (
allanime_episode["notes"] or f'{anime["title"]}' allanime_episode["notes"] or f'{anime["title"]}'
) )
@@ -271,6 +273,7 @@ class AllAnimeAPI(AnimeProvider):
yield { yield {
"server": "wetransfer", "server": "wetransfer",
"headers": {}, "headers": {},
"subtitles": [],
"episode_title": ( "episode_title": (
allanime_episode["notes"] or f'{anime["title"]}' allanime_episode["notes"] or f'{anime["title"]}'
) )
@@ -282,6 +285,7 @@ class AllAnimeAPI(AnimeProvider):
yield { yield {
"server": "sharepoint", "server": "sharepoint",
"headers": {}, "headers": {},
"subtitles": [],
"episode_title": ( "episode_title": (
allanime_episode["notes"] or f'{anime["title"]}' allanime_episode["notes"] or f'{anime["title"]}'
) )
@@ -293,6 +297,7 @@ class AllAnimeAPI(AnimeProvider):
yield { yield {
"server": "dropbox", "server": "dropbox",
"headers": {}, "headers": {},
"subtitles": [],
"episode_title": ( "episode_title": (
allanime_episode["notes"] or f'{anime["title"]}' allanime_episode["notes"] or f'{anime["title"]}'
) )
@@ -304,6 +309,7 @@ class AllAnimeAPI(AnimeProvider):
yield { yield {
"server": "wixmp", "server": "wixmp",
"headers": {}, "headers": {},
"subtitles": [],
"episode_title": ( "episode_title": (
allanime_episode["notes"] or f'{anime["title"]}' allanime_episode["notes"] or f'{anime["title"]}'
) )

View File

@@ -4,4 +4,3 @@ ALLANIME_BASE = "allanime.day"
ALLANIME_REFERER = "https://allanime.to/" ALLANIME_REFERER = "https://allanime.to/"
ALLANIME_API_ENDPOINT = "https://api.{}/api/".format(ALLANIME_BASE) ALLANIME_API_ENDPOINT = "https://api.{}/api/".format(ALLANIME_BASE)
USER_AGENT = random_user_agent() USER_AGENT = random_user_agent()
SERVERS_AVAILABLE = ["sharepoint", "dropbox", "gogoanime", "weTransfer", "wixmp"]

View File

@@ -0,0 +1 @@
SERVERS_AVAILABLE = ["kwik"]

View File

@@ -136,7 +136,7 @@ class AnimePaheApi(AnimeProvider):
}, },
"episodesInfo": [ "episodesInfo": [
{ {
"title": episode["title"] or f"{title};{episode['episode']}", "title": f"{episode['title'] or title};{episode['episode']}",
"episode": episode["episode"], "episode": episode["episode"],
"id": episode["session"], "id": episode["session"],
"translation_type": episode["audio"], "translation_type": episode["audio"],
@@ -190,6 +190,7 @@ class AnimePaheApi(AnimeProvider):
"server": "kwik", "server": "kwik",
"links": [], "links": [],
"episode_title": episode_title, "episode_title": episode_title,
"subtitles": [],
"headers": {}, "headers": {},
} }
for res_dict in res_dicts: for res_dict in res_dicts:

View File

@@ -0,0 +1 @@
SERVERS_AVAILABLE = ["HD1", "HD2", "StreamSB", "StreamTape"]

View File

@@ -0,0 +1,173 @@
import logging
import re
from itertools import cycle
from yt_dlp.utils import (
extract_attributes,
get_element_html_by_class,
get_elements_html_by_class,
)
from ..base_provider import AnimeProvider
from ..common import fetch_anime_info_from_bal
from ..mini_anilist import search_for_anime_with_anilist
from ..utils import give_random_quality
from . import SERVERS_AVAILABLE
from .types import AniWatchStream
logger = logging.getLogger(__name__)
LINK_TO_STREAMS_REGEX = re.compile(r".*://(.*)/embed-(2|4|6)/e-([0-9])/(.*)\?.*")
class AniWatchApi(AnimeProvider):
def search_for_anime(self, anime_title: str, *args):
try:
return search_for_anime_with_anilist(anime_title)
except Exception as e:
logger.error(e)
def get_anime(self, anilist_id, *args):
try:
bal_results = fetch_anime_info_from_bal(anilist_id)
if not bal_results:
return
ZORO = bal_results["Sites"]["Zoro"]
aniwatch_id = list(ZORO.keys())[0]
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:
response_json = response.json()
aniwatch_anime_page = response_json["html"]
episodes_info_container_html = get_element_html_by_class(
"ss-list", aniwatch_anime_page
)
episodes_info_html_list = get_elements_html_by_class(
"ep-item", episodes_info_container_html
)
# keys: [ data-number: episode_number, data-id: episode_id, title: episode_title , href:episode_page_url]
episodes_info_dicts = [
extract_attributes(episode_dict)
for episode_dict in episodes_info_html_list
]
episodes = [episode["data-number"] for episode in episodes_info_dicts]
self.episodes_info = [
{
"id": episode["data-id"],
"title": (
(episode["title"] or "").replace(
f"Episode {episode['data-number']}", ""
)
or ZORO[aniwatch_id]["title"]
)
+ f"; Episode {episode['data-number']}",
"episode": episode["data-number"],
}
for episode in episodes_info_dicts
]
return {
"id": aniwatch_id,
"availableEpisodesDetail": {
"dub": episodes,
"sub": episodes,
"raw": episodes,
},
"poster": ZORO[aniwatch_id]["image"],
"title": ZORO[aniwatch_id]["title"],
"episodes_info": self.episodes_info,
}
except Exception as e:
logger.error(e)
def get_episode_streams(self, anime, episode, translation_type, *args):
try:
episode_details = [
episode_details
for episode_details in self.episodes_info
if episode_details["episode"] == episode
]
if not episode_details:
return
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:
response_json = response.json()
episode_page_html = response_json["html"]
servers_containers_html = get_elements_html_by_class(
"ps__-list", episode_page_html
)
if not servers_containers_html:
return
# sub servers
try:
servers_html_sub = get_elements_html_by_class(
"server-item", servers_containers_html[0]
)
except Exception:
logger.warn("AniWatch: sub not found")
servers_html_sub = None
# dub servers
try:
servers_html_dub = get_elements_html_by_class(
"server-item", servers_containers_html[1]
)
except Exception:
logger.warn("AniWatch: dub not found")
servers_html_dub = None
if translation_type == "dub":
servers_html = servers_html_dub
else:
servers_html = servers_html_sub
if not servers_html:
return
for server_name, server_html in zip(
cycle(SERVERS_AVAILABLE), servers_html
):
try:
# keys: [ data-type: translation_type, data-id: embed_id, data-server-id: server_id ]
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:
embed_json = embed_response.json()
raw_link_to_streams = embed_json["link"]
match = LINK_TO_STREAMS_REGEX.match(raw_link_to_streams)
if not match:
continue
provider_domain = match.group(1)
embed_type = match.group(2)
episode_number = match.group(3)
source_id = match.group(4)
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:
juicy_streams_json: "AniWatchStream" = (
link_to_streams_response.json()
)
yield {
"headers": {},
"subtitles": [
{
"url": track["file"],
"language": track["label"],
}
for track in juicy_streams_json["tracks"]
if track["kind"] == "captions"
],
"server": server_name,
"episode_title": episode_details["title"],
"links": give_random_quality(
[
{"link": link["file"], "type": link["type"]}
for link in juicy_streams_json["sources"]
]
),
}
except Exception as e:
logger.error(e)
except Exception as e:
logger.error(e)

View File

@@ -0,0 +1,26 @@
from typing import Literal, TypedDict
class AniWatchSkipTime(TypedDict):
start: int
end: int
class AniWatchSource(TypedDict):
file: str
type: str
class AniWatchTrack(TypedDict):
file: str
label: str
kind: Literal["captions", "thumbnails", "audio"]
class AniWatchStream(TypedDict):
sources: list[AniWatchSource]
tracks: list[AniWatchTrack]
encrypted: bool
intro: AniWatchSkipTime
outro: AniWatchSkipTime
server: int

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

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

@@ -39,9 +39,20 @@ class AnimeEpisodeDetails(TypedDict):
raw: list[str] raw: list[str]
class AnimeEpisode(TypedDict): #
# class AnimeEpisode(TypedDict):
# id: str
# title: str
#
class AnimeEpisodeInfo(TypedDict):
id: str id: str
title: str title: str
episode: str
poster: str | None
duration: str | None
translation_type: str | None
class Anime(TypedDict): class Anime(TypedDict):
@@ -49,7 +60,7 @@ class Anime(TypedDict):
title: str title: str
availableEpisodesDetail: AnimeEpisodeDetails availableEpisodesDetail: AnimeEpisodeDetails
type: str | None type: str | None
episodesInfo: list[AnimeEpisode] | None episodesInfo: list[AnimeEpisodeInfo] | None
poster: str poster: str
year: str year: str
@@ -64,8 +75,15 @@ class EpisodeStream(TypedDict):
translation_type: Literal["dub", "sub"] translation_type: Literal["dub", "sub"]
class Subtitle(TypedDict):
url: str
language: str
class Server(TypedDict): class Server(TypedDict):
headers: dict headers: dict
subtitles: list[Subtitle]
audio: list
server: str server: str
episode_title: str episode_title: str
links: list[EpisodeStream] links: list[EpisodeStream]

View File

@@ -35,12 +35,12 @@ hex_to_char = {
} }
def give_random_quality(links: list[dict]): def give_random_quality(links):
qualities = cycle(["1080", "720", "480", "360"]) qualities = cycle(["1080", "720", "480", "360"])
return [ return [
{"link": link["link"], "quality": quality} {**episode_stream, "quality": quality}
for link, quality in zip(links, qualities) for episode_stream, quality in zip(links, qualities)
] ]

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "fastanime" name = "fastanime"
version = "2.2.6" version = "2.3.4"
description = "A browser anime site experience from the terminal" description = "A browser anime site experience from the terminal"
authors = ["Benextempest <benextempest@gmail.com>"] authors = ["Benextempest <benextempest@gmail.com>"]
license = "UNLICENSE" license = "UNLICENSE"