Compare commits

...

29 Commits

Author SHA1 Message Date
Benex254
0d2cf7ed66 chore: bump version 2024-08-18 15:18:28 +03:00
Benex254
aa6dc2b98e docs: update readme 2024-08-18 15:18:12 +03:00
Benex254
2e5cde3365 feat(grab command): include more options for finer control 2024-08-18 15:09:56 +03:00
Benex254
d75a03e594 feat(animepahe): fix episode title 2024-08-18 15:09:24 +03:00
Benex254
9268c02683 docs: update readme 2024-08-18 13:49:20 +03:00
Benex254
89913036c9 chore: bump version 2024-08-18 13:49:05 +03:00
Benex254
2244026c67 feat(update command): improve update command 2024-08-18 13:05:27 +03:00
Benex254
c70564474b feat(download command): make the download never promt for user action while downloading episodes instead warn and sleep 2024-08-18 12:47:32 +03:00
Benex254
74514c9fbc feat(animepahe): fix title order 2024-08-18 12:46:46 +03:00
Benex254
077e9ab8c4 feat(grab command): include translation type in data 2024-08-18 12:37:39 +03:00
Benex254
b05f7f1640 feat(cli): add help for download and search command 2024-08-18 12:34:18 +03:00
Benex254
3382b720e3 feat(cli): add grab command 2024-08-18 12:33:51 +03:00
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
26 changed files with 666 additions and 183 deletions

153
README.md
View File

@@ -41,15 +41,17 @@ Heavily inspired by [animdl](https://github.com/justfoolingaround/animdl), [magi
- [Subcommands](#subcommands)
- [download subcommand](#download-subcommand)
- [search subcommand](#search-subcommand)
- [grab subcommand](#grab-subcommand)
- [downloads subcommand](#downloads-subcommand)
- [config subcommand](#config-subcommand)
- [cache subcommand](#cache-subcommand)
- [update subcommand](#update-subcommand)
- [completions subcommand](#completions-subcommand)
- [MPV specific commands](#mpv-specific-commands)
- [Added keybindings](#added-keybindings)
- [Added script messages](#added-script-messages)
- [MPV specific commands](#mpv-specific-commands)
- [Key Bindings](#key-bindings)
- [Script Messages](#script-messages)
- [Configuration](#configuration)
- [The python api](#the-python-api)
- [Contributing](#contributing)
- [Receiving Support](#receiving-support)
- [Supporting the Project](#supporting-the-project)
@@ -57,7 +59,7 @@ Heavily inspired by [animdl](https://github.com/justfoolingaround/animdl), [magi
> [!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
@@ -120,7 +122,7 @@ Requirements:
To build from the source, follow these steps:
1. Clone the repository: `git clone https://github.com/Benex254/FastAnime.git`
1. Clone the repository: `git clone https://github.com/Benex254/FastAnime.git --depth 1`
2. Navigate into the folder: `cd FastAnime`
3. Then build and Install the app:
@@ -164,28 +166,25 @@ The only required external dependency, unless you won't be streaming, is [MPV](h
**Other external dependencies that will just make your experience better:**
- [ffmpeg](https://www.ffmpeg.org/) is required to be in your path environment variables to properly download [hls](https://www.cloudflare.com/en-gb/learning/video/what-is-http-live-streaming/) streams.
- [fzf](https://github.com/junegunn/fzf) 🔥 which is used as a better alternative to the ui.
- [rofi](https://github.com/davatorium/rofi) 🔥 which is used as another alternative ui + the the desktop entry ui
- [chafa](https://github.com/hpjansson/chafa) currently the best cross platform and cross terminal image viewer for the terminal.
- [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.
- [ani-skip](https://github.com/synacktraa/ani-skip) used for skipping the opening and ending theme songs
- [ffmpegthumbnailer](https://github.com/dirkvdb/ffmpegthumbnailer) used for local previews of downloaded anime
- [syncplay](https://syncplay.pl/) to enable watch together.
## Usage
The app offers both a graphical interface (under development) and a robust command-line interface.
> [!NOTE]
>
> The GUI is mostly in hiatus; use the CLI for now.
> However, you can try it out before i decided to change my objective by checking out this [release](https://github.com/Benex254/FastAnime/tree/v0.20.0).
> But be reassured for those who aren't terminal chads, i will still complete the GUI for the fun of it
The project offers a featureful command-line interface and MPV interface through the use of python-mpv.
### The Commandline interface :fire:
Designed for power users who prefer efficiency over browser-based streaming and still want the experience in their terminal.
Designed for efficiency and automation. Plus has a beautiful pseudo-TUI in some of the commands.
Overview of main commands:
**Overview of main commands:**
- `fastanime anilist`: Powerful command for browsing and exploring anime due to AniList integration.
- `fastanime download`: Download anime.
@@ -193,10 +192,20 @@ Overview of main commands:
- `fastanime downloads`: View downloaded anime and watch with MPV.
- `fastanime config`: Quickly edit configuration settings.
- `fastanime cache`: Quickly manage the cache fastanime uses
- `fastanime update`: Quickly update fastanime
- `fastanime grab`: print streams to stdout to use in non python application.
Configuration is directly passed into this command at run time to override your config.
**Overview of options**
Available options for the fastanime command include:
Most options are directly passed into fastanime directly and are shared by multiple subcommands.
Most of the options override your config file.
This is a convention to make the dev time faster since it reduces redundancy and also makes switching of subcommands with the same options easier to the end user.
In general `fastanime --<option-name>`
Available options for the fastanime include:
- `--server <server>` or `-s <server>` set the default server to auto select
- `--continue/--no-continue` or `-c/-no-c` whether to continue from the last episode you were watching
@@ -223,19 +232,19 @@ Available options for the fastanime command include:
- `--log-file` allow logging to a file
- `--rich-traceback` allow rich traceback
- `--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
```bash
# example of syncplay intergration
fastanime --sync-play search -t <anime-title>
fastanime --sync-play --server sharepoint search -t <anime-title>
# --- or ---
# to watch with anilist intergration
fastanime --sync-play anilist
fastanime --sync-play --server sharepoint anilist
# downloading dubbed anime
fastanime --dub download <anime>
@@ -289,7 +298,7 @@ fastanime --log anilist notifier
fastanime --log-file anilist notifier
```
The above commands will start a loop that checks every 2 minutes if any of the anime in your watch list that are aireing has just released a new episode.
The above commands will start a loop that checks every 2 minutes if any of the anime in your watch list that are airing has just released a new episode.
The notification will consist of a cover image of the anime in none windows systems.
@@ -313,6 +322,7 @@ end
Download anime to watch later dub or sub with this one command.
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.
Uses a list slicing syntax similar to that of python as the value for the `-r` option.
@@ -340,18 +350,24 @@ fastanime download -t <anime-title> -t <anime-title> -r '-5'
# Download specific episode range
# be sure to observe the range Syntax
fastanime download <anime-title> -r '<episodes-start>:<episodes-end>:<step>'
fastanime download -t <anime-title> -r '<episodes-start>:<episodes-end>:<step>'
fastanime download <anime-title> -r '<episodes-start>:<episodes-end>'
fastanime download -t <anime-title> -r '<episodes-start>:<episodes-end>'
fastanime download <anime-title> -r '<episodes-start>:'
fastanime download -t <anime-title> -r '<episodes-start>:'
fastanime download -t <anime-title> -r ':<episodes-end>'
# download specific episode
# remember python indexing starts at 0
fastanime download -t <anime-title> -r '<episode-1>:<episode>'
fastanime download <anime-title> -r ':<episodes-end>'
```
#### search subcommand
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:**
@@ -378,6 +394,57 @@ fastanime search -t <anime-title> -r '<start>:'
fastanime search -t <anime-title> -r ':<end>'
```
#### grab subcommand
Helper command to print streams to stdout so it can be used by non-python applications.
The format of the printed out data is json and can be either an array or object depending on how many anime titles have been specified in the command-line or through a subprocess.
> [!TIP]
> For python applications just use its python api, for even greater and easier control.
> So just add fastanime as one of your dependencies.
Uses a list slicing syntax similar to that of python as the value of the `-r` option.
**Syntax:**
```bash
# --- print anime info + episode streams ---
# multiple titles can be specified with the -t option
fastanime grab -t <anime-title> -t <anime-title>
# -- or --
# print all available episodes
fastanime grab -t <anime-title> -r ':'
# print the latest episode
fastanime grab -t <anime-title> -r '-1'
# print a specific episode range
# be sure to observe the range Syntax
fastanime grab -t <anime-title> -r '<start>:<stop>'
fastanime grab -t <anime-title> -r '<start>:<stop>:<step>'
fastanime grab -t <anime-title> -r '<start>:'
fastanime grab -t <anime-title> -r ':<end>'
# --- grab options ---
# print search results only
fastanime grab -t <anime-title> -r <range> --search-results-only
# print anime info only
fastanime grab -t <anime-title> -r <range> --anime-info-only
# print episode streams only
fastanime grab -t <anime-title> -r <range> --episode-streams-only
```
#### downloads subcommand
View and stream the anime you downloaded using MPV.
@@ -478,12 +545,12 @@ fastanime completions --bash
fastanime completions --zsh
```
## MPV specific commands
### MPV specific commands
The project now allows on the fly media controls directly from mpv. This means you can go to the next or previous episode without the window ever closing thus offering a seamless experience.
This is all powered with [python-mpv]() which enables writing mpv scripts with python just like how it would be done in lua.
### Added keybindings
#### Key Bindings
`<shift>+n` fetch the next episode
@@ -495,7 +562,9 @@ This is all powered with [python-mpv]() which enables writing mpv scripts with p
`<shit>+r` reload episode
### Added script messages
#### Script Messages
Commands issued in the MPV console.
Examples:
@@ -510,7 +579,7 @@ script-message select-server <server-name>
script-message select-quality <1080/720/480/360>
```
## configuration
## Configuration
The app includes sensible defaults but can be customized extensively. Configuration is stored in `.ini` format at `~/.config/FastAnime/config.ini` on arch linux; for the other operating systems you can check by running `fastanime config --path`.
@@ -521,6 +590,10 @@ continue_from_history = True # Auto continue from watch history
# which history to use [local/remote]
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)
server = top # Default server (options: dropbox, sharepoint, wetransfer.gogoanime, top, wixmp)
@@ -581,6 +654,28 @@ notification_duration=2
# Not implemented yet
```
## The python api
The project offers a python api that can be used in other python programs.
```python
from fastanime.AnimeProvider import AnimeProvider
# all output is typed, so will be easy to work with
# providers include [allanime, animepahe]
provider = AnimeProvider(provider="allanime")
# to search for anime
provider.search_for_anime()
# to get anime info
provider.get_anime()
# to get streams of an episode
provider.get_episode_streams()
```
## Contributing
We welcome your issues and feature requests. However, due to time constraints, we currently do not plan to add another provider.
@@ -589,7 +684,7 @@ If you wish to contribute directly, please first open an issue describing your p
## Receiving Support
For inquiries, join our [Discord Server](https://discord.gg/4NUTj5Pt).
For inquiries, join our [Discord Server](https://discord.gg/C4rhMA4mmK).
<p align="center">
<a href="https://discord.gg/C4rhMA4mmK">

View File

@@ -35,6 +35,9 @@ class YtDLPDownloader:
download_dir: str,
silent: bool,
vid_format: str = "best",
force_unknown_ext=False,
verbose=False,
headers={},
):
"""Helper function that downloads anime given url and path details
@@ -50,10 +53,12 @@ class YtDLPDownloader:
episode_title = sanitize_filename(episode_title)
ydl_opts = {
# Specify the output path and template
"http_headers": headers,
"outtmpl": f"{download_dir}/{anime_title}/{episode_title}.%(ext)s",
"silent": silent,
"verbose": False,
"verbose": verbose,
"format": vid_format,
"compat_opts": ("allow-unsafe-ext",) if force_unknown_ext else tuple(),
}
with yt_dlp.YoutubeDL(ydl_opts) as ydl:

View File

@@ -11,6 +11,13 @@ if TYPE_CHECKING:
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(
possible_user_requested_anime_title: str, anime: "AnilistBaseMediaDataSchema"
) -> float:

View File

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

View File

@@ -16,6 +16,7 @@ commands = {
"cache": "cache.cache",
"completions": "completions.completions",
"update": "update.update",
"grab": "grab.grab",
}
@@ -192,12 +193,12 @@ def run_cli(
elif log_file:
import logging
from ..constants import NOTIFIER_LOG_FILE_PATH
from ..constants import LOG_FILE_PATH
format = "%(asctime)s%(levelname)s: %(message)s"
logging.basicConfig(
level=logging.DEBUG,
filename=NOTIFIER_LOG_FILE_PATH,
filename=LOG_FILE_PATH,
format=format,
datefmt="[%d/%m/%Y@%H:%M:%S]",
filemode="w",

View File

@@ -26,7 +26,24 @@ def check_for_updates():
if request.status_code == 200:
release_json = request.json()
return (release_json["tag_name"] == __version__, release_json)
remote_tag = list(
map(int, release_json["tag_name"].replace("v", "").split("."))
)
local_tag = list(map(int, __version__.replace("v", "").split(".")))
if (
(remote_tag[0] > local_tag[0])
or (remote_tag[1] > local_tag[1] and remote_tag[0] == local_tag[0])
or (
remote_tag[2] > local_tag[2]
and remote_tag[0] == local_tag[0]
and remote_tag[1] == local_tag[1]
)
):
is_update = True
else:
is_update = False
return (is_update, release_json)
else:
print(request.text)
return (False, {})

View File

@@ -20,17 +20,35 @@ if TYPE_CHECKING:
required=True,
shell_complete=anime_titles_shell_complete,
multiple=True,
help="Specify which anime to download",
)
@click.option(
"--episode-range",
"-r",
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
def download(
config: "Config",
anime_titles: list,
episode_range,
force_unknown_ext,
silent,
verbose,
):
from rich import print
from rich.progress import Progress
@@ -61,9 +79,7 @@ def download(
print("Search results failed")
input("Enter to retry")
download(
config,
anime_title,
episode_range,
config, anime_title, episode_range, force_unknown_ext, silent, verbose
)
return
search_results = search_results["results"]
@@ -96,15 +112,14 @@ def download(
print("Sth went wring anime no found")
input("Enter to continue...")
download(
config,
anime_title,
episode_range,
config, anime_title, episode_range, force_unknown_ext, silent, verbose
)
return
episodes = sorted(
anime["availableEpisodesDetail"][config.translation_type], key=float
)
# where the magic happens
if episode_range:
if ":" in episode_range:
ep_range_tuple = episode_range.split(":")
@@ -131,6 +146,7 @@ def download(
else:
episodes_range = sorted(episodes, key=float)
# lets download em
for episode in episodes_range:
try:
episode = str(episode)
@@ -155,10 +171,12 @@ def download(
continue
stream_link = filter_by_quality(config.quality, server["links"])
if not stream_link:
print("Quality not found")
input("Enter to continue")
print("[yellow bold]WARNING:[/] No streams found")
time.sleep(1)
print("Continuing...")
continue
link = stream_link["link"]
provider_headers = server["headers"]
episode_title = server["episode_title"]
else:
with Progress() as progress:
@@ -180,9 +198,12 @@ def download(
config.quality, servers[server]["links"]
)
if not stream_link:
print("Quality not found")
print("[yellow bold]WARNING:[/] No streams found")
time.sleep(1)
print("Continuing...")
continue
link = stream_link["link"]
provider_headers = servers[server]["headers"]
episode_title = servers[server]["episode_title"]
print(f"[purple]Now Downloading:[/] {search_result} Episode {episode}")
@@ -192,12 +213,15 @@ def download(
anime["title"],
episode_title,
download_dir,
True,
silent,
config.format,
force_unknown_ext,
verbose,
headers=provider_headers,
)
except Exception as e:
print(e)
time.sleep(1)
print("Continuing")
print("Continuing...")
print("Done Downloading")
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 ...libs.fzf import fzf
from ...libs.rofi import Rofi
from ...Utility.utils import sort_by_episode_number
from ..utils.tools import exit_app
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):
print("Downloads directory specified does not exist")
return
anime_downloads = sorted(os.listdir(USER_VIDEOS_DIR))
anime_downloads = sorted(
os.listdir(USER_VIDEOS_DIR),
)
anime_downloads.append("Exit")
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):
import concurrent.futures
import random
import shutil
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)
if not os.path.isdir(anime_path):
continue
playlist = sorted(os.listdir(anime_path))
playlist = [
anime
for anime in sorted(
os.listdir(anime_path),
)
if "mp4" in anime
]
if playlist:
# 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[
executor.submit(
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)
if not os.path.isdir(anime_playlist_path):
return
anime_episodes = sorted(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:
# load the jobs
future_to_url = {}
@@ -223,7 +235,9 @@ def downloads(config: "Config", path: bool, view_episodes, ffmpegthumbnailer_see
print(anime_playlist_path, "is not dir")
exit_app(1)
return
episodes = sorted(os.listdir(anime_playlist_path))
episodes = sorted(
os.listdir(anime_playlist_path), key=sort_by_episode_number
)
downloaded_episodes = [*episodes, "Back"]
if config.use_fzf:
if not config.preview:
@@ -249,7 +263,12 @@ def downloads(config: "Config", path: bool, view_episodes, ffmpegthumbnailer_see
stream_anime()
return
episode_path = os.path.join(anime_playlist_path, episode_title)
run_mpv(episode_path)
if config.sync_play:
from ..utils.syncplay import SyncPlayer
SyncPlayer(episode_path)
else:
run_mpv(episode_path)
stream_episode(anime_playlist_path)
def stream_anime():
@@ -282,7 +301,12 @@ def downloads(config: "Config", path: bool, view_episodes, ffmpegthumbnailer_see
playlist,
)
else:
run_mpv(playlist)
if config.sync_play:
from ..utils.syncplay import SyncPlayer
SyncPlayer(playlist)
else:
run_mpv(playlist)
stream_anime()
stream_anime()

View File

@@ -0,0 +1,162 @@
from typing import TYPE_CHECKING
import click
from ..completion_functions import anime_titles_shell_complete
if TYPE_CHECKING:
from ..config import Config
@click.command(
help="Helper command to get streams for anime to use externally in a non-python application",
short_help="Print anime streams to standard out",
)
@click.option(
"--anime-titles",
"--anime_title",
"-t",
required=True,
shell_complete=anime_titles_shell_complete,
multiple=True,
help="Specify which anime to download",
)
@click.option(
"--episode-range",
"-r",
help="A range of episodes to download (start-end)",
)
@click.option(
"--search-results-only",
"-s",
help="print only the search results to stdout",
is_flag=True,
)
@click.option(
"--anime-info-only", "-i", help="print only selected anime title info", is_flag=True
)
@click.option(
"--episode-streams-only",
"-e",
help="print only selected anime episodes streams of given range",
is_flag=True,
)
@click.pass_obj
def grab(
config: "Config",
anime_titles: tuple,
episode_range,
search_results_only,
anime_info_only,
episode_streams_only,
):
import json
from logging import getLogger
from sys import exit
from thefuzz import fuzz
from ...AnimeProvider import AnimeProvider
logger = getLogger(__name__)
anime_provider = AnimeProvider(config.provider)
grabbed_animes = []
for anime_title in anime_titles:
# ---- search for anime ----
search_results = anime_provider.search_for_anime(
anime_title, translation_type=config.translation_type
)
if not search_results:
exit(1)
if search_results_only:
# grab only search results skipping all lines after this
grabbed_animes.append(search_results)
continue
search_results = search_results["results"]
search_results_ = {
search_result["title"]: search_result for search_result in search_results
}
search_result = max(
search_results_.keys(), key=lambda title: fuzz.ratio(title, anime_title)
)
# ---- fetch anime ----
anime = anime_provider.get_anime(search_results_[search_result]["id"])
if not anime:
exit(1)
episodes = sorted(
anime["availableEpisodesDetail"][config.translation_type], key=float
)
if anime_info_only:
# grab only the anime data skipping all lines after this
grabbed_animes.append(anime)
continue
# where the magic happens
if episode_range:
if ":" in episode_range:
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) :]
else:
episodes_range = sorted(episodes, key=float)
if not episode_streams_only:
grabbed_anime = dict(anime)
grabbed_anime["requested_episodes"] = episodes_range
grabbed_anime["translation_type"] = config.translation_type
grabbed_anime["episodes_streams"] = {}
else:
grabbed_anime = {}
# lets download em
for episode in episodes_range:
try:
if episode not in episodes:
continue
streams = anime_provider.get_episode_streams(
anime, episode, config.translation_type
)
if not streams:
continue
episode_streams = {server["server"]: server for server in streams}
if episode_streams_only:
grabbed_anime[episode] = episode_streams
else:
grabbed_anime["episodes_streams"][
episode
] = episode_streams # pyright:ignore
except Exception as e:
logger.error(e)
# grab the full data for single title and appen to final result or episode streams
grabbed_animes.append(grabbed_anime)
# print out the final result either {} or [] depending if more than one title os requested
if len(grabbed_animes) == 1:
print(json.dumps(grabbed_animes[0]))
else:
print(json.dumps(grabbed_animes))

View File

@@ -15,6 +15,7 @@ from ..completion_functions import anime_titles_shell_complete
required=True,
shell_complete=anime_titles_shell_complete,
multiple=True,
help="Specify which anime to download",
)
@click.option(
"--episode-range",
@@ -176,6 +177,7 @@ def search(config: Config, anime_titles: str, episode_range: str):
stream_anime()
return
link = stream_link["link"]
stream_headers = server["headers"]
episode_title = server["episode_title"]
else:
with Progress() as progress:
@@ -204,16 +206,17 @@ def search(config: Config, anime_titles: str, episode_range: str):
stream_anime()
return
link = stream_link["link"]
stream_headers = servers[server]["headers"]
episode_title = servers[server]["episode_title"]
print(f"[purple]Now Playing:[/] {search_result} Episode {episode}")
if config.sync_play:
from ..utils.syncplay import SyncPlayer
SyncPlayer(link, episode_title)
SyncPlayer(link, episode_title, headers=stream_headers)
else:
run_mpv(link, episode_title)
except Exception as e:
run_mpv(link, episode_title, headers=stream_headers)
except IndexError as e:
print(e)
input("Enter to continue")
stream_anime()

View File

@@ -9,6 +9,7 @@ def update(
from rich.console import Console
from rich.markdown import Markdown
from ... import __version__
from ..app_updater import check_for_updates, update_app
def _print_release(release_data):
@@ -26,11 +27,11 @@ def update(
is_update, github_release_data = check_for_updates()
if is_update:
print(
"You are running an older version of fastanime please update to get the latest features"
f"You are running an older version ({__version__}) of fastanime please update to get the latest features"
)
_print_release(github_release_data)
else:
print("You are running the latest version of fastanime")
print(f"You are running the latest version ({__version__}) of fastanime")
_print_release(github_release_data)
else:
success, github_release_data = update_app()

View File

@@ -296,6 +296,10 @@ error = {self.error}
# adding more options to it
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
# based on yt-dlp format and passed directly to it
# learn more by looking it up on their site

View File

@@ -117,7 +117,9 @@ def media_player_controls(
from ..utils.syncplay import SyncPlayer
stop_time, total_time = SyncPlayer(
current_episode_stream_link, selected_server["episode_title"]
current_episode_stream_link,
selected_server["episode_title"],
headers=selected_server["headers"],
)
elif config.use_mpv_mod:
from ..utils.player import player
@@ -128,6 +130,7 @@ def media_player_controls(
fastanime_runtime_state,
config,
selected_server["episode_title"],
headers=selected_server["headers"],
)
# TODO: implement custom aniskip
@@ -148,6 +151,7 @@ def media_player_controls(
selected_server["episode_title"],
start_time=start_time,
custom_args=custom_args,
headers=selected_server["headers"],
)
# either update the watch history to the next episode or current depending on progress
@@ -509,7 +513,9 @@ def provider_anime_episode_servers_menu(
from ..utils.syncplay import SyncPlayer
stop_time, total_time = SyncPlayer(
current_stream_link, selected_server["episode_title"]
current_stream_link,
selected_server["episode_title"],
headers=selected_server["headers"],
)
elif config.use_mpv_mod:
from ..utils.player import player
@@ -520,6 +526,7 @@ def provider_anime_episode_servers_menu(
fastanime_runtime_state,
config,
selected_server["episode_title"],
headers=selected_server["headers"],
)
# TODO: implement custom aniskip intergration
@@ -543,6 +550,7 @@ def provider_anime_episode_servers_menu(
selected_server["episode_title"],
start_time=start_time,
custom_args=custom_args,
headers=selected_server["headers"],
)
print("Finished at: ", stop_time)

View File

@@ -2,6 +2,8 @@ import re
import shutil
import subprocess
from fastanime.constants import S_PLATFORM
def stream_video(MPV, url, mpv_args, custom_args):
process = subprocess.Popen(
@@ -52,6 +54,7 @@ def run_mpv(
start_time: str = "0",
ytdl_format="",
custom_args=[],
headers={},
):
# Determine if mpv is available
MPV = shutil.which("mpv")
@@ -61,7 +64,7 @@ def run_mpv(
# Regex to check if the link is a YouTube URL
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
if re.match(youtube_regex, link):
# Android specific commands to launch mpv with a YouTube URL
@@ -100,6 +103,11 @@ def run_mpv(
else:
# General mpv command with custom arguments
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":
mpv_args.append(f"--start={start_time}")
if title:

View File

@@ -151,6 +151,7 @@ class MpvPlayer(object):
fastanime_runtime_state,
config: "Config",
title,
headers={},
):
self.anime_provider = anime_provider
self.fastanime_runtime_state = fastanime_runtime_state
@@ -174,6 +175,11 @@ class MpvPlayer(object):
# mpv_player.cache = "yes"
# mpv_player.cache_pause = "no"
mpv_player.title = title
mpv_headers = ""
if headers:
for header_name, header_value in headers.items():
mpv_headers += f"{header_name}:{header_value},"
mpv_player.http_header_fields = mpv_headers
mpv_player.play(stream_link)

View File

@@ -4,7 +4,7 @@ import subprocess
from .tools import exit_app
def SyncPlayer(url: str, anime_title, *args):
def SyncPlayer(url: str, anime_title=None, headers={}, *args):
# TODO: handle m3u8 multi quality streams
#
# check for SyncPlay
@@ -14,8 +14,29 @@ def SyncPlayer(url: str, anime_title, *args):
exit_app(1)
return "0", "0"
# start SyncPlayer
subprocess.run(
[SYNCPLAY_EXECUTABLE, url, "--", f"--force-media-title={anime_title}"]
)
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:
q = float(quality)
Q = float(stream_link["quality"])
# some providers have inaccurate eg qualities 718 instead of 720
if Q < q + 80 and Q > q - 80:
# some providers have inaccurate/weird/non-standard eg qualities 718 instead of 720
if Q <= q + 80 and Q >= q - 80:
return stream_link
else:
if stream_links and default:

View File

@@ -76,7 +76,7 @@ Path(USER_VIDEOS_DIR).mkdir(parents=True, exist_ok=True)
# useful paths
USER_DATA_PATH = os.path.join(APP_DATA_DIR, "user_data.json")
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")

View File

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

View File

@@ -10,7 +10,7 @@ from typing import TYPE_CHECKING
from requests.exceptions import Timeout
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 (
ALLANIME_API_ENDPOINT,
ALLANIME_BASE,
@@ -205,23 +205,45 @@ class AllAnimeAPI(AnimeProvider):
# 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??
if embed.get("sourceName", "") not in (
"Sak",
"Kir",
"S-mp4",
"Luf-mp4",
"Default",
# priorities based on death note
"Sak", # 7
"S-mp4", # 7.9
"Luf-mp4", # 7.7
"Default", # 8.5
"Yt-mp4", # 7.9
"Kir", # NA
# "Vid-mp4" # 4
# "Ok", # 3.5
# "Ss-Hls", # 5.5
# "Mp4", # 4
):
continue
url = embed.get("sourceUrl")
#
if not url:
continue
if url.startswith("--"):
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
parsed_url = decode_hex_string(url)
embed_url = f"https://{ALLANIME_BASE}{parsed_url.replace('clock', 'clock.json')}"
embed_url = (
f"https://{ALLANIME_BASE}{url.replace('clock', 'clock.json')}"
)
resp = self.session.get(
embed_url,
headers={
@@ -230,12 +252,14 @@ class AllAnimeAPI(AnimeProvider):
},
timeout=10,
)
if resp.status_code == 200:
match embed["sourceName"]:
case "Luf-mp4":
logger.debug("allanime:Found streams from gogoanime")
yield {
"server": "gogoanime",
"headers": {},
"episode_title": (
allanime_episode["notes"] or f'{anime["title"]}'
)
@@ -246,6 +270,7 @@ class AllAnimeAPI(AnimeProvider):
logger.debug("allanime:Found streams from wetransfer")
yield {
"server": "wetransfer",
"headers": {},
"episode_title": (
allanime_episode["notes"] or f'{anime["title"]}'
)
@@ -256,6 +281,7 @@ class AllAnimeAPI(AnimeProvider):
logger.debug("allanime:Found streams from sharepoint")
yield {
"server": "sharepoint",
"headers": {},
"episode_title": (
allanime_episode["notes"] or f'{anime["title"]}'
)
@@ -266,6 +292,7 @@ class AllAnimeAPI(AnimeProvider):
logger.debug("allanime:Found streams from dropbox")
yield {
"server": "dropbox",
"headers": {},
"episode_title": (
allanime_episode["notes"] or f'{anime["title"]}'
)
@@ -276,20 +303,22 @@ class AllAnimeAPI(AnimeProvider):
logger.debug("allanime:Found streams from wixmp")
yield {
"server": "wixmp",
"headers": {},
"episode_title": (
allanime_episode["notes"] or f'{anime["title"]}'
)
+ f"; Episode {episode_number}",
"links": give_random_quality(resp.json()["links"]),
} # pyright:ignore
except Timeout:
logger.error(
"Timeout has been exceeded this could mean allanime is down or you have lost internet connection"
)
return []
except Exception as e:
logger.error(f"FA(Allanime): {e}")
return []
except Exception as e:
logger.error(f"FA(Allanime): {e}")
return []
@@ -301,7 +330,7 @@ if __name__ == "__main__":
import subprocess
import sys
from InquirerPy import inquirer, validator
from InquirerPy import inquirer, validator # pyright:ignore
anime = input("Enter the anime name: ")
translation = input("Enter the translation type: ")

View File

@@ -1,15 +1,12 @@
import logging
import random
import re
import shutil
import subprocess
import time
from typing import TYPE_CHECKING
from yt_dlp.utils import (
extract_attributes,
get_element_by_id,
get_element_text_and_html_by_tag,
get_elements_html_by_class,
)
@@ -20,6 +17,7 @@ from .constants import (
REQUEST_HEADERS,
SERVER_HEADERS,
)
from .utils import process_animepahe_embed_page
if TYPE_CHECKING:
from ..types import Anime
@@ -27,6 +25,8 @@ if TYPE_CHECKING:
JUICY_STREAM_REGEX = re.compile(r"source='(.*)';")
logger = logging.getLogger(__name__)
KWIK_RE = re.compile(r"Player\|(.+?)'")
# TODO: hack this to completion
class AnimePaheApi(AnimeProvider):
@@ -153,101 +153,79 @@ class AnimePaheApi(AnimeProvider):
def get_episode_streams(
self, anime: "Anime", episode_number: str, translation_type, *args
):
# extract episode details from memory
episode = [
episode
for episode in self.anime["data"]
if float(episode["episode"]) == float(episode_number)
]
try:
# extract episode details from memory
episode = [
episode
for episode in self.anime["data"]
if float(episode["episode"]) == float(episode_number)
]
if not episode:
logger.error(f"AnimePahe(streams): episode {episode_number} doesn't exist")
return []
episode = episode[0]
anime_id = anime["id"]
# fetch the episode page
url = f"{ANIMEPAHE_BASE}/play/{anime_id}/{episode['session']}"
response = self.session.get(url, headers=REQUEST_HEADERS)
# get the element containing links to juicy streams
c = get_element_by_id("resolutionMenu", response.text)
resolutionMenuItems = get_elements_html_by_class("dropdown-item", c)
# convert the elements containing embed links to a neat dict containing:
# data-src
# data-audio
# data-resolution
res_dicts = [extract_attributes(item) for item in resolutionMenuItems]
# get the episode title
episode_title = (
episode["title"] or f"{anime['title']}; Episode {episode['episode']}"
)
# get all links
streams = {"server": "kwik", "links": [], "episode_title": episode_title}
for res_dict in res_dicts:
# get embed url
embed_url = res_dict["data-src"]
data_audio = "dub" if res_dict["data-audio"] == "eng" else "sub"
# filter streams by translation_type
if data_audio != translation_type:
continue
if not embed_url:
logger.warn(
"AnimePahe: embed url not found please report to the developers"
if not episode:
logger.error(
f"AnimePahe(streams): episode {episode_number} doesn't exist"
)
return []
# get embed page
embed_response = self.session.get(embed_url, headers=SERVER_HEADERS)
embed = embed_response.text
# search for the encoded js
encoded_js = None
for _ in range(7):
content, html = get_element_text_and_html_by_tag("script", embed)
if not content:
embed = embed.replace(html, "")
episode = episode[0]
anime_id = anime["id"]
# fetch the episode page
url = f"{ANIMEPAHE_BASE}/play/{anime_id}/{episode['session']}"
response = self.session.get(url, headers=REQUEST_HEADERS)
# get the element containing links to juicy streams
c = get_element_by_id("resolutionMenu", response.text)
resolutionMenuItems = get_elements_html_by_class("dropdown-item", c)
# convert the elements containing embed links to a neat dict containing:
# data-src
# data-audio
# data-resolution
res_dicts = [extract_attributes(item) for item in resolutionMenuItems]
# get the episode title
episode_title = (
f"{episode["title"] or anime['title']}; Episode {episode['episode']}"
)
# get all links
streams = {
"server": "kwik",
"links": [],
"episode_title": episode_title,
"headers": {},
}
for res_dict in res_dicts:
# get embed url
embed_url = res_dict["data-src"]
data_audio = "dub" if res_dict["data-audio"] == "eng" else "sub"
# filter streams by translation_type
if data_audio != translation_type:
continue
encoded_js = content
break
if not encoded_js:
logger.warn(
"AnimePahe: Encoded js not found please report to the developers"
if not embed_url:
logger.warn(
"AnimePahe: embed url not found please report to the developers"
)
return []
# get embed page
embed_response = self.session.get(embed_url, headers=SERVER_HEADERS)
embed_page = embed_response.text
decoded_js = process_animepahe_embed_page(embed_page)
if not decoded_js:
logger.error("Animepahe: failed to decode embed page")
return
juicy_stream = JUICY_STREAM_REGEX.search(decoded_js)
if not juicy_stream:
logger.error("Animepahe: failed to find juicy stream")
return
juicy_stream = juicy_stream.group(1)
# add the link
streams["links"].append(
{
"quality": res_dict["data-resolution"],
"translation_type": data_audio,
"link": juicy_stream,
}
)
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
streams["links"].append(
{
"quality": res_dict["data-resolution"],
"translation_type": data_audio,
"link": juicy_stream,
}
)
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
mp4: bool | None
priority: int | None
headers: dict | None
quality: Literal["360", "720", "1080", "unknown"]
translation_type: Literal["dub", "sub"]
class Server(TypedDict):
headers: dict
server: str
episode_title: str
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):
"""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]]
name = "pyright"
version = "1.1.375"
version = "1.1.376"
description = "Command line wrapper for pyright"
optional = false
python-versions = ">=3.7"
files = [
{file = "pyright-1.1.375-py3-none-any.whl", hash = "sha256:4c5e27eddeaee8b41cc3120736a1dda6ae120edf8523bb2446b6073a52f286e3"},
{file = "pyright-1.1.375.tar.gz", hash = "sha256:7765557b0d6782b2fadabff455da2014476404c9e9214f49977a4e49dec19a0f"},
{file = "pyright-1.1.376-py3-none-any.whl", hash = "sha256:0f2473b12c15c46b3207f0eec224c3cea2bdc07cd45dd4a037687cbbca0fbeff"},
{file = "pyright-1.1.376.tar.gz", hash = "sha256:bffd63b197cd0810395bb3245c06b01f95a85ddf6bfa0e5644ed69c841e954dd"},
]
[package.dependencies]
@@ -1157,13 +1157,13 @@ files = [
[[package]]
name = "tox"
version = "4.17.1"
version = "4.18.0"
description = "tox is a generic virtualenv management and test command line tool"
optional = false
python-versions = ">=3.8"
files = [
{file = "tox-4.17.1-py3-none-any.whl", hash = "sha256:2974597c0353577126ab014f52d1a399fb761049e165ff34427f84e8cfe6c990"},
{file = "tox-4.17.1.tar.gz", hash = "sha256:2c41565a571e34480bd401d668a4899806169a4633e972ac296c54406d2ded8a"},
{file = "tox-4.18.0-py3-none-any.whl", hash = "sha256:0a457400cf70615dc0627eb70d293e80cd95d8ce174bb40ac011011f0c03a249"},
{file = "tox-4.18.0.tar.gz", hash = "sha256:5dfa1cab9f146becd6e351333a82f9e0ade374451630ba65ee54584624c27b58"},
]
[package.dependencies]

View File

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