mirror of
https://github.com/Benexl/FastAnime.git
synced 2025-12-10 23:00:41 -08:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be7f464073 | ||
|
|
c7f8f168f5 | ||
|
|
ba59fbdcb0 | ||
|
|
9f54fa4998 | ||
|
|
3c9688b32c | ||
|
|
1f046447bb |
25
README.md
25
README.md
@@ -1,6 +1,6 @@
|
||||
# **FastAnime**
|
||||
|
||||
 
|
||||
 
|
||||

|
||||

|
||||

|
||||
@@ -68,7 +68,7 @@ Heavily inspired by [animdl](https://github.com/justfoolingaround/animdl), [jerr
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> This project currently scrapes allanime, aniwatch and animepahe. The site is in the public domain and can be accessed by any one with a browser.
|
||||
> This project currently scrapes allanime, hianime and animepahe, nyaa. The site is in the public domain and can be accessed by any one with a browser.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -182,6 +182,7 @@ The only required external dependency, unless you won't be streaming, is [MPV](h
|
||||
|
||||
**Other external dependencies that will just make your experience better:**
|
||||
|
||||
- [webtorrent-cli](https://github.com/webtorrent/webtorrent-cli) used when the provider is nyaa
|
||||
- [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
|
||||
@@ -196,7 +197,7 @@ The only required external dependency, unless you won't be streaming, is [MPV](h
|
||||
## Usage
|
||||
|
||||
The project offers a featureful command-line interface and MPV interface through the use of python-mpv.
|
||||
The project also offers subs in different languages thanks to aniwatch provider.
|
||||
The project also offers subs in different languages thanks to hianime provider.
|
||||
|
||||
### The Commandline interface :fire:
|
||||
|
||||
@@ -240,7 +241,7 @@ Available options for the fastanime include:
|
||||
- `--default` use the default ui
|
||||
- `--preview` show a preview when using fzf
|
||||
- `--no-preview` dont show a preview when using fzf
|
||||
- `--format <yt-dlp format string>` or `-f <yt-dlp format string>` set the format of anime downloaded and streamed based on [yt-dlp format](https://github.com/yt-dlp/yt-dlp#format-selection). Works when `--server gogoanime` or on providers that provide multi quality streams eg aniwatch
|
||||
- `--format <yt-dlp format string>` or `-f <yt-dlp format string>` set the format of anime downloaded and streamed based on [yt-dlp format](https://github.com/yt-dlp/yt-dlp#format-selection). Works when `--server gogoanime` or on providers that provide multi quality streams eg hianime
|
||||
- `--icons/--no-icons` toggle the visibility of the icons
|
||||
- `--skip/--no-skip` whether to skip the opening and ending theme songs.
|
||||
- `--rofi` use rofi for the ui
|
||||
@@ -251,9 +252,9 @@ Available options for the fastanime 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/animepahe>` anime site of choice to scrape from
|
||||
- `--provider <allanime/animepahe/hianime/nyaa>` anime site of choice to scrape from
|
||||
- `--sync-play` or `-sp` use syncplay for streaming anime so you can watch with your friends
|
||||
- `--sub-lang <en/or any other common shortform for country>` regex is used to determine the appropriate. Only works when provider is aniwatch.
|
||||
- `--sub-lang <en/or any other common shortform for country>` regex is used to determine the appropriate. Only works when provider is hianime.
|
||||
- `--normalize-titles/--no-normalize-titles` whether to normalize provider titles
|
||||
- `--manga` toggle experimental manga mode
|
||||
|
||||
@@ -431,7 +432,7 @@ 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
|
||||
# merge subtitles with ffmpeg to mkv format; hianime tends to give subs as separate files
|
||||
# and dont prompt for anything
|
||||
# eg existing file in destination instead remove
|
||||
# and clean
|
||||
@@ -717,10 +718,10 @@ quality = 1080
|
||||
# this also applies to episode titles
|
||||
normalize_titles = True
|
||||
|
||||
# can be [allanime, animepahe, aniwatch]
|
||||
# can be [allanime, animepahe, hianime]
|
||||
# allanime is the most realible
|
||||
# animepahe provides different links to streams of different quality so a quality can be selected reliably with --quality option
|
||||
# aniwatch which is now hianime usually provides subs in different languuages and its servers are generally faster
|
||||
# hianime which is now hianime usually provides subs in different languuages and its servers are generally faster
|
||||
provider = allanime
|
||||
|
||||
# Display language [english, romaji]
|
||||
@@ -772,7 +773,7 @@ notification_duration = 2
|
||||
|
||||
# used when the provider gives subs of different languages
|
||||
# currently its the case for:
|
||||
# aniwatch
|
||||
# hianime
|
||||
# the values for this option are the short names for countries
|
||||
# regex is used to determine what you selected
|
||||
sub_lang = eng
|
||||
@@ -799,7 +800,7 @@ translation_type = sub
|
||||
# what server to use for a particular provider
|
||||
# allanime: [dropbox, sharepoint, wetransfer, gogoanime, wixmp]
|
||||
# animepahe: [kwik]
|
||||
# aniwatch: [HD1, HD2, StreamSB, StreamTape]
|
||||
# hianime: [HD1, HD2, StreamSB, StreamTape]
|
||||
# 'top' can also be used as a value for this option
|
||||
# 'top' will cause fastanime to auto select the first server it sees
|
||||
# this saves on resources and is faster since not all servers are being fetched
|
||||
@@ -860,7 +861,7 @@ force_window = immediate
|
||||
# only works for downloaded anime if:
|
||||
# provider=allanime, server=gogoanime
|
||||
# provider=allanime, server=wixmp
|
||||
# provider=aniwatch
|
||||
# provider=hianime
|
||||
# this is because they provider a m3u8 file that contans multiple quality streams
|
||||
format = best[height<=1080]/bestvideo[height<=1080]+bestaudio/best
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ anime_normalizer_raw = {
|
||||
},
|
||||
"hianime": {"My Star": "Oshi no Ko"},
|
||||
"animepahe": {"Azumanga Daiou The Animation": "Azumanga Daioh"},
|
||||
"nyaa": {},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -59,6 +59,25 @@ class YtDLPDownloader:
|
||||
"""
|
||||
anime_title = sanitize_filename(anime_title)
|
||||
episode_title = sanitize_filename(episode_title)
|
||||
if url.endswith(".torrent"):
|
||||
WEBTORRENT_CLI = shutil.which("webtorrent")
|
||||
if not WEBTORRENT_CLI:
|
||||
import time
|
||||
|
||||
print(
|
||||
"webtorrent cli is not installed which is required for downloading and streaming from nyaa\nplease install it or use another provider"
|
||||
)
|
||||
time.sleep(120)
|
||||
return
|
||||
cmd = [
|
||||
WEBTORRENT_CLI,
|
||||
"download",
|
||||
url,
|
||||
"--out",
|
||||
os.path.join(download_dir, anime_title, episode_title),
|
||||
]
|
||||
subprocess.run(cmd)
|
||||
return
|
||||
ydl_opts = {
|
||||
# Specify the output path and template
|
||||
"http_headers": headers,
|
||||
|
||||
@@ -6,7 +6,7 @@ if sys.version_info < (3, 10):
|
||||
) # noqa: F541
|
||||
|
||||
|
||||
__version__ = "v2.5.5"
|
||||
__version__ = "v2.5.6"
|
||||
|
||||
APP_NAME = "FastAnime"
|
||||
AUTHOR = "Benex254"
|
||||
|
||||
@@ -42,7 +42,7 @@ if TYPE_CHECKING:
|
||||
# remember python indexing starts at 0
|
||||
fastanime download -t <anime-title> -r '<episode-1>:<episode>'
|
||||
\b
|
||||
# merge subtitles with ffmpeg to mkv format; aniwatch tends to give subs as separate files
|
||||
# merge subtitles with ffmpeg to mkv format; hianime tends to give subs as separate files
|
||||
# and dont prompt for anything
|
||||
# eg existing file in destination instead remove
|
||||
# and clean
|
||||
|
||||
@@ -295,10 +295,10 @@ quality = {self.quality}
|
||||
# this also applies to episode titles
|
||||
normalize_titles = {self.normalize_titles}
|
||||
|
||||
# can be [allanime, animepahe, aniwatch]
|
||||
# can be [allanime, animepahe, hianime]
|
||||
# allanime is the most realible
|
||||
# animepahe provides different links to streams of different quality so a quality can be selected reliably with --quality option
|
||||
# aniwatch which is now hianime usually provides subs in different languuages and its servers are generally faster
|
||||
# hianime which is now hianime usually provides subs in different languuages and its servers are generally faster
|
||||
provider = {self.provider}
|
||||
|
||||
# Display language [english, romaji]
|
||||
@@ -350,7 +350,7 @@ notification_duration = {self.notification_duration}
|
||||
|
||||
# used when the provider gives subs of different languages
|
||||
# currently its the case for:
|
||||
# aniwatch
|
||||
# hianime
|
||||
# the values for this option are the short names for countries
|
||||
# regex is used to determine what you selected
|
||||
sub_lang = {self.sub_lang}
|
||||
@@ -388,7 +388,7 @@ translation_type = {self.translation_type}
|
||||
# what server to use for a particular provider
|
||||
# allanime: [dropbox, sharepoint, wetransfer, gogoanime, wixmp]
|
||||
# animepahe: [kwik]
|
||||
# aniwatch: [HD1, HD2, StreamSB, StreamTape]
|
||||
# hianime: [HD1, HD2, StreamSB, StreamTape]
|
||||
# 'top' can also be used as a value for this option
|
||||
# 'top' will cause fastanime to auto select the first server it sees
|
||||
# this saves on resources and is faster since not all servers are being fetched
|
||||
@@ -450,7 +450,7 @@ force_window = immediate
|
||||
# only works for downloaded anime if:
|
||||
# provider=allanime, server=gogoanime
|
||||
# provider=allanime, server=wixmp
|
||||
# provider=aniwatch
|
||||
# provider=hianime
|
||||
# this is because they provider a m3u8 file that contans multiple quality streams
|
||||
format = {self.format}
|
||||
|
||||
|
||||
@@ -63,6 +63,19 @@ 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 link.endswith(".torrent"):
|
||||
WEBTORRENT_CLI = shutil.which("webtorrent")
|
||||
if not WEBTORRENT_CLI:
|
||||
import time
|
||||
|
||||
print(
|
||||
"webtorrent cli is not installed which is required for downloading and streaming from nyaa\nplease install it or use another provider"
|
||||
)
|
||||
time.sleep(120)
|
||||
return "0", "0"
|
||||
cmd = [WEBTORRENT_CLI, link, f"--{player}"]
|
||||
subprocess.run(cmd)
|
||||
return "0", "0"
|
||||
if player == "vlc":
|
||||
VLC = shutil.which("vlc")
|
||||
if not VLC and not S_PLATFORM == "win32":
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
from .allanime.constants import SERVERS_AVAILABLE as ALLANIME_SERVERS
|
||||
from .animepahe.constants import SERVERS_AVAILABLE as ANIMEPAHESERVERS
|
||||
from .hianime.constants import SERVERS_AVAILABLE as ANIWATCHSERVERS
|
||||
from .animepahe.constants import SERVERS_AVAILABLE as ANIMEPAHE_SERVERS
|
||||
from .hianime.constants import SERVERS_AVAILABLE as HIANIME_SERVERS
|
||||
|
||||
anime_sources = {
|
||||
"allanime": "api.AllAnimeAPI",
|
||||
"animepahe": "api.AnimePaheApi",
|
||||
"hianime": "api.HiAnimeApi",
|
||||
"nyaa": "api.NyaaApi",
|
||||
}
|
||||
SERVERS_AVAILABLE = [*ALLANIME_SERVERS, *ANIMEPAHESERVERS, *ANIWATCHSERVERS]
|
||||
SERVERS_AVAILABLE = [*ALLANIME_SERVERS, *ANIMEPAHE_SERVERS, *HIANIME_SERVERS]
|
||||
|
||||
@@ -17,7 +17,7 @@ from ..base_provider import AnimeProvider
|
||||
from ..decorators import debug_provider
|
||||
from ..utils import give_random_quality
|
||||
from .constants import SERVERS_AVAILABLE
|
||||
from .types import AniWatchStream
|
||||
from .types import HiAnimeStream
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -41,7 +41,7 @@ class ParseAnchorAndImgTag(HTMLParser):
|
||||
class HiAnimeApi(AnimeProvider):
|
||||
# HEADERS = {"Referer": "https://hianime.to/home"}
|
||||
|
||||
@debug_provider("ANIWATCH")
|
||||
@debug_provider("HIANIME")
|
||||
def search_for_anime(self, anime_title: str, *args):
|
||||
query = quote_plus(anime_title)
|
||||
url = f"https://hianime.to/search?keyword={query}"
|
||||
@@ -88,20 +88,20 @@ class HiAnimeApi(AnimeProvider):
|
||||
self.search_results = results
|
||||
return {"pageInfo": {}, "results": results}
|
||||
|
||||
@debug_provider("ANIWATCH")
|
||||
def get_anime(self, aniwatch_id, *args):
|
||||
@debug_provider("HIANIME")
|
||||
def get_anime(self, hianime_id, *args):
|
||||
anime_result = {}
|
||||
for anime in self.search_results:
|
||||
if anime["id"] == aniwatch_id:
|
||||
if anime["id"] == hianime_id:
|
||||
anime_result = anime
|
||||
break
|
||||
anime_url = f"https://hianime.to/ajax/v2/episode/list/{aniwatch_id}"
|
||||
anime_url = f"https://hianime.to/ajax/v2/episode/list/{hianime_id}"
|
||||
response = self.session.get(anime_url, timeout=10)
|
||||
if response.ok:
|
||||
response_json = response.json()
|
||||
aniwatch_anime_page = response_json["html"]
|
||||
hianime_anime_page = response_json["html"]
|
||||
episodes_info_container_html = get_element_html_by_class(
|
||||
"ss-list", aniwatch_anime_page
|
||||
"ss-list", hianime_anime_page
|
||||
)
|
||||
episodes_info_html_list = get_elements_html_by_class(
|
||||
"ep-item", episodes_info_container_html
|
||||
@@ -127,7 +127,7 @@ class HiAnimeApi(AnimeProvider):
|
||||
for episode in episodes_info_dicts
|
||||
]
|
||||
return {
|
||||
"id": aniwatch_id,
|
||||
"id": hianime_id,
|
||||
"availableEpisodesDetail": {
|
||||
"dub": episodes,
|
||||
"sub": episodes,
|
||||
@@ -138,7 +138,7 @@ class HiAnimeApi(AnimeProvider):
|
||||
"episodes_info": self.episodes_info,
|
||||
}
|
||||
|
||||
@debug_provider("ANIWATCH")
|
||||
@debug_provider("HIANIME")
|
||||
def get_episode_streams(
|
||||
self, anime_id, anime_title, episode, translation_type, *args
|
||||
):
|
||||
@@ -166,7 +166,7 @@ class HiAnimeApi(AnimeProvider):
|
||||
"server-item", servers_containers_html[0]
|
||||
)
|
||||
except Exception:
|
||||
logger.warning("AniWatch: sub not found")
|
||||
logger.warning("HiAnime: sub not found")
|
||||
servers_html_sub = None
|
||||
|
||||
# dub servers
|
||||
@@ -175,7 +175,7 @@ class HiAnimeApi(AnimeProvider):
|
||||
"server-item", servers_containers_html[1]
|
||||
)
|
||||
except Exception:
|
||||
logger.warning("AniWatch: dub not found")
|
||||
logger.warning("HiAnime: dub not found")
|
||||
servers_html_dub = None
|
||||
|
||||
if translation_type == "dub":
|
||||
@@ -185,7 +185,7 @@ class HiAnimeApi(AnimeProvider):
|
||||
if not servers_html:
|
||||
return
|
||||
|
||||
@debug_provider("ANIWATCH")
|
||||
@debug_provider("HIANIME")
|
||||
def _get_server(server_name, server_html):
|
||||
# keys: [ data-type: translation_type, data-id: embed_id, data-server-id: server_id ]
|
||||
servers_info = extract_attributes(server_html)
|
||||
@@ -205,7 +205,7 @@ class HiAnimeApi(AnimeProvider):
|
||||
link_to_streams = f"https://{provider_domain}/embed-{embed_type}/ajax/e-{episode_number}/getSources?id={source_id}"
|
||||
link_to_streams_response = self.session.get(link_to_streams)
|
||||
if link_to_streams_response.ok:
|
||||
juicy_streams_json: "AniWatchStream" = (
|
||||
juicy_streams_json: "HiAnimeStream" = (
|
||||
link_to_streams_response.json()
|
||||
)
|
||||
return {
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
from typing import Literal, TypedDict
|
||||
|
||||
|
||||
class AniWatchSkipTime(TypedDict):
|
||||
class HiAnimeSkipTime(TypedDict):
|
||||
start: int
|
||||
end: int
|
||||
|
||||
|
||||
class AniWatchSource(TypedDict):
|
||||
class HiAnimeSource(TypedDict):
|
||||
file: str
|
||||
type: str
|
||||
|
||||
|
||||
class AniWatchTrack(TypedDict):
|
||||
class HiAnimeTrack(TypedDict):
|
||||
file: str
|
||||
label: str
|
||||
kind: Literal["captions", "thumbnails", "audio"]
|
||||
|
||||
|
||||
class AniWatchStream(TypedDict):
|
||||
sources: list[AniWatchSource]
|
||||
tracks: list[AniWatchTrack]
|
||||
class HiAnimeStream(TypedDict):
|
||||
sources: list[HiAnimeSource]
|
||||
tracks: list[HiAnimeTrack]
|
||||
encrypted: bool
|
||||
intro: AniWatchSkipTime
|
||||
outro: AniWatchSkipTime
|
||||
intro: HiAnimeSkipTime
|
||||
outro: HiAnimeSkipTime
|
||||
server: int
|
||||
|
||||
344
fastanime/libs/anime_provider/nyaa/api.py
Normal file
344
fastanime/libs/anime_provider/nyaa/api.py
Normal file
@@ -0,0 +1,344 @@
|
||||
import os
|
||||
import re
|
||||
from logging import getLogger
|
||||
|
||||
from yt_dlp.utils import (
|
||||
extract_attributes,
|
||||
get_element_html_by_attribute,
|
||||
get_element_html_by_class,
|
||||
get_element_text_and_html_by_tag,
|
||||
get_elements_html_by_class,
|
||||
)
|
||||
|
||||
from ...common.mini_anilist import search_for_anime_with_anilist
|
||||
from ..base_provider import AnimeProvider
|
||||
from ..decorators import debug_provider
|
||||
from ..types import SearchResults
|
||||
from .constants import NYAA_ENDPOINT
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
EXTRACT_USEFUL_INFO_PATTERN_1 = re.compile(
|
||||
r"\[(\w+)\] (.+) - (\d+) [\[\(](\d+)p[\]\)].*"
|
||||
)
|
||||
|
||||
EXTRACT_USEFUL_INFO_PATTERN_2 = re.compile(
|
||||
r"\[(\w+)\] (.+)E(\d+) [\[\(]?(\d+)p.*[\]\)]?.*"
|
||||
)
|
||||
|
||||
|
||||
class NyaaApi(AnimeProvider):
|
||||
search_results: SearchResults
|
||||
|
||||
@debug_provider("NYAA")
|
||||
def search_for_anime(self, user_query: str, *args, **_):
|
||||
self.search_results = search_for_anime_with_anilist(
|
||||
user_query, True
|
||||
) # pyright: ignore
|
||||
self.user_query = user_query
|
||||
return self.search_results
|
||||
|
||||
@debug_provider("NYAA")
|
||||
def get_anime(self, anilist_id: str, *_):
|
||||
for anime in self.search_results["results"]:
|
||||
if anime["id"] == anilist_id:
|
||||
self.titles = [anime["title"], *anime["otherTitles"], self.user_query]
|
||||
return {
|
||||
"id": anime["id"],
|
||||
"title": anime["title"],
|
||||
"poster": anime["poster"],
|
||||
"availableEpisodesDetail": {
|
||||
"dub": anime["availableEpisodes"],
|
||||
"sub": anime["availableEpisodes"],
|
||||
"raw": anime["availableEpisodes"],
|
||||
},
|
||||
}
|
||||
|
||||
@debug_provider("NYAA")
|
||||
def get_episode_streams(
|
||||
self,
|
||||
anime_id: str,
|
||||
anime_title: str,
|
||||
episode_number: str,
|
||||
translation_type: str,
|
||||
trusted_only=bool(int(os.environ.get("FA_NYAA_TRUSTED_ONLY", "0"))),
|
||||
allow_dangerous=bool(int(os.environ.get("FA_NYAA_ALLOW_DANGEROUS", "0"))),
|
||||
sort_by="seeders",
|
||||
*args,
|
||||
):
|
||||
logger.debug(f"Searching nyaa for query: '{anime_title} {episode_number}'")
|
||||
servers = {}
|
||||
|
||||
torrents_table = ""
|
||||
for title in self.titles:
|
||||
try:
|
||||
url_arguments: dict[str, str] = {
|
||||
"c": "1_2", # Language (English)
|
||||
"q": f"{title} {'0' if len(episode_number)==1 else ''}{episode_number}", # Search Query
|
||||
}
|
||||
# url_arguments["q"] = anime_title
|
||||
|
||||
# if trusted_only:
|
||||
# url_arguments["f"] = "2" # Trusted uploaders only
|
||||
|
||||
# What to sort torrents by
|
||||
if sort_by == "seeders":
|
||||
url_arguments["s"] = "seeders"
|
||||
elif sort_by == "date":
|
||||
url_arguments["s"] = "id"
|
||||
elif sort_by == "size":
|
||||
url_arguments["s"] = "size"
|
||||
elif sort_by == "comments":
|
||||
url_arguments["s"] = "comments"
|
||||
|
||||
logger.debug(f"URL Arguments: {url_arguments}")
|
||||
|
||||
response = self.session.get(NYAA_ENDPOINT, params=url_arguments)
|
||||
if not response.ok:
|
||||
logger.error(f"[NYAA]: {response.text}")
|
||||
return
|
||||
|
||||
try:
|
||||
torrents_table = get_element_text_and_html_by_tag(
|
||||
"table", response.text
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"[NYAA]: {e}")
|
||||
continue
|
||||
|
||||
if not torrents_table:
|
||||
continue
|
||||
|
||||
for anime_torrent in get_elements_html_by_class(
|
||||
"success", torrents_table[1]
|
||||
):
|
||||
td_title = get_element_html_by_attribute(
|
||||
"colspan", "2", anime_torrent
|
||||
)
|
||||
if not td_title:
|
||||
continue
|
||||
title_anchor_tag = get_element_text_and_html_by_tag("a", td_title)
|
||||
|
||||
if not title_anchor_tag:
|
||||
continue
|
||||
title_anchor_tag_attrs = extract_attributes(title_anchor_tag[1])
|
||||
if not title_anchor_tag_attrs:
|
||||
continue
|
||||
if "class" in title_anchor_tag_attrs:
|
||||
td_title = td_title.replace(title_anchor_tag[1], "")
|
||||
title_anchor_tag = get_element_text_and_html_by_tag(
|
||||
"a", td_title
|
||||
)
|
||||
|
||||
if not title_anchor_tag:
|
||||
continue
|
||||
title_anchor_tag_attrs = extract_attributes(title_anchor_tag[1])
|
||||
if not title_anchor_tag_attrs:
|
||||
continue
|
||||
anime_title_info = title_anchor_tag_attrs["title"]
|
||||
if not anime_title_info:
|
||||
continue
|
||||
match = EXTRACT_USEFUL_INFO_PATTERN_1.search(
|
||||
anime_title_info.strip()
|
||||
)
|
||||
if not match:
|
||||
continue
|
||||
server = match[1]
|
||||
match[2]
|
||||
_episode_number = match[3]
|
||||
quality = match[4]
|
||||
if float(episode_number) != float(_episode_number):
|
||||
continue
|
||||
|
||||
links_td = get_element_html_by_class("text-center", anime_torrent)
|
||||
if not links_td:
|
||||
continue
|
||||
torrent_anchor_tag = get_element_text_and_html_by_tag("a", links_td)
|
||||
if not torrent_anchor_tag:
|
||||
continue
|
||||
torrent_anchor_tag_atrrs = extract_attributes(torrent_anchor_tag[1])
|
||||
if not torrent_anchor_tag_atrrs:
|
||||
continue
|
||||
torrent_file_url = (
|
||||
f'{NYAA_ENDPOINT}{torrent_anchor_tag_atrrs["href"]}'
|
||||
)
|
||||
if server in servers:
|
||||
link = {
|
||||
"translation_type": "sub",
|
||||
"link": torrent_file_url,
|
||||
"quality": quality,
|
||||
}
|
||||
if link not in servers[server]["links"]:
|
||||
servers[server]["links"].append(link)
|
||||
else:
|
||||
servers[server] = {
|
||||
"server": server,
|
||||
"headers": {},
|
||||
"episode_title": f"{anime_title}; Episode {episode_number}",
|
||||
"subtitles": [],
|
||||
"links": [
|
||||
{
|
||||
"translation_type": "sub",
|
||||
"link": torrent_file_url,
|
||||
"quality": quality,
|
||||
}
|
||||
],
|
||||
}
|
||||
for anime_torrent in get_elements_html_by_class(
|
||||
"default", torrents_table[1]
|
||||
):
|
||||
td_title = get_element_html_by_attribute(
|
||||
"colspan", "2", anime_torrent
|
||||
)
|
||||
if not td_title:
|
||||
continue
|
||||
title_anchor_tag = get_element_text_and_html_by_tag("a", td_title)
|
||||
|
||||
if not title_anchor_tag:
|
||||
continue
|
||||
title_anchor_tag_attrs = extract_attributes(title_anchor_tag[1])
|
||||
if not title_anchor_tag_attrs:
|
||||
continue
|
||||
if "class" in title_anchor_tag_attrs:
|
||||
td_title = td_title.replace(title_anchor_tag[1], "")
|
||||
title_anchor_tag = get_element_text_and_html_by_tag(
|
||||
"a", td_title
|
||||
)
|
||||
|
||||
if not title_anchor_tag:
|
||||
continue
|
||||
title_anchor_tag_attrs = extract_attributes(title_anchor_tag[1])
|
||||
if not title_anchor_tag_attrs:
|
||||
continue
|
||||
anime_title_info = title_anchor_tag_attrs["title"]
|
||||
if not anime_title_info:
|
||||
continue
|
||||
match = EXTRACT_USEFUL_INFO_PATTERN_2.search(
|
||||
anime_title_info.strip()
|
||||
)
|
||||
if not match:
|
||||
continue
|
||||
server = match[1]
|
||||
match[2]
|
||||
_episode_number = match[3]
|
||||
quality = match[4]
|
||||
if float(episode_number) != float(_episode_number):
|
||||
continue
|
||||
|
||||
links_td = get_element_html_by_class("text-center", anime_torrent)
|
||||
if not links_td:
|
||||
continue
|
||||
torrent_anchor_tag = get_element_text_and_html_by_tag("a", links_td)
|
||||
if not torrent_anchor_tag:
|
||||
continue
|
||||
torrent_anchor_tag_atrrs = extract_attributes(torrent_anchor_tag[1])
|
||||
if not torrent_anchor_tag_atrrs:
|
||||
continue
|
||||
torrent_file_url = (
|
||||
f'{NYAA_ENDPOINT}{torrent_anchor_tag_atrrs["href"]}'
|
||||
)
|
||||
if server in servers:
|
||||
link = {
|
||||
"translation_type": "sub",
|
||||
"link": torrent_file_url,
|
||||
"quality": quality,
|
||||
}
|
||||
if link not in servers[server]["links"]:
|
||||
servers[server]["links"].append(link)
|
||||
else:
|
||||
servers[server] = {
|
||||
"server": server,
|
||||
"headers": {},
|
||||
"episode_title": f"{anime_title}; Episode {episode_number}",
|
||||
"subtitles": [],
|
||||
"links": [
|
||||
{
|
||||
"translation_type": "sub",
|
||||
"link": torrent_file_url,
|
||||
"quality": quality,
|
||||
}
|
||||
],
|
||||
}
|
||||
if not allow_dangerous:
|
||||
break
|
||||
for anime_torrent in get_elements_html_by_class(
|
||||
"danger", torrents_table[1]
|
||||
):
|
||||
td_title = get_element_html_by_attribute(
|
||||
"colspan", "2", anime_torrent
|
||||
)
|
||||
if not td_title:
|
||||
continue
|
||||
title_anchor_tag = get_element_text_and_html_by_tag("a", td_title)
|
||||
|
||||
if not title_anchor_tag:
|
||||
continue
|
||||
title_anchor_tag_attrs = extract_attributes(title_anchor_tag[1])
|
||||
if not title_anchor_tag_attrs:
|
||||
continue
|
||||
if "class" in title_anchor_tag_attrs:
|
||||
td_title = td_title.replace(title_anchor_tag[1], "")
|
||||
title_anchor_tag = get_element_text_and_html_by_tag(
|
||||
"a", td_title
|
||||
)
|
||||
|
||||
if not title_anchor_tag:
|
||||
continue
|
||||
title_anchor_tag_attrs = extract_attributes(title_anchor_tag[1])
|
||||
if not title_anchor_tag_attrs:
|
||||
continue
|
||||
anime_title_info = title_anchor_tag_attrs["title"]
|
||||
if not anime_title_info:
|
||||
continue
|
||||
match = EXTRACT_USEFUL_INFO_PATTERN_2.search(
|
||||
anime_title_info.strip()
|
||||
)
|
||||
if not match:
|
||||
continue
|
||||
server = match[1]
|
||||
match[2]
|
||||
_episode_number = match[3]
|
||||
quality = match[4]
|
||||
if float(episode_number) != float(_episode_number):
|
||||
continue
|
||||
|
||||
links_td = get_element_html_by_class("text-center", anime_torrent)
|
||||
if not links_td:
|
||||
continue
|
||||
torrent_anchor_tag = get_element_text_and_html_by_tag("a", links_td)
|
||||
if not torrent_anchor_tag:
|
||||
continue
|
||||
torrent_anchor_tag_atrrs = extract_attributes(torrent_anchor_tag[1])
|
||||
if not torrent_anchor_tag_atrrs:
|
||||
continue
|
||||
torrent_file_url = (
|
||||
f'{NYAA_ENDPOINT}{torrent_anchor_tag_atrrs["href"]}'
|
||||
)
|
||||
if server in servers:
|
||||
link = {
|
||||
"translation_type": "sub",
|
||||
"link": torrent_file_url,
|
||||
"quality": quality,
|
||||
}
|
||||
if link not in servers[server]["links"]:
|
||||
servers[server]["links"].append(link)
|
||||
else:
|
||||
servers[server] = {
|
||||
"server": server,
|
||||
"headers": {},
|
||||
"episode_title": f"{anime_title}; Episode {episode_number}",
|
||||
"subtitles": [],
|
||||
"links": [
|
||||
{
|
||||
"translation_type": "sub",
|
||||
"link": torrent_file_url,
|
||||
"quality": quality,
|
||||
}
|
||||
],
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"[NYAA]: {e}")
|
||||
continue
|
||||
|
||||
for server in servers:
|
||||
yield servers[server]
|
||||
1
fastanime/libs/anime_provider/nyaa/constants.py
Normal file
1
fastanime/libs/anime_provider/nyaa/constants.py
Normal file
@@ -0,0 +1 @@
|
||||
NYAA_ENDPOINT = "https://nyaa.si"
|
||||
126
fastanime/libs/anime_provider/nyaa/utils.py
Normal file
126
fastanime/libs/anime_provider/nyaa/utils.py
Normal file
@@ -0,0 +1,126 @@
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
import libtorrent # pyright: ignore
|
||||
from rich import print
|
||||
from rich.progress import (
|
||||
BarColumn,
|
||||
DownloadColumn,
|
||||
Progress,
|
||||
TextColumn,
|
||||
TimeRemainingColumn,
|
||||
TransferSpeedColumn,
|
||||
)
|
||||
|
||||
logger = logging.getLogger("nyaa")
|
||||
|
||||
|
||||
def download_torrent(
|
||||
filename: str,
|
||||
result_filename: str | None = None,
|
||||
show_progress: bool = True,
|
||||
base_path: str = "Anime",
|
||||
) -> str:
|
||||
session = libtorrent.session({"listen_interfaces": "0.0.0.0:6881"})
|
||||
logger.debug("Started libtorrent session")
|
||||
|
||||
base_path = os.path.expanduser(base_path)
|
||||
logger.debug(f"Downloading output to: '{base_path}'")
|
||||
|
||||
info = libtorrent.torrent_info(filename)
|
||||
|
||||
logger.debug("Started downloading torrent")
|
||||
handle: libtorrent.torrent_handle = session.add_torrent(
|
||||
{"ti": info, "save_path": base_path}
|
||||
)
|
||||
|
||||
status: libtorrent.session_status = handle.status()
|
||||
|
||||
progress_bar = Progress(
|
||||
"[progress.description]{task.description}",
|
||||
BarColumn(bar_width=None),
|
||||
"[progress.percentage]{task.percentage:>3.1f}%",
|
||||
"•",
|
||||
DownloadColumn(),
|
||||
"•",
|
||||
TransferSpeedColumn(),
|
||||
"•",
|
||||
TimeRemainingColumn(),
|
||||
"•",
|
||||
TextColumn("[green]Peers: {task.fields[peers]}[/green]"),
|
||||
)
|
||||
|
||||
if show_progress:
|
||||
with progress_bar:
|
||||
download_task = progress_bar.add_task(
|
||||
"downloading",
|
||||
filename=status.name,
|
||||
total=status.total_wanted,
|
||||
peers=0,
|
||||
start=False,
|
||||
)
|
||||
|
||||
while not status.total_done:
|
||||
# Checking files
|
||||
status = handle.status()
|
||||
description = "[bold yellow]Checking files[/bold yellow]"
|
||||
progress_bar.update(
|
||||
download_task,
|
||||
completed=status.total_done,
|
||||
peers=status.num_peers,
|
||||
description=description,
|
||||
)
|
||||
|
||||
# Started download
|
||||
progress_bar.start_task(download_task)
|
||||
description = f"[bold blue]Downloading[/bold blue] [bold yellow]{result_filename}[/bold yellow]"
|
||||
|
||||
while not status.is_seeding:
|
||||
status = handle.status()
|
||||
|
||||
progress_bar.update(
|
||||
download_task,
|
||||
completed=status.total_done,
|
||||
peers=status.num_peers,
|
||||
description=description,
|
||||
)
|
||||
|
||||
alerts = session.pop_alerts()
|
||||
|
||||
alert: libtorrent.alert
|
||||
for alert in alerts:
|
||||
if (
|
||||
alert.category()
|
||||
& libtorrent.alert.category_t.error_notification
|
||||
):
|
||||
logger.debug(f"[Alert] {alert}")
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
progress_bar.update(
|
||||
download_task,
|
||||
description=f"[bold blue]Finished Downloading[/bold blue] [bold green]{result_filename}[/bold green]",
|
||||
completed=status.total_wanted,
|
||||
)
|
||||
|
||||
if result_filename:
|
||||
old_name = f"{base_path}/{status.name}"
|
||||
new_name = f"{base_path}/{result_filename}"
|
||||
|
||||
os.rename(old_name, new_name)
|
||||
|
||||
logger.debug(f"Finished torrent download, renamed '{old_name}' to '{new_name}'")
|
||||
|
||||
return new_name
|
||||
|
||||
return ""
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("You need to pass in the .torrent file path.")
|
||||
sys.exit(1)
|
||||
|
||||
download_torrent(sys.argv[1])
|
||||
@@ -19,6 +19,7 @@ class PageInfo(TypedDict):
|
||||
class SearchResult(TypedDict):
|
||||
id: str
|
||||
title: str
|
||||
otherTitles: list[str]
|
||||
availableEpisodes: list[str]
|
||||
type: str
|
||||
score: int
|
||||
|
||||
@@ -96,32 +96,36 @@ def search_for_manga_with_anilist(manga_title: str):
|
||||
}
|
||||
|
||||
|
||||
def search_for_anime_with_anilist(anime_title: str):
|
||||
def search_for_anime_with_anilist(anime_title: str, prefer_eng_titles=False):
|
||||
query = """
|
||||
query ($query: String) {
|
||||
Page(perPage: 50) {
|
||||
pageInfo {
|
||||
total
|
||||
currentPage
|
||||
hasNextPage
|
||||
}
|
||||
media(search: $query, type: ANIME,genre_not_in: ["hentai"]) {
|
||||
id
|
||||
idMal
|
||||
title {
|
||||
romaji
|
||||
english
|
||||
}
|
||||
episodes
|
||||
status
|
||||
nextAiringEpisode {
|
||||
timeUntilAiring
|
||||
airingAt
|
||||
episode
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
query ($query: String) {
|
||||
Page(perPage: 50) {
|
||||
pageInfo {
|
||||
total
|
||||
currentPage
|
||||
hasNextPage
|
||||
}
|
||||
media(search: $query, type: ANIME, genre_not_in: ["hentai"]) {
|
||||
id
|
||||
idMal
|
||||
title {
|
||||
romaji
|
||||
english
|
||||
}
|
||||
episodes
|
||||
status
|
||||
synonyms
|
||||
nextAiringEpisode {
|
||||
timeUntilAiring
|
||||
airingAt
|
||||
episode
|
||||
}
|
||||
coverImage {
|
||||
large
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
response = post(
|
||||
ANILIST_ENDPOINT,
|
||||
@@ -134,22 +138,49 @@ def search_for_anime_with_anilist(anime_title: str):
|
||||
"pageInfo": anilist_data["data"]["Page"]["pageInfo"],
|
||||
"results": [
|
||||
{
|
||||
"id": anime_result["id"],
|
||||
"title": anime_result["title"]["romaji"]
|
||||
or anime_result["title"]["english"],
|
||||
"type": "anime",
|
||||
"availableEpisodes": list(
|
||||
range(
|
||||
1,
|
||||
"id": str(anime_result["id"]),
|
||||
"title": (
|
||||
(
|
||||
anime_result["title"]["english"]
|
||||
or anime_result["title"]["romaji"]
|
||||
)
|
||||
if prefer_eng_titles
|
||||
else (
|
||||
anime_result["title"]["romaji"]
|
||||
or anime_result["title"]["english"]
|
||||
)
|
||||
),
|
||||
"otherTitles": [
|
||||
(
|
||||
(
|
||||
anime_result["episodes"]
|
||||
if not anime_result["status"] == "RELEASING"
|
||||
and anime_result["episodes"]
|
||||
else (
|
||||
anime_result["nextAiringEpisode"]["episode"] - 1
|
||||
if anime_result["nextAiringEpisode"]
|
||||
else 0
|
||||
)
|
||||
anime_result["title"]["romaji"]
|
||||
or anime_result["title"]["english"]
|
||||
)
|
||||
if prefer_eng_titles
|
||||
else (
|
||||
anime_result["title"]["english"]
|
||||
or anime_result["title"]["romaji"]
|
||||
)
|
||||
),
|
||||
*(anime_result["synonyms"] or []),
|
||||
],
|
||||
"type": "anime",
|
||||
"poster": anime_result["coverImage"]["large"],
|
||||
"availableEpisodes": list(
|
||||
map(
|
||||
str,
|
||||
range(
|
||||
1,
|
||||
(
|
||||
anime_result["episodes"]
|
||||
if not anime_result["status"] == "RELEASING"
|
||||
and anime_result["episodes"]
|
||||
else (
|
||||
anime_result["nextAiringEpisode"]["episode"] - 1
|
||||
if anime_result["nextAiringEpisode"]
|
||||
else 0
|
||||
)
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
|
||||
31
poetry.lock
generated
31
poetry.lock
generated
@@ -523,7 +523,7 @@ files = [
|
||||
name = "inquirerpy"
|
||||
version = "0.3.4"
|
||||
description = "Python port of Inquirer.js (A collection of common interactive command-line user interfaces)"
|
||||
optional = true
|
||||
optional = false
|
||||
python-versions = ">=3.7,<4.0"
|
||||
files = [
|
||||
{file = "InquirerPy-0.3.4-py3-none-any.whl", hash = "sha256:c65fdfbac1fa00e3ee4fb10679f4d3ed7a012abf4833910e63c295827fe2a7d4"},
|
||||
@@ -551,11 +551,21 @@ files = [
|
||||
[package.extras]
|
||||
colors = ["colorama (>=0.4.6)"]
|
||||
|
||||
[[package]]
|
||||
name = "lbry-libtorrent"
|
||||
version = "1.2.4"
|
||||
description = "Python bindings for libtorrent-rasterbar (lbry fork for packaging)"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "lbry_libtorrent-1.2.4-py3-none-any.whl", hash = "sha256:dedfdbc83c0243676e4e7e709cb47d9da142fa39e2fae18513c9b310baf222b4"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown-it-py"
|
||||
version = "3.0.0"
|
||||
description = "Python port of markdown-it. Markdown parsing, done right!"
|
||||
optional = true
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"},
|
||||
@@ -579,7 +589,7 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
|
||||
name = "mdurl"
|
||||
version = "0.1.2"
|
||||
description = "Markdown URL utilities"
|
||||
optional = true
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
|
||||
@@ -660,7 +670,7 @@ files = [
|
||||
name = "pfzy"
|
||||
version = "0.3.4"
|
||||
description = "Python port of the fzy fuzzy string matching algorithm"
|
||||
optional = true
|
||||
optional = false
|
||||
python-versions = ">=3.7,<4.0"
|
||||
files = [
|
||||
{file = "pfzy-0.3.4-py3-none-any.whl", hash = "sha256:5f50d5b2b3207fa72e7ec0ef08372ef652685470974a107d0d4999fc5a903a96"},
|
||||
@@ -740,7 +750,7 @@ virtualenv = ">=20.10.0"
|
||||
name = "prompt-toolkit"
|
||||
version = "3.0.47"
|
||||
description = "Library for building powerful interactive command lines in Python"
|
||||
optional = true
|
||||
optional = false
|
||||
python-versions = ">=3.7.0"
|
||||
files = [
|
||||
{file = "prompt_toolkit-3.0.47-py3-none-any.whl", hash = "sha256:0d7bfa67001d5e39d02c224b663abc33687405033a8c422d0d675a5a13361d10"},
|
||||
@@ -817,7 +827,7 @@ files = [
|
||||
name = "pygments"
|
||||
version = "2.18.0"
|
||||
description = "Pygments is a syntax highlighting package written in Python."
|
||||
optional = true
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"},
|
||||
@@ -1093,7 +1103,7 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
|
||||
name = "rich"
|
||||
version = "13.8.1"
|
||||
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
|
||||
optional = true
|
||||
optional = false
|
||||
python-versions = ">=3.7.0"
|
||||
files = [
|
||||
{file = "rich-13.8.1-py3-none-any.whl", hash = "sha256:1760a3c0848469b97b558fc61c85233e3dafb69c7a071b4d60c38099d3cd4c06"},
|
||||
@@ -1237,7 +1247,7 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess
|
||||
name = "wcwidth"
|
||||
version = "0.2.13"
|
||||
description = "Measures the displayed width of unicode strings in a terminal"
|
||||
optional = true
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"},
|
||||
@@ -1371,12 +1381,11 @@ static-analysis = ["autopep8 (>=2.0,<3.0)", "ruff (>=0.5.0,<0.6.0)"]
|
||||
test = ["pytest (>=8.1,<9.0)"]
|
||||
|
||||
[extras]
|
||||
cli = ["click", "inquirerpy", "rich"]
|
||||
full = ["click", "inquirerpy", "mpv", "plyer", "rich"]
|
||||
full = ["mpv", "plyer"]
|
||||
mpv = ["mpv"]
|
||||
notifications = ["plyer"]
|
||||
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "aa6445db170dfcb5ed647d78bf5969696025eb935107c1a5ff2812666216f67c"
|
||||
content-hash = "94eb46eebad30c13907af11f4c87b6d6670b733bcc5af502294b20e5de3e1d20"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "fastanime"
|
||||
version = "2.5.5.dev1"
|
||||
version = "2.5.6.dev1"
|
||||
description = "A browser anime site experience from the terminal"
|
||||
authors = ["Benextempest <benextempest@gmail.com>"]
|
||||
license = "UNLICENSE"
|
||||
@@ -17,7 +17,7 @@ inquirerpy = { version = "^0.3.4", optional = false }
|
||||
mpv = { version = "^1.0.7", optional = true }
|
||||
plyer = { version = "^2.1.0", optional = true }
|
||||
|
||||
|
||||
lbry-libtorrent = "^1.2.4"
|
||||
[tool.poetry.extras]
|
||||
full = ["plyer", "mpv"]
|
||||
# cli = ["rich", "click", "inquirerpy"]
|
||||
|
||||
Reference in New Issue
Block a user