mirror of
https://github.com/Benexl/FastAnime.git
synced 2026-01-21 17:03:20 -08:00
Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ec3c7a5bb | ||
|
|
8e0619863a | ||
|
|
e8a05ec4b8 | ||
|
|
34e8b2abd1 | ||
|
|
161b6eb961 | ||
|
|
dd2090f85d | ||
|
|
8b1595a5da | ||
|
|
77ffa27ed8 | ||
|
|
15f79b65c9 | ||
|
|
33c3af0241 | ||
|
|
9badde62fb | ||
|
|
4e401dca40 | ||
|
|
25422b1b7d | ||
|
|
e8463f13b4 | ||
|
|
556f42e41f | ||
|
|
b99a4f7efc | ||
|
|
f6f45cf322 | ||
|
|
ae6db1847a | ||
|
|
20d04ea07b | ||
|
|
8f3834453c | ||
|
|
7ad8b8a0e3 | ||
|
|
80b41f06da | ||
|
|
e79321ed50 | ||
|
|
f7b5898dfa | ||
|
|
144bf53081 | ||
|
|
16dded9724 | ||
|
|
c47b158bff | ||
|
|
9a36e15d9d | ||
|
|
d6b2bd7761 | ||
|
|
2346552dc4 | ||
|
|
ba275055db | ||
|
|
de4ddf2f3a | ||
|
|
9c94d824d1 | ||
|
|
495f3cfbf6 | ||
|
|
b56c9ae3dd | ||
|
|
5e9ef87526 | ||
|
|
b68d6d6fe9 | ||
|
|
5870cc6640 | ||
|
|
7a43d58d82 | ||
|
|
fc7efebc8d | ||
|
|
528be74194 | ||
|
|
ab782acf2f | ||
|
|
45836d1ebc |
67
README.md
67
README.md
@@ -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
|
||||||
|
|
||||||
@@ -267,6 +267,7 @@ Run `fastanime anilist` to access the main interface.
|
|||||||
##### Subcommands
|
##### Subcommands
|
||||||
|
|
||||||
The subcommands are mainly their as convenience. Since all the features already exist in the main interface.
|
The subcommands are mainly their as convenience. Since all the features already exist in the main interface.
|
||||||
|
Most of the subcommands share the common option `--dump-json` or `-d` which will print only the json data and supress the ui.
|
||||||
|
|
||||||
- `fastanime anilist trending`: Top 15 trending anime.
|
- `fastanime anilist trending`: Top 15 trending anime.
|
||||||
- `fastanime anilist recent`: Top 15 recently updated anime.
|
- `fastanime anilist recent`: Top 15 recently updated anime.
|
||||||
@@ -276,6 +277,28 @@ The subcommands are mainly their as convenience. Since all the features already
|
|||||||
- `fastanime anilist favourites`: Top 15 favorite anime.
|
- `fastanime anilist favourites`: Top 15 favorite anime.
|
||||||
- `fastanime anilist random`: get random anime
|
- `fastanime anilist random`: get random anime
|
||||||
|
|
||||||
|
**FastAnime Anilist Search subcommand**
|
||||||
|
|
||||||
|
It is by far one of the most powerful commands.
|
||||||
|
It offers the following options:
|
||||||
|
|
||||||
|
- `--sort <MediaSort>` or `-s <MediaSort>`
|
||||||
|
- `--title <anime-title>` or `-t <anime-title>`
|
||||||
|
- `--tags <tag>` or `-t <tag>` can be specified multiple times for different tags to filter by.
|
||||||
|
- `--year <year>` or `-y <year>`
|
||||||
|
- `--status <MediaStatus>` or `-S <MediaStatus>`
|
||||||
|
- `--media-format <MediaFormat>` or `-f <MediaFormat>`
|
||||||
|
- `--season <MediaSeason>`
|
||||||
|
- `--genres <genre>` or `-g <genre>` can be specified multiple times.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fastanime anilist search -t isekai
|
||||||
|
```
|
||||||
|
|
||||||
|
For more details visit the anilist docs or just get the completions which will improve the experience.
|
||||||
|
|
||||||
The following are commands you can only run if you are signed in to your AniList account:
|
The following are commands you can only run if you are signed in to your AniList account:
|
||||||
|
|
||||||
- `fastanime anilist watching`
|
- `fastanime anilist watching`
|
||||||
@@ -362,6 +385,24 @@ fastanime download -t <anime-title> -r ':<episodes-end>'
|
|||||||
# remember python indexing starts at 0
|
# remember python indexing starts at 0
|
||||||
fastanime download -t <anime-title> -r '<episode-1>:<episode>'
|
fastanime download -t <anime-title> -r '<episode-1>:<episode>'
|
||||||
|
|
||||||
|
# merge subtitles with ffmpeg to mkv format; aniwatch tends to give subs as separate files
|
||||||
|
# and dont prompt for anything
|
||||||
|
# eg existing file in destination instead remove
|
||||||
|
# and clean
|
||||||
|
# ie remove original files (sub file and vid file)
|
||||||
|
# only keep merged files
|
||||||
|
fastanime download -t <anime-title> --merge --clean --no-prompt
|
||||||
|
|
||||||
|
# EOF is used since -t always expects a title
|
||||||
|
# you can supply anime titles from file or -t at the same time
|
||||||
|
#
|
||||||
|
# from stdin
|
||||||
|
echo -e "<anime-title>\n<anime-title>\n<anime-title>" | fastanime download -t "EOF" -r <range> -f -
|
||||||
|
|
||||||
|
# from file
|
||||||
|
fastanime download -t "EOF" -r <range> -f <file-path>
|
||||||
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### search subcommand
|
#### search subcommand
|
||||||
@@ -654,28 +695,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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
from queue import Queue
|
from queue import Queue
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
|
|
||||||
import yt_dlp
|
import yt_dlp
|
||||||
|
from rich import print
|
||||||
|
from rich.prompt import Confirm
|
||||||
from yt_dlp.utils import sanitize_filename
|
from yt_dlp.utils import sanitize_filename
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -38,6 +44,10 @@ class YtDLPDownloader:
|
|||||||
force_unknown_ext=False,
|
force_unknown_ext=False,
|
||||||
verbose=False,
|
verbose=False,
|
||||||
headers={},
|
headers={},
|
||||||
|
sub="",
|
||||||
|
merge=False,
|
||||||
|
clean=False,
|
||||||
|
prompt=True,
|
||||||
):
|
):
|
||||||
"""Helper function that downloads anime given url and path details
|
"""Helper function that downloads anime given url and path details
|
||||||
|
|
||||||
@@ -60,9 +70,85 @@ 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)
|
||||||
|
vid_path = ""
|
||||||
|
sub_path = ""
|
||||||
|
for i, url in enumerate(urls):
|
||||||
|
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||||
|
info = ydl.extract_info(url, download=True)
|
||||||
|
if not info:
|
||||||
|
continue
|
||||||
|
if i == 0:
|
||||||
|
vid_path = info["requested_downloads"][0]["filepath"]
|
||||||
|
else:
|
||||||
|
sub_path = info["requested_downloads"][0]["filepath"]
|
||||||
|
if sub_path and vid_path and merge:
|
||||||
|
self.merge_subtitles(vid_path, sub_path, clean, prompt)
|
||||||
|
|
||||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
def merge_subtitles(self, video_path, sub_path, clean, prompt):
|
||||||
ydl.download([url])
|
# Extract the directory and filename
|
||||||
|
video_dir = os.path.dirname(video_path)
|
||||||
|
video_name = os.path.basename(video_path)
|
||||||
|
video_name, _ = os.path.splitext(video_name)
|
||||||
|
video_name += ".mkv"
|
||||||
|
|
||||||
|
FFMPEG_EXECUTABLE = shutil.which("ffmpeg")
|
||||||
|
if not FFMPEG_EXECUTABLE:
|
||||||
|
print("[yellow bold]WARNING: [/]FFmpeg not found")
|
||||||
|
return
|
||||||
|
# Create a temporary directory
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
# Temporary output path in the temporary directory
|
||||||
|
temp_output_path = os.path.join(temp_dir, video_name)
|
||||||
|
# FFmpeg command to merge subtitles
|
||||||
|
command = [
|
||||||
|
FFMPEG_EXECUTABLE,
|
||||||
|
"-hide_banner",
|
||||||
|
"-i",
|
||||||
|
video_path,
|
||||||
|
"-i",
|
||||||
|
sub_path,
|
||||||
|
"-c",
|
||||||
|
"copy",
|
||||||
|
"-map",
|
||||||
|
"0",
|
||||||
|
"-map",
|
||||||
|
"1",
|
||||||
|
temp_output_path,
|
||||||
|
]
|
||||||
|
|
||||||
|
# Run the command
|
||||||
|
try:
|
||||||
|
subprocess.run(command, check=True)
|
||||||
|
|
||||||
|
# Move the file back to the original directory with the original name
|
||||||
|
final_output_path = os.path.join(video_dir, video_name)
|
||||||
|
|
||||||
|
if os.path.exists(final_output_path):
|
||||||
|
if not prompt or Confirm.ask(
|
||||||
|
f"File exists({final_output_path}) would you like to overwrite it",
|
||||||
|
default=True,
|
||||||
|
):
|
||||||
|
# move file to dest
|
||||||
|
os.remove(final_output_path)
|
||||||
|
shutil.move(temp_output_path, final_output_path)
|
||||||
|
else:
|
||||||
|
shutil.move(temp_output_path, final_output_path)
|
||||||
|
# clean up
|
||||||
|
if clean:
|
||||||
|
print("[cyan]Cleaning original files...[/]")
|
||||||
|
os.remove(video_path)
|
||||||
|
os.remove(sub_path)
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"[green bold]Subtitles merged successfully.[/] Output file: {final_output_path}"
|
||||||
|
)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"[red bold]Error[/] during merging subtitles: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[red bold]An error[/] occurred: {e}")
|
||||||
|
|
||||||
# WARN: May remove this legacy functionality
|
# WARN: May remove this legacy functionality
|
||||||
def download_file(self, url: str, title, silent=True):
|
def download_file(self, url: str, title, silent=True):
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ if sys.version_info < (3, 10):
|
|||||||
) # noqa: F541
|
) # noqa: F541
|
||||||
|
|
||||||
|
|
||||||
__version__ = "v2.2.5"
|
__version__ = "v2.3.7"
|
||||||
|
|
||||||
APP_NAME = "FastAnime"
|
APP_NAME = "FastAnime"
|
||||||
AUTHOR = "Benex254"
|
AUTHOR = "Benex254"
|
||||||
|
|||||||
@@ -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=logging.DEBUG, format=FORMAT, datefmt="[%X]", handlers=[RichHandler()]
|
||||||
)
|
)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
logger.info("logging has been initialized")
|
logger.info("logging has been initialized")
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -7,16 +7,23 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
@click.command(help="View anime you completed")
|
@click.command(help="View anime you completed")
|
||||||
|
@click.option(
|
||||||
|
"--dump-json",
|
||||||
|
"-d",
|
||||||
|
is_flag=True,
|
||||||
|
help="Only print out the results dont open anilist menu",
|
||||||
|
)
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def completed(config: "Config"):
|
def completed(config: "Config", dump_json):
|
||||||
|
from sys import exit
|
||||||
|
|
||||||
from ....anilist import AniList
|
from ....anilist import AniList
|
||||||
from ...interfaces import anilist_interfaces
|
from ...utils.tools import FastAnimeRuntimeState
|
||||||
from ...utils.tools import FastAnimeRuntimeState, exit_app
|
|
||||||
|
|
||||||
if not config.user:
|
if not config.user:
|
||||||
print("Not authenticated")
|
print("Not authenticated")
|
||||||
print("Please run: fastanime anilist loggin")
|
print("Please run: fastanime anilist loggin")
|
||||||
exit_app()
|
exit(1)
|
||||||
anime_list = AniList.get_anime_list("COMPLETED")
|
anime_list = AniList.get_anime_list("COMPLETED")
|
||||||
if not anime_list or not anime_list[1]:
|
if not anime_list or not anime_list[1]:
|
||||||
return
|
return
|
||||||
@@ -27,6 +34,13 @@ def completed(config: "Config"):
|
|||||||
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
|
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
|
||||||
] # pyright:ignore
|
] # pyright:ignore
|
||||||
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
|
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
|
||||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
if dump_json:
|
||||||
fastanime_runtime_state.anilist_data = anime_list[1]
|
import json
|
||||||
anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state)
|
|
||||||
|
print(json.dumps(anime_list))
|
||||||
|
else:
|
||||||
|
from ...interfaces import anilist_interfaces
|
||||||
|
|
||||||
|
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||||
|
fastanime_runtime_state.anilist_data = anime_list[1]
|
||||||
|
anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state)
|
||||||
|
|||||||
@@ -7,26 +7,40 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
@click.command(help="View anime you dropped")
|
@click.command(help="View anime you dropped")
|
||||||
|
@click.option(
|
||||||
|
"--dump-json",
|
||||||
|
"-d",
|
||||||
|
is_flag=True,
|
||||||
|
help="Only print out the results dont open anilist menu",
|
||||||
|
)
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def dropped(config: "Config"):
|
def dropped(config: "Config", dump_json):
|
||||||
|
from sys import exit
|
||||||
|
|
||||||
from ....anilist import AniList
|
from ....anilist import AniList
|
||||||
from ...interfaces import anilist_interfaces
|
|
||||||
from ...utils.tools import FastAnimeRuntimeState, exit_app
|
|
||||||
|
|
||||||
if not config.user:
|
if not config.user:
|
||||||
print("Not authenticated")
|
print("Not authenticated")
|
||||||
print("Please run: fastanime anilist loggin")
|
print("Please run: fastanime anilist loggin")
|
||||||
exit_app()
|
exit(1)
|
||||||
anime_list = AniList.get_anime_list("DROPPED")
|
anime_list = AniList.get_anime_list("DROPPED")
|
||||||
if not anime_list:
|
if not anime_list:
|
||||||
return
|
exit(1)
|
||||||
if not anime_list[0] or not anime_list[1]:
|
if not anime_list[0] or not anime_list[1]:
|
||||||
return
|
exit(1)
|
||||||
media = [
|
media = [
|
||||||
mediaListItem["media"]
|
mediaListItem["media"]
|
||||||
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
|
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
|
||||||
] # pyright:ignore
|
] # pyright:ignore
|
||||||
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
|
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
|
||||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
if dump_json:
|
||||||
fastanime_runtime_state.anilist_data = anime_list[1]
|
import json
|
||||||
anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state)
|
|
||||||
|
print(json.dumps(anime_list[1]))
|
||||||
|
else:
|
||||||
|
from ...interfaces import anilist_interfaces
|
||||||
|
from ...utils.tools import FastAnimeRuntimeState
|
||||||
|
|
||||||
|
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||||
|
fastanime_runtime_state.anilist_data = anime_list[1]
|
||||||
|
anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state)
|
||||||
|
|||||||
@@ -5,14 +5,30 @@ import click
|
|||||||
help="Fetch the top 15 most favourited anime from anilist",
|
help="Fetch the top 15 most favourited anime from anilist",
|
||||||
short_help="View most favourited anime",
|
short_help="View most favourited anime",
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
"--dump-json",
|
||||||
|
"-d",
|
||||||
|
is_flag=True,
|
||||||
|
help="Only print out the results dont open anilist menu",
|
||||||
|
)
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def favourites(config):
|
def favourites(config, dump_json):
|
||||||
from ....anilist import AniList
|
from ....anilist import AniList
|
||||||
from ...interfaces.anilist_interfaces import anilist_results_menu
|
|
||||||
from ...utils.tools import FastAnimeRuntimeState
|
|
||||||
|
|
||||||
anime_data = AniList.get_most_favourite()
|
anime_data = AniList.get_most_favourite()
|
||||||
if anime_data[0]:
|
if anime_data[0]:
|
||||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
if dump_json:
|
||||||
fastanime_runtime_state.anilist_data = anime_data[1]
|
import json
|
||||||
anilist_results_menu(config, fastanime_runtime_state)
|
|
||||||
|
print(json.dumps(anime_data[1]))
|
||||||
|
else:
|
||||||
|
from ...interfaces.anilist_interfaces import anilist_results_menu
|
||||||
|
from ...utils.tools import FastAnimeRuntimeState
|
||||||
|
|
||||||
|
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||||
|
fastanime_runtime_state.anilist_data = anime_data[1]
|
||||||
|
anilist_results_menu(config, fastanime_runtime_state)
|
||||||
|
else:
|
||||||
|
from sys import exit
|
||||||
|
|
||||||
|
exit(1)
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ if TYPE_CHECKING:
|
|||||||
@click.option("--erase", "-e", help="Erase your login details", is_flag=True)
|
@click.option("--erase", "-e", help="Erase your login details", is_flag=True)
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def login(config: "Config", status, erase):
|
def login(config: "Config", status, erase):
|
||||||
|
from sys import exit
|
||||||
|
|
||||||
from rich import print
|
from rich import print
|
||||||
from rich.prompt import Confirm, Prompt
|
from rich.prompt import Confirm, Prompt
|
||||||
|
|
||||||
from ...utils.tools import exit_app
|
|
||||||
|
|
||||||
if status:
|
if status:
|
||||||
is_logged_in = True if config.user else False
|
is_logged_in = True if config.user else False
|
||||||
message = (
|
message = (
|
||||||
@@ -23,16 +23,16 @@ def login(config: "Config", status, erase):
|
|||||||
)
|
)
|
||||||
print(message)
|
print(message)
|
||||||
print(config.user)
|
print(config.user)
|
||||||
exit_app()
|
exit(0)
|
||||||
elif erase:
|
elif erase:
|
||||||
if Confirm.ask(
|
if Confirm.ask(
|
||||||
"Are you sure you want to erase your login status", default=False
|
"Are you sure you want to erase your login status", default=False
|
||||||
):
|
):
|
||||||
config.update_user({})
|
config.update_user({})
|
||||||
print("Success")
|
print("Success")
|
||||||
exit_app(0)
|
exit(0)
|
||||||
else:
|
else:
|
||||||
exit_app(1)
|
exit(1)
|
||||||
else:
|
else:
|
||||||
from click import launch
|
from click import launch
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ def login(config: "Config", status, erase):
|
|||||||
if config.user:
|
if config.user:
|
||||||
print("Already logged in :confused:")
|
print("Already logged in :confused:")
|
||||||
if not Confirm.ask("or would you like to reloggin", default=True):
|
if not Confirm.ask("or would you like to reloggin", default=True):
|
||||||
exit_app()
|
exit(0)
|
||||||
# ---- new loggin -----
|
# ---- new loggin -----
|
||||||
print(
|
print(
|
||||||
f"A browser session will be opened ( [link]{config.fastanime_anilist_app_login_url}[/link] )",
|
f"A browser session will be opened ( [link]{config.fastanime_anilist_app_login_url}[/link] )",
|
||||||
@@ -52,10 +52,10 @@ def login(config: "Config", status, erase):
|
|||||||
user = AniList.login_user(token)
|
user = AniList.login_user(token)
|
||||||
if not user:
|
if not user:
|
||||||
print("Sth went wrong", user)
|
print("Sth went wrong", user)
|
||||||
exit_app()
|
exit(1)
|
||||||
return
|
return
|
||||||
user["token"] = token
|
user["token"] = token
|
||||||
config.update_user(user)
|
config.update_user(user)
|
||||||
print("Successfully saved credentials")
|
print("Successfully saved credentials")
|
||||||
print(user)
|
print(user)
|
||||||
exit_app()
|
exit(0)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ def notifier(config: "Config"):
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
|
from sys import exit
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from plyer import notification
|
from plyer import notification
|
||||||
@@ -30,7 +31,7 @@ def notifier(config: "Config"):
|
|||||||
if not config.user:
|
if not config.user:
|
||||||
print("Not Authenticated")
|
print("Not Authenticated")
|
||||||
print("Run the following to get started: fastanime anilist loggin")
|
print("Run the following to get started: fastanime anilist loggin")
|
||||||
return
|
exit(1)
|
||||||
run = True
|
run = True
|
||||||
# WARNING: Mess around with this value at your own risk
|
# WARNING: Mess around with this value at your own risk
|
||||||
timeout = 2 # time is in minutes
|
timeout = 2 # time is in minutes
|
||||||
|
|||||||
@@ -7,26 +7,40 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
@click.command(help="View anime you paused on watching")
|
@click.command(help="View anime you paused on watching")
|
||||||
|
@click.option(
|
||||||
|
"--dump-json",
|
||||||
|
"-d",
|
||||||
|
is_flag=True,
|
||||||
|
help="Only print out the results dont open anilist menu",
|
||||||
|
)
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def paused(config: "Config"):
|
def paused(config: "Config", dump_json):
|
||||||
|
from sys import exit
|
||||||
|
|
||||||
from ....anilist import AniList
|
from ....anilist import AniList
|
||||||
from ...interfaces import anilist_interfaces
|
|
||||||
from ...utils.tools import FastAnimeRuntimeState, exit_app
|
|
||||||
|
|
||||||
if not config.user:
|
if not config.user:
|
||||||
print("Not authenticated")
|
print("Not authenticated")
|
||||||
print("Please run: fastanime anilist loggin")
|
print("Please run: fastanime anilist loggin")
|
||||||
exit_app()
|
exit(1)
|
||||||
anime_list = AniList.get_anime_list("PAUSED")
|
anime_list = AniList.get_anime_list("PAUSED")
|
||||||
if not anime_list:
|
if not anime_list:
|
||||||
return
|
exit(1)
|
||||||
if not anime_list[0] or not anime_list[1]:
|
if not anime_list[0] or not anime_list[1]:
|
||||||
return
|
exit(1)
|
||||||
media = [
|
media = [
|
||||||
mediaListItem["media"]
|
mediaListItem["media"]
|
||||||
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
|
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
|
||||||
] # pyright:ignore
|
] # pyright:ignore
|
||||||
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
|
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
|
||||||
anilist_config = FastAnimeRuntimeState()
|
if dump_json:
|
||||||
anilist_config.data = anime_list[1]
|
import json
|
||||||
anilist_interfaces.anilist_results_menu(config, anilist_config)
|
|
||||||
|
print(json.dumps(anime_list[1]))
|
||||||
|
else:
|
||||||
|
from ...interfaces import anilist_interfaces
|
||||||
|
from ...utils.tools import FastAnimeRuntimeState
|
||||||
|
|
||||||
|
anilist_config = FastAnimeRuntimeState()
|
||||||
|
anilist_config.data = anime_list[1]
|
||||||
|
anilist_interfaces.anilist_results_menu(config, anilist_config)
|
||||||
|
|||||||
@@ -7,26 +7,40 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
@click.command(help="View anime you are planning on watching")
|
@click.command(help="View anime you are planning on watching")
|
||||||
|
@click.option(
|
||||||
|
"--dump-json",
|
||||||
|
"-d",
|
||||||
|
is_flag=True,
|
||||||
|
help="Only print out the results dont open anilist menu",
|
||||||
|
)
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def planning(config: "Config"):
|
def planning(config: "Config", dump_json):
|
||||||
|
from sys import exit
|
||||||
|
|
||||||
from ....anilist import AniList
|
from ....anilist import AniList
|
||||||
from ...interfaces import anilist_interfaces
|
|
||||||
from ...utils.tools import FastAnimeRuntimeState, exit_app
|
|
||||||
|
|
||||||
if not config.user:
|
if not config.user:
|
||||||
print("Not authenticated")
|
print("Not authenticated")
|
||||||
print("Please run: fastanime anilist loggin")
|
print("Please run: fastanime anilist loggin")
|
||||||
exit_app()
|
exit(1)
|
||||||
anime_list = AniList.get_anime_list("PLANNING")
|
anime_list = AniList.get_anime_list("PLANNING")
|
||||||
if not anime_list:
|
if not anime_list:
|
||||||
return
|
exit(1)
|
||||||
if not anime_list[0] or not anime_list[1]:
|
if not anime_list[0] or not anime_list[1]:
|
||||||
return
|
exit(1)
|
||||||
media = [
|
media = [
|
||||||
mediaListItem["media"]
|
mediaListItem["media"]
|
||||||
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
|
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
|
||||||
] # pyright:ignore
|
] # pyright:ignore
|
||||||
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
|
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
|
||||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
if dump_json:
|
||||||
fastanime_runtime_state.anilist_data = anime_list[1]
|
import json
|
||||||
anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state)
|
|
||||||
|
print(json.dumps(anime_list[1]))
|
||||||
|
else:
|
||||||
|
from ...interfaces import anilist_interfaces
|
||||||
|
from ...utils.tools import FastAnimeRuntimeState
|
||||||
|
|
||||||
|
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||||
|
fastanime_runtime_state.anilist_data = anime_list[1]
|
||||||
|
anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state)
|
||||||
|
|||||||
@@ -4,14 +4,30 @@ import click
|
|||||||
@click.command(
|
@click.command(
|
||||||
help="Fetch the top 15 most popular anime", short_help="View most popular anime"
|
help="Fetch the top 15 most popular anime", short_help="View most popular anime"
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
"--dump-json",
|
||||||
|
"-d",
|
||||||
|
is_flag=True,
|
||||||
|
help="Only print out the results dont open anilist menu",
|
||||||
|
)
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def popular(config):
|
def popular(config, dump_json):
|
||||||
from ....anilist import AniList
|
from ....anilist import AniList
|
||||||
from ...interfaces.anilist_interfaces import anilist_results_menu
|
|
||||||
from ...utils.tools import FastAnimeRuntimeState
|
|
||||||
|
|
||||||
anime_data = AniList.get_most_popular()
|
anime_data = AniList.get_most_popular()
|
||||||
if anime_data[0]:
|
if anime_data[0]:
|
||||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
if dump_json:
|
||||||
fastanime_runtime_state.anilist_data = anime_data[1]
|
import json
|
||||||
anilist_results_menu(config, fastanime_runtime_state)
|
|
||||||
|
print(json.dumps(anime_data[1]))
|
||||||
|
else:
|
||||||
|
from ...interfaces.anilist_interfaces import anilist_results_menu
|
||||||
|
from ...utils.tools import FastAnimeRuntimeState
|
||||||
|
|
||||||
|
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||||
|
fastanime_runtime_state.anilist_data = anime_data[1]
|
||||||
|
anilist_results_menu(config, fastanime_runtime_state)
|
||||||
|
else:
|
||||||
|
from sys import exit
|
||||||
|
|
||||||
|
exit(1)
|
||||||
|
|||||||
@@ -5,23 +5,35 @@ import click
|
|||||||
help="Get random anime from anilist based on a range of anilist anime ids that are seected at random",
|
help="Get random anime from anilist based on a range of anilist anime ids that are seected at random",
|
||||||
short_help="View random anime",
|
short_help="View random anime",
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
"--dump-json",
|
||||||
|
"-d",
|
||||||
|
is_flag=True,
|
||||||
|
help="Only print out the results dont open anilist menu",
|
||||||
|
)
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def random_anime(config):
|
def random_anime(config, dump_json):
|
||||||
import random
|
import random
|
||||||
|
|
||||||
from ....anilist import AniList
|
from ....anilist import AniList
|
||||||
from ...interfaces.anilist_interfaces import anilist_results_menu
|
|
||||||
from ...utils.tools import FastAnimeRuntimeState
|
|
||||||
|
|
||||||
random_anime = range(1, 15000)
|
random_anime = range(1, 100000)
|
||||||
|
|
||||||
random_anime = random.sample(random_anime, k=50)
|
random_anime = random.sample(random_anime, k=50)
|
||||||
|
|
||||||
anime_data = AniList.search(id_in=list(random_anime))
|
anime_data = AniList.search(id_in=list(random_anime))
|
||||||
|
|
||||||
if anime_data[0]:
|
if anime_data[0]:
|
||||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
if dump_json:
|
||||||
fastanime_runtime_state.anilist_data = anime_data[1]
|
import json
|
||||||
anilist_results_menu(config, fastanime_runtime_state)
|
|
||||||
|
print(json.dumps(anime_data[1]))
|
||||||
|
else:
|
||||||
|
from ...interfaces.anilist_interfaces import anilist_results_menu
|
||||||
|
from ...utils.tools import FastAnimeRuntimeState
|
||||||
|
|
||||||
|
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||||
|
fastanime_runtime_state.anilist_data = anime_data[1]
|
||||||
|
anilist_results_menu(config, fastanime_runtime_state)
|
||||||
else:
|
else:
|
||||||
print(anime_data[1])
|
exit(1)
|
||||||
|
|||||||
@@ -5,14 +5,30 @@ import click
|
|||||||
help="Fetch the 15 most recently updated anime from anilist that are currently releasing",
|
help="Fetch the 15 most recently updated anime from anilist that are currently releasing",
|
||||||
short_help="View recently updated anime",
|
short_help="View recently updated anime",
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
"--dump-json",
|
||||||
|
"-d",
|
||||||
|
is_flag=True,
|
||||||
|
help="Only print out the results dont open anilist menu",
|
||||||
|
)
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def recent(config):
|
def recent(config, dump_json):
|
||||||
from ....anilist import AniList
|
from ....anilist import AniList
|
||||||
from ...interfaces.anilist_interfaces import anilist_results_menu
|
|
||||||
from ...utils.tools import FastAnimeRuntimeState
|
|
||||||
|
|
||||||
anime_data = AniList.get_most_recently_updated()
|
anime_data = AniList.get_most_recently_updated()
|
||||||
if anime_data[0]:
|
if anime_data[0]:
|
||||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
if dump_json:
|
||||||
fastanime_runtime_state.anilist_data = anime_data[1]
|
import json
|
||||||
anilist_results_menu(config, fastanime_runtime_state)
|
|
||||||
|
print(json.dumps(anime_data[1]))
|
||||||
|
else:
|
||||||
|
from ...interfaces.anilist_interfaces import anilist_results_menu
|
||||||
|
from ...utils.tools import FastAnimeRuntimeState
|
||||||
|
|
||||||
|
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||||
|
fastanime_runtime_state.anilist_data = anime_data[1]
|
||||||
|
anilist_results_menu(config, fastanime_runtime_state)
|
||||||
|
else:
|
||||||
|
from sys import exit
|
||||||
|
|
||||||
|
exit(1)
|
||||||
|
|||||||
@@ -7,26 +7,40 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
@click.command(help="View anime you are rewatching")
|
@click.command(help="View anime you are rewatching")
|
||||||
|
@click.option(
|
||||||
|
"--dump-json",
|
||||||
|
"-d",
|
||||||
|
is_flag=True,
|
||||||
|
help="Only print out the results dont open anilist menu",
|
||||||
|
)
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def rewatching(config: "Config"):
|
def rewatching(config: "Config", dump_json):
|
||||||
|
from sys import exit
|
||||||
|
|
||||||
from ....anilist import AniList
|
from ....anilist import AniList
|
||||||
from ...interfaces import anilist_interfaces
|
|
||||||
from ...utils.tools import FastAnimeRuntimeState, exit_app
|
|
||||||
|
|
||||||
if not config.user:
|
if not config.user:
|
||||||
print("Not authenticated")
|
print("Not authenticated")
|
||||||
print("Please run: fastanime anilist loggin")
|
print("Please run: fastanime anilist loggin")
|
||||||
exit_app()
|
exit(1)
|
||||||
anime_list = AniList.get_anime_list("REPEATING")
|
anime_list = AniList.get_anime_list("REPEATING")
|
||||||
if not anime_list:
|
if not anime_list:
|
||||||
return
|
exit(1)
|
||||||
if not anime_list[0] or not anime_list[1]:
|
if not anime_list[0] or not anime_list[1]:
|
||||||
return
|
exit(1)
|
||||||
media = [
|
media = [
|
||||||
mediaListItem["media"]
|
mediaListItem["media"]
|
||||||
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
|
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
|
||||||
] # pyright:ignore
|
] # pyright:ignore
|
||||||
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
|
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
|
||||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
if dump_json:
|
||||||
fastanime_runtime_state.anilist_data = anime_list[1]
|
import json
|
||||||
anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state)
|
|
||||||
|
print(json.dumps(anime_list[1]))
|
||||||
|
else:
|
||||||
|
from ...interfaces import anilist_interfaces
|
||||||
|
from ...utils.tools import FastAnimeRuntimeState
|
||||||
|
|
||||||
|
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||||
|
fastanime_runtime_state.anilist_data = anime_list[1]
|
||||||
|
anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state)
|
||||||
|
|||||||
@@ -4,14 +4,30 @@ import click
|
|||||||
@click.command(
|
@click.command(
|
||||||
help="Fetch the 15 most scored anime", short_help="View most scored anime"
|
help="Fetch the 15 most scored anime", short_help="View most scored anime"
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
"--dump-json",
|
||||||
|
"-d",
|
||||||
|
is_flag=True,
|
||||||
|
help="Only print out the results dont open anilist menu",
|
||||||
|
)
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def scores(config):
|
def scores(config, dump_json):
|
||||||
from ....anilist import AniList
|
from ....anilist import AniList
|
||||||
from ...interfaces.anilist_interfaces import anilist_results_menu
|
|
||||||
from ...utils.tools import FastAnimeRuntimeState
|
|
||||||
|
|
||||||
anime_data = AniList.get_most_scored()
|
anime_data = AniList.get_most_scored()
|
||||||
if anime_data[0]:
|
if anime_data[0]:
|
||||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
if dump_json:
|
||||||
fastanime_runtime_state.data = anime_data[1]
|
import json
|
||||||
anilist_results_menu(config, fastanime_runtime_state)
|
|
||||||
|
print(json.dumps(anime_data[1]))
|
||||||
|
else:
|
||||||
|
from ...interfaces.anilist_interfaces import anilist_results_menu
|
||||||
|
from ...utils.tools import FastAnimeRuntimeState
|
||||||
|
|
||||||
|
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||||
|
fastanime_runtime_state.data = anime_data[1]
|
||||||
|
anilist_results_menu(config, fastanime_runtime_state)
|
||||||
|
else:
|
||||||
|
from sys import exit
|
||||||
|
|
||||||
|
exit(1)
|
||||||
|
|||||||
@@ -7,15 +7,153 @@ from ...completion_functions import anime_titles_shell_complete
|
|||||||
help="Search for anime using anilists api and get top ~50 results",
|
help="Search for anime using anilists api and get top ~50 results",
|
||||||
short_help="Search for anime",
|
short_help="Search for anime",
|
||||||
)
|
)
|
||||||
@click.argument("title", shell_complete=anime_titles_shell_complete)
|
@click.option("--title", "-t", shell_complete=anime_titles_shell_complete)
|
||||||
|
@click.option(
|
||||||
|
"--dump-json",
|
||||||
|
"-d",
|
||||||
|
is_flag=True,
|
||||||
|
help="Only print out the results dont open anilist menu",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--season",
|
||||||
|
help="The season the media was released",
|
||||||
|
type=click.Choice(["WINTER", "SPRING", "SUMMER", "FALL"]),
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--status",
|
||||||
|
"-S",
|
||||||
|
help="The media status of the anime",
|
||||||
|
type=click.Choice(
|
||||||
|
["FINISHED", "RELEASING", "NOT_YET_RELEASED", "CANCELLED", "HIATUS"]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--sort",
|
||||||
|
"-s",
|
||||||
|
help="What to sort the search results on",
|
||||||
|
type=click.Choice(
|
||||||
|
[
|
||||||
|
"ID",
|
||||||
|
"ID_DESC",
|
||||||
|
"TITLE_ROMAJI",
|
||||||
|
"TITLE_ROMAJI_DESC",
|
||||||
|
"TITLE_ENGLISH",
|
||||||
|
"TITLE_ENGLISH_DESC",
|
||||||
|
"TITLE_NATIVE",
|
||||||
|
"TITLE_NATIVE_DESC",
|
||||||
|
"TYPE",
|
||||||
|
"TYPE_DESC",
|
||||||
|
"FORMAT",
|
||||||
|
"FORMAT_DESC",
|
||||||
|
"START_DATE",
|
||||||
|
"START_DATE_DESC",
|
||||||
|
"END_DATE",
|
||||||
|
"END_DATE_DESC",
|
||||||
|
"SCORE",
|
||||||
|
"SCORE_DESC",
|
||||||
|
"POPULARITY",
|
||||||
|
"POPULARITY_DESC",
|
||||||
|
"TRENDING",
|
||||||
|
"TRENDING_DESC",
|
||||||
|
"EPISODES",
|
||||||
|
"EPISODES_DESC",
|
||||||
|
"DURATION",
|
||||||
|
"DURATION_DESC",
|
||||||
|
"STATUS",
|
||||||
|
"STATUS_DESC",
|
||||||
|
"CHAPTERS",
|
||||||
|
"CHAPTERS_DESC",
|
||||||
|
"VOLUMES",
|
||||||
|
"VOLUMES_DESC",
|
||||||
|
"UPDATED_AT",
|
||||||
|
"UPDATED_AT_DESC",
|
||||||
|
"SEARCH_MATCH",
|
||||||
|
"FAVOURITES",
|
||||||
|
"FAVOURITES_DESC",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
@click.option("--genres", "-g", multiple=True, help="the genres to filter by")
|
||||||
|
@click.option("--tags", "-t", multiple=True, help="the tags to filter by")
|
||||||
|
@click.option(
|
||||||
|
"--media-format",
|
||||||
|
"-f",
|
||||||
|
multiple=True,
|
||||||
|
help="Media format",
|
||||||
|
type=click.Choice(
|
||||||
|
["TV", "TV_SHORT", "MOVIE", "SPECIAL", "OVA", "MUSIC", "NOVEL", "ONE_SHOT"]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--year",
|
||||||
|
"-y",
|
||||||
|
type=click.Choice(
|
||||||
|
[
|
||||||
|
"2024",
|
||||||
|
"2023",
|
||||||
|
"2022",
|
||||||
|
"2021",
|
||||||
|
"2020",
|
||||||
|
"2019",
|
||||||
|
"2018",
|
||||||
|
"2017",
|
||||||
|
"2016",
|
||||||
|
"2015",
|
||||||
|
"2014",
|
||||||
|
"2013",
|
||||||
|
"2012",
|
||||||
|
"2011",
|
||||||
|
"2010",
|
||||||
|
"2009",
|
||||||
|
"2008",
|
||||||
|
"2007",
|
||||||
|
"2006",
|
||||||
|
"2005",
|
||||||
|
"2004",
|
||||||
|
"2000",
|
||||||
|
"1990",
|
||||||
|
"1980",
|
||||||
|
"1970",
|
||||||
|
"1960",
|
||||||
|
"1950",
|
||||||
|
"1940",
|
||||||
|
"1930",
|
||||||
|
"1920",
|
||||||
|
"1910",
|
||||||
|
"1900",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
help="the year the media was released",
|
||||||
|
)
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def search(config, title):
|
def search(
|
||||||
|
config, title, dump_json, season, status, sort, genres, tags, media_format, year
|
||||||
|
):
|
||||||
from ....anilist import AniList
|
from ....anilist import AniList
|
||||||
from ...interfaces.anilist_interfaces import anilist_results_menu
|
|
||||||
from ...utils.tools import FastAnimeRuntimeState
|
|
||||||
|
|
||||||
success, search_results = AniList.search(title)
|
success, search_results = AniList.search(
|
||||||
|
query=title,
|
||||||
|
sort=sort,
|
||||||
|
status=status,
|
||||||
|
genre_in=list(genres),
|
||||||
|
season=season,
|
||||||
|
tag_in=list(tags),
|
||||||
|
seasonYear=year,
|
||||||
|
format_in=list(media_format),
|
||||||
|
)
|
||||||
if success:
|
if success:
|
||||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
if dump_json:
|
||||||
fastanime_runtime_state.anilist_data = search_results
|
import json
|
||||||
anilist_results_menu(config, fastanime_runtime_state)
|
|
||||||
|
print(json.dumps(search_results))
|
||||||
|
else:
|
||||||
|
from ...interfaces.anilist_interfaces import anilist_results_menu
|
||||||
|
from ...utils.tools import FastAnimeRuntimeState
|
||||||
|
|
||||||
|
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||||
|
fastanime_runtime_state.anilist_data = search_results
|
||||||
|
anilist_results_menu(config, fastanime_runtime_state)
|
||||||
|
else:
|
||||||
|
from sys import exit
|
||||||
|
|
||||||
|
exit(1)
|
||||||
|
|||||||
@@ -5,14 +5,30 @@ import click
|
|||||||
help="Fetch the top 15 anime that are currently trending",
|
help="Fetch the top 15 anime that are currently trending",
|
||||||
short_help="Trending anime 🔥🔥🔥",
|
short_help="Trending anime 🔥🔥🔥",
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
"--dump-json",
|
||||||
|
"-d",
|
||||||
|
is_flag=True,
|
||||||
|
help="Only print out the results dont open anilist menu",
|
||||||
|
)
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def trending(config):
|
def trending(config, dump_json):
|
||||||
from ....anilist import AniList
|
from ....anilist import AniList
|
||||||
from ...interfaces.anilist_interfaces import anilist_results_menu
|
|
||||||
from ...utils.tools import FastAnimeRuntimeState
|
|
||||||
|
|
||||||
success, data = AniList.get_trending()
|
success, data = AniList.get_trending()
|
||||||
if success:
|
if success:
|
||||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
if dump_json:
|
||||||
fastanime_runtime_state.anilist_data = data
|
import json
|
||||||
anilist_results_menu(config, fastanime_runtime_state)
|
|
||||||
|
print(json.dumps(data))
|
||||||
|
else:
|
||||||
|
from ...interfaces.anilist_interfaces import anilist_results_menu
|
||||||
|
from ...utils.tools import FastAnimeRuntimeState
|
||||||
|
|
||||||
|
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||||
|
fastanime_runtime_state.anilist_data = data
|
||||||
|
anilist_results_menu(config, fastanime_runtime_state)
|
||||||
|
else:
|
||||||
|
from sys import exit
|
||||||
|
|
||||||
|
exit(1)
|
||||||
|
|||||||
@@ -4,14 +4,30 @@ import click
|
|||||||
@click.command(
|
@click.command(
|
||||||
help="Fetch the 15 most anticipited anime", short_help="View upcoming anime"
|
help="Fetch the 15 most anticipited anime", short_help="View upcoming anime"
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
"--dump-json",
|
||||||
|
"-d",
|
||||||
|
is_flag=True,
|
||||||
|
help="Only print out the results dont open anilist menu",
|
||||||
|
)
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def upcoming(config):
|
def upcoming(config, dump_json):
|
||||||
from ....anilist import AniList
|
from ....anilist import AniList
|
||||||
from ...interfaces.anilist_interfaces import anilist_results_menu
|
|
||||||
from ...utils.tools import FastAnimeRuntimeState
|
|
||||||
|
|
||||||
success, data = AniList.get_upcoming_anime()
|
success, data = AniList.get_upcoming_anime()
|
||||||
if success:
|
if success:
|
||||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
if dump_json:
|
||||||
fastanime_runtime_state.anilist_data = data
|
import json
|
||||||
anilist_results_menu(config, fastanime_runtime_state)
|
|
||||||
|
print(json.dumps(data))
|
||||||
|
else:
|
||||||
|
from ...interfaces.anilist_interfaces import anilist_results_menu
|
||||||
|
from ...utils.tools import FastAnimeRuntimeState
|
||||||
|
|
||||||
|
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||||
|
fastanime_runtime_state.anilist_data = data
|
||||||
|
anilist_results_menu(config, fastanime_runtime_state)
|
||||||
|
else:
|
||||||
|
from sys import exit
|
||||||
|
|
||||||
|
exit(1)
|
||||||
|
|||||||
@@ -7,26 +7,40 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
@click.command(help="View anime you are watching")
|
@click.command(help="View anime you are watching")
|
||||||
|
@click.option(
|
||||||
|
"--dump-json",
|
||||||
|
"-d",
|
||||||
|
is_flag=True,
|
||||||
|
help="Only print out the results dont open anilist menu",
|
||||||
|
)
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def watching(config: "Config"):
|
def watching(config: "Config", dump_json):
|
||||||
|
from sys import exit
|
||||||
|
|
||||||
from ....anilist import AniList
|
from ....anilist import AniList
|
||||||
from ...interfaces import anilist_interfaces
|
|
||||||
from ...utils.tools import FastAnimeRuntimeState, exit_app
|
|
||||||
|
|
||||||
if not config.user:
|
if not config.user:
|
||||||
print("Not authenticated")
|
print("Not authenticated")
|
||||||
print("Please run: fastanime anilist loggin")
|
print("Please run: fastanime anilist loggin")
|
||||||
exit_app()
|
exit(1)
|
||||||
anime_list = AniList.get_anime_list("CURRENT")
|
anime_list = AniList.get_anime_list("CURRENT")
|
||||||
if not anime_list:
|
if not anime_list:
|
||||||
return
|
exit(1)
|
||||||
if not anime_list[0] or not anime_list[1]:
|
if not anime_list[0] or not anime_list[1]:
|
||||||
return
|
exit(1)
|
||||||
media = [
|
media = [
|
||||||
mediaListItem["media"]
|
mediaListItem["media"]
|
||||||
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
|
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
|
||||||
] # pyright:ignore
|
] # pyright:ignore
|
||||||
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
|
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
|
||||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
if dump_json:
|
||||||
fastanime_runtime_state.anilist_data = anime_list[1]
|
import json
|
||||||
anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state)
|
|
||||||
|
print(json.dumps(anime_list[1]))
|
||||||
|
else:
|
||||||
|
from ...interfaces import anilist_interfaces
|
||||||
|
from ...utils.tools import FastAnimeRuntimeState
|
||||||
|
|
||||||
|
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||||
|
fastanime_runtime_state.anilist_data = anime_list[1]
|
||||||
|
anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state)
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import time
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import click
|
import click
|
||||||
@@ -28,8 +27,14 @@ if TYPE_CHECKING:
|
|||||||
help="A range of episodes to download (start-end)",
|
help="A range of episodes to download (start-end)",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--force-unknown-ext",
|
"--file",
|
||||||
"-f",
|
"-f",
|
||||||
|
type=click.File(),
|
||||||
|
help="A file to read from all anime to download",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--force-unknown-ext",
|
||||||
|
"-F",
|
||||||
help="This option forces yt-dlp to download extensions its not aware of",
|
help="This option forces yt-dlp to download extensions its not aware of",
|
||||||
is_flag=True,
|
is_flag=True,
|
||||||
)
|
)
|
||||||
@@ -41,15 +46,43 @@ if TYPE_CHECKING:
|
|||||||
default=True,
|
default=True,
|
||||||
)
|
)
|
||||||
@click.option("--verbose", "-v", is_flag=True, help="Download verbosely (everywhere)")
|
@click.option("--verbose", "-v", is_flag=True, help="Download verbosely (everywhere)")
|
||||||
|
@click.option(
|
||||||
|
"--merge", "-m", is_flag=True, help="Merge the subfile with video using ffmpeg"
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--clean",
|
||||||
|
"-c",
|
||||||
|
is_flag=True,
|
||||||
|
help="After merging delete the original files",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--wait-time",
|
||||||
|
"-w",
|
||||||
|
type=int,
|
||||||
|
help="The amount of time to wait after downloading is complete before the screen is completely cleared",
|
||||||
|
default=60,
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--prompt/--no-prompt",
|
||||||
|
help="Dont prompt for anything instead just do the best thing",
|
||||||
|
default=True,
|
||||||
|
)
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def download(
|
def download(
|
||||||
config: "Config",
|
config: "Config",
|
||||||
anime_titles: list,
|
anime_titles: tuple,
|
||||||
episode_range,
|
episode_range,
|
||||||
|
file,
|
||||||
force_unknown_ext,
|
force_unknown_ext,
|
||||||
silent,
|
silent,
|
||||||
verbose,
|
verbose,
|
||||||
|
merge,
|
||||||
|
clean,
|
||||||
|
wait_time,
|
||||||
|
prompt,
|
||||||
):
|
):
|
||||||
|
import time
|
||||||
|
|
||||||
from rich import print
|
from rich import print
|
||||||
from rich.progress import Progress
|
from rich.progress import Progress
|
||||||
from thefuzz import fuzz
|
from thefuzz import fuzz
|
||||||
@@ -59,15 +92,28 @@ 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)
|
||||||
|
|
||||||
translation_type = config.translation_type
|
translation_type = config.translation_type
|
||||||
download_dir = config.downloads_dir
|
download_dir = config.downloads_dir
|
||||||
|
if file:
|
||||||
|
contents = file.read()
|
||||||
|
anime_titles_from_file = tuple(
|
||||||
|
[title for title in contents.split("\n") if title]
|
||||||
|
)
|
||||||
|
file.close()
|
||||||
|
|
||||||
|
anime_titles = (*anime_titles_from_file, *anime_titles)
|
||||||
print(f"[green bold]Queued:[/] {anime_titles}")
|
print(f"[green bold]Queued:[/] {anime_titles}")
|
||||||
for anime_title in anime_titles:
|
for anime_title in anime_titles:
|
||||||
|
if anime_title == "EOF":
|
||||||
|
break
|
||||||
print(f"[green bold]Now Downloading: [/] {anime_title}")
|
print(f"[green bold]Now Downloading: [/] {anime_title}")
|
||||||
# ---- search for anime ----
|
# ---- search for anime ----
|
||||||
with Progress() as progress:
|
with Progress() as progress:
|
||||||
@@ -79,10 +125,23 @@ def download(
|
|||||||
print("Search results failed")
|
print("Search results failed")
|
||||||
input("Enter to retry")
|
input("Enter to retry")
|
||||||
download(
|
download(
|
||||||
config, anime_title, episode_range, force_unknown_ext, silent, verbose
|
config,
|
||||||
|
anime_title,
|
||||||
|
episode_range,
|
||||||
|
file,
|
||||||
|
force_unknown_ext,
|
||||||
|
silent,
|
||||||
|
verbose,
|
||||||
|
merge,
|
||||||
|
clean,
|
||||||
|
wait_time,
|
||||||
|
prompt,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
search_results = search_results["results"]
|
search_results = search_results["results"]
|
||||||
|
if not search_results:
|
||||||
|
print("Nothing muches your search term")
|
||||||
|
continue
|
||||||
search_results_ = {
|
search_results_ = {
|
||||||
search_result["title"]: search_result for search_result in search_results
|
search_result["title"]: search_result for search_result in search_results
|
||||||
}
|
}
|
||||||
@@ -112,7 +171,17 @@ def download(
|
|||||||
print("Sth went wring anime no found")
|
print("Sth went wring anime no found")
|
||||||
input("Enter to continue...")
|
input("Enter to continue...")
|
||||||
download(
|
download(
|
||||||
config, anime_title, episode_range, force_unknown_ext, silent, verbose
|
config,
|
||||||
|
anime_title,
|
||||||
|
episode_range,
|
||||||
|
file,
|
||||||
|
force_unknown_ext,
|
||||||
|
silent,
|
||||||
|
verbose,
|
||||||
|
merge,
|
||||||
|
clean,
|
||||||
|
wait_time,
|
||||||
|
prompt,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -165,37 +234,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")
|
||||||
@@ -203,14 +247,45 @@ 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"],
|
search_result,
|
||||||
episode_title,
|
episode_title,
|
||||||
download_dir,
|
download_dir,
|
||||||
silent,
|
silent,
|
||||||
@@ -218,10 +293,15 @@ def download(
|
|||||||
force_unknown_ext,
|
force_unknown_ext,
|
||||||
verbose,
|
verbose,
|
||||||
headers=provider_headers,
|
headers=provider_headers,
|
||||||
|
sub=subtitles[0]["url"] if subtitles else "",
|
||||||
|
merge=merge,
|
||||||
|
clean=clean,
|
||||||
|
prompt=prompt,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
print("Continuing...")
|
print("Continuing...")
|
||||||
print("Done Downloading")
|
print("Done Downloading")
|
||||||
|
time.sleep(wait_time)
|
||||||
exit_app()
|
exit_app()
|
||||||
|
|||||||
@@ -76,6 +76,9 @@ def grab(
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
search_results = search_results["results"]
|
search_results = search_results["results"]
|
||||||
|
if not search_results:
|
||||||
|
logger.error("no results for your search")
|
||||||
|
exit(1)
|
||||||
search_results_ = {
|
search_results_ = {
|
||||||
search_result["title"]: search_result for search_result in search_results
|
search_result["title"]: search_result for search_result in search_results
|
||||||
}
|
}
|
||||||
@@ -88,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:
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ query($query:String){
|
|||||||
Page(perPage:50){
|
Page(perPage:50){
|
||||||
pageInfo{
|
pageInfo{
|
||||||
total
|
total
|
||||||
currentPage
|
|
||||||
hasNextPage
|
|
||||||
}
|
}
|
||||||
media(search:$query,type:ANIME){
|
media(search:$query,type:ANIME){
|
||||||
id
|
id
|
||||||
@@ -46,20 +44,6 @@ def get_anime_titles(query: str, variables: dict = {}):
|
|||||||
)
|
)
|
||||||
anilist_data = response.json()
|
anilist_data = response.json()
|
||||||
|
|
||||||
# ensuring you dont get blocked
|
|
||||||
if (
|
|
||||||
int(response.headers.get("X-RateLimit-Remaining", 0)) < 30
|
|
||||||
and not response.status_code == 500
|
|
||||||
):
|
|
||||||
print("Warning you are exceeding the allowed number of calls per minute")
|
|
||||||
logger.warning(
|
|
||||||
"You are exceeding the allowed number of calls per minute for the AniList api enforcing timeout"
|
|
||||||
)
|
|
||||||
print("Forced timeout will now be initiated")
|
|
||||||
import time
|
|
||||||
|
|
||||||
print("sleeping...")
|
|
||||||
time.sleep(1 * 60)
|
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
eng_titles = [
|
eng_titles = [
|
||||||
anime["title"]["english"]
|
anime["title"]["english"]
|
||||||
@@ -80,4 +64,16 @@ def get_anime_titles(query: str, variables: dict = {}):
|
|||||||
|
|
||||||
|
|
||||||
def anime_titles_shell_complete(ctx, param, incomplete):
|
def anime_titles_shell_complete(ctx, param, incomplete):
|
||||||
return [name for name in get_anime_titles(anime_title_query, {"query": incomplete})]
|
incomplete = incomplete.strip()
|
||||||
|
if not incomplete:
|
||||||
|
incomplete = None
|
||||||
|
variables = {}
|
||||||
|
else:
|
||||||
|
variables = {"query": incomplete}
|
||||||
|
return get_anime_titles(anime_title_query, variables)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
t = input("Enter title")
|
||||||
|
results = get_anime_titles(anime_title_query, {"query": t})
|
||||||
|
print(results)
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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(
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -19,6 +19,46 @@ BG_GREEN = "\033[48;2;120;233;12;m"
|
|||||||
GREEN = "\033[38;2;45;24;45;m"
|
GREEN = "\033[38;2;45;24;45;m"
|
||||||
|
|
||||||
|
|
||||||
|
def get_requested_quality_or_default_to_first(url, quality):
|
||||||
|
import yt_dlp
|
||||||
|
|
||||||
|
with yt_dlp.YoutubeDL({"quiet": True, "silent": True, "no_warnings": True}) as ydl:
|
||||||
|
m3u8_info = ydl.extract_info(url, False)
|
||||||
|
if not m3u8_info:
|
||||||
|
return
|
||||||
|
|
||||||
|
m3u8_formats = m3u8_info["formats"]
|
||||||
|
quality = int(quality)
|
||||||
|
quality_u = quality - 80
|
||||||
|
quality_l = quality + 80
|
||||||
|
for m3u8_format in m3u8_formats:
|
||||||
|
if m3u8_format["height"] == quality or (
|
||||||
|
m3u8_format["height"] < quality_u and m3u8_format["height"] > quality_l
|
||||||
|
):
|
||||||
|
return m3u8_format["url"]
|
||||||
|
else:
|
||||||
|
return m3u8_formats[0]["url"]
|
||||||
|
|
||||||
|
|
||||||
|
def move_preferred_subtitle_lang_to_top(sub_list, lang_str):
|
||||||
|
"""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
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import sys
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from platform import system
|
from platform import system
|
||||||
|
|
||||||
from . import APP_NAME, AUTHOR, __version__
|
import click
|
||||||
|
|
||||||
|
from . import APP_NAME, __version__
|
||||||
|
|
||||||
PLATFORM = system()
|
PLATFORM = system()
|
||||||
|
|
||||||
@@ -17,19 +19,20 @@ if PLATFORM == "Windows":
|
|||||||
ICON_PATH = os.path.join(ASSETS_DIR, "logo.ico")
|
ICON_PATH = os.path.join(ASSETS_DIR, "logo.ico")
|
||||||
else:
|
else:
|
||||||
ICON_PATH = os.path.join(ASSETS_DIR, "logo.png")
|
ICON_PATH = os.path.join(ASSETS_DIR, "logo.png")
|
||||||
PREVIEW_IMAGE = os.path.join(ASSETS_DIR, "preview")
|
# PREVIEW_IMAGE = os.path.join(ASSETS_DIR, "preview")
|
||||||
|
|
||||||
|
|
||||||
# ----- user configs and data -----
|
# ----- user configs and data -----
|
||||||
|
|
||||||
S_PLATFORM = sys.platform
|
S_PLATFORM = sys.platform
|
||||||
|
APP_DATA_DIR = click.get_app_dir(APP_NAME)
|
||||||
if S_PLATFORM == "win32":
|
if S_PLATFORM == "win32":
|
||||||
# app data
|
# app data
|
||||||
app_data_dir_base = os.getenv("LOCALAPPDATA")
|
# app_data_dir_base = os.getenv("LOCALAPPDATA")
|
||||||
if not app_data_dir_base:
|
# if not app_data_dir_base:
|
||||||
raise RuntimeError("Could not determine app data dir please report to devs")
|
# raise RuntimeError("Could not determine app data dir please report to devs")
|
||||||
APP_DATA_DIR = os.path.join(app_data_dir_base, AUTHOR, APP_NAME)
|
# APP_DATA_DIR = os.path.join(app_data_dir_base, AUTHOR, APP_NAME)
|
||||||
|
#
|
||||||
# cache dir
|
# cache dir
|
||||||
APP_CACHE_DIR = os.path.join(APP_DATA_DIR, "cache")
|
APP_CACHE_DIR = os.path.join(APP_DATA_DIR, "cache")
|
||||||
|
|
||||||
@@ -39,9 +42,9 @@ if S_PLATFORM == "win32":
|
|||||||
|
|
||||||
elif S_PLATFORM == "darwin":
|
elif S_PLATFORM == "darwin":
|
||||||
# app data
|
# app data
|
||||||
app_data_dir_base = os.path.expanduser("~/Library/Application Support")
|
# app_data_dir_base = os.path.expanduser("~/Library/Application Support")
|
||||||
APP_DATA_DIR = os.path.join(app_data_dir_base, APP_NAME, __version__)
|
# APP_DATA_DIR = os.path.join(app_data_dir_base, APP_NAME, __version__)
|
||||||
|
#
|
||||||
# cache dir
|
# cache dir
|
||||||
cache_dir_base = os.path.expanduser("~/Library/Caches")
|
cache_dir_base = os.path.expanduser("~/Library/Caches")
|
||||||
APP_CACHE_DIR = os.path.join(cache_dir_base, APP_NAME, __version__)
|
APP_CACHE_DIR = os.path.join(cache_dir_base, APP_NAME, __version__)
|
||||||
@@ -50,12 +53,12 @@ elif S_PLATFORM == "darwin":
|
|||||||
video_dir_base = os.path.expanduser("~/Movies")
|
video_dir_base = os.path.expanduser("~/Movies")
|
||||||
USER_VIDEOS_DIR = os.path.join(video_dir_base, APP_NAME)
|
USER_VIDEOS_DIR = os.path.join(video_dir_base, APP_NAME)
|
||||||
else:
|
else:
|
||||||
# app data
|
# # app data
|
||||||
app_data_dir_base = os.environ.get("XDG_CONFIG_HOME", "")
|
# app_data_dir_base = os.environ.get("XDG_CONFIG_HOME", "")
|
||||||
if not app_data_dir_base.strip():
|
# if not app_data_dir_base.strip():
|
||||||
app_data_dir_base = os.path.expanduser("~/.config")
|
# app_data_dir_base = os.path.expanduser("~/.config")
|
||||||
APP_DATA_DIR = os.path.join(app_data_dir_base, APP_NAME)
|
# APP_DATA_DIR = os.path.join(app_data_dir_base, APP_NAME)
|
||||||
|
#
|
||||||
# cache dir
|
# cache dir
|
||||||
cache_dir_base = os.environ.get("XDG_CACHE_HOME", "")
|
cache_dir_base = os.environ.get("XDG_CACHE_HOME", "")
|
||||||
if not cache_dir_base.strip():
|
if not cache_dir_base.strip():
|
||||||
|
|||||||
@@ -309,9 +309,13 @@ class AniListApi:
|
|||||||
status_not_in: list[str] | None = None,
|
status_not_in: list[str] | None = None,
|
||||||
endDate_greater: int | None = None,
|
endDate_greater: int | None = None,
|
||||||
endDate_lesser: int | None = None,
|
endDate_lesser: int | None = None,
|
||||||
start_greater: int | None = None,
|
startDate_greater: int | None = None,
|
||||||
start_lesser: int | None = None,
|
startDate_lesser: int | None = None,
|
||||||
|
startDate: str | None = None,
|
||||||
|
seasonYear: str | None = None,
|
||||||
page: int | None = None,
|
page: int | None = None,
|
||||||
|
season: str | None = None,
|
||||||
|
format_in: list[str] | None = None,
|
||||||
type="ANIME",
|
type="ANIME",
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
@@ -320,7 +324,7 @@ class AniListApi:
|
|||||||
"""
|
"""
|
||||||
variables = {}
|
variables = {}
|
||||||
for key, val in list(locals().items())[1:]:
|
for key, val in list(locals().items())[1:]:
|
||||||
if val is not None and key not in ["variables"]:
|
if val and key not in ["variables"]:
|
||||||
variables[key] = val
|
variables[key] = val
|
||||||
search_results = self.get_data(search_query, variables=variables)
|
search_results = self.get_data(search_query, variables=variables)
|
||||||
return search_results
|
return search_results
|
||||||
|
|||||||
@@ -221,11 +221,15 @@ $popularity_greater:Int,\
|
|||||||
$popularity_lesser:Int,\
|
$popularity_lesser:Int,\
|
||||||
$averageScore_greater:Int,\
|
$averageScore_greater:Int,\
|
||||||
$averageScore_lesser:Int,\
|
$averageScore_lesser:Int,\
|
||||||
|
$seasonYear:Int,\
|
||||||
$startDate_greater:FuzzyDateInt,\
|
$startDate_greater:FuzzyDateInt,\
|
||||||
$startDate_lesser:FuzzyDateInt,\
|
$startDate_lesser:FuzzyDateInt,\
|
||||||
|
$startDate:FuzzyDateInt,\
|
||||||
$endDate_greater:FuzzyDateInt,\
|
$endDate_greater:FuzzyDateInt,\
|
||||||
$endDate_lesser:FuzzyDateInt,\
|
$endDate_lesser:FuzzyDateInt,\
|
||||||
|
$format_in:[MediaFormat],\
|
||||||
$type:MediaType\
|
$type:MediaType\
|
||||||
|
$season:MediaSeason\
|
||||||
"
|
"
|
||||||
# FuzzyDateInt = (yyyymmdd)
|
# FuzzyDateInt = (yyyymmdd)
|
||||||
# MediaStatus = (FINISHED,RELEASING,NOT_YET_RELEASED,CANCELLED,HIATUS)
|
# MediaStatus = (FINISHED,RELEASING,NOT_YET_RELEASED,CANCELLED,HIATUS)
|
||||||
@@ -247,6 +251,7 @@ query($query:String,%s){
|
|||||||
tag_not_in:$tag_not_in,
|
tag_not_in:$tag_not_in,
|
||||||
status_in:$status_in,
|
status_in:$status_in,
|
||||||
status:$status,
|
status:$status,
|
||||||
|
startDate:$startDate,
|
||||||
status_not_in:$status_not_in,
|
status_not_in:$status_not_in,
|
||||||
popularity_greater:$popularity_greater,
|
popularity_greater:$popularity_greater,
|
||||||
popularity_lesser:$popularity_lesser,
|
popularity_lesser:$popularity_lesser,
|
||||||
@@ -256,7 +261,10 @@ query($query:String,%s){
|
|||||||
startDate_lesser:$startDate_lesser,
|
startDate_lesser:$startDate_lesser,
|
||||||
endDate_greater:$endDate_greater,
|
endDate_greater:$endDate_greater,
|
||||||
endDate_lesser:$endDate_lesser,
|
endDate_lesser:$endDate_lesser,
|
||||||
|
format_in:$format_in,
|
||||||
sort:$sort,
|
sort:$sort,
|
||||||
|
season:$season,
|
||||||
|
seasonYear:$seasonYear,
|
||||||
type:$type
|
type:$type
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
|
from .allanime.constants import SERVERS_AVAILABLE as ALLANIME_SERVERS
|
||||||
|
from .animepahe.constants import SERVERS_AVAILABLE as ANIMEPAHESERVERS
|
||||||
|
from .aniwatch.constants import SERVERS_AVAILABLE as ANIWATCHSERVERS
|
||||||
|
|
||||||
anime_sources = {
|
anime_sources = {
|
||||||
"allanime": "api.AllAnimeAPI",
|
"allanime": "api.AllAnimeAPI",
|
||||||
"animepahe": "api.AnimePaheApi",
|
"animepahe": "api.AnimePaheApi",
|
||||||
|
"aniwatch": "api.AniWatchApi",
|
||||||
|
"aniwave": "api.AniWaveApi",
|
||||||
}
|
}
|
||||||
SERVERS_AVAILABLE = [
|
SERVERS_AVAILABLE = [*ALLANIME_SERVERS, *ANIMEPAHESERVERS, *ANIWATCHSERVERS]
|
||||||
"sharepoint",
|
|
||||||
"dropbox",
|
|
||||||
"gogoanime",
|
|
||||||
"weTransfer",
|
|
||||||
"wixmp",
|
|
||||||
"kwik",
|
|
||||||
"Yt",
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -11,12 +11,7 @@ from requests.exceptions import Timeout
|
|||||||
|
|
||||||
from ...anime_provider.base_provider import AnimeProvider
|
from ...anime_provider.base_provider import AnimeProvider
|
||||||
from ..utils import give_random_quality, one_digit_symmetric_xor
|
from ..utils import give_random_quality, one_digit_symmetric_xor
|
||||||
from .constants import (
|
from .constants import ALLANIME_API_ENDPOINT, ALLANIME_BASE, ALLANIME_REFERER
|
||||||
ALLANIME_API_ENDPOINT,
|
|
||||||
ALLANIME_BASE,
|
|
||||||
ALLANIME_REFERER,
|
|
||||||
USER_AGENT,
|
|
||||||
)
|
|
||||||
from .gql_queries import ALLANIME_EPISODES_GQL, ALLANIME_SEARCH_GQL, ALLANIME_SHOW_GQL
|
from .gql_queries import ALLANIME_EPISODES_GQL, ALLANIME_SEARCH_GQL, ALLANIME_SHOW_GQL
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -36,6 +31,9 @@ class AllAnimeAPI(AnimeProvider):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
api_endpoint = ALLANIME_API_ENDPOINT
|
api_endpoint = ALLANIME_API_ENDPOINT
|
||||||
|
HEADERS = {
|
||||||
|
"Referer": ALLANIME_REFERER,
|
||||||
|
}
|
||||||
|
|
||||||
def _fetch_gql(self, query: str, variables: dict):
|
def _fetch_gql(self, query: str, variables: dict):
|
||||||
"""main abstraction over all requests to the allanime api
|
"""main abstraction over all requests to the allanime api
|
||||||
@@ -54,7 +52,6 @@ class AllAnimeAPI(AnimeProvider):
|
|||||||
"variables": json.dumps(variables),
|
"variables": json.dumps(variables),
|
||||||
"query": query,
|
"query": query,
|
||||||
},
|
},
|
||||||
headers={"Referer": ALLANIME_REFERER, "User-Agent": USER_AGENT},
|
|
||||||
timeout=10,
|
timeout=10,
|
||||||
)
|
)
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
@@ -231,6 +228,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,
|
||||||
@@ -246,10 +244,6 @@ class AllAnimeAPI(AnimeProvider):
|
|||||||
)
|
)
|
||||||
resp = self.session.get(
|
resp = self.session.get(
|
||||||
embed_url,
|
embed_url,
|
||||||
headers={
|
|
||||||
"Referer": ALLANIME_REFERER,
|
|
||||||
"User-Agent": USER_AGENT,
|
|
||||||
},
|
|
||||||
timeout=10,
|
timeout=10,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -260,6 +254,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 +266,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 +278,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 +290,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 +302,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"]}'
|
||||||
)
|
)
|
||||||
@@ -322,85 +321,3 @@ class AllAnimeAPI(AnimeProvider):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"FA(Allanime): {e}")
|
logger.error(f"FA(Allanime): {e}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
anime_provider = AllAnimeAPI()
|
|
||||||
# lets see if it works :)
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from InquirerPy import inquirer, validator # pyright:ignore
|
|
||||||
|
|
||||||
anime = input("Enter the anime name: ")
|
|
||||||
translation = input("Enter the translation type: ")
|
|
||||||
|
|
||||||
search_results = anime_provider.search_for_anime(
|
|
||||||
anime, translation_type=translation.strip()
|
|
||||||
)
|
|
||||||
|
|
||||||
if not search_results:
|
|
||||||
raise Exception("No results found")
|
|
||||||
|
|
||||||
search_results = search_results["results"]
|
|
||||||
options = {show["title"]: show for show in search_results}
|
|
||||||
anime = inquirer.fuzzy(
|
|
||||||
"Enter the anime title",
|
|
||||||
list(options.keys()),
|
|
||||||
validate=validator.EmptyInputValidator(),
|
|
||||||
).execute()
|
|
||||||
if anime is None:
|
|
||||||
print("No anime was selected")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
anime_result = options[anime]
|
|
||||||
anime_data = anime_provider.get_anime(anime_result["id"])
|
|
||||||
if not anime_data:
|
|
||||||
raise Exception("Anime not found")
|
|
||||||
availableEpisodesDetail = anime_data["availableEpisodesDetail"]
|
|
||||||
if not availableEpisodesDetail.get(translation.strip()):
|
|
||||||
raise Exception("No episodes found")
|
|
||||||
|
|
||||||
stream_link = True
|
|
||||||
while stream_link != "quit":
|
|
||||||
print("select episode")
|
|
||||||
episode = inquirer.fuzzy(
|
|
||||||
"Choose an episode",
|
|
||||||
availableEpisodesDetail[translation.strip()],
|
|
||||||
validate=validator.EmptyInputValidator(),
|
|
||||||
).execute()
|
|
||||||
if episode is None:
|
|
||||||
print("No episode was selected")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if not anime_data:
|
|
||||||
print("Sth went wrong")
|
|
||||||
break
|
|
||||||
episode_streams_ = anime_provider.get_episode_streams(
|
|
||||||
anime_data, # pyright: ignore
|
|
||||||
episode,
|
|
||||||
translation.strip(),
|
|
||||||
)
|
|
||||||
if episode_streams_ is None:
|
|
||||||
raise Exception("Episode not found")
|
|
||||||
|
|
||||||
episode_streams = list(episode_streams_)
|
|
||||||
stream_links = []
|
|
||||||
for server in episode_streams:
|
|
||||||
stream_links.extend([link["link"] for link in server["links"]])
|
|
||||||
stream_links.append("back")
|
|
||||||
stream_link = inquirer.fuzzy(
|
|
||||||
"Choose a link to stream",
|
|
||||||
stream_links,
|
|
||||||
validate=validator.EmptyInputValidator(),
|
|
||||||
).execute()
|
|
||||||
if stream_link == "quit":
|
|
||||||
print("Have a nice day")
|
|
||||||
sys.exit()
|
|
||||||
if not stream_link:
|
|
||||||
raise Exception("No stream was selected")
|
|
||||||
|
|
||||||
title = episode_streams[0].get(
|
|
||||||
"episode_title", "%s: Episode %s" % (anime_data["title"], episode)
|
|
||||||
)
|
|
||||||
subprocess.run(["mpv", f"--title={title}", stream_link])
|
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
from yt_dlp.utils.networking import random_user_agent
|
SERVERS_AVAILABLE = ["sharepoint", "dropbox", "gogoanime", "weTransfer", "wixmp", "Yt"]
|
||||||
|
|
||||||
ALLANIME_BASE = "allanime.day"
|
ALLANIME_BASE = "allanime.day"
|
||||||
ALLANIME_REFERER = "https://allanime.to/"
|
ALLANIME_REFERER = "https://allanime.to/"
|
||||||
ALLANIME_API_ENDPOINT = "https://api.{}/api/".format(ALLANIME_BASE)
|
ALLANIME_API_ENDPOINT = "https://api.{}/api/".format(ALLANIME_BASE)
|
||||||
USER_AGENT = random_user_agent()
|
|
||||||
SERVERS_AVAILABLE = ["sharepoint", "dropbox", "gogoanime", "weTransfer", "wixmp"]
|
|
||||||
|
|||||||
0
fastanime/libs/anime_provider/animepahe/__init__.py
Normal file
0
fastanime/libs/anime_provider/animepahe/__init__.py
Normal file
@@ -32,12 +32,14 @@ KWIK_RE = re.compile(r"Player\|(.+?)'")
|
|||||||
class AnimePaheApi(AnimeProvider):
|
class AnimePaheApi(AnimeProvider):
|
||||||
search_page: "AnimePaheSearchPage"
|
search_page: "AnimePaheSearchPage"
|
||||||
anime: "AnimePaheAnimePage"
|
anime: "AnimePaheAnimePage"
|
||||||
|
HEADERS = REQUEST_HEADERS
|
||||||
|
|
||||||
def search_for_anime(self, user_query: str, *args):
|
def search_for_anime(self, user_query: str, *args):
|
||||||
try:
|
try:
|
||||||
url = f"{ANIMEPAHE_ENDPOINT}m=search&q={user_query}"
|
url = f"{ANIMEPAHE_ENDPOINT}m=search&q={user_query}"
|
||||||
headers = {**REQUEST_HEADERS}
|
response = self.session.get(
|
||||||
response = self.session.get(url, headers=headers)
|
url,
|
||||||
|
)
|
||||||
if not response.status_code == 200:
|
if not response.status_code == 200:
|
||||||
return
|
return
|
||||||
data: "AnimePaheSearchPage" = response.json()
|
data: "AnimePaheSearchPage" = response.json()
|
||||||
@@ -85,7 +87,9 @@ class AnimePaheApi(AnimeProvider):
|
|||||||
url,
|
url,
|
||||||
page,
|
page,
|
||||||
):
|
):
|
||||||
response = self.session.get(url, headers=REQUEST_HEADERS)
|
response = self.session.get(
|
||||||
|
url,
|
||||||
|
)
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
if not data:
|
if not data:
|
||||||
data.update(response.json())
|
data.update(response.json())
|
||||||
@@ -136,7 +140,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"],
|
||||||
@@ -171,7 +175,7 @@ class AnimePaheApi(AnimeProvider):
|
|||||||
anime_id = anime["id"]
|
anime_id = anime["id"]
|
||||||
# fetch the episode page
|
# fetch the episode page
|
||||||
url = f"{ANIMEPAHE_BASE}/play/{anime_id}/{episode['session']}"
|
url = f"{ANIMEPAHE_BASE}/play/{anime_id}/{episode['session']}"
|
||||||
response = self.session.get(url, headers=REQUEST_HEADERS)
|
response = self.session.get(url)
|
||||||
# get the element containing links to juicy streams
|
# get the element containing links to juicy streams
|
||||||
c = get_element_by_id("resolutionMenu", response.text)
|
c = get_element_by_id("resolutionMenu", response.text)
|
||||||
resolutionMenuItems = get_elements_html_by_class("dropdown-item", c)
|
resolutionMenuItems = get_elements_html_by_class("dropdown-item", c)
|
||||||
@@ -190,6 +194,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:
|
||||||
@@ -206,7 +211,11 @@ class AnimePaheApi(AnimeProvider):
|
|||||||
)
|
)
|
||||||
return []
|
return []
|
||||||
# get embed page
|
# get embed page
|
||||||
embed_response = self.session.get(embed_url, headers=SERVER_HEADERS)
|
embed_response = self.session.get(
|
||||||
|
embed_url, headers={"User-Agent": self.USER_AGENT, **SERVER_HEADERS}
|
||||||
|
)
|
||||||
|
if not response.status_code == 200:
|
||||||
|
continue
|
||||||
embed_page = embed_response.text
|
embed_page = embed_response.text
|
||||||
|
|
||||||
decoded_js = process_animepahe_embed_page(embed_page)
|
decoded_js = process_animepahe_embed_page(embed_page)
|
||||||
|
|||||||
@@ -1,18 +1,14 @@
|
|||||||
from yt_dlp.utils.networking import random_user_agent
|
|
||||||
|
|
||||||
USER_AGENT = random_user_agent()
|
|
||||||
ANIMEPAHE = "animepahe.ru"
|
ANIMEPAHE = "animepahe.ru"
|
||||||
ANIMEPAHE_BASE = f"https://{ANIMEPAHE}"
|
ANIMEPAHE_BASE = f"https://{ANIMEPAHE}"
|
||||||
ANIMEPAHE_ENDPOINT = f"{ANIMEPAHE_BASE}/api?"
|
ANIMEPAHE_ENDPOINT = f"{ANIMEPAHE_BASE}/api?"
|
||||||
|
|
||||||
|
SERVERS_AVAILABLE = ["kwik"]
|
||||||
REQUEST_HEADERS = {
|
REQUEST_HEADERS = {
|
||||||
"Cookie": "__ddgid_=VvX0ebHrH2DsFZo4; __ddgmark_=3savRpSVFhvZcn5x; __ddg2_=buBJ3c4pNBYKFZNp; __ddg1_=rbVADKr9URtt55zoIGFa; SERVERID=janna; XSRF-TOKEN=eyJpdiI6IjV5bFNtd0phUHgvWGJxc25wL0VJSUE9PSIsInZhbHVlIjoicEJTZktlR2hxR2JZTWhnL0JzazlvZU5TQTR2bjBWZ2dDb0RwUXVUUWNSclhQWUhLRStYSmJmWmUxWkpiYkFRYU12RjFWejlSWHorME1wZG5qQ1U0TnFlNnBFR2laQjN1MjdyNjc5TjVPdXdJb2o5VkU1bEduRW9pRHNDTHh6Sy8iLCJtYWMiOiI0OTc0ZmNjY2UwMGJkOWY2MWNkM2NlMjk2ZGMyZGJmMWE0NTdjZTdkNGI2Y2IwNTIzZmFiZWU5ZTE2OTk0YmU4IiwidGFnIjoiIn0%3D; laravel_session=eyJpdiI6ImxvdlpqREFnTjdaeFJubUlXQWlJVWc9PSIsInZhbHVlIjoiQnE4R3VHdjZ4M1NDdEVWM1ZqMUxtNnVERnJCcmtCUHZKNzRPR2RFbzNFcStTL29xdnVTbWhsNVRBUXEybVZWNU1UYVlTazFqYlN5UjJva1k4czNGaXBTbkJJK01oTUd3VHRYVHBoc3dGUWxHYnFlS2NJVVNFbTFqMVBWdFpuVUgiLCJtYWMiOiI1NDdjZTVkYmNhNjUwZTMxZmRlZmVmMmRlMGNiYjAwYjlmYjFjY2U0MDc1YTQzZThiMTIxMjJlYTg1NTA4YjBmIiwidGFnIjoiIn0%3D; latest=5592 ",
|
"Cookie": "__ddgid_=VvX0ebHrH2DsFZo4; __ddgmark_=3savRpSVFhvZcn5x; __ddg2_=buBJ3c4pNBYKFZNp; __ddg1_=rbVADKr9URtt55zoIGFa; SERVERID=janna; XSRF-TOKEN=eyJpdiI6IjV5bFNtd0phUHgvWGJxc25wL0VJSUE9PSIsInZhbHVlIjoicEJTZktlR2hxR2JZTWhnL0JzazlvZU5TQTR2bjBWZ2dDb0RwUXVUUWNSclhQWUhLRStYSmJmWmUxWkpiYkFRYU12RjFWejlSWHorME1wZG5qQ1U0TnFlNnBFR2laQjN1MjdyNjc5TjVPdXdJb2o5VkU1bEduRW9pRHNDTHh6Sy8iLCJtYWMiOiI0OTc0ZmNjY2UwMGJkOWY2MWNkM2NlMjk2ZGMyZGJmMWE0NTdjZTdkNGI2Y2IwNTIzZmFiZWU5ZTE2OTk0YmU4IiwidGFnIjoiIn0%3D; laravel_session=eyJpdiI6ImxvdlpqREFnTjdaeFJubUlXQWlJVWc9PSIsInZhbHVlIjoiQnE4R3VHdjZ4M1NDdEVWM1ZqMUxtNnVERnJCcmtCUHZKNzRPR2RFbzNFcStTL29xdnVTbWhsNVRBUXEybVZWNU1UYVlTazFqYlN5UjJva1k4czNGaXBTbkJJK01oTUd3VHRYVHBoc3dGUWxHYnFlS2NJVVNFbTFqMVBWdFpuVUgiLCJtYWMiOiI1NDdjZTVkYmNhNjUwZTMxZmRlZmVmMmRlMGNiYjAwYjlmYjFjY2U0MDc1YTQzZThiMTIxMjJlYTg1NTA4YjBmIiwidGFnIjoiIn0%3D; latest=5592 ",
|
||||||
"Host": ANIMEPAHE,
|
"Host": ANIMEPAHE,
|
||||||
"User-Agent": USER_AGENT,
|
|
||||||
"Accept": "application , text/javascript, */*; q=0.01",
|
"Accept": "application , text/javascript, */*; q=0.01",
|
||||||
"Accept-Encoding": "gzip, deflate, br, zstd",
|
"Accept-Encoding": "Utf-8",
|
||||||
"Referer": ANIMEPAHE_BASE,
|
"Referer": ANIMEPAHE_BASE,
|
||||||
"X-Requested-With": "XMLHttpRequest",
|
|
||||||
"DNT": "1",
|
"DNT": "1",
|
||||||
"Connection": "keep-alive",
|
"Connection": "keep-alive",
|
||||||
"Sec-Fetch-Dest": "empty",
|
"Sec-Fetch-Dest": "empty",
|
||||||
@@ -21,19 +17,17 @@ REQUEST_HEADERS = {
|
|||||||
"TE": "trailers",
|
"TE": "trailers",
|
||||||
}
|
}
|
||||||
SERVER_HEADERS = {
|
SERVER_HEADERS = {
|
||||||
"User-Agent": USER_AGENT,
|
"Host": "kwik.si",
|
||||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/png,image/svg+xml,*/*;q=0.8",
|
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/png,image/svg+xml,*/*;q=0.8",
|
||||||
"Accept-Language": "en-US,en;q=0.5",
|
"Accept-Language": "en-US,en;q=0.5",
|
||||||
"Accept-Encoding": "gzip, deflate, br, zstd",
|
"Accept-Encoding": "Utf-8",
|
||||||
"DNT": "1",
|
"DNT": "1",
|
||||||
"Alt-Used": "kwik.si",
|
|
||||||
"Connection": "keep-alive",
|
"Connection": "keep-alive",
|
||||||
"Referer": ANIMEPAHE_BASE,
|
"Referer": "https://animepahe.ru/",
|
||||||
"Cookie": "kwik_session=eyJpdiI6IlZ5UDd0c0lKTDB1NXlhTHZPeWxFc2c9PSIsInZhbHVlIjoieDJZbGhZUG1QZDNaeWtqR3lwWFNnREdhaHBxNVZRMWNDOHVucGpiMHRJOVdhVmpBc3lpTko1VExRMTFWcE1yUVJtVitoTWdOOU5ObTQ0Q0dHU0MzZU0yRUVvNmtWcUdmY3R4UWx4YklJTmpUL0ZodjhtVEpjWU96cEZoUUhUbVYiLCJtYWMiOiI2OGY2YThkOGU0MTgwOThmYzcyZThmNzFlZjlhMzQzMDgwNjlmMTc4NTIzMzc2YjE3YjNmMWQyNTk4NzczMmZiIiwidGFnIjoiIn0%3D; srv=s0; cf_clearance=QMoZtUpZrX0Mh4XJiFmFSSmoWndISPne5FcsGmKKvTQ-1723297585-1.0.1.1-6tVUnP.aef9XeNj0CnN.19D1el_r53t.lhqddX.J88gohH9UnsPWKeJ4yT0pTbcaGRbPuXTLOS.U72.wdy.gMg",
|
|
||||||
"Upgrade-Insecure-Requests": "1",
|
"Upgrade-Insecure-Requests": "1",
|
||||||
"Sec-Fetch-Dest": "iframe",
|
"Sec-Fetch-Dest": "iframe",
|
||||||
"Sec-Fetch-Mode": "navigate",
|
"Sec-Fetch-Mode": "navigate",
|
||||||
"Sec-Fetch-Site": "cross-site",
|
"Sec-Fetch-Site": "cross-site",
|
||||||
"Sec-Fetch-User": "?1",
|
|
||||||
"Priority": "u=4",
|
"Priority": "u=4",
|
||||||
|
"TE": "trailers",
|
||||||
}
|
}
|
||||||
|
|||||||
0
fastanime/libs/anime_provider/aniwatch/__init__.py
Normal file
0
fastanime/libs/anime_provider/aniwatch/__init__.py
Normal file
236
fastanime/libs/anime_provider/aniwatch/api.py
Normal file
236
fastanime/libs/anime_provider/aniwatch/api.py
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
import logging
|
||||||
|
import re
|
||||||
|
from html.parser import HTMLParser
|
||||||
|
from itertools import cycle
|
||||||
|
from urllib.parse import quote_plus
|
||||||
|
|
||||||
|
from yt_dlp.utils import (
|
||||||
|
clean_html,
|
||||||
|
extract_attributes,
|
||||||
|
get_element_by_class,
|
||||||
|
get_element_html_by_class,
|
||||||
|
get_elements_by_class,
|
||||||
|
get_elements_html_by_class,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ..base_provider import AnimeProvider
|
||||||
|
from ..utils import give_random_quality
|
||||||
|
from .constants 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])/(.*)\?.*")
|
||||||
|
IMAGE_HTML_ELEMENT_REGEX = re.compile(r"<img.*?>")
|
||||||
|
|
||||||
|
|
||||||
|
class ParseAnchorAndImgTag(HTMLParser):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.img_tag = None
|
||||||
|
self.a_tag = None
|
||||||
|
|
||||||
|
def handle_starttag(self, tag, attrs):
|
||||||
|
if tag == "img":
|
||||||
|
self.img_tag = {attr[0]: attr[1] for attr in attrs}
|
||||||
|
if tag == "a":
|
||||||
|
self.a_tag = {attr[0]: attr[1] for attr in attrs}
|
||||||
|
|
||||||
|
|
||||||
|
class AniWatchApi(AnimeProvider):
|
||||||
|
# HEADERS = {"Referer": "https://hianime.to/home"}
|
||||||
|
|
||||||
|
def search_for_anime(self, anime_title: str, *args):
|
||||||
|
try:
|
||||||
|
query = quote_plus(anime_title)
|
||||||
|
url = f"https://hianime.to/search?keyword={query}"
|
||||||
|
response = self.session.get(url)
|
||||||
|
if response.status_code != 200:
|
||||||
|
return
|
||||||
|
search_page = response.text
|
||||||
|
search_results_html_items = get_elements_by_class("flw-item", search_page)
|
||||||
|
results = []
|
||||||
|
for search_results_html_item in search_results_html_items:
|
||||||
|
film_poster_html = get_element_by_class(
|
||||||
|
"film-poster", search_results_html_item
|
||||||
|
)
|
||||||
|
|
||||||
|
if not film_poster_html:
|
||||||
|
continue
|
||||||
|
# get availableEpisodes
|
||||||
|
episodes_html = get_element_html_by_class("tick-sub", film_poster_html)
|
||||||
|
episodes = clean_html(episodes_html) or 12
|
||||||
|
|
||||||
|
# get anime id and poster image url
|
||||||
|
parser = ParseAnchorAndImgTag()
|
||||||
|
parser.feed(film_poster_html)
|
||||||
|
image_data = parser.img_tag
|
||||||
|
anime_link_data = parser.a_tag
|
||||||
|
if not image_data or not anime_link_data:
|
||||||
|
continue
|
||||||
|
|
||||||
|
episodes = int(episodes)
|
||||||
|
|
||||||
|
# finally!!
|
||||||
|
image_link = image_data["data-src"]
|
||||||
|
anime_id = anime_link_data["data-id"]
|
||||||
|
title = anime_link_data["title"]
|
||||||
|
|
||||||
|
results.append(
|
||||||
|
{
|
||||||
|
"availableEpisodes": list(range(1, episodes)),
|
||||||
|
"id": anime_id,
|
||||||
|
"title": title,
|
||||||
|
"poster": image_link,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.search_results = results
|
||||||
|
return {"pageInfo": {}, "results": results}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
|
||||||
|
def get_anime(self, aniwatch_id, *args):
|
||||||
|
try:
|
||||||
|
anime_result = {}
|
||||||
|
for anime in self.search_results:
|
||||||
|
if anime["id"] == aniwatch_id:
|
||||||
|
anime_result = anime
|
||||||
|
break
|
||||||
|
anime_url = f"https://hianime.to/ajax/v2/episode/list/{aniwatch_id}"
|
||||||
|
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 anime_result["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": anime_result["poster"],
|
||||||
|
"title": anime_result["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)
|
||||||
1
fastanime/libs/anime_provider/aniwatch/constants.py
Normal file
1
fastanime/libs/anime_provider/aniwatch/constants.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
SERVERS_AVAILABLE = ["HD1", "HD2", "StreamSB", "StreamTape"]
|
||||||
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
|
||||||
65
fastanime/libs/anime_provider/aniwave/api.py
Normal file
65
fastanime/libs/anime_provider/aniwave/api.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
from html.parser import HTMLParser
|
||||||
|
|
||||||
|
from yt_dlp.utils import clean_html, get_element_by_class, get_elements_by_class
|
||||||
|
|
||||||
|
from ..base_provider import AnimeProvider
|
||||||
|
from .constants import ANIWAVE_BASE, SEARCH_HEADERS
|
||||||
|
|
||||||
|
|
||||||
|
class ParseAnchorAndImgTag(HTMLParser):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.img_tag = None
|
||||||
|
self.a_tag = None
|
||||||
|
|
||||||
|
def handle_starttag(self, tag, attrs):
|
||||||
|
if tag == "img":
|
||||||
|
self.img_tag = {attr[0]: attr[1] for attr in attrs}
|
||||||
|
if tag == "a":
|
||||||
|
self.a_tag = {attr[0]: attr[1] for attr in attrs}
|
||||||
|
|
||||||
|
|
||||||
|
class AniWaveApi(AnimeProvider):
|
||||||
|
def search_for_anime(self, anime_title, *args):
|
||||||
|
self.session.headers.update(SEARCH_HEADERS)
|
||||||
|
search_url = f"{ANIWAVE_BASE}/filter"
|
||||||
|
params = {"keyword": anime_title}
|
||||||
|
res = self.session.get(search_url, params=params)
|
||||||
|
search_page = res.text
|
||||||
|
search_results_html_list = get_elements_by_class("item", search_page)
|
||||||
|
results = []
|
||||||
|
for result_html in search_results_html_list:
|
||||||
|
aniposter_html = get_element_by_class("poster", result_html)
|
||||||
|
episode_html = get_element_by_class("sub", aniposter_html)
|
||||||
|
episodes = clean_html(episode_html) or 12
|
||||||
|
if not aniposter_html:
|
||||||
|
return
|
||||||
|
parser = ParseAnchorAndImgTag()
|
||||||
|
parser.feed(aniposter_html)
|
||||||
|
image_data = parser.img_tag
|
||||||
|
anime_link_data = parser.a_tag
|
||||||
|
if not image_data or not anime_link_data:
|
||||||
|
continue
|
||||||
|
|
||||||
|
episodes = int(episodes)
|
||||||
|
|
||||||
|
# finally!!
|
||||||
|
image_link = image_data["src"]
|
||||||
|
title = image_data["alt"]
|
||||||
|
anime_id = anime_link_data["href"]
|
||||||
|
|
||||||
|
results.append(
|
||||||
|
{
|
||||||
|
"availableEpisodes": list(range(1, episodes)),
|
||||||
|
"id": anime_id,
|
||||||
|
"title": title,
|
||||||
|
"poster": image_link,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.search_results = results
|
||||||
|
return {"pageInfo": {}, "results": results}
|
||||||
|
|
||||||
|
def get_anime(self, anime_id, *args):
|
||||||
|
anime_page_url = f"{ANIWAVE_BASE}{anime_id}"
|
||||||
|
self.session.get(anime_page_url)
|
||||||
|
# TODO: to be continued; mostly js so very difficult
|
||||||
20
fastanime/libs/anime_provider/aniwave/constants.py
Normal file
20
fastanime/libs/anime_provider/aniwave/constants.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
ANIWAVE_BASE = "https://aniwave.to"
|
||||||
|
|
||||||
|
SEARCH_HEADERS = {
|
||||||
|
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/png,image/svg+xml,*/*;q=0.8",
|
||||||
|
"Accept-Language": "en-US,en;q=0.5",
|
||||||
|
# 'Accept-Encoding': 'Utf-8',
|
||||||
|
"Referer": "https://aniwave.to/filter",
|
||||||
|
"DNT": "1",
|
||||||
|
"Upgrade-Insecure-Requests": "1",
|
||||||
|
"Sec-Fetch-Dest": "document",
|
||||||
|
"Sec-Fetch-Mode": "navigate",
|
||||||
|
"Sec-Fetch-Site": "same-origin",
|
||||||
|
"Sec-Fetch-User": "?1",
|
||||||
|
"Connection": "keep-alive",
|
||||||
|
"Alt-Used": "aniwave.to",
|
||||||
|
# 'Cookie': '__pf=1; usertype=guest; session=BElk9DJdO3sFdDmLiGxuNiM9eGYO1TjktGsmdwjV',
|
||||||
|
"Priority": "u=0, i",
|
||||||
|
# Requests doesn't support trailers
|
||||||
|
# 'TE': 'trailers',
|
||||||
|
}
|
||||||
@@ -1,8 +1,13 @@
|
|||||||
import requests
|
import requests
|
||||||
|
from yt_dlp.utils.networking import random_user_agent
|
||||||
|
|
||||||
|
|
||||||
class AnimeProvider:
|
class AnimeProvider:
|
||||||
session: requests.Session
|
session: requests.Session
|
||||||
|
|
||||||
|
USER_AGENT = random_user_agent()
|
||||||
|
HEADERS = {}
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.session = requests.session()
|
self.session = requests.session()
|
||||||
|
self.session.headers.update({"User-Agent": self.USER_AGENT, **self.HEADERS})
|
||||||
|
|||||||
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]
|
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]
|
||||||
|
|||||||
@@ -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)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "fastanime"
|
name = "fastanime"
|
||||||
version = "2.2.5"
|
version = "2.3.7"
|
||||||
description = "A browser anime site experience from the terminal"
|
description = "A browser anime site experience from the terminal"
|
||||||
authors = ["Benextempest <benextempest@gmail.com>"]
|
authors = ["Benextempest <benextempest@gmail.com>"]
|
||||||
license = "UNLICENSE"
|
license = "UNLICENSE"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
"typeCheckingMode": "standard",
|
"venvPath": ".",
|
||||||
"reportPrivateImportUsage": false
|
"venv": ".venv",
|
||||||
|
"pythonVersion": "3.10"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user