Compare commits

...

17 Commits

Author SHA1 Message Date
Benex254
77ffa27ed8 chore: bump version 2024-08-21 17:37:09 +03:00
Benex254
15f79b65c9 feat: aniwave?? 2024-08-21 17:18:30 +03:00
Benex254
33c3af0241 chore: remove print and input statements 2024-08-21 16:00:52 +03:00
Benex254
9badde62fb feat: improve providers 2024-08-21 15:58:01 +03:00
Benex254
4e401dca40 fix: logging issue 2024-08-21 14:53:30 +03:00
Benex254
25422b1b7d feat: improve aniwatch provider api 2024-08-21 14:52:56 +03:00
Benex254
e8463f13b4 chore: reconfigure pyright 2024-08-21 11:42:48 +03:00
Benex254
556f42e41f fix: clean option of download command 2024-08-21 11:41:55 +03:00
Benex254
b99a4f7efc chore: bump version 2024-08-19 23:44:05 +03:00
Benex254
f6f45cf322 docs: update readme 2024-08-19 23:43:50 +03:00
Benex254
ae6db1847a feat: improve download functionality 2024-08-19 23:43:34 +03:00
Benex254
20d04ea07b feat(utils): add m3u8 quality selector 2024-08-19 17:27:52 +03:00
Benex254
8f3834453c chore: bump version 2024-08-19 15:28:04 +03:00
Benex254
7ad8b8a0e3 fix: return values 2024-08-19 15:25:36 +03:00
Benex254
80b41f06da feat:add new ui command 2024-08-19 15:25:05 +03:00
Benex254
e79321ed50 chore: bump version 2024-08-19 13:05:03 +03:00
Benex254
f7b5898dfa fix: some stuff 2024-08-19 13:04:30 +03:00
24 changed files with 433 additions and 162 deletions

View File

@@ -362,6 +362,17 @@ fastanime download -t <anime-title> -r ':<episodes-end>'
# remember python indexing starts at 0
fastanime download -t <anime-title> -r '<episode-1>:<episode>'
# merge subtitles with ffmpeg to mkv format; aniwatch tends to give subs as separate files
# and dont prompt for anything
# eg existing file in destination instead remove
# and clean
# ie remove original files (sub file and vid file)
# only keep merged files
fastanime download -t <anime-title> --merge --clean --no-prompt
```
#### search subcommand

View File

@@ -37,12 +37,12 @@ class AnimeProvider:
self.provider = provider
self.dynamic = dynamic
self.retries = retries
self.lazyload_provider()
self.lazyload_provider(self.provider)
def lazyload_provider(self):
def lazyload_provider(self, provider):
"""updates the current provider being used"""
_, anime_provider_cls_name = anime_sources[self.provider].split(".", 1)
package = f"fastanime.libs.anime_provider.{self.provider}"
_, anime_provider_cls_name = anime_sources[provider].split(".", 1)
package = f"fastanime.libs.anime_provider.{provider}"
provider_api = importlib.import_module(".api", package)
anime_provider = getattr(provider_api, anime_provider_cls_name)
self.anime_provider = anime_provider()

View File

@@ -1,8 +1,14 @@
import logging
import os
import shutil
import subprocess
import tempfile
from queue import Queue
from threading import Thread
import yt_dlp
from rich import print
from rich.prompt import Confirm
from yt_dlp.utils import sanitize_filename
logger = logging.getLogger(__name__)
@@ -39,6 +45,9 @@ class YtDLPDownloader:
verbose=False,
headers={},
sub="",
merge=False,
clean=False,
prompt=True,
):
"""Helper function that downloads anime given url and path details
@@ -64,8 +73,82 @@ class YtDLPDownloader:
urls = [url]
if sub:
urls.append(sub)
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
ydl.download(urls)
vid_path = ""
sub_path = ""
for i, url in enumerate(urls):
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
info = ydl.extract_info(url, download=True)
if not info:
continue
if i == 0:
vid_path = info["requested_downloads"][0]["filepath"]
else:
sub_path = info["requested_downloads"][0]["filepath"]
if sub_path and vid_path and merge:
self.merge_subtitles(vid_path, sub_path, clean, prompt)
def merge_subtitles(self, video_path, sub_path, clean, prompt):
# Extract the directory and filename
video_dir = os.path.dirname(video_path)
video_name = os.path.basename(video_path)
video_name, _ = os.path.splitext(video_name)
video_name += ".mkv"
FFMPEG_EXECUTABLE = shutil.which("ffmpeg")
if not FFMPEG_EXECUTABLE:
print("[yellow bold]WARNING: [/]FFmpeg not found")
return
# Create a temporary directory
with tempfile.TemporaryDirectory() as temp_dir:
# Temporary output path in the temporary directory
temp_output_path = os.path.join(temp_dir, video_name)
# FFmpeg command to merge subtitles
command = [
FFMPEG_EXECUTABLE,
"-hide_banner",
"-i",
video_path,
"-i",
sub_path,
"-c",
"copy",
"-map",
"0",
"-map",
"1",
temp_output_path,
]
# Run the command
try:
subprocess.run(command, check=True)
# Move the file back to the original directory with the original name
final_output_path = os.path.join(video_dir, video_name)
if os.path.exists(final_output_path):
if not prompt or Confirm.ask(
f"File exists({final_output_path}) would you like to overwrite it",
default=True,
):
# move file to dest
os.remove(final_output_path)
shutil.move(temp_output_path, final_output_path)
else:
shutil.move(temp_output_path, final_output_path)
# clean up
if clean:
print("[cyan]Cleaning original files...[/]")
os.remove(video_path)
os.remove(sub_path)
print(
f"[green bold]Subtitles merged successfully.[/] Output file: {final_output_path}"
)
except subprocess.CalledProcessError as e:
print(f"[red bold]Error[/] during merging subtitles: {e}")
except Exception as e:
print(f"[red bold]An error[/] occurred: {e}")
# WARN: May remove this legacy functionality
def download_file(self, url: str, title, silent=True):

View File

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

View File

@@ -192,7 +192,7 @@ def run_cli(
FORMAT = "%(message)s"
logging.basicConfig(
level="debug", format=FORMAT, datefmt="[%X]", handlers=[RichHandler()]
level=logging.DEBUG, format=FORMAT, datefmt="[%X]", handlers=[RichHandler()]
)
logger = logging.getLogger(__name__)
logger.info("logging has been initialized")

View File

@@ -1,4 +1,3 @@
import time
from typing import TYPE_CHECKING
import click
@@ -41,6 +40,27 @@ if TYPE_CHECKING:
default=True,
)
@click.option("--verbose", "-v", is_flag=True, help="Download verbosely (everywhere)")
@click.option(
"--merge", "-m", is_flag=True, help="Merge the subfile with video using ffmpeg"
)
@click.option(
"--clean",
"-c",
is_flag=True,
help="After merging delete the original files",
)
@click.option(
"--wait-time",
"-w",
type=int,
help="The amount of time to wait after downloading is complete before the screen is completely cleared",
default=10,
)
@click.option(
"--prompt/--no-prompt",
help="Dont prompt for anything instead just do the best thing",
default=True,
)
@click.pass_obj
def download(
config: "Config",
@@ -49,7 +69,13 @@ def download(
force_unknown_ext,
silent,
verbose,
merge,
clean,
wait_time,
prompt,
):
import time
from rich import print
from rich.progress import Progress
from thefuzz import fuzz
@@ -83,7 +109,16 @@ def download(
print("Search results failed")
input("Enter to retry")
download(
config, anime_title, episode_range, force_unknown_ext, silent, verbose
config,
anime_title,
episode_range,
force_unknown_ext,
silent,
verbose,
merge,
clean,
wait_time,
prompt,
)
return
search_results = search_results["results"]
@@ -119,7 +154,16 @@ def download(
print("Sth went wring anime no found")
input("Enter to continue...")
download(
config, anime_title, episode_range, force_unknown_ext, silent, verbose
config,
anime_title,
episode_range,
force_unknown_ext,
silent,
verbose,
merge,
clean,
wait_time,
prompt,
)
return
@@ -223,7 +267,7 @@ def download(
)
downloader._download_file(
link,
anime["title"],
search_result,
episode_title,
download_dir,
silent,
@@ -232,10 +276,14 @@ def download(
verbose,
headers=provider_headers,
sub=subtitles[0]["url"] if subtitles else "",
merge=merge,
clean=clean,
prompt=prompt,
)
except Exception as e:
print(e)
time.sleep(1)
print("Continuing...")
print("Done Downloading")
time.sleep(wait_time)
exit_app()

View File

@@ -502,6 +502,8 @@ def provider_anime_episode_servers_menu(
)
if start_time != "0" and episode_in_history == current_episode_number:
print("[green]Continuing from:[/] ", start_time)
else:
start_time = "0"
custom_args = []
if config.skip:
if args := aniskip(
@@ -680,14 +682,14 @@ def provider_anime_episodes_menu(
if current_episode_number == "Back":
media_actions_menu(config, fastanime_runtime_state)
return
# try to get the start time and if not found default to "0"
start_time = user_watch_history.get(str(anime_id_anilist), {}).get(
"start_time", "0"
)
config.update_watch_history(
anime_id_anilist, current_episode_number, start_time=start_time
)
#
# # try to get the start time and if not found default to "0"
# start_time = user_watch_history.get(str(anime_id_anilist), {}).get(
# "start_time", "0"
# )
# config.update_watch_history(
# anime_id_anilist, current_episode_number, start_time=start_time
# )
# update runtime data
fastanime_runtime_state.provider_available_episodes = total_episodes
@@ -1008,6 +1010,42 @@ def media_actions_menu(
media_actions_menu(config, fastanime_runtime_state)
def _change_player(
config: "Config", fastanime_runtime_state: "FastAnimeRuntimeState"
):
"""Change the translation type to use
Args:
config: [TODO:description]
fastanime_runtime_state: [TODO:description]
"""
# prompt for new translation type
options = ["syncplay", "mpv-mod", "default"]
if config.use_fzf:
player = fzf.run(
options,
prompt="Select Player:",
)
elif config.use_rofi:
player = Rofi.run(options, "Select Player: ")
else:
player = fuzzy_inquirer(
options,
"Select Player",
)
# update internal config
if player == "syncplay":
config.sync_play = True
config.use_mpv_mod = False
else:
config.sync_play = False
if player == "mpv-mod":
config.use_mpv_mod = True
else:
config.use_mpv_mod = False
media_actions_menu(config, fastanime_runtime_state)
def _view_info(config: "Config", fastanime_runtime_state: "FastAnimeRuntimeState"):
"""helper function to view info of an anime from terminal
@@ -1138,7 +1176,7 @@ def media_actions_menu(
config.provider = provider
config.anime_provider.provider = provider
config.anime_provider.lazyload_provider()
config.anime_provider.lazyload_provider(provider)
media_actions_menu(config, fastanime_runtime_state)
@@ -1176,6 +1214,7 @@ def media_actions_menu(
f"{'📖 ' if icons else ''}View Info": _view_info,
f"{'🎧 ' if icons else ''}Change Translation Type": _change_translation_type,
f"{'💽 ' if icons else ''}Change Provider": _change_provider,
f"{'💽 ' if icons else ''}Change Player": _change_player,
f"{'🔘 ' if icons else ''}Toggle auto select anime": _toggle_auto_select, # WARN: problematic if you choose an anime that doesnt match id
f"{'💠 ' if icons else ''}Toggle auto next episode": _toggle_auto_next,
f"{'🔘 ' if icons else ''}Toggle continue from history": _toggle_continue_from_history,
@@ -1428,6 +1467,9 @@ def fastanime_main_menu(
else:
config.load_config()
config.anime_provider.provider = config.provider
config.anime_provider.lazyload_provider(config.provider)
fastanime_main_menu(config, fastanime_runtime_state)
icons = config.icons

View File

@@ -71,7 +71,7 @@ class MpvPlayer(object):
elif type == "reload":
if current_episode_number not in total_episodes:
self.mpv_player.show_text("Episode not available")
return None, None
return
self.mpv_player.show_text("Replaying Episode...")
elif type == "custom":
if not ep_no or ep_no not in total_episodes:
@@ -79,7 +79,7 @@ class MpvPlayer(object):
self.mpv_player.show_text(
f"Acceptable episodes are: {total_episodes}",
)
return None, None
return
self.mpv_player.show_text(f"Fetching episode {ep_no}")
current_episode_number = ep_no
@@ -114,14 +114,14 @@ class MpvPlayer(object):
)
if not episode_streams:
self.mpv_player.show_text("No streams were found")
return None, None
return
# always select the first
if server == "top":
selected_server = next(episode_streams, None)
if not selected_server:
self.mpv_player.show_text("Sth went wrong when loading the episode")
return None, None
return
else:
episode_streams_dict = {
episode_stream["server"]: episode_stream
@@ -132,14 +132,14 @@ class MpvPlayer(object):
self.mpv_player.show_text(
f"Invalid server!!; servers available are: {episode_streams_dict.keys()}",
)
return None, None
return
self.current_media_title = selected_server["episode_title"]
links = selected_server["links"]
stream_link_ = filter_by_quality(quality, links)
if not stream_link_:
self.mpv_player.show_text("Quality not found")
return None, None
return
self.mpv_player._set_property("start", "0")
stream_link = stream_link_["link"]
fastanime_runtime_state.provider_current_episode_stream_link = stream_link
@@ -200,6 +200,8 @@ class MpvPlayer(object):
self.subs = []
except mpv.ShutdownError:
pass
except Exception:
pass
@mpv_player.property_observer("time-pos")
def handle_time_start_update(*args):

View File

@@ -19,6 +19,27 @@ BG_GREEN = "\033[48;2;120;233;12;m"
GREEN = "\033[38;2;45;24;45;m"
def get_requested_quality_or_default_to_first(url, quality):
import yt_dlp
with yt_dlp.YoutubeDL({"quiet": True, "silent": True, "no_warnings": True}) as ydl:
m3u8_info = ydl.extract_info(url, False)
if not m3u8_info:
return
m3u8_formats = m3u8_info["formats"]
quality = int(quality)
quality_u = quality - 80
quality_l = quality + 80
for m3u8_format in m3u8_formats:
if m3u8_format["height"] == quality or (
m3u8_format["height"] < quality_u and m3u8_format["height"] > quality_l
):
return m3u8_format["url"]
else:
return m3u8_formats[0]["url"]
def move_preferred_subtitle_lang_to_top(sub_list, lang_str):
"""Moves the dictionary with the given ID to the front of the list.

View File

@@ -1,10 +1,11 @@
from .allanime import SERVERS_AVAILABLE as ALLANIME_SERVERS
from .animepahe import SERVERS_AVAILABLE as ANIMEPAHESERVERS
from .aniwatch import SERVERS_AVAILABLE as ANIWATCHSERVERS
from .allanime.constants import SERVERS_AVAILABLE as ALLANIME_SERVERS
from .animepahe.constants import SERVERS_AVAILABLE as ANIMEPAHESERVERS
from .aniwatch.constants import SERVERS_AVAILABLE as ANIWATCHSERVERS
anime_sources = {
"allanime": "api.AllAnimeAPI",
"animepahe": "api.AnimePaheApi",
"aniwatch": "api.AniWatchApi",
"aniwave": "api.AniWaveApi",
}
SERVERS_AVAILABLE = [*ALLANIME_SERVERS, *ANIMEPAHESERVERS, *ANIWATCHSERVERS]

View File

@@ -1 +0,0 @@
SERVERS_AVAILABLE = ["sharepoint", "dropbox", "gogoanime", "weTransfer", "wixmp", "Yt"]

View File

@@ -11,12 +11,7 @@ from requests.exceptions import Timeout
from ...anime_provider.base_provider import AnimeProvider
from ..utils import give_random_quality, one_digit_symmetric_xor
from .constants import (
ALLANIME_API_ENDPOINT,
ALLANIME_BASE,
ALLANIME_REFERER,
USER_AGENT,
)
from .constants import ALLANIME_API_ENDPOINT, ALLANIME_BASE, ALLANIME_REFERER
from .gql_queries import ALLANIME_EPISODES_GQL, ALLANIME_SEARCH_GQL, ALLANIME_SHOW_GQL
if TYPE_CHECKING:
@@ -36,6 +31,9 @@ class AllAnimeAPI(AnimeProvider):
"""
api_endpoint = ALLANIME_API_ENDPOINT
HEADERS = {
"Referer": ALLANIME_REFERER,
}
def _fetch_gql(self, query: str, variables: dict):
"""main abstraction over all requests to the allanime api
@@ -54,7 +52,6 @@ class AllAnimeAPI(AnimeProvider):
"variables": json.dumps(variables),
"query": query,
},
headers={"Referer": ALLANIME_REFERER, "User-Agent": USER_AGENT},
timeout=10,
)
if response.status_code == 200:
@@ -247,10 +244,6 @@ class AllAnimeAPI(AnimeProvider):
)
resp = self.session.get(
embed_url,
headers={
"Referer": ALLANIME_REFERER,
"User-Agent": USER_AGENT,
},
timeout=10,
)
@@ -328,85 +321,3 @@ class AllAnimeAPI(AnimeProvider):
except Exception as e:
logger.error(f"FA(Allanime): {e}")
return []
if __name__ == "__main__":
anime_provider = AllAnimeAPI()
# lets see if it works :)
import subprocess
import sys
from InquirerPy import inquirer, validator # pyright:ignore
anime = input("Enter the anime name: ")
translation = input("Enter the translation type: ")
search_results = anime_provider.search_for_anime(
anime, translation_type=translation.strip()
)
if not search_results:
raise Exception("No results found")
search_results = search_results["results"]
options = {show["title"]: show for show in search_results}
anime = inquirer.fuzzy(
"Enter the anime title",
list(options.keys()),
validate=validator.EmptyInputValidator(),
).execute()
if anime is None:
print("No anime was selected")
sys.exit(1)
anime_result = options[anime]
anime_data = anime_provider.get_anime(anime_result["id"])
if not anime_data:
raise Exception("Anime not found")
availableEpisodesDetail = anime_data["availableEpisodesDetail"]
if not availableEpisodesDetail.get(translation.strip()):
raise Exception("No episodes found")
stream_link = True
while stream_link != "quit":
print("select episode")
episode = inquirer.fuzzy(
"Choose an episode",
availableEpisodesDetail[translation.strip()],
validate=validator.EmptyInputValidator(),
).execute()
if episode is None:
print("No episode was selected")
sys.exit(1)
if not anime_data:
print("Sth went wrong")
break
episode_streams_ = anime_provider.get_episode_streams(
anime_data, # pyright: ignore
episode,
translation.strip(),
)
if episode_streams_ is None:
raise Exception("Episode not found")
episode_streams = list(episode_streams_)
stream_links = []
for server in episode_streams:
stream_links.extend([link["link"] for link in server["links"]])
stream_links.append("back")
stream_link = inquirer.fuzzy(
"Choose a link to stream",
stream_links,
validate=validator.EmptyInputValidator(),
).execute()
if stream_link == "quit":
print("Have a nice day")
sys.exit()
if not stream_link:
raise Exception("No stream was selected")
title = episode_streams[0].get(
"episode_title", "%s: Episode %s" % (anime_data["title"], episode)
)
subprocess.run(["mpv", f"--title={title}", stream_link])

View File

@@ -1,6 +1,4 @@
from yt_dlp.utils.networking import random_user_agent
SERVERS_AVAILABLE = ["sharepoint", "dropbox", "gogoanime", "weTransfer", "wixmp", "Yt"]
ALLANIME_BASE = "allanime.day"
ALLANIME_REFERER = "https://allanime.to/"
ALLANIME_API_ENDPOINT = "https://api.{}/api/".format(ALLANIME_BASE)
USER_AGENT = random_user_agent()

View File

@@ -1 +0,0 @@
SERVERS_AVAILABLE = ["kwik"]

View File

@@ -32,12 +32,14 @@ KWIK_RE = re.compile(r"Player\|(.+?)'")
class AnimePaheApi(AnimeProvider):
search_page: "AnimePaheSearchPage"
anime: "AnimePaheAnimePage"
HEADERS = REQUEST_HEADERS
def search_for_anime(self, user_query: str, *args):
try:
url = f"{ANIMEPAHE_ENDPOINT}m=search&q={user_query}"
headers = {**REQUEST_HEADERS}
response = self.session.get(url, headers=headers)
response = self.session.get(
url,
)
if not response.status_code == 200:
return
data: "AnimePaheSearchPage" = response.json()
@@ -85,7 +87,9 @@ class AnimePaheApi(AnimeProvider):
url,
page,
):
response = self.session.get(url, headers=REQUEST_HEADERS)
response = self.session.get(
url,
)
if response.status_code == 200:
if not data:
data.update(response.json())
@@ -171,7 +175,7 @@ class AnimePaheApi(AnimeProvider):
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)
response = self.session.get(url)
# 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)
@@ -207,7 +211,11 @@ class AnimePaheApi(AnimeProvider):
)
return []
# get embed page
embed_response = self.session.get(embed_url, headers=SERVER_HEADERS)
embed_response = self.session.get(
embed_url, headers={"User-Agent": self.USER_AGENT, **SERVER_HEADERS}
)
if not response.status_code == 200:
continue
embed_page = embed_response.text
decoded_js = process_animepahe_embed_page(embed_page)

View File

@@ -1,18 +1,14 @@
from yt_dlp.utils.networking import random_user_agent
USER_AGENT = random_user_agent()
ANIMEPAHE = "animepahe.ru"
ANIMEPAHE_BASE = f"https://{ANIMEPAHE}"
ANIMEPAHE_ENDPOINT = f"{ANIMEPAHE_BASE}/api?"
SERVERS_AVAILABLE = ["kwik"]
REQUEST_HEADERS = {
"Cookie": "__ddgid_=VvX0ebHrH2DsFZo4; __ddgmark_=3savRpSVFhvZcn5x; __ddg2_=buBJ3c4pNBYKFZNp; __ddg1_=rbVADKr9URtt55zoIGFa; SERVERID=janna; XSRF-TOKEN=eyJpdiI6IjV5bFNtd0phUHgvWGJxc25wL0VJSUE9PSIsInZhbHVlIjoicEJTZktlR2hxR2JZTWhnL0JzazlvZU5TQTR2bjBWZ2dDb0RwUXVUUWNSclhQWUhLRStYSmJmWmUxWkpiYkFRYU12RjFWejlSWHorME1wZG5qQ1U0TnFlNnBFR2laQjN1MjdyNjc5TjVPdXdJb2o5VkU1bEduRW9pRHNDTHh6Sy8iLCJtYWMiOiI0OTc0ZmNjY2UwMGJkOWY2MWNkM2NlMjk2ZGMyZGJmMWE0NTdjZTdkNGI2Y2IwNTIzZmFiZWU5ZTE2OTk0YmU4IiwidGFnIjoiIn0%3D; laravel_session=eyJpdiI6ImxvdlpqREFnTjdaeFJubUlXQWlJVWc9PSIsInZhbHVlIjoiQnE4R3VHdjZ4M1NDdEVWM1ZqMUxtNnVERnJCcmtCUHZKNzRPR2RFbzNFcStTL29xdnVTbWhsNVRBUXEybVZWNU1UYVlTazFqYlN5UjJva1k4czNGaXBTbkJJK01oTUd3VHRYVHBoc3dGUWxHYnFlS2NJVVNFbTFqMVBWdFpuVUgiLCJtYWMiOiI1NDdjZTVkYmNhNjUwZTMxZmRlZmVmMmRlMGNiYjAwYjlmYjFjY2U0MDc1YTQzZThiMTIxMjJlYTg1NTA4YjBmIiwidGFnIjoiIn0%3D; latest=5592 ",
"Host": ANIMEPAHE,
"User-Agent": USER_AGENT,
"Accept": "application , text/javascript, */*; q=0.01",
"Accept-Encoding": "gzip, deflate, br, zstd",
"Accept-Encoding": "Utf-8",
"Referer": ANIMEPAHE_BASE,
"X-Requested-With": "XMLHttpRequest",
"DNT": "1",
"Connection": "keep-alive",
"Sec-Fetch-Dest": "empty",
@@ -21,19 +17,17 @@ REQUEST_HEADERS = {
"TE": "trailers",
}
SERVER_HEADERS = {
"User-Agent": USER_AGENT,
"Host": "kwik.si",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/png,image/svg+xml,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.5",
"Accept-Encoding": "gzip, deflate, br, zstd",
"Accept-Encoding": "Utf-8",
"DNT": "1",
"Alt-Used": "kwik.si",
"Connection": "keep-alive",
"Referer": ANIMEPAHE_BASE,
"Cookie": "kwik_session=eyJpdiI6IlZ5UDd0c0lKTDB1NXlhTHZPeWxFc2c9PSIsInZhbHVlIjoieDJZbGhZUG1QZDNaeWtqR3lwWFNnREdhaHBxNVZRMWNDOHVucGpiMHRJOVdhVmpBc3lpTko1VExRMTFWcE1yUVJtVitoTWdOOU5ObTQ0Q0dHU0MzZU0yRUVvNmtWcUdmY3R4UWx4YklJTmpUL0ZodjhtVEpjWU96cEZoUUhUbVYiLCJtYWMiOiI2OGY2YThkOGU0MTgwOThmYzcyZThmNzFlZjlhMzQzMDgwNjlmMTc4NTIzMzc2YjE3YjNmMWQyNTk4NzczMmZiIiwidGFnIjoiIn0%3D; srv=s0; cf_clearance=QMoZtUpZrX0Mh4XJiFmFSSmoWndISPne5FcsGmKKvTQ-1723297585-1.0.1.1-6tVUnP.aef9XeNj0CnN.19D1el_r53t.lhqddX.J88gohH9UnsPWKeJ4yT0pTbcaGRbPuXTLOS.U72.wdy.gMg",
"Referer": "https://animepahe.ru/",
"Upgrade-Insecure-Requests": "1",
"Sec-Fetch-Dest": "iframe",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "cross-site",
"Sec-Fetch-User": "?1",
"Priority": "u=4",
"TE": "trailers",
}

View File

@@ -1 +0,0 @@
SERVERS_AVAILABLE = ["HD1", "HD2", "StreamSB", "StreamTape"]

View File

@@ -1,39 +1,102 @@
import logging
import re
from html.parser import HTMLParser
from itertools import cycle
from urllib.parse import quote_plus
from yt_dlp.utils import (
clean_html,
extract_attributes,
get_element_by_class,
get_element_html_by_class,
get_elements_by_class,
get_elements_html_by_class,
)
from ..base_provider import AnimeProvider
from ..common import fetch_anime_info_from_bal
from ..mini_anilist import search_for_anime_with_anilist
from ..utils import give_random_quality
from . import SERVERS_AVAILABLE
from .constants import SERVERS_AVAILABLE
from .types import AniWatchStream
logger = logging.getLogger(__name__)
LINK_TO_STREAMS_REGEX = re.compile(r".*://(.*)/embed-(2|4|6)/e-([0-9])/(.*)\?.*")
IMAGE_HTML_ELEMENT_REGEX = re.compile(r"<img.*?>")
class ParseAnchorAndImgTag(HTMLParser):
def __init__(self):
super().__init__()
self.img_tag = None
self.a_tag = None
def handle_starttag(self, tag, attrs):
if tag == "img":
self.img_tag = {attr[0]: attr[1] for attr in attrs}
if tag == "a":
self.a_tag = {attr[0]: attr[1] for attr in attrs}
class AniWatchApi(AnimeProvider):
# HEADERS = {"Referer": "https://hianime.to/home"}
def search_for_anime(self, anime_title: str, *args):
try:
return search_for_anime_with_anilist(anime_title)
query = quote_plus(anime_title)
url = f"https://hianime.to/search?keyword={query}"
response = self.session.get(url)
if response.status_code != 200:
return
search_page = response.text
search_results_html_items = get_elements_by_class("flw-item", search_page)
results = []
for search_results_html_item in search_results_html_items:
film_poster_html = get_element_by_class(
"film-poster", search_results_html_item
)
if not film_poster_html:
continue
# get availableEpisodes
episodes_html = get_element_html_by_class("tick-sub", film_poster_html)
episodes = clean_html(episodes_html) or 12
# get anime id and poster image url
parser = ParseAnchorAndImgTag()
parser.feed(film_poster_html)
image_data = parser.img_tag
anime_link_data = parser.a_tag
if not image_data or not anime_link_data:
continue
episodes = int(episodes)
# finally!!
image_link = image_data["data-src"]
anime_id = anime_link_data["data-id"]
title = anime_link_data["title"]
results.append(
{
"availableEpisodes": list(range(1, episodes)),
"id": anime_id,
"title": title,
"poster": image_link,
}
)
self.search_results = results
return {"pageInfo": {}, "results": results}
except Exception as e:
logger.error(e)
def get_anime(self, anilist_id, *args):
def get_anime(self, aniwatch_id, *args):
try:
bal_results = fetch_anime_info_from_bal(anilist_id)
if not bal_results:
return
ZORO = bal_results["Sites"]["Zoro"]
aniwatch_id = list(ZORO.keys())[0]
anime_result = {}
for anime in self.search_results:
if anime["id"] == aniwatch_id:
anime_result = anime
break
anime_url = f"https://hianime.to/ajax/v2/episode/list/{aniwatch_id}"
response = self.session.get(anime_url, timeout=10)
if response.status_code == 200:
@@ -58,7 +121,7 @@ class AniWatchApi(AnimeProvider):
(episode["title"] or "").replace(
f"Episode {episode['data-number']}", ""
)
or ZORO[aniwatch_id]["title"]
or anime_result["title"]
)
+ f"; Episode {episode['data-number']}",
"episode": episode["data-number"],
@@ -72,8 +135,8 @@ class AniWatchApi(AnimeProvider):
"sub": episodes,
"raw": episodes,
},
"poster": ZORO[aniwatch_id]["image"],
"title": ZORO[aniwatch_id]["title"],
"poster": anime_result["poster"],
"title": anime_result["title"],
"episodes_info": self.episodes_info,
}
except Exception as e:

View File

@@ -0,0 +1 @@
SERVERS_AVAILABLE = ["HD1", "HD2", "StreamSB", "StreamTape"]

View File

@@ -0,0 +1,65 @@
from html.parser import HTMLParser
from yt_dlp.utils import clean_html, get_element_by_class, get_elements_by_class
from ..base_provider import AnimeProvider
from .constants import ANIWAVE_BASE, SEARCH_HEADERS
class ParseAnchorAndImgTag(HTMLParser):
def __init__(self):
super().__init__()
self.img_tag = None
self.a_tag = None
def handle_starttag(self, tag, attrs):
if tag == "img":
self.img_tag = {attr[0]: attr[1] for attr in attrs}
if tag == "a":
self.a_tag = {attr[0]: attr[1] for attr in attrs}
class AniWaveApi(AnimeProvider):
def search_for_anime(self, anime_title, *args):
self.session.headers.update(SEARCH_HEADERS)
search_url = f"{ANIWAVE_BASE}/filter"
params = {"keyword": anime_title}
res = self.session.get(search_url, params=params)
search_page = res.text
search_results_html_list = get_elements_by_class("item", search_page)
results = []
for result_html in search_results_html_list:
aniposter_html = get_element_by_class("poster", result_html)
episode_html = get_element_by_class("sub", aniposter_html)
episodes = clean_html(episode_html) or 12
if not aniposter_html:
return
parser = ParseAnchorAndImgTag()
parser.feed(aniposter_html)
image_data = parser.img_tag
anime_link_data = parser.a_tag
if not image_data or not anime_link_data:
continue
episodes = int(episodes)
# finally!!
image_link = image_data["src"]
title = image_data["alt"]
anime_id = anime_link_data["href"]
results.append(
{
"availableEpisodes": list(range(1, episodes)),
"id": anime_id,
"title": title,
"poster": image_link,
}
)
self.search_results = results
return {"pageInfo": {}, "results": results}
def get_anime(self, anime_id, *args):
anime_page_url = f"{ANIWAVE_BASE}{anime_id}"
self.session.get(anime_page_url)
# TODO: to be continued; mostly js so very difficult

View File

@@ -0,0 +1,20 @@
ANIWAVE_BASE = "https://aniwave.to"
SEARCH_HEADERS = {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/png,image/svg+xml,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.5",
# 'Accept-Encoding': 'Utf-8',
"Referer": "https://aniwave.to/filter",
"DNT": "1",
"Upgrade-Insecure-Requests": "1",
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "same-origin",
"Sec-Fetch-User": "?1",
"Connection": "keep-alive",
"Alt-Used": "aniwave.to",
# 'Cookie': '__pf=1; usertype=guest; session=BElk9DJdO3sFdDmLiGxuNiM9eGYO1TjktGsmdwjV',
"Priority": "u=0, i",
# Requests doesn't support trailers
# 'TE': 'trailers',
}

View File

@@ -1,8 +1,13 @@
import requests
from yt_dlp.utils.networking import random_user_agent
class AnimeProvider:
session: requests.Session
USER_AGENT = random_user_agent()
HEADERS = {}
def __init__(self) -> None:
self.session = requests.session()
self.session.headers.update({"User-Agent": self.USER_AGENT, **self.HEADERS})

View File

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

View File

@@ -1,4 +1,5 @@
{
"typeCheckingMode": "standard",
"reportPrivateImportUsage": false
"venvPath": ".",
"venv": ".venv",
"pythonVersion": "3.10"
}