Compare commits

..

32 Commits

Author SHA1 Message Date
Benex254
f72c2d4b17 chore: bump version 2024-08-17 22:52:25 +03:00
Benex254
ff027991e0 chore: rename logfile 2024-08-17 22:52:11 +03:00
Benex254
21cdc6b015 docs: update readme 2024-08-17 22:50:04 +03:00
Benex254
29a2e3e6d1 feat(utils): include boundary in quality selector function 2024-08-17 22:46:44 +03:00
Benex254
5b3b9f740b feat(animepahe): remove use of node and implement custom logic to decode the string 2024-08-17 22:46:10 +03:00
Benex254
5bc0e52179 feat(download command): add headers functionality 2024-08-17 15:31:53 +03:00
Benex254
40f1c4fba5 chore: bump version 2024-08-17 15:25:26 +03:00
Benex254
454341eaf5 feat: enable use of http headers for providers 2024-08-17 15:17:53 +03:00
Benex254
abab2540a3 chore: update packages 2024-08-17 11:01:56 +03:00
Benex254
b2bc8cbace feat(download command): add more download options 2024-08-17 11:01:37 +03:00
Benex254
90bbf3c033 chore: bump version 2024-08-17 00:29:39 +03:00
Benex254
ac91b1770a feat(downloads command): use random episode for anime preview 2024-08-17 00:28:59 +03:00
Benex254
19d42b7924 feat(downloads command): add syncplay intergration 2024-08-16 23:37:37 +03:00
Benex254
9ec3136734 chore bump version 2024-08-16 23:02:24 +03:00
Benex254
943fca43cf docs: update readme 2024-08-16 23:02:24 +03:00
Benex254
b2e00feb94 feat(downloads command): sort by episode number 2024-08-16 23:02:24 +03:00
BeneX254
f726c8d55c Update README.md 2024-08-16 22:17:33 +03:00
Benex254
57db2e0626 chore: bump version 2024-08-16 22:09:56 +03:00
Benex254
40f66b5fde docs: update readme 2024-08-16 22:08:04 +03:00
Benex254
c87417e5e7 feat: add syncplay intergration 2024-08-16 22:03:22 +03:00
Benex254
a841dd6f66 chore: bump version 2024-08-16 20:04:57 +03:00
Benex254
d6e85bad5c docs: update readme 2024-08-16 20:04:45 +03:00
Benex254
b590ac1e91 feat(cli): improve download and search command 2024-08-16 19:49:40 +03:00
Benex254
9cfa3aeea5 feat(cli): use an option for providing anime title for search and download command 2024-08-16 19:45:00 +03:00
Benex254
18c60691ca feat(search command): improve binge power 2024-08-16 19:37:10 +03:00
Benex254
2e9fadf3b2 feat(download command): improve download command 2024-08-16 19:02:22 +03:00
Benex254
510b47b187 feat(downloads command): improve output by sorting the titles and episodes 2024-08-16 15:01:55 +03:00
Benex254
49c4d0eec0 docs: update readme 2024-08-16 14:57:15 +03:00
Benex254
8367f7bbed chore: bump version 2024-08-16 14:55:29 +03:00
Benex254
0182f674e0 feat: add status to graphql medialist query 2024-08-16 14:55:02 +03:00
Benex254
2b50fb4c97 fix(interface): improve error handling for non logged in user 2024-08-16 14:54:36 +03:00
Benex254
2602a20aa7 feat(login command): add option to erase login data 2024-08-16 14:53:57 +03:00
25 changed files with 817 additions and 436 deletions

View File

@@ -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)

View File

@@ -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:

View File

@@ -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:

View File

@@ -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"

View File

@@ -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:

View File

@@ -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):

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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

View File

@@ -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:

View File

@@ -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:

View File

@@ -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)

View 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"

View File

@@ -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:

View File

@@ -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")

View File

@@ -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
} }

View File

@@ -9,4 +9,5 @@ SERVERS_AVAILABLE = [
"weTransfer", "weTransfer",
"wixmp", "wixmp",
"kwik", "kwik",
"Yt",
] ]

View File

@@ -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: ")

View File

@@ -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}")

View 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();

View File

@@ -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]

View File

@@ -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
View File

@@ -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]

View File

@@ -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"