mirror of
https://github.com/Benexl/FastAnime.git
synced 2025-12-14 00:20:45 -08:00
Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f72c2d4b17 | ||
|
|
ff027991e0 | ||
|
|
21cdc6b015 | ||
|
|
29a2e3e6d1 | ||
|
|
5b3b9f740b | ||
|
|
5bc0e52179 | ||
|
|
40f1c4fba5 | ||
|
|
454341eaf5 | ||
|
|
abab2540a3 | ||
|
|
b2bc8cbace | ||
|
|
90bbf3c033 | ||
|
|
ac91b1770a | ||
|
|
19d42b7924 | ||
|
|
9ec3136734 | ||
|
|
943fca43cf | ||
|
|
b2e00feb94 | ||
|
|
f726c8d55c | ||
|
|
57db2e0626 | ||
|
|
40f66b5fde | ||
|
|
c87417e5e7 | ||
|
|
a841dd6f66 | ||
|
|
d6e85bad5c | ||
|
|
b590ac1e91 | ||
|
|
9cfa3aeea5 | ||
|
|
18c60691ca | ||
|
|
2e9fadf3b2 | ||
|
|
510b47b187 | ||
|
|
49c4d0eec0 | ||
|
|
8367f7bbed | ||
|
|
0182f674e0 | ||
|
|
2b50fb4c97 | ||
|
|
2602a20aa7 |
62
README.md
62
README.md
@@ -57,7 +57,7 @@ Heavily inspired by [animdl](https://github.com/justfoolingaround/animdl), [magi
|
|||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
>
|
>
|
||||||
> This project currently scrapes allanime and animepahe and is in no way related to them nor does the project own any content servers. The site is in the public domain and can be access by any one with a browser.
|
> This project currently scrapes allanime and animepahe. The site is in the public domain and can be accessed by any one with a browser.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -170,6 +170,7 @@ The only required external dependency, unless you won't be streaming, is [MPV](h
|
|||||||
- [icat](https://sw.kovidgoyal.net/kitty/kittens/icat/) an image viewer that only works in [kitty terminal](https://sw.kovidgoyal.net/kitty/), which is currently the best terminal in my opinion, and by far the best image renderer for the terminal thanks to kitty's terminal graphics protocol. Its terminal graphics is so op that you can [run a browser on it](https://github.com/chase/awrit?tab=readme-ov-file)!!
|
- [icat](https://sw.kovidgoyal.net/kitty/kittens/icat/) an image viewer that only works in [kitty terminal](https://sw.kovidgoyal.net/kitty/), which is currently the best terminal in my opinion, and by far the best image renderer for the terminal thanks to kitty's terminal graphics protocol. Its terminal graphics is so op that you can [run a browser on it](https://github.com/chase/awrit?tab=readme-ov-file)!!
|
||||||
- [bash](https://www.gnu.org/software/bash/) is used as the preview script language.
|
- [bash](https://www.gnu.org/software/bash/) is used as the preview script language.
|
||||||
- [ani-skip](https://github.com/synacktraa/ani-skip) used for skipping the opening and ending theme songs
|
- [ani-skip](https://github.com/synacktraa/ani-skip) used for skipping the opening and ending theme songs
|
||||||
|
- [ffmpegthumbnailer]() used for local previews of downloaded anime
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@@ -223,11 +224,20 @@ Available options for the fastanime command include:
|
|||||||
- `--log-file` allow logging to a file
|
- `--log-file` allow logging to a file
|
||||||
- `--rich-traceback` allow rich traceback
|
- `--rich-traceback` allow rich traceback
|
||||||
- `--use-mpv-mod/--use-default-player` whether to use python-mpv
|
- `--use-mpv-mod/--use-default-player` whether to use python-mpv
|
||||||
- `--provider <allanime>` 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
|
||||||
|
|
||||||
Example usage of the above options
|
Example usage of the above options
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# example of syncplay intergration
|
||||||
|
fastanime --sync-play --server sharepoint search -t <anime-title>
|
||||||
|
|
||||||
|
# --- or ---
|
||||||
|
|
||||||
|
# to watch with anilist intergration
|
||||||
|
fastanime --sync-play --server sharepoint anilist
|
||||||
|
|
||||||
# downloading dubbed anime
|
# downloading dubbed anime
|
||||||
fastanime --dub download <anime>
|
fastanime --dub download <anime>
|
||||||
|
|
||||||
@@ -298,12 +308,14 @@ end
|
|||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> To sign in just run `fastanime anilist login` and follow the instructions.
|
> To sign in just run `fastanime anilist login` and follow the instructions.
|
||||||
> To view your login status `fastanime anilist login --status`
|
> To view your login status `fastanime anilist login --status`
|
||||||
|
> To erase login data `fastanime anilist login --erase`
|
||||||
|
|
||||||
#### download subcommand
|
#### download subcommand
|
||||||
|
|
||||||
Download anime to watch later dub or sub with this one command.
|
Download anime to watch later dub or sub with this one command.
|
||||||
Its optimized for scripting due to fuzzy matching.
|
Its optimized for scripting due to fuzzy matching; basically you don't have to manually select search results.
|
||||||
So every step of the way has been and can be automated.
|
So every step of the way has been and can be automated.
|
||||||
|
Uses a list slicing syntax similar to that of python as the value for the `-r` option.
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
>
|
>
|
||||||
@@ -314,29 +326,57 @@ So every step of the way has been and can be automated.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Download all available episodes
|
# Download all available episodes
|
||||||
fastanime download <anime-title>
|
# multiple titles can be specified with -t option
|
||||||
|
fastanime download -t <anime-title> -t <anime-title>
|
||||||
|
# -- or --
|
||||||
|
fastanime download -t <anime-title> -t <anime-title> -r ':'
|
||||||
|
|
||||||
|
# download latest episode for the two anime titles
|
||||||
|
# the number can be any no of latest episodes but a minus sign
|
||||||
|
# must be present
|
||||||
|
fastanime download -t <anime-title> -t <anime-title> -r '-1'
|
||||||
|
|
||||||
|
# latest 5
|
||||||
|
fastanime download -t <anime-title> -t <anime-title> -r '-5'
|
||||||
|
|
||||||
# Download specific episode range
|
# Download specific episode range
|
||||||
# be sure to observe the range Syntax
|
# be sure to observe the range Syntax
|
||||||
fastanime download <anime-title> -r <episodes-start>-<episodes-end>
|
fastanime download <anime-title> -r '<episodes-start>:<episodes-end>:<step>'
|
||||||
|
|
||||||
|
fastanime download <anime-title> -r '<episodes-start>:<episodes-end>'
|
||||||
|
|
||||||
|
fastanime download <anime-title> -r '<episodes-start>:'
|
||||||
|
|
||||||
|
fastanime download <anime-title> -r ':<episodes-end>'
|
||||||
```
|
```
|
||||||
|
|
||||||
#### search subcommand
|
#### search subcommand
|
||||||
|
|
||||||
Powerful command mainly aimed at binging anime. Since it doesn't require interaction with the interfaces.
|
Powerful command mainly aimed at binging anime. Since it doesn't require interaction with the interfaces.
|
||||||
|
Uses a list slicing syntax similar to that of python as the value of the `-r` option.
|
||||||
|
|
||||||
**Syntax:**
|
**Syntax:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# basic form where you will still be prompted for the episode number
|
# basic form where you will still be prompted for the episode number
|
||||||
fastanime search <anime-title>
|
# multiple titles can be specified with the -t option
|
||||||
|
fastanime search -t <anime-title> -t <anime-title>
|
||||||
|
|
||||||
# binge all episodes with this command
|
# binge all episodes with this command
|
||||||
fastanime search <anime-title> -r -
|
fastanime search -t <anime-title> -r ':'
|
||||||
|
|
||||||
|
# watch latest episode
|
||||||
|
fastanime search -t <anime-title> -r '-1'
|
||||||
|
|
||||||
# binge a specific episode range with this command
|
# binge a specific episode range with this command
|
||||||
# be sure to observe the range Syntax
|
# be sure to observe the range Syntax
|
||||||
fastanime search <anime-title> -r <episodes-start>-<episodes-end>
|
fastanime search -t <anime-title> -r '<start>:<stop>'
|
||||||
|
|
||||||
|
fastanime search -t <anime-title> -r '<start>:<stop>:<step>'
|
||||||
|
|
||||||
|
fastanime search -t <anime-title> -r '<start>:'
|
||||||
|
|
||||||
|
fastanime search -t <anime-title> -r ':<end>'
|
||||||
```
|
```
|
||||||
|
|
||||||
#### downloads subcommand
|
#### downloads subcommand
|
||||||
@@ -357,7 +397,7 @@ fastanime downloads -v
|
|||||||
# -1 means random and is the default
|
# -1 means random and is the default
|
||||||
fastanime downloads --time-to-seek <intRange(-1,100)>
|
fastanime downloads --time-to-seek <intRange(-1,100)>
|
||||||
# --- or ---
|
# --- or ---
|
||||||
fastanime downloads --t <intRange(-1,100)>
|
fastanime downloads -t <intRange(-1,100)>
|
||||||
|
|
||||||
# to get the path to the downloads folder set
|
# to get the path to the downloads folder set
|
||||||
fastanime downloads --path
|
fastanime downloads --path
|
||||||
@@ -482,6 +522,10 @@ continue_from_history = True # Auto continue from watch history
|
|||||||
# which history to use [local/remote]
|
# which history to use [local/remote]
|
||||||
preferred_history = local
|
preferred_history = local
|
||||||
|
|
||||||
|
# force mpv window
|
||||||
|
# passed directly to mpv so values are same
|
||||||
|
force_window = immediate
|
||||||
|
|
||||||
translation_type = sub # Preferred language for anime (options: dub, sub)
|
translation_type = sub # Preferred language for anime (options: dub, sub)
|
||||||
|
|
||||||
server = top # Default server (options: dropbox, sharepoint, wetransfer.gogoanime, top, wixmp)
|
server = top # Default server (options: dropbox, sharepoint, wetransfer.gogoanime, top, wixmp)
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ class YtDLPDownloader:
|
|||||||
download_dir: str,
|
download_dir: str,
|
||||||
silent: bool,
|
silent: bool,
|
||||||
vid_format: str = "best",
|
vid_format: str = "best",
|
||||||
|
force_unknown_ext=False,
|
||||||
|
verbose=False,
|
||||||
|
headers={},
|
||||||
):
|
):
|
||||||
"""Helper function that downloads anime given url and path details
|
"""Helper function that downloads anime given url and path details
|
||||||
|
|
||||||
@@ -50,10 +53,12 @@ class YtDLPDownloader:
|
|||||||
episode_title = sanitize_filename(episode_title)
|
episode_title = sanitize_filename(episode_title)
|
||||||
ydl_opts = {
|
ydl_opts = {
|
||||||
# Specify the output path and template
|
# Specify the output path and template
|
||||||
|
"http_headers": headers,
|
||||||
"outtmpl": f"{download_dir}/{anime_title}/{episode_title}.%(ext)s",
|
"outtmpl": f"{download_dir}/{anime_title}/{episode_title}.%(ext)s",
|
||||||
"silent": silent,
|
"silent": silent,
|
||||||
"verbose": False,
|
"verbose": verbose,
|
||||||
"format": vid_format,
|
"format": vid_format,
|
||||||
|
"compat_opts": ("allow-unsafe-ext",) if force_unknown_ext else tuple(),
|
||||||
}
|
}
|
||||||
|
|
||||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||||
|
|||||||
@@ -11,6 +11,13 @@ if TYPE_CHECKING:
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def sort_by_episode_number(filename: str):
|
||||||
|
import re
|
||||||
|
|
||||||
|
match = re.search(r"\d+", filename)
|
||||||
|
return int(match.group()) if match else 0
|
||||||
|
|
||||||
|
|
||||||
def anime_title_percentage_match(
|
def anime_title_percentage_match(
|
||||||
possible_user_requested_anime_title: str, anime: "AnilistBaseMediaDataSchema"
|
possible_user_requested_anime_title: str, anime: "AnilistBaseMediaDataSchema"
|
||||||
) -> float:
|
) -> float:
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ if sys.version_info < (3, 10):
|
|||||||
) # noqa: F541
|
) # noqa: F541
|
||||||
|
|
||||||
|
|
||||||
__version__ = "v1.7.1"
|
__version__ = "v2.2.0"
|
||||||
|
|
||||||
APP_NAME = "FastAnime"
|
APP_NAME = "FastAnime"
|
||||||
AUTHOR = "Benex254"
|
AUTHOR = "Benex254"
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ signal.signal(signal.SIGINT, handle_exit)
|
|||||||
@click.option(
|
@click.option(
|
||||||
"--use-mpv-mod/--use-default-player", help="Whether to use python-mpv", type=bool
|
"--use-mpv-mod/--use-default-player", help="Whether to use python-mpv", type=bool
|
||||||
)
|
)
|
||||||
|
@click.option("--sync-play", "-sp", help="Use sync play", is_flag=True)
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def run_cli(
|
def run_cli(
|
||||||
ctx: click.Context,
|
ctx: click.Context,
|
||||||
@@ -171,6 +172,7 @@ def run_cli(
|
|||||||
rofi_theme_confirm,
|
rofi_theme_confirm,
|
||||||
rofi_theme_input,
|
rofi_theme_input,
|
||||||
use_mpv_mod,
|
use_mpv_mod,
|
||||||
|
sync_play,
|
||||||
):
|
):
|
||||||
from .config import Config
|
from .config import Config
|
||||||
|
|
||||||
@@ -190,12 +192,12 @@ def run_cli(
|
|||||||
elif log_file:
|
elif log_file:
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from ..constants import NOTIFIER_LOG_FILE_PATH
|
from ..constants import LOG_FILE_PATH
|
||||||
|
|
||||||
format = "%(asctime)s%(levelname)s: %(message)s"
|
format = "%(asctime)s%(levelname)s: %(message)s"
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.DEBUG,
|
level=logging.DEBUG,
|
||||||
filename=NOTIFIER_LOG_FILE_PATH,
|
filename=LOG_FILE_PATH,
|
||||||
format=format,
|
format=format,
|
||||||
datefmt="[%d/%m/%Y@%H:%M:%S]",
|
datefmt="[%d/%m/%Y@%H:%M:%S]",
|
||||||
filemode="w",
|
filemode="w",
|
||||||
@@ -205,6 +207,8 @@ def run_cli(
|
|||||||
|
|
||||||
install()
|
install()
|
||||||
|
|
||||||
|
if sync_play:
|
||||||
|
ctx.obj.sync_play = sync_play
|
||||||
if provider:
|
if provider:
|
||||||
ctx.obj.provider = provider
|
ctx.obj.provider = provider
|
||||||
if server:
|
if server:
|
||||||
|
|||||||
@@ -8,23 +8,36 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
@click.command(help="Login to your anilist account")
|
@click.command(help="Login to your anilist account")
|
||||||
@click.option("--status", "-s", help="Whether you are logged in or not", is_flag=True)
|
@click.option("--status", "-s", help="Whether you are logged in or not", 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):
|
def login(config: "Config", status, erase):
|
||||||
from click import launch
|
|
||||||
from rich import print
|
from rich import print
|
||||||
from rich.prompt import Confirm, Prompt
|
from rich.prompt import Confirm, Prompt
|
||||||
|
|
||||||
from ....anilist import AniList
|
|
||||||
from ...utils.tools import exit_app
|
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 = (
|
||||||
"You are logged in :happy:" if is_logged_in else "You arent logged in :cry"
|
"You are logged in :smile:" if is_logged_in else "You arent logged in :cry:"
|
||||||
)
|
)
|
||||||
print(message)
|
print(message)
|
||||||
print(config.user)
|
print(config.user)
|
||||||
exit_app()
|
exit_app()
|
||||||
|
elif erase:
|
||||||
|
if Confirm.ask(
|
||||||
|
"Are you sure you want to erase your login status", default=False
|
||||||
|
):
|
||||||
|
config.update_user({})
|
||||||
|
print("Success")
|
||||||
|
exit_app(0)
|
||||||
|
else:
|
||||||
|
exit_app(1)
|
||||||
|
else:
|
||||||
|
from click import launch
|
||||||
|
|
||||||
|
from ....anilist import AniList
|
||||||
|
|
||||||
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):
|
||||||
|
|||||||
@@ -13,21 +13,42 @@ if TYPE_CHECKING:
|
|||||||
help="Download anime using the anime provider for a specified range",
|
help="Download anime using the anime provider for a specified range",
|
||||||
short_help="Download anime",
|
short_help="Download anime",
|
||||||
)
|
)
|
||||||
@click.argument(
|
@click.option(
|
||||||
"anime-title", required=True, shell_complete=anime_titles_shell_complete
|
"--anime-titles",
|
||||||
|
"--anime_title",
|
||||||
|
"-t",
|
||||||
|
required=True,
|
||||||
|
shell_complete=anime_titles_shell_complete,
|
||||||
|
multiple=True,
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--episode-range",
|
"--episode-range",
|
||||||
"-r",
|
"-r",
|
||||||
help="A range of episodes to download (start-end)",
|
help="A range of episodes to download (start-end)",
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
"--force-unknown-ext",
|
||||||
|
"-f",
|
||||||
|
help="This option forces yt-dlp to download extensions its not aware of",
|
||||||
|
is_flag=True,
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--silent/--no-silent",
|
||||||
|
"-q/-V",
|
||||||
|
type=bool,
|
||||||
|
help="Download silently (during download)",
|
||||||
|
default=True,
|
||||||
|
)
|
||||||
|
@click.option("--verbose", "-v", is_flag=True, help="Download verbosely (everywhere)")
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def download(
|
def download(
|
||||||
config: "Config",
|
config: "Config",
|
||||||
anime_title,
|
anime_titles: list,
|
||||||
episode_range,
|
episode_range,
|
||||||
|
force_unknown_ext,
|
||||||
|
silent,
|
||||||
|
verbose,
|
||||||
):
|
):
|
||||||
from click import clear
|
|
||||||
from rich import print
|
from rich import print
|
||||||
from rich.progress import Progress
|
from rich.progress import Progress
|
||||||
from thefuzz import fuzz
|
from thefuzz import fuzz
|
||||||
@@ -44,6 +65,9 @@ def download(
|
|||||||
translation_type = config.translation_type
|
translation_type = config.translation_type
|
||||||
download_dir = config.downloads_dir
|
download_dir = config.downloads_dir
|
||||||
|
|
||||||
|
print(f"[green bold]Queued:[/] {anime_titles}")
|
||||||
|
for anime_title in anime_titles:
|
||||||
|
print(f"[green bold]Now Downloading: [/] {anime_title}")
|
||||||
# ---- search for anime ----
|
# ---- search for anime ----
|
||||||
with Progress() as progress:
|
with Progress() as progress:
|
||||||
progress.add_task("Fetching Search Results...", total=None)
|
progress.add_task("Fetching Search Results...", total=None)
|
||||||
@@ -54,9 +78,7 @@ def download(
|
|||||||
print("Search results failed")
|
print("Search results failed")
|
||||||
input("Enter to retry")
|
input("Enter to retry")
|
||||||
download(
|
download(
|
||||||
config,
|
config, anime_title, episode_range, force_unknown_ext, silent, verbose
|
||||||
anime_title,
|
|
||||||
episode_range,
|
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
search_results = search_results["results"]
|
search_results = search_results["results"]
|
||||||
@@ -89,20 +111,41 @@ 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,
|
config, anime_title, episode_range, force_unknown_ext, silent, verbose
|
||||||
anime_title,
|
|
||||||
episode_range,
|
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
episodes = anime["availableEpisodesDetail"][config.translation_type]
|
episodes = sorted(
|
||||||
|
anime["availableEpisodesDetail"][config.translation_type], key=float
|
||||||
|
)
|
||||||
|
# where the magic happens
|
||||||
if episode_range:
|
if episode_range:
|
||||||
episodes_start, episodes_end = episode_range.split("-")
|
if ":" in episode_range:
|
||||||
episodes_range = range(round(float(episodes_start)), round(float(episodes_end)))
|
ep_range_tuple = episode_range.split(":")
|
||||||
|
if len(ep_range_tuple) == 2 and all(ep_range_tuple):
|
||||||
|
episodes_start, episodes_end = ep_range_tuple
|
||||||
|
episodes_range = episodes[int(episodes_start) : int(episodes_end)]
|
||||||
|
elif len(ep_range_tuple) == 3 and all(ep_range_tuple):
|
||||||
|
episodes_start, episodes_end, step = ep_range_tuple
|
||||||
|
episodes_range = episodes[
|
||||||
|
int(episodes_start) : int(episodes_end) : int(step)
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
episodes_start, episodes_end = ep_range_tuple
|
||||||
|
if episodes_start.strip():
|
||||||
|
episodes_range = episodes[int(episodes_start) :]
|
||||||
|
elif episodes_end.strip():
|
||||||
|
episodes_range = episodes[: int(episodes_end)]
|
||||||
|
else:
|
||||||
|
episodes_range = episodes
|
||||||
|
else:
|
||||||
|
episodes_range = episodes[int(episode_range) :]
|
||||||
|
print(f"[green bold]Downloading: [/] {episodes_range}")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
episodes_range = sorted(episodes, key=float)
|
episodes_range = sorted(episodes, key=float)
|
||||||
|
|
||||||
|
# lets download em
|
||||||
for episode in episodes_range:
|
for episode in episodes_range:
|
||||||
try:
|
try:
|
||||||
episode = str(episode)
|
episode = str(episode)
|
||||||
@@ -131,6 +174,7 @@ def download(
|
|||||||
input("Enter to continue")
|
input("Enter to continue")
|
||||||
continue
|
continue
|
||||||
link = stream_link["link"]
|
link = stream_link["link"]
|
||||||
|
provider_headers = server["headers"]
|
||||||
episode_title = server["episode_title"]
|
episode_title = server["episode_title"]
|
||||||
else:
|
else:
|
||||||
with Progress() as progress:
|
with Progress() as progress:
|
||||||
@@ -155,6 +199,7 @@ def download(
|
|||||||
print("Quality not found")
|
print("Quality not found")
|
||||||
continue
|
continue
|
||||||
link = stream_link["link"]
|
link = stream_link["link"]
|
||||||
|
provider_headers = servers[server]["headers"]
|
||||||
|
|
||||||
episode_title = servers[server]["episode_title"]
|
episode_title = servers[server]["episode_title"]
|
||||||
print(f"[purple]Now Downloading:[/] {search_result} Episode {episode}")
|
print(f"[purple]Now Downloading:[/] {search_result} Episode {episode}")
|
||||||
@@ -164,13 +209,15 @@ def download(
|
|||||||
anime["title"],
|
anime["title"],
|
||||||
episode_title,
|
episode_title,
|
||||||
download_dir,
|
download_dir,
|
||||||
True,
|
silent,
|
||||||
config.format,
|
config.format,
|
||||||
|
force_unknown_ext,
|
||||||
|
verbose,
|
||||||
|
headers=provider_headers,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
print("Continuing")
|
print("Continuing")
|
||||||
clear()
|
|
||||||
print("Done Downloading")
|
print("Done Downloading")
|
||||||
exit_app()
|
exit_app()
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ def downloads(config: "Config", path: bool, view_episodes, ffmpegthumbnailer_see
|
|||||||
from ...cli.utils.mpv import run_mpv
|
from ...cli.utils.mpv import run_mpv
|
||||||
from ...libs.fzf import fzf
|
from ...libs.fzf import fzf
|
||||||
from ...libs.rofi import Rofi
|
from ...libs.rofi import Rofi
|
||||||
|
from ...Utility.utils import sort_by_episode_number
|
||||||
from ..utils.tools import exit_app
|
from ..utils.tools import exit_app
|
||||||
from ..utils.utils import fuzzy_inquirer
|
from ..utils.utils import fuzzy_inquirer
|
||||||
|
|
||||||
@@ -39,7 +40,9 @@ def downloads(config: "Config", path: bool, view_episodes, ffmpegthumbnailer_see
|
|||||||
if not os.path.exists(USER_VIDEOS_DIR):
|
if not os.path.exists(USER_VIDEOS_DIR):
|
||||||
print("Downloads directory specified does not exist")
|
print("Downloads directory specified does not exist")
|
||||||
return
|
return
|
||||||
anime_downloads = os.listdir(USER_VIDEOS_DIR)
|
anime_downloads = sorted(
|
||||||
|
os.listdir(USER_VIDEOS_DIR),
|
||||||
|
)
|
||||||
anime_downloads.append("Exit")
|
anime_downloads.append("Exit")
|
||||||
|
|
||||||
def create_thumbnails(video_path, anime_title, downloads_thumbnail_cache_dir):
|
def create_thumbnails(video_path, anime_title, downloads_thumbnail_cache_dir):
|
||||||
@@ -76,6 +79,7 @@ def downloads(config: "Config", path: bool, view_episodes, ffmpegthumbnailer_see
|
|||||||
|
|
||||||
def get_previews_anime(workers=None, bg=True):
|
def get_previews_anime(workers=None, bg=True):
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
|
import random
|
||||||
import shutil
|
import shutil
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -99,10 +103,16 @@ def downloads(config: "Config", path: bool, view_episodes, ffmpegthumbnailer_see
|
|||||||
anime_path = os.path.join(USER_VIDEOS_DIR, anime_title)
|
anime_path = os.path.join(USER_VIDEOS_DIR, anime_title)
|
||||||
if not os.path.isdir(anime_path):
|
if not os.path.isdir(anime_path):
|
||||||
continue
|
continue
|
||||||
playlist = os.listdir(anime_path)
|
playlist = [
|
||||||
|
anime
|
||||||
|
for anime in sorted(
|
||||||
|
os.listdir(anime_path),
|
||||||
|
)
|
||||||
|
if "mp4" in anime
|
||||||
|
]
|
||||||
if playlist:
|
if playlist:
|
||||||
# actual link to download image from
|
# actual link to download image from
|
||||||
video_path = os.path.join(anime_path, playlist[0])
|
video_path = os.path.join(anime_path, random.choice(playlist))
|
||||||
future_to_url[
|
future_to_url[
|
||||||
executor.submit(
|
executor.submit(
|
||||||
create_thumbnails,
|
create_thumbnails,
|
||||||
@@ -166,7 +176,9 @@ def downloads(config: "Config", path: bool, view_episodes, ffmpegthumbnailer_see
|
|||||||
# anime_playlist_path = os.path.join(USER_VIDEOS_DIR, anime_playlist_path)
|
# anime_playlist_path = os.path.join(USER_VIDEOS_DIR, anime_playlist_path)
|
||||||
if not os.path.isdir(anime_playlist_path):
|
if not os.path.isdir(anime_playlist_path):
|
||||||
return
|
return
|
||||||
anime_episodes = os.listdir(anime_playlist_path)
|
anime_episodes = sorted(
|
||||||
|
os.listdir(anime_playlist_path), key=sort_by_episode_number
|
||||||
|
)
|
||||||
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
|
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
|
||||||
# load the jobs
|
# load the jobs
|
||||||
future_to_url = {}
|
future_to_url = {}
|
||||||
@@ -223,7 +235,9 @@ def downloads(config: "Config", path: bool, view_episodes, ffmpegthumbnailer_see
|
|||||||
print(anime_playlist_path, "is not dir")
|
print(anime_playlist_path, "is not dir")
|
||||||
exit_app(1)
|
exit_app(1)
|
||||||
return
|
return
|
||||||
episodes = os.listdir(anime_playlist_path)
|
episodes = sorted(
|
||||||
|
os.listdir(anime_playlist_path), key=sort_by_episode_number
|
||||||
|
)
|
||||||
downloaded_episodes = [*episodes, "Back"]
|
downloaded_episodes = [*episodes, "Back"]
|
||||||
if config.use_fzf:
|
if config.use_fzf:
|
||||||
if not config.preview:
|
if not config.preview:
|
||||||
@@ -249,6 +263,11 @@ def downloads(config: "Config", path: bool, view_episodes, ffmpegthumbnailer_see
|
|||||||
stream_anime()
|
stream_anime()
|
||||||
return
|
return
|
||||||
episode_path = os.path.join(anime_playlist_path, episode_title)
|
episode_path = os.path.join(anime_playlist_path, episode_title)
|
||||||
|
if config.sync_play:
|
||||||
|
from ..utils.syncplay import SyncPlayer
|
||||||
|
|
||||||
|
SyncPlayer(episode_path)
|
||||||
|
else:
|
||||||
run_mpv(episode_path)
|
run_mpv(episode_path)
|
||||||
stream_episode(anime_playlist_path)
|
stream_episode(anime_playlist_path)
|
||||||
|
|
||||||
@@ -281,6 +300,11 @@ def downloads(config: "Config", path: bool, view_episodes, ffmpegthumbnailer_see
|
|||||||
stream_episode(
|
stream_episode(
|
||||||
playlist,
|
playlist,
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
if config.sync_play:
|
||||||
|
from ..utils.syncplay import SyncPlayer
|
||||||
|
|
||||||
|
SyncPlayer(playlist)
|
||||||
else:
|
else:
|
||||||
run_mpv(playlist)
|
run_mpv(playlist)
|
||||||
stream_anime()
|
stream_anime()
|
||||||
|
|||||||
@@ -8,16 +8,21 @@ from ..completion_functions import anime_titles_shell_complete
|
|||||||
help="This subcommand directly interacts with the provider to enable basic streaming. Useful for binging anime.",
|
help="This subcommand directly interacts with the provider to enable basic streaming. Useful for binging anime.",
|
||||||
short_help="Binge anime",
|
short_help="Binge anime",
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
"--anime-titles",
|
||||||
|
"--anime_title",
|
||||||
|
"-t",
|
||||||
|
required=True,
|
||||||
|
shell_complete=anime_titles_shell_complete,
|
||||||
|
multiple=True,
|
||||||
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--episode-range",
|
"--episode-range",
|
||||||
"-r",
|
"-r",
|
||||||
help="A range of episodes to binge (start-end)",
|
help="A range of episodes to binge (start-end)",
|
||||||
)
|
)
|
||||||
@click.argument(
|
|
||||||
"anime_title", required=True, shell_complete=anime_titles_shell_complete
|
|
||||||
)
|
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def search(config: Config, anime_title: str, episode_range: str):
|
def search(config: Config, anime_titles: str, episode_range: str):
|
||||||
from click import clear
|
from click import clear
|
||||||
from rich import print
|
from rich import print
|
||||||
from rich.progress import Progress
|
from rich.progress import Progress
|
||||||
@@ -33,6 +38,8 @@ def search(config: Config, anime_title: str, episode_range: str):
|
|||||||
|
|
||||||
anime_provider = AnimeProvider(config.provider)
|
anime_provider = AnimeProvider(config.provider)
|
||||||
|
|
||||||
|
print(f"[green bold]Streaming:[/] {anime_titles}")
|
||||||
|
for anime_title in anime_titles:
|
||||||
# ---- search for anime ----
|
# ---- search for anime ----
|
||||||
with Progress() as progress:
|
with Progress() as progress:
|
||||||
progress.add_task("Fetching Search Results...", total=None)
|
progress.add_task("Fetching Search Results...", total=None)
|
||||||
@@ -82,41 +89,64 @@ def search(config: Config, anime_title: str, episode_range: str):
|
|||||||
input("Enter to continue...")
|
input("Enter to continue...")
|
||||||
search(config, anime_title, episode_range)
|
search(config, anime_title, episode_range)
|
||||||
return
|
return
|
||||||
episode_range_ = None
|
episodes_range = []
|
||||||
episodes = anime["availableEpisodesDetail"][config.translation_type]
|
episodes: list[str] = sorted(
|
||||||
if episode_range:
|
anime["availableEpisodesDetail"][config.translation_type], key=float
|
||||||
episodes_start, episodes_end = episode_range.split("-")
|
|
||||||
if episodes_start and episodes_end:
|
|
||||||
episode_range_ = iter(
|
|
||||||
range(round(float(episodes_start)), round(float(episodes_end)) + 1)
|
|
||||||
)
|
)
|
||||||
|
if episode_range:
|
||||||
|
if ":" in episode_range:
|
||||||
|
ep_range_tuple = episode_range.split(":")
|
||||||
|
if len(ep_range_tuple) == 3 and all(ep_range_tuple):
|
||||||
|
episodes_start, episodes_end, step = ep_range_tuple
|
||||||
|
episodes_range = episodes[
|
||||||
|
int(episodes_start) : int(episodes_end) : int(step)
|
||||||
|
]
|
||||||
|
|
||||||
|
elif len(ep_range_tuple) == 2 and all(ep_range_tuple):
|
||||||
|
episodes_start, episodes_end = ep_range_tuple
|
||||||
|
episodes_range = episodes[int(episodes_start) : int(episodes_end)]
|
||||||
else:
|
else:
|
||||||
episode_range_ = iter(sorted(episodes, key=float))
|
episodes_start, episodes_end = ep_range_tuple
|
||||||
|
if episodes_start.strip():
|
||||||
|
episodes_range = episodes[int(episodes_start) :]
|
||||||
|
elif episodes_end.strip():
|
||||||
|
episodes_range = episodes[: int(episodes_end)]
|
||||||
|
else:
|
||||||
|
episodes_range = episodes
|
||||||
|
else:
|
||||||
|
episodes_range = episodes[int(episode_range) :]
|
||||||
|
|
||||||
|
episodes_range = iter(episodes_range)
|
||||||
|
|
||||||
def stream_anime():
|
def stream_anime():
|
||||||
clear()
|
clear()
|
||||||
episode = None
|
episode = None
|
||||||
|
|
||||||
if episode_range_:
|
if episodes_range:
|
||||||
try:
|
try:
|
||||||
episode = str(next(episode_range_))
|
episode = next(episodes_range) # pyright:ignore
|
||||||
print(
|
print(
|
||||||
f"[cyan]Auto selecting:[/] {search_result} [cyan]Episode:[/] {episode}"
|
f"[cyan]Auto selecting:[/] {search_result} [cyan]Episode:[/] {episode}"
|
||||||
)
|
)
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
print("[green]Completed binge sequence[/]:smile:")
|
print("[green]Completed binge sequence[/]:smile:")
|
||||||
input("Enter to continue...")
|
return
|
||||||
|
|
||||||
if not episode or episode not in episodes:
|
if not episode or episode not in episodes:
|
||||||
|
choices = [*episodes, "end"]
|
||||||
if config.use_fzf:
|
if config.use_fzf:
|
||||||
episode = fzf.run(episodes, "Select an episode: ", header=search_result)
|
episode = fzf.run(
|
||||||
|
choices, "Select an episode: ", header=search_result
|
||||||
|
)
|
||||||
elif config.use_rofi:
|
elif config.use_rofi:
|
||||||
episode = Rofi.run(episodes, "Select an episode")
|
episode = Rofi.run(choices, "Select an episode")
|
||||||
else:
|
else:
|
||||||
episode = fuzzy_inquirer(
|
episode = fuzzy_inquirer(
|
||||||
episodes,
|
choices,
|
||||||
"Select episode",
|
"Select episode",
|
||||||
)
|
)
|
||||||
|
if episode == "end":
|
||||||
|
return
|
||||||
|
|
||||||
# ---- fetch streams ----
|
# ---- fetch streams ----
|
||||||
with Progress() as progress:
|
with Progress() as progress:
|
||||||
@@ -146,6 +176,7 @@ def search(config: Config, anime_title: str, episode_range: str):
|
|||||||
stream_anime()
|
stream_anime()
|
||||||
return
|
return
|
||||||
link = stream_link["link"]
|
link = stream_link["link"]
|
||||||
|
stream_headers = server["headers"]
|
||||||
episode_title = server["episode_title"]
|
episode_title = server["episode_title"]
|
||||||
else:
|
else:
|
||||||
with Progress() as progress:
|
with Progress() as progress:
|
||||||
@@ -174,11 +205,17 @@ def search(config: Config, anime_title: str, episode_range: str):
|
|||||||
stream_anime()
|
stream_anime()
|
||||||
return
|
return
|
||||||
link = stream_link["link"]
|
link = stream_link["link"]
|
||||||
|
stream_headers = servers[server]["headers"]
|
||||||
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}")
|
||||||
|
|
||||||
run_mpv(link, episode_title)
|
if config.sync_play:
|
||||||
except Exception as e:
|
from ..utils.syncplay import SyncPlayer
|
||||||
|
|
||||||
|
SyncPlayer(link, episode_title, headers=stream_headers)
|
||||||
|
else:
|
||||||
|
run_mpv(link, episode_title, headers=stream_headers)
|
||||||
|
except IndexError as e:
|
||||||
print(e)
|
print(e)
|
||||||
input("Enter to continue")
|
input("Enter to continue")
|
||||||
stream_anime()
|
stream_anime()
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ class Config(object):
|
|||||||
user: [TODO:attribute]
|
user: [TODO:attribute]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
sync_play = False
|
||||||
anime_list: list
|
anime_list: list
|
||||||
watch_history: dict
|
watch_history: dict
|
||||||
fastanime_anilist_app_login_url = (
|
fastanime_anilist_app_login_url = (
|
||||||
@@ -295,6 +296,10 @@ error = {self.error}
|
|||||||
# adding more options to it
|
# adding more options to it
|
||||||
use_mpv_mod = {self.use_mpv_mod}
|
use_mpv_mod = {self.use_mpv_mod}
|
||||||
|
|
||||||
|
# force mpv window
|
||||||
|
# passed directly to mpv so values are same
|
||||||
|
force_window = immediate
|
||||||
|
|
||||||
# the format of downloaded anime and trailer
|
# the format of downloaded anime and trailer
|
||||||
# based on yt-dlp format and passed directly to it
|
# based on yt-dlp format and passed directly to it
|
||||||
# learn more by looking it up on their site
|
# learn more by looking it up on their site
|
||||||
|
|||||||
@@ -113,7 +113,15 @@ def media_player_controls(
|
|||||||
current_episode_number,
|
current_episode_number,
|
||||||
):
|
):
|
||||||
custom_args.extend(args)
|
custom_args.extend(args)
|
||||||
if config.use_mpv_mod:
|
if config.sync_play:
|
||||||
|
from ..utils.syncplay import SyncPlayer
|
||||||
|
|
||||||
|
stop_time, total_time = SyncPlayer(
|
||||||
|
current_episode_stream_link,
|
||||||
|
selected_server["episode_title"],
|
||||||
|
headers=selected_server["headers"],
|
||||||
|
)
|
||||||
|
elif config.use_mpv_mod:
|
||||||
from ..utils.player import player
|
from ..utils.player import player
|
||||||
|
|
||||||
mpv = player.create_player(
|
mpv = player.create_player(
|
||||||
@@ -122,6 +130,7 @@ def media_player_controls(
|
|||||||
fastanime_runtime_state,
|
fastanime_runtime_state,
|
||||||
config,
|
config,
|
||||||
selected_server["episode_title"],
|
selected_server["episode_title"],
|
||||||
|
headers=selected_server["headers"],
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO: implement custom aniskip
|
# TODO: implement custom aniskip
|
||||||
@@ -142,6 +151,7 @@ def media_player_controls(
|
|||||||
selected_server["episode_title"],
|
selected_server["episode_title"],
|
||||||
start_time=start_time,
|
start_time=start_time,
|
||||||
custom_args=custom_args,
|
custom_args=custom_args,
|
||||||
|
headers=selected_server["headers"],
|
||||||
)
|
)
|
||||||
|
|
||||||
# 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
|
||||||
@@ -499,7 +509,15 @@ def provider_anime_episode_servers_menu(
|
|||||||
current_episode_number,
|
current_episode_number,
|
||||||
):
|
):
|
||||||
custom_args.extend(args)
|
custom_args.extend(args)
|
||||||
if config.use_mpv_mod:
|
if config.sync_play:
|
||||||
|
from ..utils.syncplay import SyncPlayer
|
||||||
|
|
||||||
|
stop_time, total_time = SyncPlayer(
|
||||||
|
current_stream_link,
|
||||||
|
selected_server["episode_title"],
|
||||||
|
headers=selected_server["headers"],
|
||||||
|
)
|
||||||
|
elif config.use_mpv_mod:
|
||||||
from ..utils.player import player
|
from ..utils.player import player
|
||||||
|
|
||||||
mpv = player.create_player(
|
mpv = player.create_player(
|
||||||
@@ -508,6 +526,7 @@ def provider_anime_episode_servers_menu(
|
|||||||
fastanime_runtime_state,
|
fastanime_runtime_state,
|
||||||
config,
|
config,
|
||||||
selected_server["episode_title"],
|
selected_server["episode_title"],
|
||||||
|
headers=selected_server["headers"],
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO: implement custom aniskip intergration
|
# TODO: implement custom aniskip intergration
|
||||||
@@ -531,6 +550,7 @@ def provider_anime_episode_servers_menu(
|
|||||||
selected_server["episode_title"],
|
selected_server["episode_title"],
|
||||||
start_time=start_time,
|
start_time=start_time,
|
||||||
custom_args=custom_args,
|
custom_args=custom_args,
|
||||||
|
headers=selected_server["headers"],
|
||||||
)
|
)
|
||||||
print("Finished at: ", stop_time)
|
print("Finished at: ", stop_time)
|
||||||
|
|
||||||
@@ -857,6 +877,12 @@ def media_actions_menu(
|
|||||||
config: [TODO:description]
|
config: [TODO:description]
|
||||||
fastanime_runtime_state: [TODO:description]
|
fastanime_runtime_state: [TODO:description]
|
||||||
"""
|
"""
|
||||||
|
if not config.user:
|
||||||
|
print("You aint logged in")
|
||||||
|
input("Enter to continue")
|
||||||
|
media_actions_menu(config, fastanime_runtime_state)
|
||||||
|
return
|
||||||
|
|
||||||
anime_lists = {
|
anime_lists = {
|
||||||
"Watching": "CURRENT",
|
"Watching": "CURRENT",
|
||||||
"Paused": "PAUSED",
|
"Paused": "PAUSED",
|
||||||
@@ -901,6 +927,11 @@ def media_actions_menu(
|
|||||||
config: [TODO:description]
|
config: [TODO:description]
|
||||||
fastanime_runtime_state: [TODO:description]
|
fastanime_runtime_state: [TODO:description]
|
||||||
"""
|
"""
|
||||||
|
if not config.user:
|
||||||
|
print("You aint logged in")
|
||||||
|
input("Enter to continue")
|
||||||
|
media_actions_menu(config, fastanime_runtime_state)
|
||||||
|
return
|
||||||
if config.use_rofi:
|
if config.use_rofi:
|
||||||
score = Rofi.ask("Enter Score", is_int=True)
|
score = Rofi.ask("Enter Score", is_int=True)
|
||||||
score = max(100, min(0, score))
|
score = max(100, min(0, score))
|
||||||
@@ -1199,7 +1230,7 @@ def anilist_results_menu(
|
|||||||
anime["status"] == "RELEASING"
|
anime["status"] == "RELEASING"
|
||||||
and anime["nextAiringEpisode"]
|
and anime["nextAiringEpisode"]
|
||||||
and progress > 0
|
and progress > 0
|
||||||
and anime["mediaListEntry"]
|
and (anime["mediaListEntry"] or {}).get("status", "") == "CURRENT"
|
||||||
):
|
):
|
||||||
last_aired_episode = anime["nextAiringEpisode"]["episode"] - 1
|
last_aired_episode = anime["nextAiringEpisode"]["episode"] - 1
|
||||||
if last_aired_episode - progress > 0:
|
if last_aired_episode - progress > 0:
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import re
|
|||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
|
from fastanime.constants import S_PLATFORM
|
||||||
|
|
||||||
|
|
||||||
def stream_video(MPV, url, mpv_args, custom_args):
|
def stream_video(MPV, url, mpv_args, custom_args):
|
||||||
process = subprocess.Popen(
|
process = subprocess.Popen(
|
||||||
@@ -52,6 +54,7 @@ def run_mpv(
|
|||||||
start_time: str = "0",
|
start_time: str = "0",
|
||||||
ytdl_format="",
|
ytdl_format="",
|
||||||
custom_args=[],
|
custom_args=[],
|
||||||
|
headers={},
|
||||||
):
|
):
|
||||||
# Determine if mpv is available
|
# Determine if mpv is available
|
||||||
MPV = shutil.which("mpv")
|
MPV = shutil.which("mpv")
|
||||||
@@ -61,7 +64,7 @@ def run_mpv(
|
|||||||
# Regex to check if the link is a YouTube URL
|
# Regex to check if the link is a YouTube URL
|
||||||
youtube_regex = r"(https?://)?(www\.)?(youtube|youtu|youtube-nocookie)\.(com|be)/.+"
|
youtube_regex = r"(https?://)?(www\.)?(youtube|youtu|youtube-nocookie)\.(com|be)/.+"
|
||||||
|
|
||||||
if not MPV:
|
if not MPV and not S_PLATFORM == "win32":
|
||||||
# Determine if the link is a YouTube URL
|
# Determine if the link is a YouTube URL
|
||||||
if re.match(youtube_regex, link):
|
if re.match(youtube_regex, link):
|
||||||
# Android specific commands to launch mpv with a YouTube URL
|
# Android specific commands to launch mpv with a YouTube URL
|
||||||
@@ -100,6 +103,11 @@ def run_mpv(
|
|||||||
else:
|
else:
|
||||||
# General mpv command with custom arguments
|
# General mpv command with custom arguments
|
||||||
mpv_args = []
|
mpv_args = []
|
||||||
|
if headers:
|
||||||
|
mpv_headers = "--http-header-fields="
|
||||||
|
for header_name, header_value in headers.items():
|
||||||
|
mpv_headers += f"{header_name}:{header_value},"
|
||||||
|
mpv_args.append(mpv_headers)
|
||||||
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:
|
||||||
|
|||||||
@@ -151,6 +151,7 @@ class MpvPlayer(object):
|
|||||||
fastanime_runtime_state,
|
fastanime_runtime_state,
|
||||||
config: "Config",
|
config: "Config",
|
||||||
title,
|
title,
|
||||||
|
headers={},
|
||||||
):
|
):
|
||||||
self.anime_provider = anime_provider
|
self.anime_provider = anime_provider
|
||||||
self.fastanime_runtime_state = fastanime_runtime_state
|
self.fastanime_runtime_state = fastanime_runtime_state
|
||||||
@@ -174,6 +175,11 @@ class MpvPlayer(object):
|
|||||||
# mpv_player.cache = "yes"
|
# mpv_player.cache = "yes"
|
||||||
# mpv_player.cache_pause = "no"
|
# mpv_player.cache_pause = "no"
|
||||||
mpv_player.title = title
|
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)
|
mpv_player.play(stream_link)
|
||||||
|
|
||||||
|
|||||||
42
fastanime/cli/utils/syncplay.py
Normal file
42
fastanime/cli/utils/syncplay.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from .tools import exit_app
|
||||||
|
|
||||||
|
|
||||||
|
def SyncPlayer(url: str, anime_title=None, headers={}, *args):
|
||||||
|
# TODO: handle m3u8 multi quality streams
|
||||||
|
#
|
||||||
|
# check for SyncPlay
|
||||||
|
SYNCPLAY_EXECUTABLE = shutil.which("syncplay")
|
||||||
|
if not SYNCPLAY_EXECUTABLE:
|
||||||
|
print("Syncplay not found")
|
||||||
|
exit_app(1)
|
||||||
|
return "0", "0"
|
||||||
|
# start SyncPlayer
|
||||||
|
mpv_args = []
|
||||||
|
if headers:
|
||||||
|
mpv_headers = "--http-header-fields="
|
||||||
|
for header_name, header_value in headers.items():
|
||||||
|
mpv_headers += f"{header_name}:{header_value},"
|
||||||
|
mpv_args.append(mpv_headers)
|
||||||
|
if not anime_title:
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
SYNCPLAY_EXECUTABLE,
|
||||||
|
url,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
SYNCPLAY_EXECUTABLE,
|
||||||
|
url,
|
||||||
|
"--",
|
||||||
|
f"--force-media-title={anime_title}",
|
||||||
|
*mpv_args,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# for compatability
|
||||||
|
return "0", "0"
|
||||||
@@ -32,8 +32,8 @@ def filter_by_quality(quality: str, stream_links: "list[EpisodeStream]", default
|
|||||||
for stream_link in stream_links:
|
for stream_link in stream_links:
|
||||||
q = float(quality)
|
q = float(quality)
|
||||||
Q = float(stream_link["quality"])
|
Q = float(stream_link["quality"])
|
||||||
# some providers have inaccurate eg qualities 718 instead of 720
|
# some providers have inaccurate/weird/non-standard eg qualities 718 instead of 720
|
||||||
if Q < q + 80 and Q > q - 80:
|
if Q <= q + 80 and Q >= q - 80:
|
||||||
return stream_link
|
return stream_link
|
||||||
else:
|
else:
|
||||||
if stream_links and default:
|
if stream_links and default:
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ Path(USER_VIDEOS_DIR).mkdir(parents=True, exist_ok=True)
|
|||||||
# useful paths
|
# useful paths
|
||||||
USER_DATA_PATH = os.path.join(APP_DATA_DIR, "user_data.json")
|
USER_DATA_PATH = os.path.join(APP_DATA_DIR, "user_data.json")
|
||||||
USER_CONFIG_PATH = os.path.join(APP_DATA_DIR, "config.ini")
|
USER_CONFIG_PATH = os.path.join(APP_DATA_DIR, "config.ini")
|
||||||
NOTIFIER_LOG_FILE_PATH = os.path.join(APP_DATA_DIR, "notifier.log")
|
LOG_FILE_PATH = os.path.join(APP_DATA_DIR, "fastanime.log")
|
||||||
|
|
||||||
|
|
||||||
USER_NAME = os.environ.get("USERNAME", "Anime fun")
|
USER_NAME = os.environ.get("USERNAME", "Anime fun")
|
||||||
|
|||||||
@@ -173,6 +173,7 @@ query ($userId: Int, $status: MediaListStatus,$type:MediaType) {
|
|||||||
status
|
status
|
||||||
description
|
description
|
||||||
mediaListEntry{
|
mediaListEntry{
|
||||||
|
status
|
||||||
id
|
id
|
||||||
progress
|
progress
|
||||||
}
|
}
|
||||||
@@ -275,6 +276,7 @@ query($query:String,%s){
|
|||||||
|
|
||||||
}
|
}
|
||||||
mediaListEntry{
|
mediaListEntry{
|
||||||
|
status
|
||||||
id
|
id
|
||||||
progress
|
progress
|
||||||
}
|
}
|
||||||
@@ -356,6 +358,7 @@ query($type:MediaType){
|
|||||||
day
|
day
|
||||||
}
|
}
|
||||||
mediaListEntry{
|
mediaListEntry{
|
||||||
|
status
|
||||||
id
|
id
|
||||||
progress
|
progress
|
||||||
}
|
}
|
||||||
@@ -396,6 +399,7 @@ query($type:MediaType){
|
|||||||
|
|
||||||
}
|
}
|
||||||
mediaListEntry{
|
mediaListEntry{
|
||||||
|
status
|
||||||
id
|
id
|
||||||
progress
|
progress
|
||||||
}
|
}
|
||||||
@@ -455,6 +459,7 @@ query($type:MediaType){
|
|||||||
|
|
||||||
}
|
}
|
||||||
mediaListEntry{
|
mediaListEntry{
|
||||||
|
status
|
||||||
id
|
id
|
||||||
progress
|
progress
|
||||||
}
|
}
|
||||||
@@ -520,6 +525,7 @@ query($type:MediaType){
|
|||||||
episodes
|
episodes
|
||||||
genres
|
genres
|
||||||
mediaListEntry{
|
mediaListEntry{
|
||||||
|
status
|
||||||
id
|
id
|
||||||
progress
|
progress
|
||||||
}
|
}
|
||||||
@@ -572,6 +578,7 @@ query($type:MediaType){
|
|||||||
id
|
id
|
||||||
}
|
}
|
||||||
mediaListEntry{
|
mediaListEntry{
|
||||||
|
status
|
||||||
id
|
id
|
||||||
progress
|
progress
|
||||||
}
|
}
|
||||||
@@ -630,6 +637,7 @@ query($type:MediaType){
|
|||||||
large
|
large
|
||||||
}
|
}
|
||||||
mediaListEntry{
|
mediaListEntry{
|
||||||
|
status
|
||||||
id
|
id
|
||||||
progress
|
progress
|
||||||
}
|
}
|
||||||
@@ -724,6 +732,7 @@ query ($id: Int,$type:MediaType) {
|
|||||||
large
|
large
|
||||||
}
|
}
|
||||||
mediaListEntry{
|
mediaListEntry{
|
||||||
|
status
|
||||||
id
|
id
|
||||||
progress
|
progress
|
||||||
}
|
}
|
||||||
@@ -805,6 +814,7 @@ query ($page: Int,$type:MediaType) {
|
|||||||
id
|
id
|
||||||
}
|
}
|
||||||
mediaListEntry{
|
mediaListEntry{
|
||||||
|
status
|
||||||
id
|
id
|
||||||
progress
|
progress
|
||||||
}
|
}
|
||||||
@@ -855,6 +865,7 @@ query($id:Int){
|
|||||||
english
|
english
|
||||||
}
|
}
|
||||||
mediaListEntry{
|
mediaListEntry{
|
||||||
|
status
|
||||||
id
|
id
|
||||||
progress
|
progress
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,4 +9,5 @@ SERVERS_AVAILABLE = [
|
|||||||
"weTransfer",
|
"weTransfer",
|
||||||
"wixmp",
|
"wixmp",
|
||||||
"kwik",
|
"kwik",
|
||||||
|
"Yt",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from typing import TYPE_CHECKING
|
|||||||
from requests.exceptions import Timeout
|
from requests.exceptions import Timeout
|
||||||
|
|
||||||
from ...anime_provider.base_provider import AnimeProvider
|
from ...anime_provider.base_provider import AnimeProvider
|
||||||
from ..utils import decode_hex_string, give_random_quality
|
from ..utils import give_random_quality, one_digit_symmetric_xor
|
||||||
from .constants import (
|
from .constants import (
|
||||||
ALLANIME_API_ENDPOINT,
|
ALLANIME_API_ENDPOINT,
|
||||||
ALLANIME_BASE,
|
ALLANIME_BASE,
|
||||||
@@ -205,23 +205,45 @@ class AllAnimeAPI(AnimeProvider):
|
|||||||
# filter the working streams no need to get all since the others are mostly hsl
|
# filter the working streams no need to get all since the others are mostly hsl
|
||||||
# TODO: should i just get all the servers and handle the hsl??
|
# TODO: should i just get all the servers and handle the hsl??
|
||||||
if embed.get("sourceName", "") not in (
|
if embed.get("sourceName", "") not in (
|
||||||
"Sak",
|
# priorities based on death note
|
||||||
"Kir",
|
"Sak", # 7
|
||||||
"S-mp4",
|
"S-mp4", # 7.9
|
||||||
"Luf-mp4",
|
"Luf-mp4", # 7.7
|
||||||
"Default",
|
"Default", # 8.5
|
||||||
|
"Yt-mp4", # 7.9
|
||||||
|
"Kir", # NA
|
||||||
|
# "Vid-mp4" # 4
|
||||||
|
# "Ok", # 3.5
|
||||||
|
# "Ss-Hls", # 5.5
|
||||||
|
# "Mp4", # 4
|
||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
url = embed.get("sourceUrl")
|
url = embed.get("sourceUrl")
|
||||||
|
#
|
||||||
if not url:
|
if not url:
|
||||||
continue
|
continue
|
||||||
if url.startswith("--"):
|
if url.startswith("--"):
|
||||||
url = url[2:]
|
url = url[2:]
|
||||||
|
url = one_digit_symmetric_xor(56, url)
|
||||||
|
|
||||||
|
if "tools.fast4speed.rsvp" in url:
|
||||||
|
yield {
|
||||||
|
"server": "Yt",
|
||||||
|
"episode_title": f'{anime["title"]}; Episode {episode_number}',
|
||||||
|
"headers": {"Referer": f"https://{ALLANIME_BASE}/"},
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"link": url,
|
||||||
|
"quality": "1080",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
} # pyright:ignore
|
||||||
|
continue
|
||||||
|
|
||||||
# get the stream url for an episode of the defined source names
|
# get the stream url for an episode of the defined source names
|
||||||
parsed_url = decode_hex_string(url)
|
embed_url = (
|
||||||
embed_url = f"https://{ALLANIME_BASE}{parsed_url.replace('clock', 'clock.json')}"
|
f"https://{ALLANIME_BASE}{url.replace('clock', 'clock.json')}"
|
||||||
|
)
|
||||||
resp = self.session.get(
|
resp = self.session.get(
|
||||||
embed_url,
|
embed_url,
|
||||||
headers={
|
headers={
|
||||||
@@ -230,12 +252,14 @@ class AllAnimeAPI(AnimeProvider):
|
|||||||
},
|
},
|
||||||
timeout=10,
|
timeout=10,
|
||||||
)
|
)
|
||||||
|
|
||||||
if resp.status_code == 200:
|
if resp.status_code == 200:
|
||||||
match embed["sourceName"]:
|
match embed["sourceName"]:
|
||||||
case "Luf-mp4":
|
case "Luf-mp4":
|
||||||
logger.debug("allanime:Found streams from gogoanime")
|
logger.debug("allanime:Found streams from gogoanime")
|
||||||
yield {
|
yield {
|
||||||
"server": "gogoanime",
|
"server": "gogoanime",
|
||||||
|
"headers": {},
|
||||||
"episode_title": (
|
"episode_title": (
|
||||||
allanime_episode["notes"] or f'{anime["title"]}'
|
allanime_episode["notes"] or f'{anime["title"]}'
|
||||||
)
|
)
|
||||||
@@ -246,6 +270,7 @@ class AllAnimeAPI(AnimeProvider):
|
|||||||
logger.debug("allanime:Found streams from wetransfer")
|
logger.debug("allanime:Found streams from wetransfer")
|
||||||
yield {
|
yield {
|
||||||
"server": "wetransfer",
|
"server": "wetransfer",
|
||||||
|
"headers": {},
|
||||||
"episode_title": (
|
"episode_title": (
|
||||||
allanime_episode["notes"] or f'{anime["title"]}'
|
allanime_episode["notes"] or f'{anime["title"]}'
|
||||||
)
|
)
|
||||||
@@ -256,6 +281,7 @@ class AllAnimeAPI(AnimeProvider):
|
|||||||
logger.debug("allanime:Found streams from sharepoint")
|
logger.debug("allanime:Found streams from sharepoint")
|
||||||
yield {
|
yield {
|
||||||
"server": "sharepoint",
|
"server": "sharepoint",
|
||||||
|
"headers": {},
|
||||||
"episode_title": (
|
"episode_title": (
|
||||||
allanime_episode["notes"] or f'{anime["title"]}'
|
allanime_episode["notes"] or f'{anime["title"]}'
|
||||||
)
|
)
|
||||||
@@ -266,6 +292,7 @@ class AllAnimeAPI(AnimeProvider):
|
|||||||
logger.debug("allanime:Found streams from dropbox")
|
logger.debug("allanime:Found streams from dropbox")
|
||||||
yield {
|
yield {
|
||||||
"server": "dropbox",
|
"server": "dropbox",
|
||||||
|
"headers": {},
|
||||||
"episode_title": (
|
"episode_title": (
|
||||||
allanime_episode["notes"] or f'{anime["title"]}'
|
allanime_episode["notes"] or f'{anime["title"]}'
|
||||||
)
|
)
|
||||||
@@ -276,20 +303,22 @@ class AllAnimeAPI(AnimeProvider):
|
|||||||
logger.debug("allanime:Found streams from wixmp")
|
logger.debug("allanime:Found streams from wixmp")
|
||||||
yield {
|
yield {
|
||||||
"server": "wixmp",
|
"server": "wixmp",
|
||||||
|
"headers": {},
|
||||||
"episode_title": (
|
"episode_title": (
|
||||||
allanime_episode["notes"] or f'{anime["title"]}'
|
allanime_episode["notes"] or f'{anime["title"]}'
|
||||||
)
|
)
|
||||||
+ f"; Episode {episode_number}",
|
+ f"; Episode {episode_number}",
|
||||||
"links": give_random_quality(resp.json()["links"]),
|
"links": give_random_quality(resp.json()["links"]),
|
||||||
} # pyright:ignore
|
} # pyright:ignore
|
||||||
|
|
||||||
except Timeout:
|
except Timeout:
|
||||||
logger.error(
|
logger.error(
|
||||||
"Timeout has been exceeded this could mean allanime is down or you have lost internet connection"
|
"Timeout has been exceeded this could mean allanime is down or you have lost internet connection"
|
||||||
)
|
)
|
||||||
return []
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"FA(Allanime): {e}")
|
logger.error(f"FA(Allanime): {e}")
|
||||||
return []
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"FA(Allanime): {e}")
|
logger.error(f"FA(Allanime): {e}")
|
||||||
return []
|
return []
|
||||||
@@ -301,7 +330,7 @@ if __name__ == "__main__":
|
|||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from InquirerPy import inquirer, validator
|
from InquirerPy import inquirer, validator # pyright:ignore
|
||||||
|
|
||||||
anime = input("Enter the anime name: ")
|
anime = input("Enter the anime name: ")
|
||||||
translation = input("Enter the translation type: ")
|
translation = input("Enter the translation type: ")
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
import logging
|
import logging
|
||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import time
|
import time
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from yt_dlp.utils import (
|
from yt_dlp.utils import (
|
||||||
extract_attributes,
|
extract_attributes,
|
||||||
get_element_by_id,
|
get_element_by_id,
|
||||||
get_element_text_and_html_by_tag,
|
|
||||||
get_elements_html_by_class,
|
get_elements_html_by_class,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -20,6 +17,7 @@ from .constants import (
|
|||||||
REQUEST_HEADERS,
|
REQUEST_HEADERS,
|
||||||
SERVER_HEADERS,
|
SERVER_HEADERS,
|
||||||
)
|
)
|
||||||
|
from .utils import process_animepahe_embed_page
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ..types import Anime
|
from ..types import Anime
|
||||||
@@ -27,6 +25,8 @@ if TYPE_CHECKING:
|
|||||||
JUICY_STREAM_REGEX = re.compile(r"source='(.*)';")
|
JUICY_STREAM_REGEX = re.compile(r"source='(.*)';")
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
KWIK_RE = re.compile(r"Player\|(.+?)'")
|
||||||
|
|
||||||
|
|
||||||
# TODO: hack this to completion
|
# TODO: hack this to completion
|
||||||
class AnimePaheApi(AnimeProvider):
|
class AnimePaheApi(AnimeProvider):
|
||||||
@@ -153,6 +153,7 @@ class AnimePaheApi(AnimeProvider):
|
|||||||
def get_episode_streams(
|
def get_episode_streams(
|
||||||
self, anime: "Anime", episode_number: str, translation_type, *args
|
self, anime: "Anime", episode_number: str, translation_type, *args
|
||||||
):
|
):
|
||||||
|
try:
|
||||||
# extract episode details from memory
|
# extract episode details from memory
|
||||||
episode = [
|
episode = [
|
||||||
episode
|
episode
|
||||||
@@ -161,7 +162,9 @@ class AnimePaheApi(AnimeProvider):
|
|||||||
]
|
]
|
||||||
|
|
||||||
if not episode:
|
if not episode:
|
||||||
logger.error(f"AnimePahe(streams): episode {episode_number} doesn't exist")
|
logger.error(
|
||||||
|
f"AnimePahe(streams): episode {episode_number} doesn't exist"
|
||||||
|
)
|
||||||
return []
|
return []
|
||||||
episode = episode[0]
|
episode = episode[0]
|
||||||
|
|
||||||
@@ -183,7 +186,12 @@ class AnimePaheApi(AnimeProvider):
|
|||||||
episode["title"] or f"{anime['title']}; Episode {episode['episode']}"
|
episode["title"] or f"{anime['title']}; Episode {episode['episode']}"
|
||||||
)
|
)
|
||||||
# get all links
|
# get all links
|
||||||
streams = {"server": "kwik", "links": [], "episode_title": episode_title}
|
streams = {
|
||||||
|
"server": "kwik",
|
||||||
|
"links": [],
|
||||||
|
"episode_title": episode_title,
|
||||||
|
"headers": {},
|
||||||
|
}
|
||||||
for res_dict in res_dicts:
|
for res_dict in res_dicts:
|
||||||
# get embed url
|
# get embed url
|
||||||
embed_url = res_dict["data-src"]
|
embed_url = res_dict["data-src"]
|
||||||
@@ -199,49 +207,17 @@ 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=SERVER_HEADERS)
|
||||||
embed = embed_response.text
|
embed_page = embed_response.text
|
||||||
# search for the encoded js
|
|
||||||
encoded_js = None
|
decoded_js = process_animepahe_embed_page(embed_page)
|
||||||
for _ in range(7):
|
if not decoded_js:
|
||||||
content, html = get_element_text_and_html_by_tag("script", embed)
|
logger.error("Animepahe: failed to decode embed page")
|
||||||
if not content:
|
return
|
||||||
embed = embed.replace(html, "")
|
juicy_stream = JUICY_STREAM_REGEX.search(decoded_js)
|
||||||
continue
|
if not juicy_stream:
|
||||||
encoded_js = content
|
logger.error("Animepahe: failed to find juicy stream")
|
||||||
break
|
return
|
||||||
if not encoded_js:
|
juicy_stream = juicy_stream.group(1)
|
||||||
logger.warn(
|
|
||||||
"AnimePahe: Encoded js not found please report to the developers"
|
|
||||||
)
|
|
||||||
return []
|
|
||||||
# execute the encoded js with node for now or maybe forever in odrder to get a more workable info
|
|
||||||
NODE = shutil.which("node")
|
|
||||||
if not NODE:
|
|
||||||
logger.warn(
|
|
||||||
"AnimePahe: animepahe currently requires node js to extract them juicy streams"
|
|
||||||
)
|
|
||||||
return []
|
|
||||||
result = subprocess.run(
|
|
||||||
[NODE, "-e", encoded_js],
|
|
||||||
text=True,
|
|
||||||
capture_output=True,
|
|
||||||
)
|
|
||||||
# decoded js
|
|
||||||
evaluted_js = result.stderr
|
|
||||||
if not evaluted_js:
|
|
||||||
logger.warn(
|
|
||||||
"AnimePahe: could not decode encoded js using node please report to developers"
|
|
||||||
)
|
|
||||||
return []
|
|
||||||
# get that juicy stream
|
|
||||||
match = JUICY_STREAM_REGEX.search(evaluted_js)
|
|
||||||
if not match:
|
|
||||||
logger.warn(
|
|
||||||
"AnimePahe: could not find the juicy stream please report to developers"
|
|
||||||
)
|
|
||||||
return []
|
|
||||||
# get the actual hls stream link
|
|
||||||
juicy_stream = match.group(1)
|
|
||||||
# add the link
|
# add the link
|
||||||
streams["links"].append(
|
streams["links"].append(
|
||||||
{
|
{
|
||||||
@@ -251,3 +227,5 @@ class AnimePaheApi(AnimeProvider):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
yield streams
|
yield streams
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Animepahe: {e}")
|
||||||
|
|||||||
81
fastanime/libs/anime_provider/animepahe/utils.py
Normal file
81
fastanime/libs/anime_provider/animepahe/utils.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
# from ..utils import int2base
|
||||||
|
import re
|
||||||
|
|
||||||
|
from yt_dlp.utils import encode_base_n, get_element_text_and_html_by_tag
|
||||||
|
|
||||||
|
|
||||||
|
def animepahe_key_creator(c: int, a: int):
|
||||||
|
if c < a:
|
||||||
|
val_a = ""
|
||||||
|
else:
|
||||||
|
val_a = animepahe_key_creator(int(c / a), a)
|
||||||
|
c = c % a
|
||||||
|
if c > 35:
|
||||||
|
val_b = chr(c + 29)
|
||||||
|
else:
|
||||||
|
val_b = encode_base_n(c, 36)
|
||||||
|
return val_a + val_b
|
||||||
|
|
||||||
|
|
||||||
|
def animepahe_embed_decoder(
|
||||||
|
encoded_js_p: str,
|
||||||
|
base_a: int,
|
||||||
|
no_of_keys_c: int,
|
||||||
|
key_values_k: list,
|
||||||
|
decode_mapper_d: dict = {},
|
||||||
|
):
|
||||||
|
for i in range(no_of_keys_c):
|
||||||
|
key = animepahe_key_creator(i, base_a)
|
||||||
|
val = key_values_k[i] or key
|
||||||
|
decode_mapper_d[key] = val
|
||||||
|
return re.sub(
|
||||||
|
r"\b\w+\b", lambda match: decode_mapper_d[match.group(0)], encoded_js_p
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
PARAMETERS_REGEX = re.compile(r"eval\(function\(p,a,c,k,e,d\)\{.*\}\((.*?)\)\)$")
|
||||||
|
ENCODE_JS_REGEX = re.compile(r"'(.*?);',(\d+),(\d+),'(.*)'\.split")
|
||||||
|
|
||||||
|
|
||||||
|
def process_animepahe_embed_page(embed_page: str):
|
||||||
|
encoded_js_string = ""
|
||||||
|
embed_page_content = embed_page
|
||||||
|
for _ in range(8):
|
||||||
|
text, html = get_element_text_and_html_by_tag("script", embed_page_content)
|
||||||
|
if not text:
|
||||||
|
embed_page_content = re.sub(html, "", embed_page_content)
|
||||||
|
continue
|
||||||
|
encoded_js_string = text.strip()
|
||||||
|
break
|
||||||
|
if not encoded_js_string:
|
||||||
|
return
|
||||||
|
obsfucated_js_parameter_match = PARAMETERS_REGEX.search(encoded_js_string)
|
||||||
|
if not obsfucated_js_parameter_match:
|
||||||
|
return
|
||||||
|
parameter_string = obsfucated_js_parameter_match.group(1)
|
||||||
|
encoded_js_parameter_string = ENCODE_JS_REGEX.search(parameter_string)
|
||||||
|
if not encoded_js_parameter_string:
|
||||||
|
return
|
||||||
|
p: str = encoded_js_parameter_string.group(1)
|
||||||
|
a: int = int(encoded_js_parameter_string.group(2))
|
||||||
|
c: int = int(encoded_js_parameter_string.group(3))
|
||||||
|
k: list = encoded_js_parameter_string.group(4).split("|")
|
||||||
|
return animepahe_embed_decoder(p, a, c, k).replace("\\", "")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
data = """<script>eval(function(p,a,c,k,e,d){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--){d[e(c)]=k[c]||e(c)}k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--){if(k[c]){p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c])}}return p}('f $7={H:a(2){4 B(9.7.h(y z("(?:(?:^|.*;)\\\\s*"+d(2).h(/[\\-\\.\\+\\*]/g,"\\\\$&")+"\\\\s*\\\\=\\\\s*([^;]*).*$)|^.*$"),"$1"))||G},E:a(2,q,3,6,5,t){k(!2||/^(?:8|r\\-v|o|m|p)$/i.D(2)){4 w}f b="";k(3){F(3.J){j K:b=3===P?"; 8=O, I N Q M:u:u A":"; r-v="+3;n;j L:b="; 8="+3;n;j S:b="; 8="+3.Z();n}}9.7=d(2)+"="+d(q)+b+(5?"; m="+5:"")+(6?"; o="+6:"")+(t?"; p":"");4 x},Y:a(2,6,5){k(!2||!11.C(2)){4 w}9.7=d(2)+"=; 8=12, R 10 W l:l:l A"+(5?"; m="+5:"")+(6?"; o="+6:"");4 x},C:a(2){4(y z("(?:^|;\\\\s*)"+d(2).h(/[\\-\\.\\+\\*]/g,"\\\\$&")+"\\\\s*\\\\=")).D(9.7)},X:a(){f c=9.7.h(/((?:^|\\s*;)[^\\=]+)(?=;|$)|^\\s*|\\s*(?:\\=[^;]*)?(?:\\1|$)/g,"").T(/\\s*(?:\\=[^;]*)?;\\s*/);U(f e=0;e<c.V;e++){c[e]=B(c[e])}4 c}};',62,65,'||sKey|vEnd|return|sDomain|sPath|cookie|expires|document|function|sExpires|aKeys|encodeURIComponent|nIdx|var||replace||case|if|00|domain|break|path|secure|sValue|max||bSecure|59|age|false|true|new|RegExp|GMT|decodeURIComponent|hasItem|test|setItem|switch|null|getItem|31|constructor|Number|String|23|Dec|Fri|Infinity|9999|01|Date|split|for|length|1970|keys|removeItem|toUTCString|Jan|this|Thu'.split('|'),0,{}));eval(function(p,a,c,k,e,d){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--){d[e(c)]=k[c]||e(c)}k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--){if(k[c]){p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c])}}return p}('h o=\'1D://1C-E.1B.1A.1z/1y/E/1x/1w/1v.1u\';h d=s.r(\'d\');h 0=B 1t(d,{\'1s\':{\'1r\':i},\'1q\':\'16:9\',\'D\':1,\'1p\':5,\'1o\':{\'1n\':\'1m\'},1l:[\'7-1k\',\'7\',\'1j\',\'1i-1h\',\'1g\',\'1f-1e\',\'1d\',\'D\',\'1c\',\'1b\',\'1a\',\'19\',\'C\',\'18\'],\'C\':{\'17\':i}});8(!A.15()){d.14=o}x{j z={13:12,11:10,Z:Y,X:i,W:i};h c=B A(z);c.V(o);c.U(d);g.c=c}0.3("T",6=>{g.S.R.Q("P")});0.O=1;k v(b,n,m){8(b.y){b.y(n,m,N)}x 8(b.w){b.w(\'3\'+n,m)}}j 4=k(l){g.M.L(l,\'*\')};v(g,\'l\',k(e){j a=e.a;8(a===\'7\')0.7();8(a===\'f\')0.f();8(a===\'u\')0.u()});0.3(\'t\',6=>{4(\'t\')});0.3(\'7\',6=>{4(\'7\')});0.3(\'f\',6=>{4(\'f\')});0.3(\'K\',6=>{4(0.q);s.r(\'.J-I\').H=G(0.q.F(2))});0.3(\'p\',6=>{4(\'p\')});',62,102,'player|||on|sendMessage||event|play|if||data|element|hls|video||pause|window|const|true|var|function|message|eventHandler|eventName|source|ended|currentTime|querySelector|document|ready|stop|bindEvent|attachEvent|else|addEventListener|config|Hls|new|fullscreen|volume|01|toFixed|String|innerHTML|timestamp|ss|timeupdate|postMessage|parent|false|speed|landscape|lock|orientation|screen|enterfullscreen|attachMedia|loadSource|lowLatencyMode|enableWorker|Infinity|backBufferLength|600|maxMaxBufferLength|180|maxBufferLength|src|isSupported||iosNative|capture|airplay|pip|settings|captions|mute|time|current|progress|forward|fast|rewind|large|controls|kwik|key|storage|seekTime|ratio|global|keyboard|Plyr|m3u8|uwu|b92a392054c041a3f9c6eecabeb0e127183f44e547828447b10bca8d77523e6f|03|stream|org|nextcdn|files|eu|https'.split('|'),0,{}))"""
|
||||||
|
a = 62
|
||||||
|
c = 102
|
||||||
|
k = "player|||on|sendMessage||event|play|if||data|element|hls|video||pause|window|const|true|var|function|message|eventHandler|eventName|source|ended|currentTime|querySelector|document|ready|stop|bindEvent|attachEvent|else|addEventListener|config|Hls|new|fullscreen|volume|toFixed|String|innerHTML|timestamp|ss|timeupdate|postMessage|parent|false|speed|landscape|lock|orientation|screen|enterfullscreen|attachMedia|loadSource|lowLatencyMode|enableWorker|Infinity|backBufferLength|600|maxMaxBufferLength||180|maxBufferLength|src|isSupported||iosNative|capture|airplay|pip|settings|captions|mute|time|current|progress|forward|fast|rewind|large|controls|kwik|key|storage|seekTime|ratio|global|keyboard|Plyr|m3u8|uwu|cda74eaebce25a12f5e548f7c220bb5dc245700b0280bdb45ff98b2fe4803d2b|06|stream|org|nextcdn|files|eu|https".split(
|
||||||
|
"|"
|
||||||
|
)
|
||||||
|
|
||||||
|
p = "h o='1D://1C-11.1B.1A.1z/1y/11/1x/1w/1v.1u';h d=s.r('d');h 0=B 1t(d,{'1s':{'1r':i},'1q':'16:9','D':1,'1p':5,'1o':{'1n':'1m'},1l:['7-1k','7','1j','1i-1h','1g','1f-1e','1d','D','1c','1b','1a','19','C','18'],'C':{'17':i}});8(!A.15()){d.14=o}x{j z={13:12,10:Z,Y:X,W:i,V:i};h c=B A(z);c.U(o);c.T(d);g.c=c}0.3(\"S\",6=>{g.R.Q.P(\"O\")});0.N=1;k v(b,n,m){8(b.y){b.y(n,m,M)}x 8(b.w){b.w('3'+n,m)}}j 4=k(l){g.L.K(l,'*')};v(g,'l',k(e){j a=e.a;8(a==='7')0.7();8(a==='f')0.f();8(a==='u')0.u()});0.3('t',6=>{4('t')});0.3('7',6=>{4('7')});0.3('f',6=>{4('f')});0.3('J',6=>{4(0.q);s.r('.I-H').G=F(0.q.E(2))});0.3('p',6=>{4('p')});"
|
||||||
|
result = animepahe_embed_decoder(
|
||||||
|
p,
|
||||||
|
a,
|
||||||
|
c,
|
||||||
|
k,
|
||||||
|
)
|
||||||
|
print(result) # Output: j player = B A();
|
||||||
@@ -60,12 +60,12 @@ class EpisodeStream(TypedDict):
|
|||||||
hls: bool | None
|
hls: bool | None
|
||||||
mp4: bool | None
|
mp4: bool | None
|
||||||
priority: int | None
|
priority: int | None
|
||||||
headers: dict | None
|
|
||||||
quality: Literal["360", "720", "1080", "unknown"]
|
quality: Literal["360", "720", "1080", "unknown"]
|
||||||
translation_type: Literal["dub", "sub"]
|
translation_type: Literal["dub", "sub"]
|
||||||
|
|
||||||
|
|
||||||
class Server(TypedDict):
|
class Server(TypedDict):
|
||||||
|
headers: dict
|
||||||
server: str
|
server: str
|
||||||
episode_title: str
|
episode_title: str
|
||||||
links: list[EpisodeStream]
|
links: list[EpisodeStream]
|
||||||
|
|||||||
@@ -44,6 +44,14 @@ def give_random_quality(links: list[dict]):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def one_digit_symmetric_xor(password: int, target: str):
|
||||||
|
def genexp():
|
||||||
|
for segment in bytearray.fromhex(target):
|
||||||
|
yield segment ^ password
|
||||||
|
|
||||||
|
return bytes(genexp()).decode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
def decode_hex_string(hex_string):
|
def decode_hex_string(hex_string):
|
||||||
"""some of the sources encrypt the urls into hex codes this function decrypts the urls
|
"""some of the sources encrypt the urls into hex codes this function decrypts the urls
|
||||||
|
|
||||||
|
|||||||
12
poetry.lock
generated
12
poetry.lock
generated
@@ -845,13 +845,13 @@ testing = ["covdefaults (>=2.3)", "pytest (>=8.2.2)", "pytest-cov (>=5)", "pytes
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyright"
|
name = "pyright"
|
||||||
version = "1.1.375"
|
version = "1.1.376"
|
||||||
description = "Command line wrapper for pyright"
|
description = "Command line wrapper for pyright"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
files = [
|
files = [
|
||||||
{file = "pyright-1.1.375-py3-none-any.whl", hash = "sha256:4c5e27eddeaee8b41cc3120736a1dda6ae120edf8523bb2446b6073a52f286e3"},
|
{file = "pyright-1.1.376-py3-none-any.whl", hash = "sha256:0f2473b12c15c46b3207f0eec224c3cea2bdc07cd45dd4a037687cbbca0fbeff"},
|
||||||
{file = "pyright-1.1.375.tar.gz", hash = "sha256:7765557b0d6782b2fadabff455da2014476404c9e9214f49977a4e49dec19a0f"},
|
{file = "pyright-1.1.376.tar.gz", hash = "sha256:bffd63b197cd0810395bb3245c06b01f95a85ddf6bfa0e5644ed69c841e954dd"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -1157,13 +1157,13 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tox"
|
name = "tox"
|
||||||
version = "4.17.1"
|
version = "4.18.0"
|
||||||
description = "tox is a generic virtualenv management and test command line tool"
|
description = "tox is a generic virtualenv management and test command line tool"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "tox-4.17.1-py3-none-any.whl", hash = "sha256:2974597c0353577126ab014f52d1a399fb761049e165ff34427f84e8cfe6c990"},
|
{file = "tox-4.18.0-py3-none-any.whl", hash = "sha256:0a457400cf70615dc0627eb70d293e80cd95d8ce174bb40ac011011f0c03a249"},
|
||||||
{file = "tox-4.17.1.tar.gz", hash = "sha256:2c41565a571e34480bd401d668a4899806169a4633e972ac296c54406d2ded8a"},
|
{file = "tox-4.18.0.tar.gz", hash = "sha256:5dfa1cab9f146becd6e351333a82f9e0ade374451630ba65ee54584624c27b58"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "fastanime"
|
name = "fastanime"
|
||||||
version = "1.7.1"
|
version = "2.2.0"
|
||||||
description = "A browser anime site experience from the terminal"
|
description = "A browser anime site experience from the terminal"
|
||||||
authors = ["Benextempest <benextempest@gmail.com>"]
|
authors = ["Benextempest <benextempest@gmail.com>"]
|
||||||
license = "UNLICENSE"
|
license = "UNLICENSE"
|
||||||
|
|||||||
Reference in New Issue
Block a user