mirror of
https://github.com/Benexl/FastAnime.git
synced 2025-12-07 13:20:38 -08:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f3834453c | ||
|
|
7ad8b8a0e3 | ||
|
|
80b41f06da | ||
|
|
e79321ed50 | ||
|
|
f7b5898dfa | ||
|
|
144bf53081 | ||
|
|
16dded9724 | ||
|
|
c47b158bff | ||
|
|
9a36e15d9d | ||
|
|
d6b2bd7761 | ||
|
|
2346552dc4 | ||
|
|
ba275055db | ||
|
|
de4ddf2f3a | ||
|
|
9c94d824d1 | ||
|
|
495f3cfbf6 | ||
|
|
b56c9ae3dd | ||
|
|
5e9ef87526 | ||
|
|
b68d6d6fe9 | ||
|
|
5870cc6640 | ||
|
|
7a43d58d82 | ||
|
|
fc7efebc8d | ||
|
|
528be74194 | ||
|
|
ab782acf2f | ||
|
|
45836d1ebc | ||
|
|
dff059d8eb | ||
|
|
4010cfc9c8 | ||
|
|
6329730820 | ||
|
|
006592ae7d | ||
|
|
831dcf4e88 | ||
|
|
0d2cf7ed66 | ||
|
|
aa6dc2b98e | ||
|
|
2e5cde3365 | ||
|
|
d75a03e594 |
54
README.md
54
README.md
@@ -51,7 +51,6 @@ Heavily inspired by [animdl](https://github.com/justfoolingaround/animdl), [magi
|
||||
- [Key Bindings](#key-bindings)
|
||||
- [Script Messages](#script-messages)
|
||||
- [Configuration](#configuration)
|
||||
- [The python api](#the-python-api)
|
||||
- [Contributing](#contributing)
|
||||
- [Receiving Support](#receiving-support)
|
||||
- [Supporting the Project](#supporting-the-project)
|
||||
@@ -59,7 +58,7 @@ Heavily inspired by [animdl](https://github.com/justfoolingaround/animdl), [magi
|
||||
|
||||
> [!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
|
||||
|
||||
@@ -234,6 +233,7 @@ Available options for the fastanime include:
|
||||
- `--use-mpv-mod/--use-default-player` whether to use python-mpv
|
||||
- `--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
|
||||
- `--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
|
||||
|
||||
@@ -350,13 +350,18 @@ fastanime download -t <anime-title> -t <anime-title> -r '-5'
|
||||
|
||||
# Download specific episode range
|
||||
# be sure to observe the range Syntax
|
||||
fastanime download <anime-title> -r '<episodes-start>:<episodes-end>:<step>'
|
||||
fastanime download -t <anime-title> -r '<episodes-start>:<episodes-end>:<step>'
|
||||
|
||||
fastanime download <anime-title> -r '<episodes-start>:<episodes-end>'
|
||||
fastanime download -t <anime-title> -r '<episodes-start>:<episodes-end>'
|
||||
|
||||
fastanime download <anime-title> -r '<episodes-start>:'
|
||||
fastanime download -t <anime-title> -r '<episodes-start>:'
|
||||
|
||||
fastanime download -t <anime-title> -r ':<episodes-end>'
|
||||
|
||||
# download specific episode
|
||||
# remember python indexing starts at 0
|
||||
fastanime download -t <anime-title> -r '<episode-1>:<episode>'
|
||||
|
||||
fastanime download <anime-title> -r ':<episodes-end>'
|
||||
```
|
||||
|
||||
#### search subcommand
|
||||
@@ -404,7 +409,8 @@ Uses a list slicing syntax similar to that of python as the value of the `-r` op
|
||||
**Syntax:**
|
||||
|
||||
```bash
|
||||
# print all available episodes
|
||||
# --- print anime info + episode streams ---
|
||||
|
||||
# multiple titles can be specified with the -t option
|
||||
fastanime grab -t <anime-title> -t <anime-title>
|
||||
|
||||
@@ -425,6 +431,18 @@ fastanime grab -t <anime-title> -r '<start>:<stop>:<step>'
|
||||
fastanime grab -t <anime-title> -r '<start>:'
|
||||
|
||||
fastanime grab -t <anime-title> -r ':<end>'
|
||||
|
||||
# --- grab options ---
|
||||
|
||||
# print search results only
|
||||
fastanime grab -t <anime-title> -r <range> --search-results-only
|
||||
|
||||
# print anime info only
|
||||
fastanime grab -t <anime-title> -r <range> --anime-info-only
|
||||
|
||||
# print episode streams only
|
||||
fastanime grab -t <anime-title> -r <range> --episode-streams-only
|
||||
|
||||
```
|
||||
|
||||
#### downloads subcommand
|
||||
@@ -636,28 +654,6 @@ notification_duration=2
|
||||
# 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
|
||||
|
||||
We welcome your issues and feature requests. However, due to time constraints, we currently do not plan to add another provider.
|
||||
|
||||
@@ -37,12 +37,12 @@ class AnimeProvider:
|
||||
self.provider = provider
|
||||
self.dynamic = dynamic
|
||||
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"""
|
||||
_, anime_provider_cls_name = anime_sources[self.provider].split(".", 1)
|
||||
package = f"fastanime.libs.anime_provider.{self.provider}"
|
||||
_, anime_provider_cls_name = anime_sources[provider].split(".", 1)
|
||||
package = f"fastanime.libs.anime_provider.{provider}"
|
||||
provider_api = importlib.import_module(".api", package)
|
||||
anime_provider = getattr(provider_api, anime_provider_cls_name)
|
||||
self.anime_provider = anime_provider()
|
||||
@@ -73,7 +73,7 @@ class AnimeProvider:
|
||||
user_query, translation_type, nsfw, unknown
|
||||
)
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
logger.error(e)
|
||||
results = None
|
||||
return results
|
||||
|
||||
@@ -95,7 +95,7 @@ class AnimeProvider:
|
||||
try:
|
||||
results = anime_provider.get_anime(anime_id)
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
logger.error(e)
|
||||
results = None
|
||||
return results
|
||||
|
||||
@@ -123,6 +123,6 @@ class AnimeProvider:
|
||||
anime, episode, translation_type
|
||||
)
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
logger.error(e)
|
||||
results = None
|
||||
return results # pyright:ignore
|
||||
|
||||
@@ -38,6 +38,7 @@ class YtDLPDownloader:
|
||||
force_unknown_ext=False,
|
||||
verbose=False,
|
||||
headers={},
|
||||
sub="",
|
||||
):
|
||||
"""Helper function that downloads anime given url and path details
|
||||
|
||||
@@ -60,9 +61,11 @@ class YtDLPDownloader:
|
||||
"format": vid_format,
|
||||
"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:
|
||||
ydl.download([url])
|
||||
ydl.download(urls)
|
||||
|
||||
# WARN: May remove this legacy functionality
|
||||
def download_file(self, url: str, title, silent=True):
|
||||
|
||||
@@ -6,7 +6,7 @@ if sys.version_info < (3, 10):
|
||||
) # noqa: F541
|
||||
|
||||
|
||||
__version__ = "v2.2.3"
|
||||
__version__ = "v2.3.4"
|
||||
|
||||
APP_NAME = "FastAnime"
|
||||
AUTHOR = "Benex254"
|
||||
|
||||
@@ -98,6 +98,11 @@ signal.signal(signal.SIGINT, handle_exit)
|
||||
type=click.Choice(["dub", "sub"]),
|
||||
help="Anime language[dub/sub]",
|
||||
)
|
||||
@click.option(
|
||||
"-sl",
|
||||
"--sub-lang",
|
||||
help="Set the preferred language for subs",
|
||||
)
|
||||
@click.option(
|
||||
"-A/-no-A",
|
||||
"--auto-next/--no-auto-next",
|
||||
@@ -156,6 +161,7 @@ def run_cli(
|
||||
local_history,
|
||||
skip,
|
||||
translation_type,
|
||||
sub_lang,
|
||||
quality,
|
||||
auto_next,
|
||||
auto_select,
|
||||
@@ -186,7 +192,7 @@ def run_cli(
|
||||
FORMAT = "%(message)s"
|
||||
|
||||
logging.basicConfig(
|
||||
level="NOTSET", format=FORMAT, datefmt="[%X]", handlers=[RichHandler()]
|
||||
level="debug", format=FORMAT, datefmt="[%X]", handlers=[RichHandler()]
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info("logging has been initialized")
|
||||
@@ -203,6 +209,10 @@ def run_cli(
|
||||
datefmt="[%d/%m/%Y@%H:%M:%S]",
|
||||
filemode="w",
|
||||
)
|
||||
else:
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.CRITICAL)
|
||||
if rich_traceback:
|
||||
from rich.traceback import install
|
||||
|
||||
@@ -216,6 +226,8 @@ def run_cli(
|
||||
ctx.obj.server = server
|
||||
if format:
|
||||
ctx.obj.format = format
|
||||
if sub_lang:
|
||||
ctx.obj.sub_lang = sub_lang
|
||||
if ctx.get_parameter_source("continue_") == click.core.ParameterSource.COMMANDLINE:
|
||||
ctx.obj.continue_from_history = continue_
|
||||
if ctx.get_parameter_source("skip") == click.core.ParameterSource.COMMANDLINE:
|
||||
|
||||
@@ -39,11 +39,11 @@ def check_for_updates():
|
||||
and remote_tag[1] == local_tag[1]
|
||||
)
|
||||
):
|
||||
is_update = True
|
||||
is_latest = False
|
||||
else:
|
||||
is_update = False
|
||||
is_latest = True
|
||||
|
||||
return (is_update, release_json)
|
||||
return (is_latest, release_json)
|
||||
else:
|
||||
print(request.text)
|
||||
return (False, {})
|
||||
|
||||
@@ -59,7 +59,11 @@ def download(
|
||||
from ...libs.fzf import fzf
|
||||
from ...Utility.downloader.downloader import downloader
|
||||
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)
|
||||
|
||||
@@ -83,6 +87,9 @@ def download(
|
||||
)
|
||||
return
|
||||
search_results = search_results["results"]
|
||||
if not search_results:
|
||||
print("Nothing muches your search term")
|
||||
exit_app(1)
|
||||
search_results_ = {
|
||||
search_result["title"]: search_result for search_result in search_results
|
||||
}
|
||||
@@ -165,37 +172,12 @@ def download(
|
||||
if config.server == "top":
|
||||
with Progress() as progress:
|
||||
progress.add_task("Fetching top server...", total=None)
|
||||
server = next(streams, None)
|
||||
if not server:
|
||||
server_name = next(streams, None)
|
||||
if not server_name:
|
||||
print("Sth went wrong when fetching the server")
|
||||
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(
|
||||
config.quality, servers[server]["links"]
|
||||
config.quality, server_name["links"]
|
||||
)
|
||||
if not stream_link:
|
||||
print("[yellow bold]WARNING:[/] No streams found")
|
||||
@@ -203,11 +185,42 @@ def download(
|
||||
print("Continuing...")
|
||||
continue
|
||||
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}")
|
||||
|
||||
subtitles = move_preferred_subtitle_lang_to_top(
|
||||
subtitles, config.sub_lang
|
||||
)
|
||||
downloader._download_file(
|
||||
link,
|
||||
anime["title"],
|
||||
@@ -218,6 +231,7 @@ def download(
|
||||
force_unknown_ext,
|
||||
verbose,
|
||||
headers=provider_headers,
|
||||
sub=subtitles[0]["url"] if subtitles else "",
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
@@ -26,11 +26,29 @@ if TYPE_CHECKING:
|
||||
"-r",
|
||||
help="A range of episodes to download (start-end)",
|
||||
)
|
||||
@click.option(
|
||||
"--search-results-only",
|
||||
"-s",
|
||||
help="print only the search results to stdout",
|
||||
is_flag=True,
|
||||
)
|
||||
@click.option(
|
||||
"--anime-info-only", "-i", help="print only selected anime title info", is_flag=True
|
||||
)
|
||||
@click.option(
|
||||
"--episode-streams-only",
|
||||
"-e",
|
||||
help="print only selected anime episodes streams of given range",
|
||||
is_flag=True,
|
||||
)
|
||||
@click.pass_obj
|
||||
def grab(
|
||||
config: "Config",
|
||||
anime_titles: tuple,
|
||||
episode_range,
|
||||
search_results_only,
|
||||
anime_info_only,
|
||||
episode_streams_only,
|
||||
):
|
||||
import json
|
||||
from logging import getLogger
|
||||
@@ -52,7 +70,15 @@ def grab(
|
||||
)
|
||||
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
|
||||
}
|
||||
@@ -65,9 +91,14 @@ def grab(
|
||||
anime = anime_provider.get_anime(search_results_[search_result]["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:
|
||||
@@ -94,10 +125,14 @@ def grab(
|
||||
else:
|
||||
episodes_range = sorted(episodes, key=float)
|
||||
|
||||
grabbed_anime = dict(anime)
|
||||
grabbed_anime["requested_episodes"] = episodes_range
|
||||
grabbed_anime["translation_type"] = config.translation_type
|
||||
grabbed_anime["episodes_streams"] = {}
|
||||
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:
|
||||
@@ -108,14 +143,23 @@ def grab(
|
||||
)
|
||||
if not streams:
|
||||
continue
|
||||
grabbed_anime["episodes_streams"][episode] = {
|
||||
server["server"]: server for server in streams
|
||||
}
|
||||
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)
|
||||
if len(grabbed_animes) == 1:
|
||||
print(json.dumps(grabbed_animes[0]))
|
||||
else:
|
||||
print(json.dumps(grabbed_animes))
|
||||
|
||||
# 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))
|
||||
|
||||
@@ -35,7 +35,11 @@ def search(config: Config, anime_titles: str, episode_range: str):
|
||||
from ...libs.rofi import Rofi
|
||||
from ..utils.mpv import run_mpv
|
||||
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)
|
||||
|
||||
@@ -177,6 +181,7 @@ def search(config: Config, anime_titles: str, episode_range: str):
|
||||
stream_anime()
|
||||
return
|
||||
link = stream_link["link"]
|
||||
subtitles = server["subtitles"]
|
||||
stream_headers = server["headers"]
|
||||
episode_title = server["episode_title"]
|
||||
else:
|
||||
@@ -207,15 +212,23 @@ def search(config: Config, anime_titles: str, episode_range: str):
|
||||
return
|
||||
link = stream_link["link"]
|
||||
stream_headers = servers[server]["headers"]
|
||||
subtitles = servers[server]["subtitles"]
|
||||
episode_title = servers[server]["episode_title"]
|
||||
print(f"[purple]Now Playing:[/] {search_result} 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)
|
||||
SyncPlayer(
|
||||
link, episode_title, headers=stream_headers, subtitles=subtitles
|
||||
)
|
||||
else:
|
||||
run_mpv(link, episode_title, headers=stream_headers)
|
||||
run_mpv(
|
||||
link, episode_title, headers=stream_headers, subtitles=subtitles
|
||||
)
|
||||
except IndexError as e:
|
||||
print(e)
|
||||
input("Enter to continue")
|
||||
|
||||
@@ -24,8 +24,8 @@ def update(
|
||||
console.print(body)
|
||||
|
||||
if check:
|
||||
is_update, github_release_data = check_for_updates()
|
||||
if is_update:
|
||||
is_latest, github_release_data = check_for_updates()
|
||||
if not is_latest:
|
||||
print(
|
||||
f"You are running an older version ({__version__}) of fastanime please update to get the latest features"
|
||||
)
|
||||
@@ -36,3 +36,7 @@ def update(
|
||||
else:
|
||||
success, github_release_data = update_app()
|
||||
_print_release(github_release_data)
|
||||
if success:
|
||||
print("Successfully updated")
|
||||
else:
|
||||
print("failed to update")
|
||||
|
||||
@@ -96,6 +96,7 @@ class Config(object):
|
||||
"rofi_theme_input": "",
|
||||
"rofi_theme_confirm": "",
|
||||
"ffmpegthumnailer_seek_time": "-1",
|
||||
"sub_lang": "eng",
|
||||
}
|
||||
)
|
||||
self.configparser.add_section("stream")
|
||||
@@ -109,6 +110,7 @@ class Config(object):
|
||||
|
||||
# --- set config values from file or using defaults ---
|
||||
self.downloads_dir = self.get_downloads_dir()
|
||||
self.sub_lang = self.get_sub_lang()
|
||||
self.provider = self.get_provider()
|
||||
self.use_fzf = self.get_use_fzf()
|
||||
self.use_rofi = self.get_use_rofi()
|
||||
@@ -187,6 +189,9 @@ class Config(object):
|
||||
def get_preferred_language(self):
|
||||
return self.configparser.get("general", "preferred_language")
|
||||
|
||||
def get_sub_lang(self):
|
||||
return self.configparser.get("general", "sub_lang")
|
||||
|
||||
def get_downloads_dir(self):
|
||||
return self.configparser.get("general", "downloads_dir")
|
||||
|
||||
|
||||
@@ -21,7 +21,11 @@ from ...Utility.data import anime_normalizer
|
||||
from ...Utility.utils import anime_title_percentage_match
|
||||
from ..utils.mpv import run_mpv
|
||||
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
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -113,6 +117,9 @@ def media_player_controls(
|
||||
current_episode_number,
|
||||
):
|
||||
custom_args.extend(args)
|
||||
subtitles = move_preferred_subtitle_lang_to_top(
|
||||
selected_server["subtitles"], config.sub_lang
|
||||
)
|
||||
if config.sync_play:
|
||||
from ..utils.syncplay import SyncPlayer
|
||||
|
||||
@@ -120,29 +127,21 @@ def media_player_controls(
|
||||
current_episode_stream_link,
|
||||
selected_server["episode_title"],
|
||||
headers=selected_server["headers"],
|
||||
subtitles=subtitles,
|
||||
)
|
||||
elif config.use_mpv_mod:
|
||||
from ..utils.player import player
|
||||
|
||||
mpv = player.create_player(
|
||||
player.create_player(
|
||||
current_episode_stream_link,
|
||||
config.anime_provider,
|
||||
fastanime_runtime_state,
|
||||
config,
|
||||
selected_server["episode_title"],
|
||||
start_time,
|
||||
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
|
||||
total_time = player.last_total_time
|
||||
else:
|
||||
@@ -152,6 +151,7 @@ def media_player_controls(
|
||||
start_time=start_time,
|
||||
custom_args=custom_args,
|
||||
headers=selected_server["headers"],
|
||||
subtitles=subtitles,
|
||||
)
|
||||
|
||||
# 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:
|
||||
print("[green]Continuing from:[/] ", start_time)
|
||||
else:
|
||||
start_time = "0"
|
||||
custom_args = []
|
||||
if config.skip:
|
||||
if args := aniskip(
|
||||
@@ -509,6 +511,9 @@ def provider_anime_episode_servers_menu(
|
||||
current_episode_number,
|
||||
):
|
||||
custom_args.extend(args)
|
||||
subtitles = move_preferred_subtitle_lang_to_top(
|
||||
selected_server["subtitles"], config.sub_lang
|
||||
)
|
||||
if config.sync_play:
|
||||
from ..utils.syncplay import SyncPlayer
|
||||
|
||||
@@ -516,29 +521,24 @@ def provider_anime_episode_servers_menu(
|
||||
current_stream_link,
|
||||
selected_server["episode_title"],
|
||||
headers=selected_server["headers"],
|
||||
subtitles=subtitles,
|
||||
)
|
||||
elif config.use_mpv_mod:
|
||||
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,
|
||||
anime_provider,
|
||||
fastanime_runtime_state,
|
||||
config,
|
||||
selected_server["episode_title"],
|
||||
start_time,
|
||||
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
|
||||
total_time = player.last_total_time
|
||||
current_episode_number = fastanime_runtime_state.provider_current_episode_number
|
||||
@@ -551,6 +551,7 @@ def provider_anime_episode_servers_menu(
|
||||
start_time=start_time,
|
||||
custom_args=custom_args,
|
||||
headers=selected_server["headers"],
|
||||
subtitles=subtitles,
|
||||
)
|
||||
print("Finished at: ", stop_time)
|
||||
|
||||
@@ -681,14 +682,14 @@ def provider_anime_episodes_menu(
|
||||
if current_episode_number == "Back":
|
||||
media_actions_menu(config, fastanime_runtime_state)
|
||||
return
|
||||
|
||||
# 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", "0"
|
||||
)
|
||||
config.update_watch_history(
|
||||
anime_id_anilist, current_episode_number, start_time=start_time
|
||||
)
|
||||
#
|
||||
# # 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", "0"
|
||||
# )
|
||||
# config.update_watch_history(
|
||||
# anime_id_anilist, current_episode_number, start_time=start_time
|
||||
# )
|
||||
|
||||
# update runtime data
|
||||
fastanime_runtime_state.provider_available_episodes = total_episodes
|
||||
@@ -1009,6 +1010,42 @@ def media_actions_menu(
|
||||
|
||||
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"):
|
||||
"""helper function to view info of an anime from terminal
|
||||
|
||||
@@ -1122,7 +1159,9 @@ def media_actions_menu(
|
||||
config: [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:
|
||||
provider = fzf.run(
|
||||
options, prompt="Select Translation Type:", header="Language Options"
|
||||
@@ -1137,7 +1176,7 @@ def media_actions_menu(
|
||||
|
||||
config.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)
|
||||
|
||||
@@ -1175,6 +1214,7 @@ def media_actions_menu(
|
||||
f"{'📖 ' if icons else ''}View Info": _view_info,
|
||||
f"{'🎧 ' if icons else ''}Change Translation Type": _change_translation_type,
|
||||
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 next episode": _toggle_auto_next,
|
||||
f"{'🔘 ' if icons else ''}Toggle continue from history": _toggle_continue_from_history,
|
||||
@@ -1427,6 +1467,9 @@ def fastanime_main_menu(
|
||||
else:
|
||||
config.load_config()
|
||||
|
||||
config.anime_provider.provider = config.provider
|
||||
config.anime_provider.lazyload_provider(config.provider)
|
||||
|
||||
fastanime_main_menu(config, fastanime_runtime_state)
|
||||
|
||||
icons = config.icons
|
||||
|
||||
@@ -55,6 +55,7 @@ def run_mpv(
|
||||
ytdl_format="",
|
||||
custom_args=[],
|
||||
headers={},
|
||||
subtitles=[],
|
||||
):
|
||||
# Determine if mpv is available
|
||||
MPV = shutil.which("mpv")
|
||||
@@ -108,6 +109,8 @@ def run_mpv(
|
||||
for header_name, header_value in headers.items():
|
||||
mpv_headers += f"{header_name}:{header_value},"
|
||||
mpv_args.append(mpv_headers)
|
||||
for subtitle in subtitles:
|
||||
mpv_args.append(f"--sub-file={subtitle['url']}")
|
||||
if start_time != "0":
|
||||
mpv_args.append(f"--start={start_time}")
|
||||
if title:
|
||||
|
||||
@@ -3,7 +3,7 @@ from typing import TYPE_CHECKING
|
||||
import mpv
|
||||
|
||||
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:
|
||||
from typing import Literal
|
||||
@@ -22,6 +22,7 @@ def format_time(duration_in_secs: float):
|
||||
class MpvPlayer(object):
|
||||
anime_provider: "AnimeProvider"
|
||||
config: "Config"
|
||||
subs = []
|
||||
mpv_player: "mpv.MPV"
|
||||
last_stop_time: str = "0"
|
||||
last_total_time: str = "0"
|
||||
@@ -113,7 +114,7 @@ class MpvPlayer(object):
|
||||
)
|
||||
if not episode_streams:
|
||||
self.mpv_player.show_text("No streams were found")
|
||||
return None
|
||||
return
|
||||
|
||||
# always select the first
|
||||
if server == "top":
|
||||
@@ -131,7 +132,7 @@ class MpvPlayer(object):
|
||||
self.mpv_player.show_text(
|
||||
f"Invalid server!!; servers available are: {episode_streams_dict.keys()}",
|
||||
)
|
||||
return None
|
||||
return
|
||||
self.current_media_title = selected_server["episode_title"]
|
||||
links = selected_server["links"]
|
||||
|
||||
@@ -142,6 +143,9 @@ class MpvPlayer(object):
|
||||
self.mpv_player._set_property("start", "0")
|
||||
stream_link = stream_link_["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
|
||||
|
||||
def create_player(
|
||||
@@ -151,8 +155,11 @@ class MpvPlayer(object):
|
||||
fastanime_runtime_state,
|
||||
config: "Config",
|
||||
title,
|
||||
start_time,
|
||||
headers={},
|
||||
subtitles=[],
|
||||
):
|
||||
self.subs = subtitles
|
||||
self.anime_provider = anime_provider
|
||||
self.fastanime_runtime_state = fastanime_runtime_state
|
||||
self.config = config
|
||||
@@ -171,17 +178,6 @@ class MpvPlayer(object):
|
||||
osc=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 --
|
||||
@mpv_player.event_callback("file-loaded")
|
||||
@@ -190,6 +186,22 @@ class MpvPlayer(object):
|
||||
self.player_fetching = False
|
||||
if isinstance(d, float):
|
||||
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")
|
||||
def handle_time_start_update(*args):
|
||||
@@ -218,7 +230,9 @@ class MpvPlayer(object):
|
||||
def _next_episode():
|
||||
url = self.get_episode("next")
|
||||
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.on_key_press("shift+p")
|
||||
@@ -327,7 +341,23 @@ class MpvPlayer(object):
|
||||
mpv_player.register_message_handler("select-quality", select_quality)
|
||||
|
||||
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()
|
||||
|
||||
@@ -4,7 +4,7 @@ import subprocess
|
||||
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
|
||||
#
|
||||
# check for SyncPlay
|
||||
@@ -20,6 +20,8 @@ def SyncPlayer(url: str, anime_title=None, headers={}, *args):
|
||||
for header_name, header_value in headers.items():
|
||||
mpv_headers += f"{header_name}:{header_value},"
|
||||
mpv_args.append(mpv_headers)
|
||||
for subtitle in subtitles:
|
||||
mpv_args.append(f"--sub-file={subtitle['url']}")
|
||||
if not anime_title:
|
||||
subprocess.run(
|
||||
[
|
||||
|
||||
@@ -15,25 +15,14 @@ class FastAnimeRuntimeState(dict):
|
||||
|
||||
|
||||
def exit_app(exit_code=0, *args):
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
from rich.console import Console
|
||||
|
||||
from ...constants import APP_NAME, ICON_PATH, USER_NAME
|
||||
|
||||
def is_running_in_terminal():
|
||||
try:
|
||||
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():
|
||||
console = Console()
|
||||
if not console.is_terminal:
|
||||
from plyer import notification
|
||||
|
||||
notification.notify(
|
||||
@@ -43,7 +32,6 @@ def exit_app(exit_code=0, *args):
|
||||
title="Shutting down",
|
||||
) # pyright:ignore
|
||||
else:
|
||||
from rich import print
|
||||
|
||||
print("Have a good day :smile:", USER_NAME)
|
||||
console.clear()
|
||||
console.print("Have a good day :smile:", USER_NAME)
|
||||
sys.exit(exit_code)
|
||||
|
||||
@@ -19,6 +19,25 @@ BG_GREEN = "\033[48;2;120;233;12;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):
|
||||
"""Helper function used to filter a list of EpisodeStream objects to one that has a corresponding quality
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
"allanime": "api.AllAnimeAPI",
|
||||
"animepahe": "api.AnimePaheApi",
|
||||
"aniwatch": "api.AniWatchApi",
|
||||
}
|
||||
SERVERS_AVAILABLE = [
|
||||
"sharepoint",
|
||||
"dropbox",
|
||||
"gogoanime",
|
||||
"weTransfer",
|
||||
"wixmp",
|
||||
"kwik",
|
||||
"Yt",
|
||||
]
|
||||
SERVERS_AVAILABLE = [*ALLANIME_SERVERS, *ANIMEPAHESERVERS, *ANIWATCHSERVERS]
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
SERVERS_AVAILABLE = ["sharepoint", "dropbox", "gogoanime", "weTransfer", "wixmp", "Yt"]
|
||||
|
||||
@@ -231,6 +231,7 @@ class AllAnimeAPI(AnimeProvider):
|
||||
"server": "Yt",
|
||||
"episode_title": f'{anime["title"]}; Episode {episode_number}',
|
||||
"headers": {"Referer": f"https://{ALLANIME_BASE}/"},
|
||||
"subtitles": [],
|
||||
"links": [
|
||||
{
|
||||
"link": url,
|
||||
@@ -260,6 +261,7 @@ class AllAnimeAPI(AnimeProvider):
|
||||
yield {
|
||||
"server": "gogoanime",
|
||||
"headers": {},
|
||||
"subtitles": [],
|
||||
"episode_title": (
|
||||
allanime_episode["notes"] or f'{anime["title"]}'
|
||||
)
|
||||
@@ -271,6 +273,7 @@ class AllAnimeAPI(AnimeProvider):
|
||||
yield {
|
||||
"server": "wetransfer",
|
||||
"headers": {},
|
||||
"subtitles": [],
|
||||
"episode_title": (
|
||||
allanime_episode["notes"] or f'{anime["title"]}'
|
||||
)
|
||||
@@ -282,6 +285,7 @@ class AllAnimeAPI(AnimeProvider):
|
||||
yield {
|
||||
"server": "sharepoint",
|
||||
"headers": {},
|
||||
"subtitles": [],
|
||||
"episode_title": (
|
||||
allanime_episode["notes"] or f'{anime["title"]}'
|
||||
)
|
||||
@@ -293,6 +297,7 @@ class AllAnimeAPI(AnimeProvider):
|
||||
yield {
|
||||
"server": "dropbox",
|
||||
"headers": {},
|
||||
"subtitles": [],
|
||||
"episode_title": (
|
||||
allanime_episode["notes"] or f'{anime["title"]}'
|
||||
)
|
||||
@@ -304,6 +309,7 @@ class AllAnimeAPI(AnimeProvider):
|
||||
yield {
|
||||
"server": "wixmp",
|
||||
"headers": {},
|
||||
"subtitles": [],
|
||||
"episode_title": (
|
||||
allanime_episode["notes"] or f'{anime["title"]}'
|
||||
)
|
||||
|
||||
@@ -4,4 +4,3 @@ ALLANIME_BASE = "allanime.day"
|
||||
ALLANIME_REFERER = "https://allanime.to/"
|
||||
ALLANIME_API_ENDPOINT = "https://api.{}/api/".format(ALLANIME_BASE)
|
||||
USER_AGENT = random_user_agent()
|
||||
SERVERS_AVAILABLE = ["sharepoint", "dropbox", "gogoanime", "weTransfer", "wixmp"]
|
||||
|
||||
1
fastanime/libs/anime_provider/animepahe/__init__.py
Normal file
1
fastanime/libs/anime_provider/animepahe/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
SERVERS_AVAILABLE = ["kwik"]
|
||||
@@ -136,7 +136,7 @@ class AnimePaheApi(AnimeProvider):
|
||||
},
|
||||
"episodesInfo": [
|
||||
{
|
||||
"title": episode["title"] or f"{title};{episode['episode']}",
|
||||
"title": f"{episode['title'] or title};{episode['episode']}",
|
||||
"episode": episode["episode"],
|
||||
"id": episode["session"],
|
||||
"translation_type": episode["audio"],
|
||||
@@ -183,14 +183,14 @@ class AnimePaheApi(AnimeProvider):
|
||||
|
||||
# get the episode title
|
||||
episode_title = (
|
||||
episode["title"] + f"; {episode['episode']}"
|
||||
or f"{anime['title']}; Episode {episode['episode']}"
|
||||
f"{episode['title'] or anime['title']}; Episode {episode['episode']}"
|
||||
)
|
||||
# get all links
|
||||
streams = {
|
||||
"server": "kwik",
|
||||
"links": [],
|
||||
"episode_title": episode_title,
|
||||
"subtitles": [],
|
||||
"headers": {},
|
||||
}
|
||||
for res_dict in res_dicts:
|
||||
|
||||
1
fastanime/libs/anime_provider/aniwatch/__init__.py
Normal file
1
fastanime/libs/anime_provider/aniwatch/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
SERVERS_AVAILABLE = ["HD1", "HD2", "StreamSB", "StreamTape"]
|
||||
173
fastanime/libs/anime_provider/aniwatch/api.py
Normal file
173
fastanime/libs/anime_provider/aniwatch/api.py
Normal 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)
|
||||
0
fastanime/libs/anime_provider/aniwatch/constants.py
Normal file
0
fastanime/libs/anime_provider/aniwatch/constants.py
Normal file
26
fastanime/libs/anime_provider/aniwatch/types.py
Normal file
26
fastanime/libs/anime_provider/aniwatch/types.py
Normal 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
|
||||
15
fastanime/libs/anime_provider/common.py
Normal file
15
fastanime/libs/anime_provider/common.py
Normal file
@@ -0,0 +1,15 @@
|
||||
import logging
|
||||
|
||||
from requests import get
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def fetch_anime_info_from_bal(anilist_id):
|
||||
try:
|
||||
url = f"https://raw.githubusercontent.com/bal-mackup/mal-backup/master/anilist/anime/{anilist_id}.json"
|
||||
response = get(url, timeout=11)
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
153
fastanime/libs/anime_provider/mini_anilist.py
Normal file
153
fastanime/libs/anime_provider/mini_anilist.py
Normal 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}")
|
||||
@@ -39,9 +39,20 @@ class AnimeEpisodeDetails(TypedDict):
|
||||
raw: list[str]
|
||||
|
||||
|
||||
class AnimeEpisode(TypedDict):
|
||||
#
|
||||
# class AnimeEpisode(TypedDict):
|
||||
# id: str
|
||||
# title: str
|
||||
#
|
||||
|
||||
|
||||
class AnimeEpisodeInfo(TypedDict):
|
||||
id: str
|
||||
title: str
|
||||
episode: str
|
||||
poster: str | None
|
||||
duration: str | None
|
||||
translation_type: str | None
|
||||
|
||||
|
||||
class Anime(TypedDict):
|
||||
@@ -49,7 +60,7 @@ class Anime(TypedDict):
|
||||
title: str
|
||||
availableEpisodesDetail: AnimeEpisodeDetails
|
||||
type: str | None
|
||||
episodesInfo: list[AnimeEpisode] | None
|
||||
episodesInfo: list[AnimeEpisodeInfo] | None
|
||||
poster: str
|
||||
year: str
|
||||
|
||||
@@ -64,8 +75,15 @@ class EpisodeStream(TypedDict):
|
||||
translation_type: Literal["dub", "sub"]
|
||||
|
||||
|
||||
class Subtitle(TypedDict):
|
||||
url: str
|
||||
language: str
|
||||
|
||||
|
||||
class Server(TypedDict):
|
||||
headers: dict
|
||||
subtitles: list[Subtitle]
|
||||
audio: list
|
||||
server: str
|
||||
episode_title: str
|
||||
links: list[EpisodeStream]
|
||||
|
||||
@@ -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"])
|
||||
|
||||
return [
|
||||
{"link": link["link"], "quality": quality}
|
||||
for link, quality in zip(links, qualities)
|
||||
{**episode_stream, "quality": quality}
|
||||
for episode_stream, quality in zip(links, qualities)
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "fastanime"
|
||||
version = "2.2.3"
|
||||
version = "2.3.4"
|
||||
description = "A browser anime site experience from the terminal"
|
||||
authors = ["Benextempest <benextempest@gmail.com>"]
|
||||
license = "UNLICENSE"
|
||||
|
||||
@@ -60,6 +60,11 @@ def test_update_help(runner: CliRunner):
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
def test_grab_help(runner: CliRunner):
|
||||
result = runner.invoke(run_cli, ["grab", "--help"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
def test_anilist_help(runner: CliRunner):
|
||||
result = runner.invoke(run_cli, ["anilist", "--help"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
Reference in New Issue
Block a user