mirror of
https://github.com/Benexl/FastAnime.git
synced 2025-12-08 05:40:38 -08:00
Compare commits
109 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bcdd88c725 | ||
|
|
d45d438663 | ||
|
|
3d12059e27 | ||
|
|
677f4690fa | ||
|
|
a79b59f727 | ||
|
|
5641c245e7 | ||
|
|
058fc285cd | ||
|
|
71cfe667c9 | ||
|
|
d9692201aa | ||
|
|
1fd4087b41 | ||
|
|
787eb0c9ca | ||
|
|
acd937f8ab | ||
|
|
52af68d13f | ||
|
|
1ff3074fad | ||
|
|
debaa2ffa6 | ||
|
|
5b6ccbe748 | ||
|
|
d6ca923951 | ||
|
|
0e9bf7f2de | ||
|
|
ccad2435b0 | ||
|
|
30fa9851dd | ||
|
|
000bae9bb7 | ||
|
|
8c2bb71e08 | ||
|
|
57393b085a | ||
|
|
5f721847d7 | ||
|
|
383cb62ede | ||
|
|
434ac947dd | ||
|
|
d0fb39cede | ||
|
|
f98ae77587 | ||
|
|
33e1b0fb6f | ||
|
|
7134702eb9 | ||
|
|
cac7586a86 | ||
|
|
0b9da27def | ||
|
|
ddbb4ca451 | ||
|
|
757393aa36 | ||
|
|
eb54d5e995 | ||
|
|
0d95a38321 | ||
|
|
8d2734db74 | ||
|
|
b3abcb958b | ||
|
|
0667749e4c | ||
|
|
57e73e6799 | ||
|
|
7d890b9719 | ||
|
|
8cbbcf458d | ||
|
|
67bc25a527 | ||
|
|
e668f9326a | ||
|
|
a02db6471f | ||
|
|
08b1f0c90c | ||
|
|
3ec8dbee8c | ||
|
|
473c11faca | ||
|
|
320e3799d3 | ||
|
|
a0f28ddf6d | ||
|
|
9512c3530a | ||
|
|
72602a0ec1 | ||
|
|
4daf6a2b07 | ||
|
|
8b37927f6a | ||
|
|
9d6f785a7f | ||
|
|
897c34d98c | ||
|
|
28c75215bd | ||
|
|
8697b27fe0 | ||
|
|
b6e05c877b | ||
|
|
d8c3ba6181 | ||
|
|
8b5c917038 | ||
|
|
856f62c245 | ||
|
|
02dfc9d71c | ||
|
|
cef0bae528 | ||
|
|
4867720ad2 | ||
|
|
8d85e30150 | ||
|
|
eb99b7e6ba | ||
|
|
089c049f26 | ||
|
|
a33e47d205 | ||
|
|
25dc35eaaf | ||
|
|
525586e955 | ||
|
|
5129219e23 | ||
|
|
7cd97c78b1 | ||
|
|
27b4422ef3 | ||
|
|
1c367c8aa1 | ||
|
|
7b6cc48b90 | ||
|
|
812d0110a7 | ||
|
|
60b05bf0ac | ||
|
|
d830cca3bc | ||
|
|
209e93b6d9 | ||
|
|
b10d9dc39a | ||
|
|
fe8cda094c | ||
|
|
33c06eab0a | ||
|
|
f3f4be7410 | ||
|
|
3915ef0fb6 | ||
|
|
20d26166dd | ||
|
|
ddca724bd8 | ||
|
|
b86c1a0479 | ||
|
|
1fa7830ddf | ||
|
|
59abafbe16 | ||
|
|
b6eebb9736 | ||
|
|
7797053102 | ||
|
|
d763445f72 | ||
|
|
7bc6b14b5f | ||
|
|
f70d2ac8af | ||
|
|
defdfc5a47 | ||
|
|
e67eeda492 | ||
|
|
a17588d02c | ||
|
|
67b59305c4 | ||
|
|
61db9aeea6 | ||
|
|
4f0768a060 | ||
|
|
21704cbbea | ||
|
|
886bc4d011 | ||
|
|
e3437e066a | ||
|
|
8f2795843a | ||
|
|
c6290592e8 | ||
|
|
050ba740b8 | ||
|
|
966301bce8 | ||
|
|
d776880306 |
@@ -1,5 +1,5 @@
|
||||
default_language_version:
|
||||
python: python3.10
|
||||
python: python3.12
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/pycqa/isort
|
||||
@@ -7,7 +7,7 @@ repos:
|
||||
hooks:
|
||||
- id: isort
|
||||
name: isort (python)
|
||||
args: ["--profile", "black"] # Ensure compatibility with Black
|
||||
args: ["--profile", "black"]
|
||||
|
||||
- repo: https://github.com/PyCQA/autoflake
|
||||
rev: v2.2.1
|
||||
@@ -19,17 +19,15 @@ repos:
|
||||
"--remove-unused-variables",
|
||||
"--remove-all-unused-imports",
|
||||
]
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.4.10
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
args: [--fix]
|
||||
# - repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# rev: v0.4.10
|
||||
# hooks:
|
||||
# - id: ruff
|
||||
# args: [--fix]
|
||||
|
||||
- repo: https://github.com/psf/black-pre-commit-mirror
|
||||
rev: 24.4.2
|
||||
hooks:
|
||||
- id: black
|
||||
name: black
|
||||
language_version: python3.10 # to ensure compatibilty
|
||||
#language_version: python3.10
|
||||
|
||||
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"python.analysis.autoImportCompletions": true
|
||||
}
|
||||
@@ -15,7 +15,7 @@ if TYPE_CHECKING:
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# TODO: improve performance of this class and add cool features like auto retry
|
||||
# TODO: add cool features like auto retry
|
||||
class AnimeProvider:
|
||||
"""Class that manages all anime sources adding some extra functionality to them.
|
||||
Attributes:
|
||||
@@ -62,11 +62,7 @@ class AnimeProvider:
|
||||
)
|
||||
|
||||
def search_for_anime(
|
||||
self,
|
||||
user_query,
|
||||
translation_type,
|
||||
nsfw=True,
|
||||
unknown=True,
|
||||
self, search_keywords, translation_type, **kwargs
|
||||
) -> "SearchResults | None":
|
||||
"""core abstraction over all providers search functionality
|
||||
|
||||
@@ -82,7 +78,7 @@ class AnimeProvider:
|
||||
"""
|
||||
anime_provider = self.anime_provider
|
||||
results = anime_provider.search_for_anime(
|
||||
user_query, translation_type, nsfw, unknown
|
||||
search_keywords, translation_type, **kwargs
|
||||
)
|
||||
|
||||
return results
|
||||
@@ -90,6 +86,7 @@ class AnimeProvider:
|
||||
def get_anime(
|
||||
self,
|
||||
anime_id: str,
|
||||
**kwargs,
|
||||
) -> "Anime | None":
|
||||
"""core abstraction over getting info of an anime from all providers
|
||||
|
||||
@@ -101,7 +98,7 @@ class AnimeProvider:
|
||||
[TODO:return]
|
||||
"""
|
||||
anime_provider = self.anime_provider
|
||||
results = anime_provider.get_anime(anime_id)
|
||||
results = anime_provider.get_anime(anime_id, **kwargs)
|
||||
|
||||
return results
|
||||
|
||||
@@ -110,6 +107,7 @@ class AnimeProvider:
|
||||
anime_id,
|
||||
episode: str,
|
||||
translation_type: str,
|
||||
**kwargs,
|
||||
) -> "Iterator[Server] | None":
|
||||
"""core abstractions for getting juicy streams from all providers
|
||||
|
||||
@@ -124,6 +122,6 @@ class AnimeProvider:
|
||||
"""
|
||||
anime_provider = self.anime_provider
|
||||
results = anime_provider.get_episode_streams(
|
||||
anime_id, episode, translation_type
|
||||
anime_id, episode, translation_type, **kwargs
|
||||
)
|
||||
return results
|
||||
|
||||
@@ -12,7 +12,11 @@ anime_normalizer_raw = {
|
||||
"Re:Zero kara Hajimeru Isekai Seikatsu Season 3": "Re:Zero kara Hajimeru Isekai Seikatsu 3rd Season",
|
||||
},
|
||||
"hianime": {"My Star": "Oshi no Ko"},
|
||||
"animepahe": {"Azumanga Daiou The Animation": "Azumanga Daioh"},
|
||||
"animepahe": {
|
||||
"Azumanga Daiou The Animation": "Azumanga Daioh",
|
||||
"Mairimashita! Iruma-kun 2nd Season": "Mairimashita! Iruma-kun 2",
|
||||
"Mairimashita! Iruma-kun 3rd Season": "Mairimashita! Iruma-kun 3",
|
||||
},
|
||||
"nyaa": {},
|
||||
"yugen": {},
|
||||
}
|
||||
|
||||
@@ -43,6 +43,8 @@ class YtDLPDownloader:
|
||||
merge=False,
|
||||
clean=False,
|
||||
prompt=True,
|
||||
force_ffmpeg=False,
|
||||
hls_use_mpegts=False,
|
||||
):
|
||||
"""Helper function that downloads anime given url and path details
|
||||
|
||||
@@ -91,7 +93,21 @@ class YtDLPDownloader:
|
||||
vid_path = ""
|
||||
sub_path = ""
|
||||
for i, url in enumerate(urls):
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||
options = ydl_opts
|
||||
if i == 0:
|
||||
if force_ffmpeg:
|
||||
options = options | {
|
||||
"external_downloader": {"default": "ffmpeg"},
|
||||
"external_downloader_args": {
|
||||
"ffmpeg_i1": ["-v", "error", "-stats"],
|
||||
},
|
||||
}
|
||||
if hls_use_mpegts:
|
||||
options = options | {
|
||||
"hls_use_mpegts": hls_use_mpegts,
|
||||
}
|
||||
|
||||
with yt_dlp.YoutubeDL(options) as ydl:
|
||||
info = ydl.extract_info(url, download=True)
|
||||
if not info:
|
||||
continue
|
||||
|
||||
@@ -6,10 +6,10 @@ if sys.version_info < (3, 10):
|
||||
) # noqa: F541
|
||||
|
||||
|
||||
__version__ = "v2.8.1"
|
||||
__version__ = "v2.8.6"
|
||||
|
||||
APP_NAME = "FastAnime"
|
||||
AUTHOR = "Benex254"
|
||||
AUTHOR = "Benexl"
|
||||
GIT_REPO = "github.com"
|
||||
REPO = f"{GIT_REPO}/{AUTHOR}/{APP_NAME}"
|
||||
|
||||
|
||||
@@ -222,25 +222,41 @@ def run_cli(
|
||||
fresh_requests,
|
||||
):
|
||||
import os
|
||||
import sys
|
||||
|
||||
from .config import Config
|
||||
|
||||
ctx.obj = Config()
|
||||
if ctx.obj.check_for_updates:
|
||||
if (
|
||||
ctx.obj.check_for_updates
|
||||
and ctx.invoked_subcommand != "completions"
|
||||
and "notifier" not in sys.argv
|
||||
):
|
||||
import time
|
||||
|
||||
last_update = ctx.obj.user_data["meta"]["last_updated"]
|
||||
now = time.time()
|
||||
# checks after every 12 hours
|
||||
if (now - last_update) > 43200:
|
||||
ctx.obj.user_data["meta"]["last_updated"] = now
|
||||
ctx.obj._update_user_data()
|
||||
|
||||
from .app_updater import check_for_updates
|
||||
|
||||
print("Checking for updates...")
|
||||
print("So you can enjoy the latest features and bug fixes")
|
||||
print("Checking for updates...", file=sys.stderr)
|
||||
print("So you can enjoy the latest features and bug fixes", file=sys.stderr)
|
||||
print(
|
||||
"You can disable this by setting check_for_updates to False in the config"
|
||||
"You can disable this by setting check_for_updates to False in the config",
|
||||
file=sys.stderr,
|
||||
)
|
||||
is_latest, github_release_data = check_for_updates()
|
||||
if not is_latest:
|
||||
from rich.console import Console
|
||||
from rich.markdown import Markdown
|
||||
from .app_updater import update_app
|
||||
from rich.prompt import Confirm
|
||||
|
||||
from .app_updater import update_app
|
||||
|
||||
def _print_release(release_data):
|
||||
console = Console()
|
||||
body = Markdown(release_data["body"])
|
||||
@@ -259,6 +275,8 @@ def run_cli(
|
||||
print("Successfully updated")
|
||||
_print_release(release_json)
|
||||
exit(0)
|
||||
else:
|
||||
print("You are using the latest version of fastanime", file=sys.stderr)
|
||||
|
||||
ctx.obj.manga = manga
|
||||
if log:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import shlex
|
||||
@@ -88,7 +89,14 @@ def update_app(force=False):
|
||||
tag_name = release_json["tag_name"]
|
||||
|
||||
print("[cyan]Updating app to version %s[/]" % tag_name)
|
||||
if is_git_repo(AUTHOR, APP_NAME):
|
||||
if os.path.exists("/nix/store") and os.path.exists("/run/current-system"):
|
||||
NIX = shutil.which("nix")
|
||||
if not NIX:
|
||||
print("[red]Cannot find nix, it looks like your system is broken.[/]")
|
||||
return False, release_json
|
||||
|
||||
process = subprocess.run([NIX, "profile", "upgrade", APP_NAME.lower()])
|
||||
elif is_git_repo(AUTHOR, APP_NAME):
|
||||
GIT_EXECUTABLE = shutil.which("git")
|
||||
args = [
|
||||
GIT_EXECUTABLE,
|
||||
|
||||
@@ -21,6 +21,8 @@ commands = {
|
||||
"planning": "planning.planning",
|
||||
"notifier": "notifier.notifier",
|
||||
"stats": "stats.stats",
|
||||
"download": "download.download",
|
||||
"downloads": "downloads.downloads",
|
||||
}
|
||||
|
||||
|
||||
@@ -78,15 +80,13 @@ commands = {
|
||||
fastanime --log-file anilist notifier
|
||||
""",
|
||||
)
|
||||
@click.option("--resume", is_flag=True, help="Resume from the last session")
|
||||
@click.pass_context
|
||||
def anilist(ctx: click.Context):
|
||||
def anilist(ctx: click.Context, resume: bool):
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ....anilist import AniList
|
||||
from ....AnimeProvider import AnimeProvider
|
||||
from ...interfaces.anilist_interfaces import (
|
||||
fastanime_main_menu as anilist_interface,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ...config import Config
|
||||
@@ -96,4 +96,33 @@ def anilist(ctx: click.Context):
|
||||
AniList.update_login_info(user, user["token"])
|
||||
if ctx.invoked_subcommand is None:
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
if resume:
|
||||
from ...interfaces.anilist_interfaces import (
|
||||
anime_provider_search_results_menu,
|
||||
)
|
||||
|
||||
if not config.user_data["recent_anime"]:
|
||||
click.echo("No recent anime found", err=True, color=True)
|
||||
return
|
||||
fastanime_runtime_state.anilist_results_data = {
|
||||
"data": {"Page": {"media": config.user_data["recent_anime"]}}
|
||||
}
|
||||
|
||||
fastanime_runtime_state.selected_anime_anilist = config.user_data[
|
||||
"recent_anime"
|
||||
][0]
|
||||
fastanime_runtime_state.selected_anime_id_anilist = config.user_data[
|
||||
"recent_anime"
|
||||
][0]["id"]
|
||||
fastanime_runtime_state.selected_anime_title_anilist = (
|
||||
config.user_data["recent_anime"][0]["title"]["romaji"]
|
||||
or config.user_data["recent_anime"][0]["title"]["english"]
|
||||
)
|
||||
anime_provider_search_results_menu(config, fastanime_runtime_state)
|
||||
|
||||
else:
|
||||
from ...interfaces.anilist_interfaces import (
|
||||
fastanime_main_menu as anilist_interface,
|
||||
)
|
||||
|
||||
anilist_interface(ctx.obj, fastanime_runtime_state)
|
||||
|
||||
@@ -42,5 +42,12 @@ def completed(config: "Config", dump_json):
|
||||
from ...interfaces import anilist_interfaces
|
||||
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
|
||||
fastanime_runtime_state.current_page = 1
|
||||
fastanime_runtime_state.current_data_loader = (
|
||||
lambda config, **kwargs: anilist_interfaces._handle_animelist(
|
||||
config, fastanime_runtime_state, "Completed", **kwargs
|
||||
)
|
||||
)
|
||||
fastanime_runtime_state.anilist_results_data = anime_list[1]
|
||||
anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state)
|
||||
|
||||
476
fastanime/cli/commands/anilist/data.py
Normal file
476
fastanime/cli/commands/anilist/data.py
Normal file
@@ -0,0 +1,476 @@
|
||||
sorts_available = [
|
||||
"ID",
|
||||
"ID_DESC",
|
||||
"TITLE_ROMAJI",
|
||||
"TITLE_ROMAJI_DESC",
|
||||
"TITLE_ENGLISH",
|
||||
"TITLE_ENGLISH_DESC",
|
||||
"TITLE_NATIVE",
|
||||
"TITLE_NATIVE_DESC",
|
||||
"TYPE",
|
||||
"TYPE_DESC",
|
||||
"FORMAT",
|
||||
"FORMAT_DESC",
|
||||
"START_DATE",
|
||||
"START_DATE_DESC",
|
||||
"END_DATE",
|
||||
"END_DATE_DESC",
|
||||
"SCORE",
|
||||
"SCORE_DESC",
|
||||
"POPULARITY",
|
||||
"POPULARITY_DESC",
|
||||
"TRENDING",
|
||||
"TRENDING_DESC",
|
||||
"EPISODES",
|
||||
"EPISODES_DESC",
|
||||
"DURATION",
|
||||
"DURATION_DESC",
|
||||
"STATUS",
|
||||
"STATUS_DESC",
|
||||
"CHAPTERS",
|
||||
"CHAPTERS_DESC",
|
||||
"VOLUMES",
|
||||
"VOLUMES_DESC",
|
||||
"UPDATED_AT",
|
||||
"UPDATED_AT_DESC",
|
||||
"SEARCH_MATCH",
|
||||
"FAVOURITES",
|
||||
"FAVOURITES_DESC",
|
||||
]
|
||||
|
||||
media_statuses_available = [
|
||||
"FINISHED",
|
||||
"RELEASING",
|
||||
"NOT_YET_RELEASED",
|
||||
"CANCELLED",
|
||||
"HIATUS",
|
||||
]
|
||||
seasons_available = ["WINTER", "SPRING", "SUMMER", "FALL"]
|
||||
genres_available = [
|
||||
"Action",
|
||||
"Adventure",
|
||||
"Comedy",
|
||||
"Drama",
|
||||
"Ecchi",
|
||||
"Fantasy",
|
||||
"Horror",
|
||||
"Mahou Shoujo",
|
||||
"Mecha",
|
||||
"Music",
|
||||
"Mystery",
|
||||
"Psychological",
|
||||
"Romance",
|
||||
"Sci-Fi",
|
||||
"Slice of Life",
|
||||
"Sports",
|
||||
"Supernatural",
|
||||
"Thriller",
|
||||
"Hentai",
|
||||
]
|
||||
media_formats_available = [
|
||||
"TV",
|
||||
"TV_SHORT",
|
||||
"MOVIE",
|
||||
"SPECIAL",
|
||||
"OVA",
|
||||
"MUSIC",
|
||||
"NOVEL",
|
||||
"ONE_SHOT",
|
||||
]
|
||||
years_available = [
|
||||
"1900",
|
||||
"1910",
|
||||
"1920",
|
||||
"1930",
|
||||
"1940",
|
||||
"1950",
|
||||
"1960",
|
||||
"1970",
|
||||
"1980",
|
||||
"1990",
|
||||
"2000",
|
||||
"2004",
|
||||
"2005",
|
||||
"2006",
|
||||
"2007",
|
||||
"2008",
|
||||
"2009",
|
||||
"2010",
|
||||
"2011",
|
||||
"2012",
|
||||
"2013",
|
||||
"2014",
|
||||
"2015",
|
||||
"2016",
|
||||
"2017",
|
||||
"2018",
|
||||
"2019",
|
||||
"2020",
|
||||
"2021",
|
||||
"2022",
|
||||
"2023",
|
||||
"2024",
|
||||
]
|
||||
|
||||
tags_available = {
|
||||
"Cast": ["Polyamorous"],
|
||||
"Cast Main Cast": [
|
||||
"Anti-Hero",
|
||||
"Elderly Protagonist",
|
||||
"Ensemble Cast",
|
||||
"Estranged Family",
|
||||
"Female Protagonist",
|
||||
"Male Protagonist",
|
||||
"Primarily Adult Cast",
|
||||
"Primarily Animal Cast",
|
||||
"Primarily Child Cast",
|
||||
"Primarily Female Cast",
|
||||
"Primarily Male Cast",
|
||||
"Primarily Teen Cast",
|
||||
],
|
||||
"Cast Traits": [
|
||||
"Age Regression",
|
||||
"Agender",
|
||||
"Aliens",
|
||||
"Amnesia",
|
||||
"Angels",
|
||||
"Anthropomorphism",
|
||||
"Aromantic",
|
||||
"Arranged Marriage",
|
||||
"Artificial Intelligence",
|
||||
"Asexual",
|
||||
"Butler",
|
||||
"Centaur",
|
||||
"Chimera",
|
||||
"Chuunibyou",
|
||||
"Clone",
|
||||
"Cosplay",
|
||||
"Cowboys",
|
||||
"Crossdressing",
|
||||
"Cyborg",
|
||||
"Delinquents",
|
||||
"Demons",
|
||||
"Detective",
|
||||
"Dinosaurs",
|
||||
"Disability",
|
||||
"Dissociative Identities",
|
||||
"Dragons",
|
||||
"Dullahan",
|
||||
"Elf",
|
||||
"Fairy",
|
||||
"Femboy",
|
||||
"Ghost",
|
||||
"Goblin",
|
||||
"Gods",
|
||||
"Gyaru",
|
||||
"Hikikomori",
|
||||
"Homeless",
|
||||
"Idol",
|
||||
"Kemonomimi",
|
||||
"Kuudere",
|
||||
"Maids",
|
||||
"Mermaid",
|
||||
"Monster Boy",
|
||||
"Monster Girl",
|
||||
"Nekomimi",
|
||||
"Ninja",
|
||||
"Nudity",
|
||||
"Nun",
|
||||
"Office Lady",
|
||||
"Oiran",
|
||||
"Ojou-sama",
|
||||
"Orphan",
|
||||
"Pirates",
|
||||
"Robots",
|
||||
"Samurai",
|
||||
"Shrine Maiden",
|
||||
"Skeleton",
|
||||
"Succubus",
|
||||
"Tanned Skin",
|
||||
"Teacher",
|
||||
"Tomboy",
|
||||
"Transgender",
|
||||
"Tsundere",
|
||||
"Twins",
|
||||
"Vampire",
|
||||
"Veterinarian",
|
||||
"Vikings",
|
||||
"Villainess",
|
||||
"VTuber",
|
||||
"Werewolf",
|
||||
"Witch",
|
||||
"Yandere",
|
||||
"Zombie",
|
||||
],
|
||||
"Demographic": ["Josei", "Kids", "Seinen", "Shoujo", "Shounen"],
|
||||
"Setting": ["Matriarchy"],
|
||||
"Setting Scene": [
|
||||
"Bar",
|
||||
"Boarding School",
|
||||
"Circus",
|
||||
"Coastal",
|
||||
"College",
|
||||
"Desert",
|
||||
"Dungeon",
|
||||
"Foreign",
|
||||
"Inn",
|
||||
"Konbini",
|
||||
"Natural Disaster",
|
||||
"Office",
|
||||
"Outdoor",
|
||||
"Prison",
|
||||
"Restaurant",
|
||||
"Rural",
|
||||
"School",
|
||||
"School Club",
|
||||
"Snowscape",
|
||||
"Urban",
|
||||
"Work",
|
||||
],
|
||||
"Setting Time": [
|
||||
"Achronological Order",
|
||||
"Anachronism",
|
||||
"Ancient China",
|
||||
"Dystopian",
|
||||
"Historical",
|
||||
"Time Skip",
|
||||
],
|
||||
"Setting Universe": [
|
||||
"Afterlife",
|
||||
"Alternate Universe",
|
||||
"Augmented Reality",
|
||||
"Omegaverse",
|
||||
"Post-Apocalyptic",
|
||||
"Space",
|
||||
"Urban Fantasy",
|
||||
"Virtual World",
|
||||
],
|
||||
"Technical": [
|
||||
"4-koma",
|
||||
"Achromatic",
|
||||
"Advertisement",
|
||||
"Anthology",
|
||||
"CGI",
|
||||
"Episodic",
|
||||
"Flash",
|
||||
"Full CGI",
|
||||
"Full Color",
|
||||
"No Dialogue",
|
||||
"Non-fiction",
|
||||
"POV",
|
||||
"Puppetry",
|
||||
"Rotoscoping",
|
||||
"Stop Motion",
|
||||
],
|
||||
"Theme Action": [
|
||||
"Archery",
|
||||
"Battle Royale",
|
||||
"Espionage",
|
||||
"Fugitive",
|
||||
"Guns",
|
||||
"Martial Arts",
|
||||
"Spearplay",
|
||||
"Swordplay",
|
||||
],
|
||||
"Theme Arts": [
|
||||
"Acting",
|
||||
"Calligraphy",
|
||||
"Classic Literature",
|
||||
"Drawing",
|
||||
"Fashion",
|
||||
"Food",
|
||||
"Makeup",
|
||||
"Photography",
|
||||
"Rakugo",
|
||||
"Writing",
|
||||
],
|
||||
"Theme Arts-Music": [
|
||||
"Band",
|
||||
"Classical Music",
|
||||
"Dancing",
|
||||
"Hip-hop Music",
|
||||
"Jazz Music",
|
||||
"Metal Music",
|
||||
"Musical Theater",
|
||||
"Rock Music",
|
||||
],
|
||||
"Theme Comedy": ["Parody", "Satire", "Slapstick", "Surreal Comedy"],
|
||||
"Theme Drama": [
|
||||
"Bullying",
|
||||
"Class Struggle",
|
||||
"Coming of Age",
|
||||
"Conspiracy",
|
||||
"Eco-Horror",
|
||||
"Fake Relationship",
|
||||
"Kingdom Management",
|
||||
"Rehabilitation",
|
||||
"Revenge",
|
||||
"Suicide",
|
||||
"Tragedy",
|
||||
],
|
||||
"Theme Fantasy": [
|
||||
"Alchemy",
|
||||
"Body Swapping",
|
||||
"Cultivation",
|
||||
"Fairy Tale",
|
||||
"Henshin",
|
||||
"Isekai",
|
||||
"Kaiju",
|
||||
"Magic",
|
||||
"Mythology",
|
||||
"Necromancy",
|
||||
"Shapeshifting",
|
||||
"Steampunk",
|
||||
"Super Power",
|
||||
"Superhero",
|
||||
"Wuxia",
|
||||
"Youkai",
|
||||
],
|
||||
"Theme Game": ["Board Game", "E-Sports", "Video Games"],
|
||||
"Theme Game-Card & Board Game": [
|
||||
"Card Battle",
|
||||
"Go",
|
||||
"Karuta",
|
||||
"Mahjong",
|
||||
"Poker",
|
||||
"Shogi",
|
||||
],
|
||||
"Theme Game-Sport": [
|
||||
"Acrobatics",
|
||||
"Airsoft",
|
||||
"American Football",
|
||||
"Athletics",
|
||||
"Badminton",
|
||||
"Baseball",
|
||||
"Basketball",
|
||||
"Bowling",
|
||||
"Boxing",
|
||||
"Cheerleading",
|
||||
"Cycling",
|
||||
"Fencing",
|
||||
"Fishing",
|
||||
"Fitness",
|
||||
"Football",
|
||||
"Golf",
|
||||
"Handball",
|
||||
"Ice Skating",
|
||||
"Judo",
|
||||
"Lacrosse",
|
||||
"Parkour",
|
||||
"Rugby",
|
||||
"Scuba Diving",
|
||||
"Skateboarding",
|
||||
"Sumo",
|
||||
"Surfing",
|
||||
"Swimming",
|
||||
"Table Tennis",
|
||||
"Tennis",
|
||||
"Volleyball",
|
||||
"Wrestling",
|
||||
],
|
||||
"Theme Other": [
|
||||
"Adoption",
|
||||
"Animals",
|
||||
"Astronomy",
|
||||
"Autobiographical",
|
||||
"Biographical",
|
||||
"Body Horror",
|
||||
"Cannibalism",
|
||||
"Chibi",
|
||||
"Cosmic Horror",
|
||||
"Crime",
|
||||
"Crossover",
|
||||
"Death Game",
|
||||
"Denpa",
|
||||
"Drugs",
|
||||
"Economics",
|
||||
"Educational",
|
||||
"Environmental",
|
||||
"Ero Guro",
|
||||
"Filmmaking",
|
||||
"Found Family",
|
||||
"Gambling",
|
||||
"Gender Bending",
|
||||
"Gore",
|
||||
"Language Barrier",
|
||||
"LGBTQ+ Themes",
|
||||
"Lost Civilization",
|
||||
"Marriage",
|
||||
"Medicine",
|
||||
"Memory Manipulation",
|
||||
"Meta",
|
||||
"Mountaineering",
|
||||
"Noir",
|
||||
"Otaku Culture",
|
||||
"Pandemic",
|
||||
"Philosophy",
|
||||
"Politics",
|
||||
"Proxy Battle",
|
||||
"Psychosexual",
|
||||
"Reincarnation",
|
||||
"Religion",
|
||||
"Royal Affairs",
|
||||
"Slavery",
|
||||
"Software Development",
|
||||
"Survival",
|
||||
"Terrorism",
|
||||
"Torture",
|
||||
"Travel",
|
||||
"War",
|
||||
],
|
||||
"Theme Other-Organisations": [
|
||||
"Assassins",
|
||||
"Criminal Organization",
|
||||
"Cult",
|
||||
"Firefighters",
|
||||
"Gangs",
|
||||
"Mafia",
|
||||
"Military",
|
||||
"Police",
|
||||
"Triads",
|
||||
"Yakuza",
|
||||
],
|
||||
"Theme Other-Vehicle": [
|
||||
"Aviation",
|
||||
"Cars",
|
||||
"Mopeds",
|
||||
"Motorcycles",
|
||||
"Ships",
|
||||
"Tanks",
|
||||
"Trains",
|
||||
],
|
||||
"Theme Romance": [
|
||||
"Age Gap",
|
||||
"Bisexual",
|
||||
"Boys' Love",
|
||||
"Female Harem",
|
||||
"Heterosexual",
|
||||
"Love Triangle",
|
||||
"Male Harem",
|
||||
"Matchmaking",
|
||||
"Mixed Gender Harem",
|
||||
"Teens' Love",
|
||||
"Unrequited Love",
|
||||
"Yuri",
|
||||
],
|
||||
"Theme Sci Fi": [
|
||||
"Cyberpunk",
|
||||
"Space Opera",
|
||||
"Time Loop",
|
||||
"Time Manipulation",
|
||||
"Tokusatsu",
|
||||
],
|
||||
"Theme Sci Fi-Mecha": ["Real Robot", "Super Robot"],
|
||||
"Theme Slice of Life": [
|
||||
"Agriculture",
|
||||
"Cute Boys Doing Cute Things",
|
||||
"Cute Girls Doing Cute Things",
|
||||
"Family Life",
|
||||
"Horticulture",
|
||||
"Iyashikei",
|
||||
"Parenthood",
|
||||
],
|
||||
}
|
||||
tags_available_list = []
|
||||
for tag_category, tags_in_category in tags_available.items():
|
||||
tags_available_list.extend(tags_in_category)
|
||||
394
fastanime/cli/commands/anilist/download.py
Normal file
394
fastanime/cli/commands/anilist/download.py
Normal file
@@ -0,0 +1,394 @@
|
||||
import click
|
||||
|
||||
from ...completion_functions import anime_titles_shell_complete
|
||||
from .data import (
|
||||
genres_available,
|
||||
media_formats_available,
|
||||
media_statuses_available,
|
||||
seasons_available,
|
||||
sorts_available,
|
||||
tags_available_list,
|
||||
years_available,
|
||||
)
|
||||
|
||||
|
||||
@click.command(
|
||||
help="download anime using anilists api to get the titles",
|
||||
short_help="download anime with anilist intergration",
|
||||
)
|
||||
@click.option("--title", "-t", shell_complete=anime_titles_shell_complete)
|
||||
@click.option(
|
||||
"--season",
|
||||
help="The season the media was released",
|
||||
type=click.Choice(seasons_available),
|
||||
)
|
||||
@click.option(
|
||||
"--status",
|
||||
"-S",
|
||||
help="The media status of the anime",
|
||||
multiple=True,
|
||||
type=click.Choice(media_statuses_available),
|
||||
)
|
||||
@click.option(
|
||||
"--sort",
|
||||
"-s",
|
||||
help="What to sort the search results on",
|
||||
type=click.Choice(sorts_available),
|
||||
)
|
||||
@click.option(
|
||||
"--genres",
|
||||
"-g",
|
||||
multiple=True,
|
||||
help="the genres to filter by",
|
||||
type=click.Choice(genres_available),
|
||||
)
|
||||
@click.option(
|
||||
"--tags",
|
||||
"-T",
|
||||
multiple=True,
|
||||
help="the tags to filter by",
|
||||
type=click.Choice(tags_available_list),
|
||||
)
|
||||
@click.option(
|
||||
"--media-format",
|
||||
"-f",
|
||||
multiple=True,
|
||||
help="Media format",
|
||||
type=click.Choice(media_formats_available),
|
||||
)
|
||||
@click.option(
|
||||
"--year",
|
||||
"-y",
|
||||
type=click.Choice(years_available),
|
||||
help="the year the media was released",
|
||||
)
|
||||
@click.option(
|
||||
"--on-list/--not-on-list",
|
||||
"-L/-no-L",
|
||||
help="Whether the anime should be in your list or not",
|
||||
type=bool,
|
||||
)
|
||||
@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.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=60,
|
||||
)
|
||||
@click.option(
|
||||
"--prompt/--no-prompt",
|
||||
help="Whether to prompt for anything instead just do the best thing",
|
||||
default=True,
|
||||
)
|
||||
@click.option(
|
||||
"--force-ffmpeg",
|
||||
is_flag=True,
|
||||
help="Force the use of FFmpeg for downloading (supports large variety of streams but slower)",
|
||||
)
|
||||
@click.option(
|
||||
"--hls-use-mpegts",
|
||||
is_flag=True,
|
||||
help="Use mpegts for hls streams (useful for some streams: see Docs) (this option forces --force-ffmpeg to be True)",
|
||||
)
|
||||
@click.option(
|
||||
"--max-results", "-M", type=int, help="The maximum number of results to show"
|
||||
)
|
||||
@click.pass_obj
|
||||
def download(
|
||||
config,
|
||||
title,
|
||||
season,
|
||||
status,
|
||||
sort,
|
||||
genres,
|
||||
tags,
|
||||
media_format,
|
||||
year,
|
||||
on_list,
|
||||
episode_range,
|
||||
force_unknown_ext,
|
||||
silent,
|
||||
verbose,
|
||||
merge,
|
||||
clean,
|
||||
wait_time,
|
||||
prompt,
|
||||
force_ffmpeg,
|
||||
hls_use_mpegts,
|
||||
max_results,
|
||||
):
|
||||
from rich import print
|
||||
|
||||
from ....anilist import AniList
|
||||
|
||||
force_ffmpeg |= hls_use_mpegts
|
||||
|
||||
success, anilist_search_results = AniList.search(
|
||||
query=title,
|
||||
sort=sort,
|
||||
status_in=list(status),
|
||||
genre_in=list(genres),
|
||||
season=season,
|
||||
tag_in=list(tags),
|
||||
seasonYear=year,
|
||||
format_in=list(media_format),
|
||||
on_list=on_list,
|
||||
max_results=max_results,
|
||||
)
|
||||
if success:
|
||||
import time
|
||||
|
||||
from rich.progress import Progress
|
||||
from thefuzz import fuzz
|
||||
|
||||
from ....AnimeProvider import AnimeProvider
|
||||
from ....libs.anime_provider.types import Anime
|
||||
from ....libs.fzf import fzf
|
||||
from ....Utility.data import anime_normalizer
|
||||
from ....Utility.downloader.downloader import downloader
|
||||
from ...utils.tools import exit_app
|
||||
from ...utils.utils import (
|
||||
filter_by_quality,
|
||||
fuzzy_inquirer,
|
||||
move_preferred_subtitle_lang_to_top,
|
||||
)
|
||||
|
||||
anime_provider = AnimeProvider(config.provider)
|
||||
anilist_anime_info = None
|
||||
|
||||
translation_type = config.translation_type
|
||||
download_dir = config.downloads_dir
|
||||
anime_titles = [
|
||||
(anime["title"]["romaji"] or anime["title"]["english"])
|
||||
for anime in anilist_search_results["data"]["Page"]["media"]
|
||||
]
|
||||
print(f"[green bold]Queued:[/] {anime_titles}")
|
||||
for i, anime_title in enumerate(anime_titles):
|
||||
print(f"[green bold]Now Downloading: [/] {anime_title}")
|
||||
# ---- search for anime ----
|
||||
with Progress() as progress:
|
||||
progress.add_task("Fetching Search Results...", total=None)
|
||||
search_results = anime_provider.search_for_anime(
|
||||
anime_title, translation_type=translation_type
|
||||
)
|
||||
if not search_results:
|
||||
print(
|
||||
"No search results found from provider for {}".format(anime_title)
|
||||
)
|
||||
continue
|
||||
search_results = search_results["results"]
|
||||
if not search_results:
|
||||
print("Nothing muches your search term")
|
||||
continue
|
||||
search_results_ = {
|
||||
search_result["title"]: search_result
|
||||
for search_result in search_results
|
||||
}
|
||||
|
||||
if config.auto_select:
|
||||
selected_anime_title = max(
|
||||
search_results_.keys(),
|
||||
key=lambda title: fuzz.ratio(
|
||||
anime_normalizer.get(title, title), anime_title
|
||||
),
|
||||
)
|
||||
print("[cyan]Auto selecting:[/] ", selected_anime_title)
|
||||
else:
|
||||
choices = list(search_results_.keys())
|
||||
if config.use_fzf:
|
||||
selected_anime_title = fzf.run(
|
||||
choices, "Please Select title", "FastAnime"
|
||||
)
|
||||
else:
|
||||
selected_anime_title = fuzzy_inquirer(
|
||||
choices,
|
||||
"Please Select title",
|
||||
)
|
||||
|
||||
# ---- fetch anime ----
|
||||
with Progress() as progress:
|
||||
progress.add_task("Fetching Anime...", total=None)
|
||||
anime: Anime | None = anime_provider.get_anime(
|
||||
search_results_[selected_anime_title]["id"]
|
||||
)
|
||||
if not anime:
|
||||
print("Failed to fetch anime {}".format(selected_anime_title))
|
||||
continue
|
||||
|
||||
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(":")
|
||||
if len(ep_range_tuple) == 2 and all(ep_range_tuple):
|
||||
episodes_start, episodes_end = ep_range_tuple
|
||||
episodes_range = episodes[
|
||||
int(episodes_start) : int(episodes_end)
|
||||
]
|
||||
elif len(ep_range_tuple) == 3 and all(ep_range_tuple):
|
||||
episodes_start, episodes_end, step = ep_range_tuple
|
||||
episodes_range = episodes[
|
||||
int(episodes_start) : int(episodes_end) : int(step)
|
||||
]
|
||||
else:
|
||||
episodes_start, episodes_end = ep_range_tuple
|
||||
if episodes_start.strip():
|
||||
episodes_range = episodes[int(episodes_start) :]
|
||||
elif episodes_end.strip():
|
||||
episodes_range = episodes[: int(episodes_end)]
|
||||
else:
|
||||
episodes_range = episodes
|
||||
else:
|
||||
episodes_range = episodes[int(episode_range) :]
|
||||
print(f"[green bold]Downloading: [/] {episodes_range}")
|
||||
|
||||
else:
|
||||
episodes_range = sorted(episodes, key=float)
|
||||
|
||||
if config.normalize_titles:
|
||||
anilist_anime_info = anilist_search_results["data"]["Page"]["media"][i]
|
||||
|
||||
# lets download em
|
||||
for episode in episodes_range:
|
||||
try:
|
||||
episode = str(episode)
|
||||
if episode not in episodes:
|
||||
print(
|
||||
f"[cyan]Warning[/]: Episode {episode} not found, skipping"
|
||||
)
|
||||
continue
|
||||
with Progress() as progress:
|
||||
progress.add_task("Fetching Episode Streams...", total=None)
|
||||
streams = anime_provider.get_episode_streams(
|
||||
anime["id"], episode, config.translation_type
|
||||
)
|
||||
if not streams:
|
||||
print("No streams skipping")
|
||||
continue
|
||||
# ---- fetch servers ----
|
||||
if config.server == "top":
|
||||
with Progress() as progress:
|
||||
progress.add_task("Fetching top server...", total=None)
|
||||
server_name = next(streams, None)
|
||||
if not server_name:
|
||||
print("Sth went wrong when fetching the server")
|
||||
continue
|
||||
stream_link = filter_by_quality(
|
||||
config.quality, server_name["links"]
|
||||
)
|
||||
if not stream_link:
|
||||
print("[yellow bold]WARNING:[/] No streams found")
|
||||
time.sleep(1)
|
||||
print("Continuing...")
|
||||
continue
|
||||
link = stream_link["link"]
|
||||
provider_headers = server_name["headers"]
|
||||
episode_title = server_name["episode_title"]
|
||||
subtitles = server_name["subtitles"]
|
||||
else:
|
||||
with Progress() as progress:
|
||||
progress.add_task("Fetching servers", total=None)
|
||||
# prompt for server selection
|
||||
servers = {server["server"]: server for server in streams}
|
||||
servers_names = list(servers.keys())
|
||||
if config.server in servers_names:
|
||||
server_name = config.server
|
||||
else:
|
||||
if config.use_fzf:
|
||||
server_name = fzf.run(servers_names, "Select an link")
|
||||
else:
|
||||
server_name = fuzzy_inquirer(
|
||||
servers_names,
|
||||
"Select link",
|
||||
)
|
||||
stream_link = filter_by_quality(
|
||||
config.quality, servers[server_name]["links"]
|
||||
)
|
||||
if not stream_link:
|
||||
print("[yellow bold]WARNING:[/] No streams found")
|
||||
time.sleep(1)
|
||||
print("Continuing...")
|
||||
continue
|
||||
link = stream_link["link"]
|
||||
provider_headers = servers[server_name]["headers"]
|
||||
|
||||
subtitles = servers[server_name]["subtitles"]
|
||||
episode_title = servers[server_name]["episode_title"]
|
||||
|
||||
if anilist_anime_info:
|
||||
selected_anime_title = (
|
||||
anilist_anime_info["title"][config.preferred_language]
|
||||
or anilist_anime_info["title"]["romaji"]
|
||||
or anilist_anime_info["title"]["english"]
|
||||
)
|
||||
import re
|
||||
|
||||
for episode_detail in anilist_anime_info["streamingEpisodes"]:
|
||||
if re.match(
|
||||
f".*Episode {episode} .*", episode_detail["title"]
|
||||
):
|
||||
episode_title = episode_detail["title"]
|
||||
break
|
||||
print(f"[purple]Now Downloading:[/] {episode_title}")
|
||||
subtitles = move_preferred_subtitle_lang_to_top(
|
||||
subtitles, config.sub_lang
|
||||
)
|
||||
downloader._download_file(
|
||||
link,
|
||||
selected_anime_title,
|
||||
episode_title,
|
||||
download_dir,
|
||||
silent,
|
||||
vid_format=config.format,
|
||||
force_unknown_ext=force_unknown_ext,
|
||||
verbose=verbose,
|
||||
headers=provider_headers,
|
||||
sub=subtitles[0]["url"] if subtitles else "",
|
||||
merge=merge,
|
||||
clean=clean,
|
||||
prompt=prompt,
|
||||
force_ffmpeg=force_ffmpeg,
|
||||
hls_use_mpegts=hls_use_mpegts,
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
time.sleep(1)
|
||||
print("Continuing...")
|
||||
print("Done Downloading")
|
||||
time.sleep(wait_time)
|
||||
exit_app()
|
||||
else:
|
||||
from sys import exit
|
||||
|
||||
print("Failed to search for anime", anilist_search_results)
|
||||
exit(1)
|
||||
358
fastanime/cli/commands/anilist/downloads.py
Normal file
358
fastanime/cli/commands/anilist/downloads.py
Normal file
@@ -0,0 +1,358 @@
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import click
|
||||
|
||||
from ...completion_functions import downloaded_anime_titles
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
if TYPE_CHECKING:
|
||||
from ..config import Config
|
||||
|
||||
|
||||
@click.command(
|
||||
help="View and watch your downloads using mpv",
|
||||
short_help="Watch downloads",
|
||||
epilog="""
|
||||
\b
|
||||
\b\bExamples:
|
||||
fastanime downloads
|
||||
\b
|
||||
# view individual episodes
|
||||
fastanime downloads --view-episodes
|
||||
# --- or ---
|
||||
fastanime downloads -v
|
||||
\b
|
||||
# to set seek time when using ffmpegthumbnailer for local previews
|
||||
# -1 means random and is the default
|
||||
fastanime downloads --time-to-seek <intRange(-1,100)>
|
||||
# --- or ---
|
||||
fastanime downloads -t <intRange(-1,100)>
|
||||
\b
|
||||
# to watch a specific title
|
||||
# be sure to get the completions for the best experience
|
||||
fastanime downloads --title <title>
|
||||
\b
|
||||
# to get the path to the downloads folder set
|
||||
fastanime downloads --path
|
||||
# useful when you want to use the value for other programs
|
||||
""",
|
||||
)
|
||||
@click.option("--path", "-p", help="print the downloads folder and exit", is_flag=True)
|
||||
@click.option(
|
||||
"--title",
|
||||
"-T",
|
||||
shell_complete=downloaded_anime_titles,
|
||||
help="watch a specific title",
|
||||
)
|
||||
@click.option("--view-episodes", "-v", help="View individual episodes", is_flag=True)
|
||||
@click.option(
|
||||
"--ffmpegthumbnailer-seek-time",
|
||||
"--time-to-seek",
|
||||
"-t",
|
||||
type=click.IntRange(-1, 100),
|
||||
help="ffmpegthumbnailer seek time",
|
||||
)
|
||||
@click.pass_obj
|
||||
def downloads(
|
||||
config: "Config", path: bool, title, view_episodes, ffmpegthumbnailer_seek_time
|
||||
):
|
||||
import os
|
||||
|
||||
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
|
||||
|
||||
if not ffmpegthumbnailer_seek_time:
|
||||
ffmpegthumbnailer_seek_time = config.ffmpegthumbnailer_seek_time
|
||||
USER_VIDEOS_DIR = config.downloads_dir
|
||||
if path:
|
||||
print(USER_VIDEOS_DIR)
|
||||
return
|
||||
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.append("Exit")
|
||||
|
||||
def create_thumbnails(video_path, anime_title, downloads_thumbnail_cache_dir):
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
FFMPEG_THUMBNAILER = shutil.which("ffmpegthumbnailer")
|
||||
if not FFMPEG_THUMBNAILER:
|
||||
return
|
||||
|
||||
out = os.path.join(downloads_thumbnail_cache_dir, anime_title)
|
||||
if ffmpegthumbnailer_seek_time == -1:
|
||||
import random
|
||||
|
||||
seektime = str(random.randrange(0, 100))
|
||||
else:
|
||||
seektime = str(ffmpegthumbnailer_seek_time)
|
||||
_ = subprocess.run(
|
||||
[
|
||||
FFMPEG_THUMBNAILER,
|
||||
"-i",
|
||||
video_path,
|
||||
"-o",
|
||||
out,
|
||||
"-s",
|
||||
"0",
|
||||
"-t",
|
||||
seektime,
|
||||
],
|
||||
stderr=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
)
|
||||
|
||||
def get_previews_anime(workers=None, bg=True):
|
||||
import concurrent.futures
|
||||
import random
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
if not shutil.which("ffmpegthumbnailer"):
|
||||
print("ffmpegthumbnailer not found")
|
||||
logger.error("ffmpegthumbnailer not found")
|
||||
return
|
||||
|
||||
from ....constants import APP_CACHE_DIR
|
||||
from ...utils.scripts import fzf_preview
|
||||
|
||||
downloads_thumbnail_cache_dir = os.path.join(APP_CACHE_DIR, "video_thumbnails")
|
||||
Path(downloads_thumbnail_cache_dir).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def _worker():
|
||||
# use concurrency to download the images as fast as possible
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
|
||||
# load the jobs
|
||||
future_to_url = {}
|
||||
for anime_title in anime_downloads:
|
||||
anime_path = os.path.join(USER_VIDEOS_DIR, anime_title)
|
||||
if not os.path.isdir(anime_path):
|
||||
continue
|
||||
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, random.choice(playlist))
|
||||
future_to_url[
|
||||
executor.submit(
|
||||
create_thumbnails,
|
||||
video_path,
|
||||
anime_title,
|
||||
downloads_thumbnail_cache_dir,
|
||||
)
|
||||
] = anime_title
|
||||
|
||||
# execute the jobs
|
||||
for future in concurrent.futures.as_completed(future_to_url):
|
||||
url = future_to_url[future]
|
||||
try:
|
||||
future.result()
|
||||
except Exception as e:
|
||||
logger.error("%r generated an exception: %s" % (url, e))
|
||||
|
||||
if bg:
|
||||
from threading import Thread
|
||||
|
||||
worker = Thread(target=_worker)
|
||||
worker.daemon = True
|
||||
worker.start()
|
||||
else:
|
||||
_worker()
|
||||
os.environ["SHELL"] = shutil.which("bash") or "bash"
|
||||
preview = """
|
||||
%s
|
||||
if [ -s %s/{} ]; then
|
||||
if ! fzf-preview %s/{} 2>/dev/null; then
|
||||
echo Loading...
|
||||
fi
|
||||
else echo Loading...
|
||||
fi
|
||||
""" % (
|
||||
fzf_preview,
|
||||
downloads_thumbnail_cache_dir,
|
||||
downloads_thumbnail_cache_dir,
|
||||
)
|
||||
return preview
|
||||
|
||||
def get_previews_episodes(anime_playlist_path, workers=None, bg=True):
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from ....constants import APP_CACHE_DIR
|
||||
from ...utils.scripts import fzf_preview
|
||||
|
||||
if not shutil.which("ffmpegthumbnailer"):
|
||||
print("ffmpegthumbnailer not found")
|
||||
logger.error("ffmpegthumbnailer not found")
|
||||
return
|
||||
|
||||
downloads_thumbnail_cache_dir = os.path.join(APP_CACHE_DIR, "video_thumbnails")
|
||||
Path(downloads_thumbnail_cache_dir).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def _worker():
|
||||
import concurrent.futures
|
||||
|
||||
# use concurrency to download the images as fast as possible
|
||||
# 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), key=sort_by_episode_number
|
||||
)
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
|
||||
# load the jobs
|
||||
future_to_url = {}
|
||||
for episode_title in anime_episodes:
|
||||
episode_path = os.path.join(anime_playlist_path, episode_title)
|
||||
|
||||
# actual link to download image from
|
||||
future_to_url[
|
||||
executor.submit(
|
||||
create_thumbnails,
|
||||
episode_path,
|
||||
episode_title,
|
||||
downloads_thumbnail_cache_dir,
|
||||
)
|
||||
] = episode_title
|
||||
|
||||
# execute the jobs
|
||||
for future in concurrent.futures.as_completed(future_to_url):
|
||||
url = future_to_url[future]
|
||||
try:
|
||||
future.result()
|
||||
except Exception as e:
|
||||
logger.error("%r generated an exception: %s" % (url, e))
|
||||
|
||||
if bg:
|
||||
from threading import Thread
|
||||
|
||||
worker = Thread(target=_worker)
|
||||
worker.daemon = True
|
||||
worker.start()
|
||||
else:
|
||||
_worker()
|
||||
os.environ["SHELL"] = shutil.which("bash") or "bash"
|
||||
preview = """
|
||||
%s
|
||||
if [ -s %s/{} ]; then
|
||||
if ! fzf-preview %s/{} 2>/dev/null; then
|
||||
echo Loading...
|
||||
fi
|
||||
else echo Loading...
|
||||
fi
|
||||
""" % (
|
||||
fzf_preview,
|
||||
downloads_thumbnail_cache_dir,
|
||||
downloads_thumbnail_cache_dir,
|
||||
)
|
||||
return preview
|
||||
|
||||
def stream_episode(
|
||||
anime_playlist_path,
|
||||
):
|
||||
if view_episodes:
|
||||
if not os.path.isdir(anime_playlist_path):
|
||||
print(anime_playlist_path, "is not dir")
|
||||
exit_app(1)
|
||||
return
|
||||
episodes = sorted(
|
||||
os.listdir(anime_playlist_path), key=sort_by_episode_number
|
||||
)
|
||||
downloaded_episodes = [*episodes, "Back"]
|
||||
|
||||
if config.use_fzf:
|
||||
if not config.preview:
|
||||
episode_title = fzf.run(
|
||||
downloaded_episodes,
|
||||
"Enter Episode ",
|
||||
)
|
||||
else:
|
||||
preview = get_previews_episodes(anime_playlist_path)
|
||||
episode_title = fzf.run(
|
||||
downloaded_episodes,
|
||||
"Enter Episode ",
|
||||
preview=preview,
|
||||
)
|
||||
elif config.use_rofi:
|
||||
episode_title = Rofi.run(downloaded_episodes, "Enter Episode")
|
||||
else:
|
||||
episode_title = fuzzy_inquirer(
|
||||
downloaded_episodes,
|
||||
"Enter Playlist Name",
|
||||
)
|
||||
if episode_title == "Back":
|
||||
stream_anime()
|
||||
return
|
||||
episode_path = os.path.join(anime_playlist_path, episode_title)
|
||||
if config.sync_play:
|
||||
from ...utils.syncplay import SyncPlayer
|
||||
|
||||
SyncPlayer(episode_path)
|
||||
else:
|
||||
run_mpv(
|
||||
episode_path,
|
||||
player=config.player,
|
||||
)
|
||||
stream_episode(anime_playlist_path)
|
||||
|
||||
def stream_anime(title=None):
|
||||
if title:
|
||||
from thefuzz import fuzz
|
||||
|
||||
playlist_name = max(anime_downloads, key=lambda t: fuzz.ratio(title, t))
|
||||
elif config.use_fzf:
|
||||
if not config.preview:
|
||||
playlist_name = fzf.run(
|
||||
anime_downloads,
|
||||
"Enter Playlist Name",
|
||||
)
|
||||
else:
|
||||
preview = get_previews_anime()
|
||||
playlist_name = fzf.run(
|
||||
anime_downloads,
|
||||
"Enter Playlist Name",
|
||||
preview=preview,
|
||||
)
|
||||
elif config.use_rofi:
|
||||
playlist_name = Rofi.run(anime_downloads, "Enter Playlist Name")
|
||||
else:
|
||||
playlist_name = fuzzy_inquirer(
|
||||
anime_downloads,
|
||||
"Enter Playlist Name",
|
||||
)
|
||||
if playlist_name == "Exit":
|
||||
exit_app()
|
||||
return
|
||||
playlist = os.path.join(USER_VIDEOS_DIR, playlist_name)
|
||||
if view_episodes:
|
||||
stream_episode(
|
||||
playlist,
|
||||
)
|
||||
else:
|
||||
if config.sync_play:
|
||||
from ...utils.syncplay import SyncPlayer
|
||||
|
||||
SyncPlayer(playlist)
|
||||
else:
|
||||
run_mpv(
|
||||
playlist,
|
||||
player=config.player,
|
||||
)
|
||||
stream_anime()
|
||||
|
||||
stream_anime(title)
|
||||
@@ -42,5 +42,12 @@ def dropped(config: "Config", dump_json):
|
||||
from ...utils.tools import FastAnimeRuntimeState
|
||||
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
|
||||
fastanime_runtime_state.current_page = 1
|
||||
fastanime_runtime_state.current_data_loader = (
|
||||
lambda config, **kwargs: anilist_interfaces._handle_animelist(
|
||||
config, fastanime_runtime_state, "Dropped", **kwargs
|
||||
)
|
||||
)
|
||||
fastanime_runtime_state.anilist_results_data = anime_list[1]
|
||||
anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state)
|
||||
|
||||
@@ -26,6 +26,9 @@ def favourites(config, dump_json):
|
||||
from ...utils.tools import FastAnimeRuntimeState
|
||||
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
|
||||
fastanime_runtime_state.current_page = 1
|
||||
fastanime_runtime_state.current_data_loader = AniList.get_most_favourite
|
||||
fastanime_runtime_state.anilist_results_data = anime_data[1]
|
||||
anilist_results_menu(config, fastanime_runtime_state)
|
||||
else:
|
||||
|
||||
@@ -41,6 +41,12 @@ def paused(config: "Config", dump_json):
|
||||
from ...interfaces import anilist_interfaces
|
||||
from ...utils.tools import FastAnimeRuntimeState
|
||||
|
||||
anilist_config = FastAnimeRuntimeState()
|
||||
anilist_config.anilist_results_data = anime_list[1]
|
||||
anilist_interfaces.anilist_results_menu(config, anilist_config)
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
fastanime_runtime_state.current_page = 1
|
||||
fastanime_runtime_state.current_data_loader = (
|
||||
lambda config, **kwargs: anilist_interfaces._handle_animelist(
|
||||
config, fastanime_runtime_state, "Paused", **kwargs
|
||||
)
|
||||
)
|
||||
fastanime_runtime_state.anilist_results_data = anime_list[1]
|
||||
anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state)
|
||||
|
||||
@@ -42,5 +42,12 @@ def planning(config: "Config", dump_json):
|
||||
from ...utils.tools import FastAnimeRuntimeState
|
||||
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
|
||||
fastanime_runtime_state.current_page = 1
|
||||
fastanime_runtime_state.current_data_loader = (
|
||||
lambda config, **kwargs: anilist_interfaces._handle_animelist(
|
||||
config, fastanime_runtime_state, "Planned", **kwargs
|
||||
)
|
||||
)
|
||||
fastanime_runtime_state.anilist_results_data = anime_list[1]
|
||||
anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state)
|
||||
|
||||
@@ -25,6 +25,9 @@ def popular(config, dump_json):
|
||||
from ...utils.tools import FastAnimeRuntimeState
|
||||
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
|
||||
fastanime_runtime_state.current_page = 1
|
||||
fastanime_runtime_state.current_data_loader = AniList.get_most_popular
|
||||
fastanime_runtime_state.anilist_results_data = anime_data[1]
|
||||
anilist_results_menu(config, fastanime_runtime_state)
|
||||
else:
|
||||
|
||||
@@ -26,6 +26,11 @@ def recent(config, dump_json):
|
||||
from ...utils.tools import FastAnimeRuntimeState
|
||||
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
|
||||
fastanime_runtime_state.current_page = 1
|
||||
fastanime_runtime_state.current_data_loader = (
|
||||
AniList.get_most_recently_updated
|
||||
)
|
||||
fastanime_runtime_state.anilist_results_data = anime_data[1]
|
||||
anilist_results_menu(config, fastanime_runtime_state)
|
||||
else:
|
||||
|
||||
@@ -42,5 +42,12 @@ def rewatching(config: "Config", dump_json):
|
||||
from ...utils.tools import FastAnimeRuntimeState
|
||||
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
|
||||
fastanime_runtime_state.current_page = 1
|
||||
fastanime_runtime_state.current_data_loader = (
|
||||
lambda config, **kwargs: anilist_interfaces._handle_animelist(
|
||||
config, fastanime_runtime_state, "Rewatching", **kwargs
|
||||
)
|
||||
)
|
||||
fastanime_runtime_state.anilist_results_data = anime_list[1]
|
||||
anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state)
|
||||
|
||||
@@ -25,6 +25,9 @@ def scores(config, dump_json):
|
||||
from ...utils.tools import FastAnimeRuntimeState
|
||||
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
|
||||
fastanime_runtime_state.current_page = 1
|
||||
fastanime_runtime_state.current_data_loader = AniList.get_most_scored
|
||||
fastanime_runtime_state.anilist_results_data = anime_data[1]
|
||||
anilist_results_menu(config, fastanime_runtime_state)
|
||||
else:
|
||||
|
||||
@@ -1,369 +1,15 @@
|
||||
import click
|
||||
|
||||
from ...completion_functions import anime_titles_shell_complete
|
||||
|
||||
tags_available = {
|
||||
"Cast": ["Polyamorous"],
|
||||
"Cast Main Cast": [
|
||||
"Anti-Hero",
|
||||
"Elderly Protagonist",
|
||||
"Ensemble Cast",
|
||||
"Estranged Family",
|
||||
"Female Protagonist",
|
||||
"Male Protagonist",
|
||||
"Primarily Adult Cast",
|
||||
"Primarily Animal Cast",
|
||||
"Primarily Child Cast",
|
||||
"Primarily Female Cast",
|
||||
"Primarily Male Cast",
|
||||
"Primarily Teen Cast",
|
||||
],
|
||||
"Cast Traits": [
|
||||
"Age Regression",
|
||||
"Agender",
|
||||
"Aliens",
|
||||
"Amnesia",
|
||||
"Angels",
|
||||
"Anthropomorphism",
|
||||
"Aromantic",
|
||||
"Arranged Marriage",
|
||||
"Artificial Intelligence",
|
||||
"Asexual",
|
||||
"Butler",
|
||||
"Centaur",
|
||||
"Chimera",
|
||||
"Chuunibyou",
|
||||
"Clone",
|
||||
"Cosplay",
|
||||
"Cowboys",
|
||||
"Crossdressing",
|
||||
"Cyborg",
|
||||
"Delinquents",
|
||||
"Demons",
|
||||
"Detective",
|
||||
"Dinosaurs",
|
||||
"Disability",
|
||||
"Dissociative Identities",
|
||||
"Dragons",
|
||||
"Dullahan",
|
||||
"Elf",
|
||||
"Fairy",
|
||||
"Femboy",
|
||||
"Ghost",
|
||||
"Goblin",
|
||||
"Gods",
|
||||
"Gyaru",
|
||||
"Hikikomori",
|
||||
"Homeless",
|
||||
"Idol",
|
||||
"Kemonomimi",
|
||||
"Kuudere",
|
||||
"Maids",
|
||||
"Mermaid",
|
||||
"Monster Boy",
|
||||
"Monster Girl",
|
||||
"Nekomimi",
|
||||
"Ninja",
|
||||
"Nudity",
|
||||
"Nun",
|
||||
"Office Lady",
|
||||
"Oiran",
|
||||
"Ojou-sama",
|
||||
"Orphan",
|
||||
"Pirates",
|
||||
"Robots",
|
||||
"Samurai",
|
||||
"Shrine Maiden",
|
||||
"Skeleton",
|
||||
"Succubus",
|
||||
"Tanned Skin",
|
||||
"Teacher",
|
||||
"Tomboy",
|
||||
"Transgender",
|
||||
"Tsundere",
|
||||
"Twins",
|
||||
"Vampire",
|
||||
"Veterinarian",
|
||||
"Vikings",
|
||||
"Villainess",
|
||||
"VTuber",
|
||||
"Werewolf",
|
||||
"Witch",
|
||||
"Yandere",
|
||||
"Zombie",
|
||||
],
|
||||
"Demographic": ["Josei", "Kids", "Seinen", "Shoujo", "Shounen"],
|
||||
"Setting": ["Matriarchy"],
|
||||
"Setting Scene": [
|
||||
"Bar",
|
||||
"Boarding School",
|
||||
"Circus",
|
||||
"Coastal",
|
||||
"College",
|
||||
"Desert",
|
||||
"Dungeon",
|
||||
"Foreign",
|
||||
"Inn",
|
||||
"Konbini",
|
||||
"Natural Disaster",
|
||||
"Office",
|
||||
"Outdoor",
|
||||
"Prison",
|
||||
"Restaurant",
|
||||
"Rural",
|
||||
"School",
|
||||
"School Club",
|
||||
"Snowscape",
|
||||
"Urban",
|
||||
"Work",
|
||||
],
|
||||
"Setting Time": [
|
||||
"Achronological Order",
|
||||
"Anachronism",
|
||||
"Ancient China",
|
||||
"Dystopian",
|
||||
"Historical",
|
||||
"Time Skip",
|
||||
],
|
||||
"Setting Universe": [
|
||||
"Afterlife",
|
||||
"Alternate Universe",
|
||||
"Augmented Reality",
|
||||
"Omegaverse",
|
||||
"Post-Apocalyptic",
|
||||
"Space",
|
||||
"Urban Fantasy",
|
||||
"Virtual World",
|
||||
],
|
||||
"Technical": [
|
||||
"4-koma",
|
||||
"Achromatic",
|
||||
"Advertisement",
|
||||
"Anthology",
|
||||
"CGI",
|
||||
"Episodic",
|
||||
"Flash",
|
||||
"Full CGI",
|
||||
"Full Color",
|
||||
"No Dialogue",
|
||||
"Non-fiction",
|
||||
"POV",
|
||||
"Puppetry",
|
||||
"Rotoscoping",
|
||||
"Stop Motion",
|
||||
],
|
||||
"Theme Action": [
|
||||
"Archery",
|
||||
"Battle Royale",
|
||||
"Espionage",
|
||||
"Fugitive",
|
||||
"Guns",
|
||||
"Martial Arts",
|
||||
"Spearplay",
|
||||
"Swordplay",
|
||||
],
|
||||
"Theme Arts": [
|
||||
"Acting",
|
||||
"Calligraphy",
|
||||
"Classic Literature",
|
||||
"Drawing",
|
||||
"Fashion",
|
||||
"Food",
|
||||
"Makeup",
|
||||
"Photography",
|
||||
"Rakugo",
|
||||
"Writing",
|
||||
],
|
||||
"Theme Arts-Music": [
|
||||
"Band",
|
||||
"Classical Music",
|
||||
"Dancing",
|
||||
"Hip-hop Music",
|
||||
"Jazz Music",
|
||||
"Metal Music",
|
||||
"Musical Theater",
|
||||
"Rock Music",
|
||||
],
|
||||
"Theme Comedy": ["Parody", "Satire", "Slapstick", "Surreal Comedy"],
|
||||
"Theme Drama": [
|
||||
"Bullying",
|
||||
"Class Struggle",
|
||||
"Coming of Age",
|
||||
"Conspiracy",
|
||||
"Eco-Horror",
|
||||
"Fake Relationship",
|
||||
"Kingdom Management",
|
||||
"Rehabilitation",
|
||||
"Revenge",
|
||||
"Suicide",
|
||||
"Tragedy",
|
||||
],
|
||||
"Theme Fantasy": [
|
||||
"Alchemy",
|
||||
"Body Swapping",
|
||||
"Cultivation",
|
||||
"Fairy Tale",
|
||||
"Henshin",
|
||||
"Isekai",
|
||||
"Kaiju",
|
||||
"Magic",
|
||||
"Mythology",
|
||||
"Necromancy",
|
||||
"Shapeshifting",
|
||||
"Steampunk",
|
||||
"Super Power",
|
||||
"Superhero",
|
||||
"Wuxia",
|
||||
"Youkai",
|
||||
],
|
||||
"Theme Game": ["Board Game", "E-Sports", "Video Games"],
|
||||
"Theme Game-Card & Board Game": [
|
||||
"Card Battle",
|
||||
"Go",
|
||||
"Karuta",
|
||||
"Mahjong",
|
||||
"Poker",
|
||||
"Shogi",
|
||||
],
|
||||
"Theme Game-Sport": [
|
||||
"Acrobatics",
|
||||
"Airsoft",
|
||||
"American Football",
|
||||
"Athletics",
|
||||
"Badminton",
|
||||
"Baseball",
|
||||
"Basketball",
|
||||
"Bowling",
|
||||
"Boxing",
|
||||
"Cheerleading",
|
||||
"Cycling",
|
||||
"Fencing",
|
||||
"Fishing",
|
||||
"Fitness",
|
||||
"Football",
|
||||
"Golf",
|
||||
"Handball",
|
||||
"Ice Skating",
|
||||
"Judo",
|
||||
"Lacrosse",
|
||||
"Parkour",
|
||||
"Rugby",
|
||||
"Scuba Diving",
|
||||
"Skateboarding",
|
||||
"Sumo",
|
||||
"Surfing",
|
||||
"Swimming",
|
||||
"Table Tennis",
|
||||
"Tennis",
|
||||
"Volleyball",
|
||||
"Wrestling",
|
||||
],
|
||||
"Theme Other": [
|
||||
"Adoption",
|
||||
"Animals",
|
||||
"Astronomy",
|
||||
"Autobiographical",
|
||||
"Biographical",
|
||||
"Body Horror",
|
||||
"Cannibalism",
|
||||
"Chibi",
|
||||
"Cosmic Horror",
|
||||
"Crime",
|
||||
"Crossover",
|
||||
"Death Game",
|
||||
"Denpa",
|
||||
"Drugs",
|
||||
"Economics",
|
||||
"Educational",
|
||||
"Environmental",
|
||||
"Ero Guro",
|
||||
"Filmmaking",
|
||||
"Found Family",
|
||||
"Gambling",
|
||||
"Gender Bending",
|
||||
"Gore",
|
||||
"Language Barrier",
|
||||
"LGBTQ+ Themes",
|
||||
"Lost Civilization",
|
||||
"Marriage",
|
||||
"Medicine",
|
||||
"Memory Manipulation",
|
||||
"Meta",
|
||||
"Mountaineering",
|
||||
"Noir",
|
||||
"Otaku Culture",
|
||||
"Pandemic",
|
||||
"Philosophy",
|
||||
"Politics",
|
||||
"Proxy Battle",
|
||||
"Psychosexual",
|
||||
"Reincarnation",
|
||||
"Religion",
|
||||
"Royal Affairs",
|
||||
"Slavery",
|
||||
"Software Development",
|
||||
"Survival",
|
||||
"Terrorism",
|
||||
"Torture",
|
||||
"Travel",
|
||||
"War",
|
||||
],
|
||||
"Theme Other-Organisations": [
|
||||
"Assassins",
|
||||
"Criminal Organization",
|
||||
"Cult",
|
||||
"Firefighters",
|
||||
"Gangs",
|
||||
"Mafia",
|
||||
"Military",
|
||||
"Police",
|
||||
"Triads",
|
||||
"Yakuza",
|
||||
],
|
||||
"Theme Other-Vehicle": [
|
||||
"Aviation",
|
||||
"Cars",
|
||||
"Mopeds",
|
||||
"Motorcycles",
|
||||
"Ships",
|
||||
"Tanks",
|
||||
"Trains",
|
||||
],
|
||||
"Theme Romance": [
|
||||
"Age Gap",
|
||||
"Bisexual",
|
||||
"Boys' Love",
|
||||
"Female Harem",
|
||||
"Heterosexual",
|
||||
"Love Triangle",
|
||||
"Male Harem",
|
||||
"Matchmaking",
|
||||
"Mixed Gender Harem",
|
||||
"Teens' Love",
|
||||
"Unrequited Love",
|
||||
"Yuri",
|
||||
],
|
||||
"Theme Sci Fi": [
|
||||
"Cyberpunk",
|
||||
"Space Opera",
|
||||
"Time Loop",
|
||||
"Time Manipulation",
|
||||
"Tokusatsu",
|
||||
],
|
||||
"Theme Sci Fi-Mecha": ["Real Robot", "Super Robot"],
|
||||
"Theme Slice of Life": [
|
||||
"Agriculture",
|
||||
"Cute Boys Doing Cute Things",
|
||||
"Cute Girls Doing Cute Things",
|
||||
"Family Life",
|
||||
"Horticulture",
|
||||
"Iyashikei",
|
||||
"Parenthood",
|
||||
],
|
||||
}
|
||||
tags_available_list = []
|
||||
for tag_category, tags_in_category in tags_available.items():
|
||||
tags_available_list.extend(tags_in_category)
|
||||
from .data import (
|
||||
genres_available,
|
||||
media_formats_available,
|
||||
media_statuses_available,
|
||||
seasons_available,
|
||||
sorts_available,
|
||||
tags_available_list,
|
||||
years_available,
|
||||
)
|
||||
|
||||
|
||||
@click.command(
|
||||
@@ -380,91 +26,27 @@ for tag_category, tags_in_category in tags_available.items():
|
||||
@click.option(
|
||||
"--season",
|
||||
help="The season the media was released",
|
||||
type=click.Choice(["WINTER", "SPRING", "SUMMER", "FALL"]),
|
||||
type=click.Choice(seasons_available),
|
||||
)
|
||||
@click.option(
|
||||
"--status",
|
||||
"-S",
|
||||
help="The media status of the anime",
|
||||
multiple=True,
|
||||
type=click.Choice(
|
||||
["FINISHED", "RELEASING", "NOT_YET_RELEASED", "CANCELLED", "HIATUS"]
|
||||
),
|
||||
type=click.Choice(media_statuses_available),
|
||||
)
|
||||
@click.option(
|
||||
"--sort",
|
||||
"-s",
|
||||
help="What to sort the search results on",
|
||||
type=click.Choice(
|
||||
[
|
||||
"ID",
|
||||
"ID_DESC",
|
||||
"TITLE_ROMAJI",
|
||||
"TITLE_ROMAJI_DESC",
|
||||
"TITLE_ENGLISH",
|
||||
"TITLE_ENGLISH_DESC",
|
||||
"TITLE_NATIVE",
|
||||
"TITLE_NATIVE_DESC",
|
||||
"TYPE",
|
||||
"TYPE_DESC",
|
||||
"FORMAT",
|
||||
"FORMAT_DESC",
|
||||
"START_DATE",
|
||||
"START_DATE_DESC",
|
||||
"END_DATE",
|
||||
"END_DATE_DESC",
|
||||
"SCORE",
|
||||
"SCORE_DESC",
|
||||
"POPULARITY",
|
||||
"POPULARITY_DESC",
|
||||
"TRENDING",
|
||||
"TRENDING_DESC",
|
||||
"EPISODES",
|
||||
"EPISODES_DESC",
|
||||
"DURATION",
|
||||
"DURATION_DESC",
|
||||
"STATUS",
|
||||
"STATUS_DESC",
|
||||
"CHAPTERS",
|
||||
"CHAPTERS_DESC",
|
||||
"VOLUMES",
|
||||
"VOLUMES_DESC",
|
||||
"UPDATED_AT",
|
||||
"UPDATED_AT_DESC",
|
||||
"SEARCH_MATCH",
|
||||
"FAVOURITES",
|
||||
"FAVOURITES_DESC",
|
||||
]
|
||||
),
|
||||
type=click.Choice(sorts_available),
|
||||
)
|
||||
@click.option(
|
||||
"--genres",
|
||||
"-g",
|
||||
multiple=True,
|
||||
help="the genres to filter by",
|
||||
type=click.Choice(
|
||||
[
|
||||
"Action",
|
||||
"Adventure",
|
||||
"Comedy",
|
||||
"Drama",
|
||||
"Ecchi",
|
||||
"Fantasy",
|
||||
"Horror",
|
||||
"Mahou Shoujo",
|
||||
"Mecha",
|
||||
"Music",
|
||||
"Mystery",
|
||||
"Psychological",
|
||||
"Romance",
|
||||
"Sci-Fi",
|
||||
"Slice of Life",
|
||||
"Sports",
|
||||
"Supernatural",
|
||||
"Thriller",
|
||||
"Hentai",
|
||||
]
|
||||
),
|
||||
type=click.Choice(genres_available),
|
||||
)
|
||||
@click.option(
|
||||
"--tags",
|
||||
@@ -478,49 +60,12 @@ for tag_category, tags_in_category in tags_available.items():
|
||||
"-f",
|
||||
multiple=True,
|
||||
help="Media format",
|
||||
type=click.Choice(
|
||||
["TV", "TV_SHORT", "MOVIE", "SPECIAL", "OVA", "MUSIC", "NOVEL", "ONE_SHOT"]
|
||||
),
|
||||
type=click.Choice(media_formats_available),
|
||||
)
|
||||
@click.option(
|
||||
"--year",
|
||||
"-y",
|
||||
type=click.Choice(
|
||||
[
|
||||
"1900",
|
||||
"1910",
|
||||
"1920",
|
||||
"1930",
|
||||
"1940",
|
||||
"1950",
|
||||
"1960",
|
||||
"1970",
|
||||
"1980",
|
||||
"1990",
|
||||
"2000",
|
||||
"2004",
|
||||
"2005",
|
||||
"2006",
|
||||
"2007",
|
||||
"2008",
|
||||
"2009",
|
||||
"2010",
|
||||
"2011",
|
||||
"2012",
|
||||
"2013",
|
||||
"2014",
|
||||
"2015",
|
||||
"2016",
|
||||
"2017",
|
||||
"2018",
|
||||
"2019",
|
||||
"2020",
|
||||
"2021",
|
||||
"2022",
|
||||
"2023",
|
||||
"2024",
|
||||
]
|
||||
),
|
||||
type=click.Choice(years_available),
|
||||
help="the year the media was released",
|
||||
)
|
||||
@click.option(
|
||||
@@ -566,6 +111,22 @@ def search(
|
||||
from ...utils.tools import FastAnimeRuntimeState
|
||||
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
|
||||
fastanime_runtime_state.current_page = 1
|
||||
fastanime_runtime_state.current_data_loader = (
|
||||
lambda page=1, **kwargs: AniList.search(
|
||||
query=title,
|
||||
sort=sort,
|
||||
status_in=list(status),
|
||||
genre_in=list(genres),
|
||||
season=season,
|
||||
tag_in=list(tags),
|
||||
seasonYear=year,
|
||||
format_in=list(media_format),
|
||||
on_list=on_list,
|
||||
page=page,
|
||||
)
|
||||
)
|
||||
fastanime_runtime_state.anilist_results_data = search_results
|
||||
anilist_results_menu(config, fastanime_runtime_state)
|
||||
else:
|
||||
|
||||
@@ -26,6 +26,9 @@ def trending(config, dump_json):
|
||||
from ...utils.tools import FastAnimeRuntimeState
|
||||
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
|
||||
fastanime_runtime_state.current_page = 1
|
||||
fastanime_runtime_state.current_data_loader = AniList.get_trending
|
||||
fastanime_runtime_state.anilist_results_data = data
|
||||
anilist_results_menu(config, fastanime_runtime_state)
|
||||
else:
|
||||
|
||||
@@ -25,6 +25,9 @@ def upcoming(config, dump_json):
|
||||
from ...utils.tools import FastAnimeRuntimeState
|
||||
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
|
||||
fastanime_runtime_state.current_page = 1
|
||||
fastanime_runtime_state.current_data_loader = AniList.get_upcoming_anime
|
||||
fastanime_runtime_state.anilist_results_data = data
|
||||
anilist_results_menu(config, fastanime_runtime_state)
|
||||
else:
|
||||
|
||||
@@ -42,5 +42,12 @@ def watching(config: "Config", dump_json):
|
||||
from ...utils.tools import FastAnimeRuntimeState
|
||||
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
|
||||
fastanime_runtime_state.current_page = 1
|
||||
fastanime_runtime_state.current_data_loader = (
|
||||
lambda config, **kwargs: anilist_interfaces._handle_animelist(
|
||||
config, fastanime_runtime_state, "Watching", **kwargs
|
||||
)
|
||||
)
|
||||
fastanime_runtime_state.anilist_results_data = anime_list[1]
|
||||
anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state)
|
||||
|
||||
@@ -114,6 +114,16 @@ if TYPE_CHECKING:
|
||||
help="Whether to prompt for anything instead just do the best thing",
|
||||
default=True,
|
||||
)
|
||||
@click.option(
|
||||
"--force-ffmpeg",
|
||||
is_flag=True,
|
||||
help="Force the use of FFmpeg for downloading (supports large variety of streams but slower)",
|
||||
)
|
||||
@click.option(
|
||||
"--hls-use-mpegts",
|
||||
is_flag=True,
|
||||
help="Use mpegts for hls streams (useful for some streams: see Docs) (this option forces --force-ffmpeg to be True)",
|
||||
)
|
||||
@click.pass_obj
|
||||
def download(
|
||||
config: "Config",
|
||||
@@ -127,6 +137,8 @@ def download(
|
||||
clean,
|
||||
wait_time,
|
||||
prompt,
|
||||
force_ffmpeg,
|
||||
hls_use_mpegts,
|
||||
):
|
||||
import time
|
||||
|
||||
@@ -146,6 +158,8 @@ def download(
|
||||
move_preferred_subtitle_lang_to_top,
|
||||
)
|
||||
|
||||
force_ffmpeg |= hls_use_mpegts
|
||||
|
||||
anime_provider = AnimeProvider(config.provider)
|
||||
anilist_anime_info = None
|
||||
|
||||
@@ -185,6 +199,8 @@ def download(
|
||||
clean,
|
||||
wait_time,
|
||||
prompt,
|
||||
force_ffmpeg,
|
||||
hls_use_mpegts,
|
||||
)
|
||||
return
|
||||
search_results = search_results["results"]
|
||||
@@ -236,6 +252,8 @@ def download(
|
||||
clean,
|
||||
wait_time,
|
||||
prompt,
|
||||
force_ffmpeg,
|
||||
hls_use_mpegts,
|
||||
)
|
||||
return
|
||||
|
||||
@@ -369,6 +387,8 @@ def download(
|
||||
merge=merge,
|
||||
clean=clean,
|
||||
prompt=prompt,
|
||||
force_ffmpeg=force_ffmpeg,
|
||||
hls_use_mpegts=hls_use_mpegts,
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
@@ -213,7 +213,6 @@ def grab(
|
||||
|
||||
# lets download em
|
||||
for episode in episodes_range:
|
||||
try:
|
||||
if episode not in episodes:
|
||||
continue
|
||||
streams = anime_provider.get_episode_streams(
|
||||
@@ -230,9 +229,6 @@ def grab(
|
||||
episode
|
||||
] = episode_streams
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@@ -3,16 +3,16 @@ import logging
|
||||
import os
|
||||
from configparser import ConfigParser
|
||||
from typing import TYPE_CHECKING
|
||||
from ..libs.fzf import FZF_DEFAULT_OPTS, HEADER
|
||||
|
||||
from ..constants import (
|
||||
ASSETS_DIR,
|
||||
S_PLATFORM,
|
||||
USER_CONFIG_PATH,
|
||||
USER_DATA_PATH,
|
||||
USER_VIDEOS_DIR,
|
||||
ASSETS_DIR,
|
||||
USER_WATCH_HISTORY_PATH,
|
||||
S_PLATFORM,
|
||||
)
|
||||
from ..libs.fzf import FZF_DEFAULT_OPTS, HEADER
|
||||
from ..libs.rofi import Rofi
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -29,9 +29,15 @@ class Config(object):
|
||||
"https://anilist.co/api/v2/oauth/authorize?client_id=20148&response_type=token"
|
||||
)
|
||||
anime_provider: "AnimeProvider"
|
||||
user_data = {"recent_anime": [], "animelist": [], "user": {}}
|
||||
user_data = {
|
||||
"recent_anime": [],
|
||||
"animelist": [],
|
||||
"user": {},
|
||||
"meta": {"last_updated": 0},
|
||||
}
|
||||
default_config = {
|
||||
"auto_next": "False",
|
||||
"menu_order": "",
|
||||
"auto_select": "True",
|
||||
"cache_requests": "true",
|
||||
"check_for_updates": "True",
|
||||
@@ -39,6 +45,7 @@ class Config(object):
|
||||
"default_media_list_tracking": "None",
|
||||
"downloads_dir": USER_VIDEOS_DIR,
|
||||
"disable_mpv_popen": "True",
|
||||
"discord": "False",
|
||||
"episode_complete_at": "80",
|
||||
"ffmpegthumbnailer_seek_time": "-1",
|
||||
"force_forward_tracking": "true",
|
||||
@@ -51,6 +58,8 @@ class Config(object):
|
||||
"image_previews": "True" if S_PLATFORM != "win32" else "False",
|
||||
"normalize_titles": "True",
|
||||
"notification_duration": "2",
|
||||
"max_cache_lifetime": "03:00:00",
|
||||
"per_page": "15",
|
||||
"player": "mpv",
|
||||
"preferred_history": "local",
|
||||
"preferred_language": "english",
|
||||
@@ -105,6 +114,7 @@ class Config(object):
|
||||
self.disable_mpv_popen = self.configparser.getboolean(
|
||||
"stream", "disable_mpv_popen"
|
||||
)
|
||||
self.discord = self.configparser.getboolean("general", "discord")
|
||||
self.downloads_dir = self.configparser.get("general", "downloads_dir")
|
||||
self.episode_complete_at = self.configparser.getint(
|
||||
"stream", "episode_complete_at"
|
||||
@@ -128,6 +138,16 @@ class Config(object):
|
||||
self.notification_duration = self.configparser.getint(
|
||||
"general", "notification_duration"
|
||||
)
|
||||
self._max_cache_lifetime = self.configparser.get(
|
||||
"general", "max_cache_lifetime"
|
||||
)
|
||||
max_cache_lifetime = list(map(int, self._max_cache_lifetime.split(":")))
|
||||
self.max_cache_lifetime = (
|
||||
max_cache_lifetime[0] * 86400
|
||||
+ max_cache_lifetime[1] * 3600
|
||||
+ max_cache_lifetime[2] * 60
|
||||
)
|
||||
self.per_page = self.configparser.get("anilist", "per_page")
|
||||
self.player = self.configparser.get("stream", "player")
|
||||
self.preferred_history = self.configparser.get("stream", "preferred_history")
|
||||
self.preferred_language = self.configparser.get("general", "preferred_language")
|
||||
@@ -148,6 +168,7 @@ class Config(object):
|
||||
self.server = self.configparser.get("stream", "server")
|
||||
self.skip = self.configparser.getboolean("stream", "skip")
|
||||
self.sort_by = self.configparser.get("anilist", "sort_by")
|
||||
self.menu_order = self.configparser.get("general", "menu_order")
|
||||
self.sub_lang = self.configparser.get("general", "sub_lang")
|
||||
self.translation_type = self.configparser.get("stream", "translation_type")
|
||||
self.use_fzf = self.configparser.getboolean("general", "use_fzf")
|
||||
@@ -257,21 +278,7 @@ class Config(object):
|
||||
# ╚═╝░░░░░╚═╝░░╚═╝╚═════╝░░░░╚═╝░░░╚═╝░░╚═╝╚═╝░░╚══╝╚═╝╚═╝░░░░░╚═╝╚══════╝ ░╚════╝░░╚════╝░╚═╝░░╚══╝╚═╝░░░░░╚═╝░╚═════╝░
|
||||
#
|
||||
[general]
|
||||
# well recently somebody made a post @unixporn subreddit
|
||||
# the rice contained fastanime in preview mode
|
||||
# Did not even realise you rice with it
|
||||
# or i would have cross-posted
|
||||
# 'A new way to rice'
|
||||
# and immediately one of the mods removed it
|
||||
# am not sure why though
|
||||
# cause the preview is actually pretty good looking.
|
||||
# just like you would use fastfetch or cava
|
||||
# as means to protest against this injustice i have addded
|
||||
# the following
|
||||
# for you all ricers out there
|
||||
# and be sure to post a rice with fastanime on it
|
||||
# if you also think this is unfair
|
||||
|
||||
# Can you rice it?
|
||||
# for the preview pane
|
||||
preview_separator_color = {self.preview_separator_color}
|
||||
|
||||
@@ -284,7 +291,6 @@ header_ascii_art = {new_line.join([tab+line for line in self.header_ascii_art.sp
|
||||
header_color = {self.header_color}
|
||||
|
||||
# to be passed to fzf
|
||||
# may break it down further for now just pass the options as is
|
||||
# be sure to indent
|
||||
fzf_opts = {new_line.join([tab+line for line in self.fzf_opts.split(new_line)])}
|
||||
|
||||
@@ -336,8 +342,8 @@ downloads_dir = {self.downloads_dir}
|
||||
|
||||
# whether to show a preview window when using fzf or rofi [True/False]
|
||||
# the preview requires you have a commandline image viewer as documented in the README
|
||||
# this is only when usinf fzf
|
||||
# if you dont care about image previews it doesnt matter
|
||||
# this is only when using fzf or rofi
|
||||
# if you dont care about image and text previews it doesnt matter
|
||||
# though its awesome
|
||||
# try it and you will see
|
||||
preview = {self.preview}
|
||||
@@ -352,10 +358,19 @@ image_previews = {self.image_previews}
|
||||
|
||||
# the time to seek when using ffmpegthumbnailer [-1 to 100]
|
||||
# -1 means random and is the default
|
||||
# ffmpegthumbnailer is used to generate previews and you can select at what time in the video to extract an image
|
||||
# ffmpegthumbnailer is used to generate previews
|
||||
# and you can select at what time in the video to extract an image
|
||||
# random makes things quite exciting cause you never no at what time it will extract the image from
|
||||
# used by the ```fastanime downloads``` command
|
||||
ffmpegthumbnailer_seek_time = {self.ffmpegthumbnailer_seek_time}
|
||||
|
||||
# specify the order of menu items in a comma-separated list.
|
||||
# only include the base names of menu options (e.g., "Trending", "Recent").
|
||||
# default value is 'Trending,Recent,Watching,Paused,Dropped,Planned,Completed,Rewatching,Recently Updated Anime,Search,Watch History,Random Anime,Most Popular Anime,Most Favourite Anime,Most Scored Anime,Upcoming Anime,Edit Config,Exit'
|
||||
# leave blank to use the default menu order.
|
||||
# you can also omit some options by not including them in the list
|
||||
menu_order = {self.menu_order}
|
||||
|
||||
# whether to use fzf as the interface for the anilist command and others. [True/False]
|
||||
use_fzf = {self.use_fzf}
|
||||
|
||||
@@ -409,6 +424,11 @@ force_forward_tracking = {self.force_forward_tracking}
|
||||
# from the cached_requests_db
|
||||
cache_requests = {self.cache_requests}
|
||||
|
||||
# the max lifetime for a cached request <days:hours:minutes>
|
||||
# defaults to 3days = 03:00:00
|
||||
# this is the time after which a cached request will be deleted (technically : )
|
||||
max_cache_lifetime = {self._max_cache_lifetime}
|
||||
|
||||
# whether to use a persistent store (basically a sqlitedb) for storing some data the provider requires
|
||||
# to enable a seamless experience [true/false]
|
||||
# this option exists primarily because i think it may help in the optimization
|
||||
@@ -421,6 +441,11 @@ use_persistent_provider_store = {self.use_persistent_provider_store}
|
||||
# 0 will disable recent anime tracking
|
||||
recent = {self.recent}
|
||||
|
||||
# enable or disable discord activity updater
|
||||
# if you want to enable it, please follow the lnik below to register the app with discord account
|
||||
# https://discord.com/oauth2/authorize?client_id=1292070065583165512
|
||||
discord = {self.discord}
|
||||
|
||||
|
||||
[stream]
|
||||
# the quality of the stream [1080,720,480,360]
|
||||
@@ -438,9 +463,12 @@ continue_from_history = {self.continue_from_history}
|
||||
|
||||
# which history to use [local/remote]
|
||||
# local history means it will just use the watch history stored locally in your device
|
||||
# the file that stores it is called watch_history.json and is stored next to your config file
|
||||
# remote means it ignores the last episode stored locally and instead uses the one in your anilist anime list
|
||||
# this config option is useful if you want to overwrite your local history or import history covered from another device or platform
|
||||
# the file that stores it is called watch_history.json
|
||||
# and is stored next to your config file
|
||||
# remote means it ignores the last episode stored locally
|
||||
# and instead uses the one in your anilist anime list
|
||||
# this config option is useful if you want to overwrite your local history
|
||||
# or import history covered from another device or platform
|
||||
# since remote history will take precendence over whats available locally
|
||||
preferred_history = {self.preferred_history}
|
||||
|
||||
@@ -450,7 +478,7 @@ translation_type = {self.translation_type}
|
||||
# what server to use for a particular provider
|
||||
# allanime: [dropbox, sharepoint, wetransfer, gogoanime, wixmp]
|
||||
# animepahe: [kwik]
|
||||
# hianime: [HD1, HD2, StreamSB, StreamTape]
|
||||
# hianime: [HD1, HD2, StreamSB, StreamTape] : only HD2 for now
|
||||
# yugen: [gogoanime]
|
||||
# 'top' can also be used as a value for this option
|
||||
# 'top' will cause fastanime to auto select the first server it sees
|
||||
@@ -468,16 +496,13 @@ auto_next = {self.auto_next}
|
||||
# this is because the providers sometime use non-standard names
|
||||
# that are there own preference rather than the official names
|
||||
# But 99% of the time will be accurate
|
||||
# if this happens just turn of auto_select in the menus or from the commandline and manually select the correct anime title
|
||||
# and then please open an issue
|
||||
# highlighting the normalized title
|
||||
# and the title given by the provider for the anime you wished to watch
|
||||
# or even better edit this file <https://github.com/Benex254/FastAnime/blob/master/fastanime/Utility/data.py>
|
||||
# and open a pull request
|
||||
# prefrably, so you can give me a small break
|
||||
# of doing everything 😄
|
||||
# and its always nice to see people contributing
|
||||
# to projects they love and use
|
||||
# if this happens just turn off auto_select in the menus or from the commandline
|
||||
# and manually select the correct anime title
|
||||
# edit this file <https://github.com/Benexl/FastAnime/blob/master/fastanime/Utility/data.py>
|
||||
# and to the dictionary of the provider
|
||||
# the provider title (key) and their corresponding anilist names (value)
|
||||
# and then please open a pr
|
||||
# issues on the same will be ignored and then closed 😆
|
||||
auto_select = {self.auto_select}
|
||||
|
||||
# whether to skip the opening and ending theme songs [True/False]
|
||||
@@ -497,7 +522,7 @@ episode_complete_at = {self.episode_complete_at}
|
||||
# whether to use python-mpv [True/False]
|
||||
# to enable superior control over the player
|
||||
# adding more options to it
|
||||
# Enable this one and you will be wonder
|
||||
# Enabling this option and you will ask yourself
|
||||
# why you did not discover fastanime sooner 🙃
|
||||
# Since you basically don't have to close the player window
|
||||
# to go to the next or previous episode, switch servers,
|
||||
@@ -510,13 +535,13 @@ episode_complete_at = {self.episode_complete_at}
|
||||
# personally it took me quite sometime to figure it out
|
||||
# this is because of how windows handles shared libraries
|
||||
# so just ask when you find yourself stuck
|
||||
# or just switch to arch linux
|
||||
# or just switch to nixos 😄
|
||||
use_python_mpv = {self.use_python_mpv}
|
||||
|
||||
|
||||
# whether to use popen to get the timestamps for continue_from_history
|
||||
# implemented because popen does not work for some reason in nixos
|
||||
# if you are on nixos and you have a solution to this problem please share
|
||||
# implemented because popen does not work for some reason in nixos and apparently on mac as well
|
||||
# if you are on nixos or mac and you have a solution to this problem please share
|
||||
# i will be glad to hear it 😄
|
||||
# So for now ignore this option
|
||||
# and anyways the new method of getting timestamps is better
|
||||
@@ -543,16 +568,16 @@ format = {self.format}
|
||||
# since you will miss out on some features if you use the others
|
||||
player = {self.player}
|
||||
|
||||
# NOTE:
|
||||
# if you have any trouble setting up your config
|
||||
# please don't be afraid to ask in our discord
|
||||
# plus if there are any errors, improvements or suggestions please tell us in the discord
|
||||
# or help us by contributing
|
||||
# we appreciate all the help we can get
|
||||
# since we may not always have the time to immediately implement the changes
|
||||
[anilist]
|
||||
per_page = {self.per_page}
|
||||
|
||||
#
|
||||
# HOPE YOU ENJOY FASTANIME AND BE SURE TO STAR THE PROJECT ON GITHUB
|
||||
# https://github.com/Benex254/FastAnime
|
||||
# https://github.com/Benexl/FastAnime
|
||||
#
|
||||
# Also join the discord server
|
||||
# where the anime tech community lives :)
|
||||
# https://discord.gg/C4rhMA4mmK
|
||||
#
|
||||
"""
|
||||
return current_config_state
|
||||
|
||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import os
|
||||
import random
|
||||
import threading
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from click import clear
|
||||
@@ -14,6 +15,7 @@ from yt_dlp.utils import sanitize_filename
|
||||
|
||||
from ...anilist import AniList
|
||||
from ...constants import USER_CONFIG_PATH
|
||||
from ...libs.discord import discord
|
||||
from ...libs.fzf import fzf
|
||||
from ...libs.rofi import Rofi
|
||||
from ...Utility.data import anime_normalizer
|
||||
@@ -45,12 +47,17 @@ def calculate_percentage_completion(start_time, end_time):
|
||||
[TODO:return]
|
||||
"""
|
||||
|
||||
try:
|
||||
start = start_time.split(":")
|
||||
end = end_time.split(":")
|
||||
start_secs = int(start[0]) * 3600 + int(start[1]) * 60 + int(start[2])
|
||||
end_secs = int(end[0]) * 3600 + int(end[1]) * 60 + int(end[2])
|
||||
return start_secs / end_secs * 100
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
def discord_updater(show,episode,switch):
|
||||
discord.discord_connect(show,episode,switch)
|
||||
|
||||
def media_player_controls(
|
||||
config: "Config", fastanime_runtime_state: "FastAnimeRuntimeState"
|
||||
@@ -507,6 +514,12 @@ def provider_anime_episode_servers_menu(
|
||||
"[bold magenta] Episode: [/]",
|
||||
current_episode_number,
|
||||
)
|
||||
# update discord activity for user
|
||||
switch = threading.Event()
|
||||
if config.discord:
|
||||
discord_proc = threading.Thread(target=discord_updater, args=(provider_anime_title,current_episode_number,switch))
|
||||
discord_proc.start()
|
||||
|
||||
# try to get the timestamp you left off from if available
|
||||
start_time = config.watch_history.get(str(anime_id_anilist), {}).get(
|
||||
"episode_stopped_at", "0"
|
||||
@@ -589,6 +602,10 @@ def provider_anime_episode_servers_menu(
|
||||
)
|
||||
print("Finished at: ", stop_time)
|
||||
|
||||
# stop discord activity updater
|
||||
if config.discord:
|
||||
switch.set()
|
||||
|
||||
# update_watch_history
|
||||
# this will try to update the episode to be the next episode if delta has reached a specific threshhold
|
||||
# this update will only apply locally
|
||||
@@ -701,7 +718,7 @@ def provider_anime_episodes_menu(
|
||||
total_time = user_watch_history.get(str(anime_id_anilist), {}).get(
|
||||
"episode_total_length", "0"
|
||||
)
|
||||
if stop_time != "0" or total_time != "0":
|
||||
if stop_time != "0" and total_time != "0":
|
||||
percentage_completion_of_episode = calculate_percentage_completion(
|
||||
stop_time, total_time
|
||||
)
|
||||
@@ -1340,6 +1357,58 @@ def media_actions_menu(
|
||||
set_prefered_progress_tracking(config, fastanime_runtime_state, update=True)
|
||||
media_actions_menu(config, fastanime_runtime_state)
|
||||
|
||||
def _relations(config: "Config", fastanime_runtime_state: "FastAnimeRuntimeState"):
|
||||
"""Helper function to get anime recommendations
|
||||
Args:
|
||||
config: [TODO:description]
|
||||
fastanime_runtime_state: [TODO:description]
|
||||
"""
|
||||
relations = AniList.get_related_anime_for(
|
||||
fastanime_runtime_state.selected_anime_id_anilist
|
||||
)
|
||||
if not relations[0]:
|
||||
print("No recommendations found", relations[1])
|
||||
input("Enter to continue...")
|
||||
media_actions_menu(config, fastanime_runtime_state)
|
||||
return
|
||||
|
||||
relations = relations[1]["data"]["Page"]["relations"] # pyright:ignore
|
||||
fastanime_runtime_state.anilist_results_data = {
|
||||
"data": {"Page": {"media": relations["nodes"]}} # pyright:ignore
|
||||
}
|
||||
anilist_results_menu(config, fastanime_runtime_state)
|
||||
|
||||
def _recommendations(
|
||||
config: "Config", fastanime_runtime_state: "FastAnimeRuntimeState"
|
||||
):
|
||||
"""Helper function to get anime recommendations
|
||||
Args:
|
||||
config: [TODO:description]
|
||||
fastanime_runtime_state: [TODO:description]
|
||||
"""
|
||||
recommendations = AniList.get_recommended_anime_for(
|
||||
fastanime_runtime_state.selected_anime_id_anilist
|
||||
)
|
||||
if not recommendations[0]:
|
||||
print("No recommendations found", recommendations[1])
|
||||
input("Enter to continue...")
|
||||
media_actions_menu(config, fastanime_runtime_state)
|
||||
return
|
||||
|
||||
fastanime_runtime_state.anilist_results_data = {
|
||||
"data": {
|
||||
"Page": {
|
||||
"media": [
|
||||
media["media"]
|
||||
for media in recommendations[1]["data"]["Page"][
|
||||
"recommendations" # pyright:ignore
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
anilist_results_menu(config, fastanime_runtime_state)
|
||||
|
||||
icons = config.icons
|
||||
options = {
|
||||
f"{'📽️ ' if icons else ''}Stream ({progress}/{episodes_total})": _stream_anime,
|
||||
@@ -1349,6 +1418,8 @@ def media_actions_menu(
|
||||
f"{'✨ ' if icons else ''}Progress Tracking": _set_progress_tracking,
|
||||
f"{'📥 ' if icons else ''}Add to List": _add_to_list,
|
||||
f"{'📤 ' if icons else ''}Remove from List": _remove_from_list,
|
||||
f"{'📖 ' if icons else ''}Recommendations": _recommendations,
|
||||
f"{'📖 ' if icons else ''}Relations": _relations,
|
||||
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,
|
||||
@@ -1421,7 +1492,7 @@ def anilist_results_menu(
|
||||
anime_data[title] = anime
|
||||
|
||||
# prompt for the anime of choice
|
||||
choices = [*anime_data.keys(), "Back"]
|
||||
choices = [*anime_data.keys(), "Next Page", "Previous Page", "Back"]
|
||||
if config.use_fzf:
|
||||
if config.preview:
|
||||
from .utils import get_fzf_anime_preview
|
||||
@@ -1460,6 +1531,43 @@ def anilist_results_menu(
|
||||
if selected_anime_title == "Back":
|
||||
fastanime_main_menu(config, fastanime_runtime_state)
|
||||
return
|
||||
if selected_anime_title == "Next Page":
|
||||
fastanime_runtime_state.current_page = page = (
|
||||
fastanime_runtime_state.current_page + 1
|
||||
)
|
||||
success, data = fastanime_runtime_state.current_data_loader(
|
||||
config=config, page=page
|
||||
)
|
||||
if success:
|
||||
fastanime_runtime_state.anilist_results_data = data
|
||||
|
||||
anilist_results_menu(config, fastanime_runtime_state)
|
||||
else:
|
||||
print("Failed to get next page")
|
||||
print(data)
|
||||
input("Enter to continue...")
|
||||
anilist_results_menu(config, fastanime_runtime_state)
|
||||
|
||||
return
|
||||
if selected_anime_title == "Previous Page":
|
||||
fastanime_runtime_state.current_page = page = (
|
||||
(fastanime_runtime_state.current_page - 1)
|
||||
if fastanime_runtime_state.current_page > 1
|
||||
else 1
|
||||
)
|
||||
success, data = fastanime_runtime_state.current_data_loader(
|
||||
config=config, page=page
|
||||
)
|
||||
if success:
|
||||
fastanime_runtime_state.anilist_results_data = data
|
||||
|
||||
anilist_results_menu(config, fastanime_runtime_state)
|
||||
else:
|
||||
print("Failed to get previous page")
|
||||
print(data)
|
||||
input("Enter to continue...")
|
||||
anilist_results_menu(config, fastanime_runtime_state)
|
||||
return
|
||||
|
||||
selected_anime: "AnilistBaseMediaDataSchema" = anime_data[selected_anime_title]
|
||||
fastanime_runtime_state.selected_anime_anilist = selected_anime
|
||||
@@ -1474,8 +1582,11 @@ def anilist_results_menu(
|
||||
#
|
||||
# ---- FASTANIME MAIN MENU ----
|
||||
#
|
||||
def handle_animelist(
|
||||
config: "Config", fastanime_runtime_state: "FastAnimeRuntimeState", list_type: str
|
||||
def _handle_animelist(
|
||||
config: "Config",
|
||||
fastanime_runtime_state: "FastAnimeRuntimeState",
|
||||
list_type: str,
|
||||
page=1,
|
||||
):
|
||||
"""A helper function that handles user media lists
|
||||
|
||||
@@ -1508,13 +1619,13 @@ def handle_animelist(
|
||||
status = "DROPPED"
|
||||
case "Paused":
|
||||
status = "PAUSED"
|
||||
case "Repeating":
|
||||
case "Rewatching":
|
||||
status = "REPEATING"
|
||||
case _:
|
||||
return
|
||||
|
||||
# get the media list
|
||||
anime_list = AniList.get_anime_list(status)
|
||||
anime_list = AniList.get_anime_list(status, page=page)
|
||||
# handle null
|
||||
if not anime_list:
|
||||
print("Sth went wrong", anime_list)
|
||||
@@ -1545,17 +1656,7 @@ def handle_animelist(
|
||||
return anime_list
|
||||
|
||||
|
||||
def fastanime_main_menu(
|
||||
config: "Config", fastanime_runtime_state: "FastAnimeRuntimeState"
|
||||
):
|
||||
"""The main entry point to the anilist command
|
||||
|
||||
Args:
|
||||
config: An object containing cconfiguration data
|
||||
fastanime_runtime_state: A query dict used to store data during navigation of the ui # initially this was very messy
|
||||
"""
|
||||
|
||||
def _anilist_search():
|
||||
def _anilist_search(config: "Config", page=1):
|
||||
"""A function that enables seaching of an anime
|
||||
|
||||
Returns:
|
||||
@@ -1567,9 +1668,10 @@ def fastanime_main_menu(
|
||||
else:
|
||||
search_term = Prompt.ask("[cyan]Search for[/]")
|
||||
|
||||
return AniList.search(query=search_term)
|
||||
return AniList.search(query=search_term, page=page)
|
||||
|
||||
def _anilist_random():
|
||||
|
||||
def _anilist_random(config: "Config", page=1):
|
||||
"""A function that generates random anilist ids enabling random discovery of anime
|
||||
|
||||
Returns:
|
||||
@@ -1580,27 +1682,41 @@ def fastanime_main_menu(
|
||||
|
||||
return AniList.search(id_in=list(random_anime))
|
||||
|
||||
def _watch_history():
|
||||
|
||||
def _watch_history(config: "Config", page=1):
|
||||
"""Function that lets you see all the anime that has locally been saved to your watch history
|
||||
|
||||
Returns:
|
||||
[TODO:return]
|
||||
"""
|
||||
watch_history = list(map(int, config.watch_history.keys()))
|
||||
return AniList.search(id_in=watch_history, sort="TRENDING_DESC")
|
||||
return AniList.search(id_in=watch_history, sort="TRENDING_DESC", page=page)
|
||||
|
||||
def _recent():
|
||||
|
||||
def _recent(config: "Config", page=1):
|
||||
return (
|
||||
True,
|
||||
{"data": {"Page": {"media": config.user_data["recent_anime"]}}},
|
||||
)
|
||||
|
||||
# WARNING: Will probably be depracated
|
||||
def _anime_list():
|
||||
anime_list = config.anime_list
|
||||
return AniList.search(id_in=anime_list)
|
||||
|
||||
def _edit_config():
|
||||
# WARNING: Will probably be depracated
|
||||
def _anime_list(config: "Config", page=1):
|
||||
anime_list = config.anime_list
|
||||
return AniList.search(id_in=anime_list, pages=page)
|
||||
|
||||
|
||||
def fastanime_main_menu(
|
||||
config: "Config", fastanime_runtime_state: "FastAnimeRuntimeState"
|
||||
):
|
||||
"""The main entry point to the anilist command
|
||||
|
||||
Args:
|
||||
config: An object containing cconfiguration data
|
||||
fastanime_runtime_state: A query dict used to store data during navigation of the ui # initially this was very messy
|
||||
"""
|
||||
|
||||
def _edit_config(*args, **kwargs):
|
||||
"""Helper function to edit your config when the ui is still running"""
|
||||
|
||||
from click import edit
|
||||
@@ -1625,23 +1741,23 @@ def fastanime_main_menu(
|
||||
options = {
|
||||
f"{'🔥 ' if icons else ''}Trending": AniList.get_trending,
|
||||
f"{'🎞️ ' if icons else ''}Recent": _recent,
|
||||
f"{'📺 ' if icons else ''}Watching": lambda media_list_type="Watching": handle_animelist(
|
||||
config, fastanime_runtime_state, media_list_type
|
||||
f"{'📺 ' if icons else ''}Watching": lambda config, media_list_type="Watching", page=1: _handle_animelist(
|
||||
config, fastanime_runtime_state, media_list_type, page=page
|
||||
),
|
||||
f"{'⏸ ' if icons else ''}Paused": lambda media_list_type="Paused": handle_animelist(
|
||||
config, fastanime_runtime_state, media_list_type
|
||||
f"{'⏸ ' if icons else ''}Paused": lambda config, media_list_type="Paused", page=1: _handle_animelist(
|
||||
config, fastanime_runtime_state, media_list_type, page=page
|
||||
),
|
||||
f"{'🚮 ' if icons else ''}Dropped": lambda media_list_type="Dropped": handle_animelist(
|
||||
config, fastanime_runtime_state, media_list_type
|
||||
f"{'🚮 ' if icons else ''}Dropped": lambda config, media_list_type="Dropped", page=1: _handle_animelist(
|
||||
config, fastanime_runtime_state, media_list_type, page=page
|
||||
),
|
||||
f"{'📑 ' if icons else ''}Planned": lambda media_list_type="Planned": handle_animelist(
|
||||
config, fastanime_runtime_state, media_list_type
|
||||
f"{'📑 ' if icons else ''}Planned": lambda config, media_list_type="Planned", page=1: _handle_animelist(
|
||||
config, fastanime_runtime_state, media_list_type, page=page
|
||||
),
|
||||
f"{'✅ ' if icons else ''}Completed": lambda media_list_type="Completed": handle_animelist(
|
||||
config, fastanime_runtime_state, media_list_type
|
||||
f"{'✅ ' if icons else ''}Completed": lambda config, media_list_type="Completed", page=1: _handle_animelist(
|
||||
config, fastanime_runtime_state, media_list_type, page=page
|
||||
),
|
||||
f"{'🔁 ' if icons else ''}Rewatching": lambda media_list_type="Repeating": handle_animelist(
|
||||
config, fastanime_runtime_state, media_list_type
|
||||
f"{'🔁 ' if icons else ''}Rewatching": lambda config, media_list_type="Rewatching", page=1: _handle_animelist(
|
||||
config, fastanime_runtime_state, media_list_type, page=page
|
||||
),
|
||||
f"{'🔔 ' if icons else ''}Recently Updated Anime": AniList.get_most_recently_updated,
|
||||
f"{'🔎 ' if icons else ''}Search": _anilist_search,
|
||||
@@ -1655,6 +1771,18 @@ def fastanime_main_menu(
|
||||
f"{'📝 ' if icons else ''}Edit Config": _edit_config,
|
||||
f"{'❌ ' if icons else ''}Exit": exit_app,
|
||||
}
|
||||
|
||||
# Load main menu order if set in config file
|
||||
if config.menu_order:
|
||||
menu_order_list = config.menu_order.split(",")
|
||||
lookup = {key.split(" ", 1)[-1]: key for key in options}
|
||||
ordered_dict = {
|
||||
lookup[key]: options[lookup[key]]
|
||||
for key in menu_order_list
|
||||
if key in lookup
|
||||
}
|
||||
options = ordered_dict
|
||||
|
||||
# prompt user to select an action
|
||||
choices = list(options.keys())
|
||||
if config.use_fzf:
|
||||
@@ -1670,7 +1798,9 @@ def fastanime_main_menu(
|
||||
choices,
|
||||
"Select Action",
|
||||
)
|
||||
anilist_data = options[action]()
|
||||
fastanime_runtime_state.current_data_loader = options[action]
|
||||
fastanime_runtime_state.current_page = 1
|
||||
anilist_data = options[action](config=config)
|
||||
# anilist data is a (bool,data)
|
||||
# the bool indicated success
|
||||
if anilist_data[0]:
|
||||
|
||||
@@ -103,7 +103,6 @@ def write_search_results(
|
||||
titles: sanitized anime titles
|
||||
workers:number of threads to use defaults to as many as possible
|
||||
"""
|
||||
# NOTE: Will probably make this a configuraable option
|
||||
# use concurency to download and write as fast as possible
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
|
||||
future_to_task = {}
|
||||
@@ -285,7 +284,7 @@ def get_fzf_episode_preview(
|
||||
anilist_results: the anilist results from an anilist action
|
||||
"""
|
||||
|
||||
HEADER_COLOR = 215, 0, 95
|
||||
# HEADER_COLOR = 215, 0, 95
|
||||
import re
|
||||
|
||||
def _worker():
|
||||
@@ -293,18 +292,16 @@ def get_fzf_episode_preview(
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
|
||||
# load the jobs
|
||||
future_to_url = {}
|
||||
|
||||
for episode in episodes:
|
||||
episode_title = ""
|
||||
image_url = ""
|
||||
for episode_detail in anilist_result["streamingEpisodes"]:
|
||||
if re.match(f"Episode {episode} ", episode_detail["title"]):
|
||||
if re.match(f".*Episode {episode} .*", episode_detail["title"]):
|
||||
episode_title = episode_detail["title"]
|
||||
image_url = episode_detail["thumbnail"]
|
||||
|
||||
if episode_title and image_url:
|
||||
# actual link to download image from
|
||||
if not image_url:
|
||||
continue
|
||||
future_to_url[
|
||||
executor.submit(save_image_from_url, image_url, episode)
|
||||
] = image_url
|
||||
@@ -315,13 +312,25 @@ def get_fzf_episode_preview(
|
||||
echo -n -e "{get_true_fg("─",*SEPARATOR_COLOR,bold=False)}"
|
||||
((ll++))
|
||||
done
|
||||
echo "{get_true_fg('Anime Title:',*HEADER_COLOR)} {(anilist_result['title']['romaji'] or anilist_result['title']['english']).replace('"',SINGLE_QUOTE)}"
|
||||
echo "{get_true_fg('Episode Title:',*HEADER_COLOR)} {str(episode_title).replace('"',SINGLE_QUOTE)}"
|
||||
echo "{get_true_fg('Anime Title(eng):',*HEADER_COLOR)} {('' or anilist_result['title']['english']).replace('"',SINGLE_QUOTE)}"
|
||||
echo "{get_true_fg('Anime Title(jp):',*HEADER_COLOR)} {(anilist_result['title']['romaji'] or '').replace('"',SINGLE_QUOTE)}"
|
||||
|
||||
ll=2
|
||||
while [ $ll -le $FZF_PREVIEW_COLUMNS ];do
|
||||
echo -n -e "{get_true_fg("─",*SEPARATOR_COLOR,bold=False)}"
|
||||
((ll++))
|
||||
done
|
||||
echo "{str(episode_title).replace('"',SINGLE_QUOTE)}"
|
||||
ll=2
|
||||
while [ $ll -le $FZF_PREVIEW_COLUMNS ];do
|
||||
echo -n -e "{get_true_fg("─",*SEPARATOR_COLOR,bold=False)}"
|
||||
((ll++))
|
||||
done
|
||||
"""
|
||||
)
|
||||
future_to_url[
|
||||
executor.submit(save_info_from_str, template, episode)
|
||||
] = episode_title
|
||||
executor.submit(save_info_from_str, template, str(episode))
|
||||
] = str(episode)
|
||||
|
||||
# execute the jobs
|
||||
for future in concurrent.futures.as_completed(future_to_url):
|
||||
@@ -371,14 +380,15 @@ def get_fzf_episode_preview(
|
||||
)
|
||||
else:
|
||||
preview = """
|
||||
title={}
|
||||
%s
|
||||
show_image_previews="%s"
|
||||
if [ $show_image_previews = "true" ];then
|
||||
if [ -s %s/{} ]; then fzf-preview %s/{}
|
||||
if [ -s %s/${title}.png ]; then fzf-preview %s/${title}.png
|
||||
else echo Loading...
|
||||
fi
|
||||
fi
|
||||
if [ -s %s/{} ]; then source %s/{}
|
||||
if [ -f %s/${title} ]; then source %s/${title}
|
||||
else echo Loading...
|
||||
fi
|
||||
""" % (
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import re
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import logging
|
||||
import time
|
||||
|
||||
from ...constants import S_PLATFORM
|
||||
|
||||
@@ -24,4 +24,10 @@ def print_img(url: str):
|
||||
print("Error fetching image")
|
||||
return
|
||||
img_bytes = res.content
|
||||
"""
|
||||
Change made in call to chafa. Chafa dev dropped abilty
|
||||
to pull from urls. Keeping old line here just in case.
|
||||
|
||||
subprocess.run([EXECUTABLE, url, "--size=15x15"], input=img_bytes)
|
||||
"""
|
||||
subprocess.run([EXECUTABLE, "--size=15x15"], input=img_bytes)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
from typing import Any, Callable
|
||||
|
||||
from ...libs.anilist.types import AnilistBaseMediaDataSchema
|
||||
from ...libs.anime_provider.types import Anime, EpisodeStream, SearchResult, Server
|
||||
@@ -26,9 +26,11 @@ class FastAnimeRuntimeState(object):
|
||||
selected_anime_title_anilist: str
|
||||
# current_anilist_data: "AnilistDataSchema | AnilistMediaList"
|
||||
anilist_results_data: "Any"
|
||||
current_page: int
|
||||
current_data_loader: "Callable"
|
||||
|
||||
|
||||
def exit_app(exit_code=0, *args):
|
||||
def exit_app(exit_code=0, *args, **kwargs):
|
||||
import sys
|
||||
|
||||
from rich.console import Console
|
||||
|
||||
@@ -3,6 +3,7 @@ This is the core module availing all the abstractions of the anilist api
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import requests
|
||||
@@ -142,6 +143,9 @@ class AniListApi:
|
||||
self,
|
||||
status: "AnilistMediaListStatus",
|
||||
type="ANIME",
|
||||
page=1,
|
||||
perPage=os.environ.get("FASTANIME_PER_PAGE", 15),
|
||||
**kwargs,
|
||||
) -> tuple[bool, "AnilistMediaLists"] | tuple[bool, None]:
|
||||
"""gets an anime list from your media list given the list status
|
||||
|
||||
@@ -151,7 +155,13 @@ class AniListApi:
|
||||
Returns:
|
||||
a media list
|
||||
"""
|
||||
variables = {"status": status, "userId": self.user_id, "type": type}
|
||||
variables = {
|
||||
"status": status,
|
||||
"userId": self.user_id,
|
||||
"type": type,
|
||||
"page": page,
|
||||
"perPage": int(perPage),
|
||||
}
|
||||
return self._make_authenticated_request(media_list_query, variables)
|
||||
|
||||
def get_medialist_entry(
|
||||
@@ -306,6 +316,7 @@ class AniListApi:
|
||||
|
||||
def search(
|
||||
self,
|
||||
max_results=50,
|
||||
query: str | None = None,
|
||||
sort: str | None = None,
|
||||
genre_in: list[str] | None = None,
|
||||
@@ -350,60 +361,98 @@ class AniListApi:
|
||||
variables = {"id": id}
|
||||
return self.get_data(anime_query, variables)
|
||||
|
||||
def get_trending(self, type="ANIME", *_, **kwargs):
|
||||
def get_trending(
|
||||
self,
|
||||
type="ANIME",
|
||||
page=1,
|
||||
perPage=os.environ.get("FASTANIME_PER_PAGE", 15),
|
||||
*_,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Gets the currently trending anime
|
||||
"""
|
||||
variables = {"type": type}
|
||||
variables = {"type": type, "page": page, "perPage": int(perPage)}
|
||||
trending = self.get_data(trending_query, variables)
|
||||
return trending
|
||||
|
||||
def get_most_favourite(self, type="ANIME", *_, **kwargs):
|
||||
def get_most_favourite(
|
||||
self,
|
||||
type="ANIME",
|
||||
page=1,
|
||||
perPage=os.environ.get("FASTANIME_PER_PAGE", 15),
|
||||
*_,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Gets the most favoured anime on anilist
|
||||
"""
|
||||
variables = {"type": type}
|
||||
variables = {"type": type, "page": page, "perPage": int(perPage)}
|
||||
most_favourite = self.get_data(most_favourite_query, variables)
|
||||
return most_favourite
|
||||
|
||||
def get_most_scored(self, type="ANIME", *_, **kwargs):
|
||||
def get_most_scored(
|
||||
self,
|
||||
type="ANIME",
|
||||
page=1,
|
||||
perPage=os.environ.get("FASTANIME_PER_PAGE", 15),
|
||||
*_,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Gets most scored anime on anilist
|
||||
"""
|
||||
variables = {"type": type}
|
||||
variables = {"type": type, "page": page, "perPage": int(perPage)}
|
||||
most_scored = self.get_data(most_scored_query, variables)
|
||||
return most_scored
|
||||
|
||||
def get_most_recently_updated(self, type="ANIME", *_, **kwargs):
|
||||
def get_most_recently_updated(
|
||||
self,
|
||||
type="ANIME",
|
||||
page=1,
|
||||
perPage=os.environ.get("FASTANIME_PER_PAGE", 15),
|
||||
*_,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Gets most recently updated anime from anilist
|
||||
"""
|
||||
variables = {"type": type}
|
||||
variables = {"type": type, "page": page, "perPage": int(perPage)}
|
||||
most_recently_updated = self.get_data(most_recently_updated_query, variables)
|
||||
return most_recently_updated
|
||||
|
||||
def get_most_popular(
|
||||
self,
|
||||
type="ANIME",
|
||||
page=1,
|
||||
perPage=os.environ.get("FASTANIME_PER_PAGE", 15),
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Gets most popular anime on anilist
|
||||
"""
|
||||
variables = {"type": type}
|
||||
variables = {"type": type, "page": page, "perPage": int(perPage)}
|
||||
most_popular = self.get_data(most_popular_query, variables)
|
||||
return most_popular
|
||||
|
||||
def get_upcoming_anime(self, type="ANIME", page: int = 1, *_, **kwargs):
|
||||
def get_upcoming_anime(
|
||||
self,
|
||||
type="ANIME",
|
||||
page: int = 1,
|
||||
perPage=os.environ.get("FASTANIME_PER_PAGE", 15),
|
||||
*_,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Gets upcoming anime from anilist
|
||||
"""
|
||||
variables = {"page": page, "type": type}
|
||||
variables = {"page": page, "type": type, "perPage": int(perPage)}
|
||||
upcoming_anime = self.get_data(upcoming_anime_query, variables)
|
||||
return upcoming_anime
|
||||
|
||||
# NOTE: THe following methods will probably be scraped soon
|
||||
def get_recommended_anime_for(self, id: int, type="ANIME", *_, **kwargs):
|
||||
variables = {"type": type}
|
||||
def get_recommended_anime_for(self, mediaRecommendationId, page=1, *_, **kwargs):
|
||||
variables = {"mediaRecommendationId": mediaRecommendationId, "page": page}
|
||||
recommended_anime = self.get_data(recommended_query, variables)
|
||||
return recommended_anime
|
||||
|
||||
@@ -412,7 +461,7 @@ class AniListApi:
|
||||
characters = self.get_data(anime_characters_query, variables)
|
||||
return characters
|
||||
|
||||
def get_related_anime_for(self, id: int, type="ANIME", *_, **kwargs):
|
||||
def get_related_anime_for(self, id: int, *_, **kwargs):
|
||||
variables = {"id": id}
|
||||
related_anime = self.get_data(anime_relations_query, variables)
|
||||
return related_anime
|
||||
|
||||
@@ -193,8 +193,8 @@ mutation (
|
||||
"""
|
||||
|
||||
media_list_query = """
|
||||
query ($userId: Int, $status: MediaListStatus, $type: MediaType) {
|
||||
Page {
|
||||
query ($userId: Int, $status: MediaListStatus, $type: MediaType, $page: Int, $perPage: Int) {
|
||||
Page(perPage: $perPage, page: $page) {
|
||||
pageInfo {
|
||||
currentPage
|
||||
total
|
||||
@@ -281,6 +281,7 @@ query ($userId: Int, $status: MediaListStatus, $type: MediaType) {
|
||||
|
||||
|
||||
optional_variables = "\
|
||||
$max_results:Int,\
|
||||
$page:Int,\
|
||||
$sort:[MediaSort],\
|
||||
$id_in:[Int],\
|
||||
@@ -310,7 +311,7 @@ $on_list:Boolean\
|
||||
search_query = (
|
||||
"""
|
||||
query($query:String,%s){
|
||||
Page(perPage: 50, page: $page) {
|
||||
Page(perPage: $max_results, page: $page) {
|
||||
pageInfo {
|
||||
total
|
||||
currentPage
|
||||
@@ -405,8 +406,8 @@ query($query:String,%s){
|
||||
)
|
||||
|
||||
trending_query = """
|
||||
query ($type: MediaType) {
|
||||
Page(perPage: 15) {
|
||||
query ($type: MediaType, $page: Int,$perPage:Int) {
|
||||
Page(perPage: $perPage, page: $page) {
|
||||
media(sort: TRENDING_DESC, type: $type, genre_not_in: ["hentai"]) {
|
||||
id
|
||||
idMal
|
||||
@@ -470,8 +471,8 @@ query ($type: MediaType) {
|
||||
|
||||
# mosts
|
||||
most_favourite_query = """
|
||||
query ($type: MediaType) {
|
||||
Page(perPage: 15) {
|
||||
query ($type: MediaType, $page: Int,$perPage:Int) {
|
||||
Page(perPage: $perPage, page: $page) {
|
||||
media(sort: FAVOURITES_DESC, type: $type, genre_not_in: ["hentai"]) {
|
||||
id
|
||||
idMal
|
||||
@@ -538,8 +539,8 @@ query ($type: MediaType) {
|
||||
"""
|
||||
|
||||
most_scored_query = """
|
||||
query ($type: MediaType) {
|
||||
Page(perPage: 15) {
|
||||
query ($type: MediaType, $page: Int,$perPage:Int) {
|
||||
Page(perPage: $perPage, page: $page) {
|
||||
media(sort: SCORE_DESC, type: $type, genre_not_in: ["hentai"]) {
|
||||
id
|
||||
idMal
|
||||
@@ -602,8 +603,8 @@ query ($type: MediaType) {
|
||||
"""
|
||||
|
||||
most_popular_query = """
|
||||
query ($type: MediaType) {
|
||||
Page(perPage: 15) {
|
||||
query ($type: MediaType, $page: Int,$perPage:Int) {
|
||||
Page(perPage: $perPage, page: $page) {
|
||||
media(sort: POPULARITY_DESC, type: $type, genre_not_in: ["hentai"]) {
|
||||
id
|
||||
idMal
|
||||
@@ -666,8 +667,8 @@ query ($type: MediaType) {
|
||||
"""
|
||||
|
||||
most_recently_updated_query = """
|
||||
query ($type: MediaType) {
|
||||
Page(perPage: 15) {
|
||||
query ($type: MediaType, $page: Int,$perPage:Int) {
|
||||
Page(perPage: $perPage, page: $page) {
|
||||
media(
|
||||
sort: UPDATED_AT_DESC
|
||||
type: $type
|
||||
@@ -737,14 +738,17 @@ query ($type: MediaType) {
|
||||
"""
|
||||
|
||||
recommended_query = """
|
||||
query ($type: MediaType) {
|
||||
Page(perPage: 15) {
|
||||
media(type: $type, genre_not_in: ["hentai"]) {
|
||||
recommendations(sort: RATING_DESC) {
|
||||
nodes {
|
||||
query ($mediaRecommendationId: Int, $page: Int) {
|
||||
Page(perPage: 50, page: $page) {
|
||||
recommendations(mediaRecommendationId: $mediaRecommendationId) {
|
||||
media {
|
||||
id
|
||||
idMal
|
||||
mediaListEntry {
|
||||
status
|
||||
id
|
||||
progress
|
||||
}
|
||||
title {
|
||||
english
|
||||
romaji
|
||||
@@ -796,11 +800,10 @@ query ($type: MediaType) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
anime_characters_query = """
|
||||
query ($id: Int, $type: MediaType) {
|
||||
Page {
|
||||
@@ -837,14 +840,8 @@ query ($id: Int, $type: MediaType) {
|
||||
|
||||
|
||||
anime_relations_query = """
|
||||
query ($id: Int, $type: MediaType) {
|
||||
Page(perPage: 20) {
|
||||
media(
|
||||
id: $id
|
||||
sort: POPULARITY_DESC
|
||||
type: $type
|
||||
genre_not_in: ["hentai"]
|
||||
) {
|
||||
query ($id: Int) {
|
||||
Media(id: $id) {
|
||||
relations {
|
||||
nodes {
|
||||
id
|
||||
@@ -900,7 +897,6 @@ query ($id: Int, $type: MediaType) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
@@ -922,8 +918,8 @@ query ($id: Int,$type:MediaType) {
|
||||
"""
|
||||
|
||||
upcoming_anime_query = """
|
||||
query ($page: Int, $type: MediaType) {
|
||||
Page(page: $page) {
|
||||
query ($page: Int, $type: MediaType,$perPage:Int) {
|
||||
Page(perPage: $perPage, page: $page) {
|
||||
pageInfo {
|
||||
total
|
||||
perPage
|
||||
|
||||
@@ -3,10 +3,10 @@ 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",
|
||||
"yugen": "api.YugenApi"
|
||||
"allanime": "api.AllAnime",
|
||||
"animepahe": "api.AnimePahe",
|
||||
"hianime": "api.HiAnime",
|
||||
"nyaa": "api.Nyaa",
|
||||
"yugen": "api.Yugen",
|
||||
}
|
||||
SERVERS_AVAILABLE = [*ALLANIME_SERVERS, *ANIMEPAHE_SERVERS, *HIANIME_SERVERS]
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
"""a module that handles the scraping of allanime
|
||||
|
||||
abstraction over allanime api
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
@@ -10,207 +5,215 @@ from typing import TYPE_CHECKING
|
||||
from ...anime_provider.base_provider import AnimeProvider
|
||||
from ..decorators import debug_provider
|
||||
from ..utils import give_random_quality, one_digit_symmetric_xor
|
||||
from .constants import ALLANIME_API_ENDPOINT, ALLANIME_BASE, ALLANIME_REFERER
|
||||
from .gql_queries import ALLANIME_EPISODES_GQL, ALLANIME_SEARCH_GQL, ALLANIME_SHOW_GQL
|
||||
from .constants import (
|
||||
API_BASE_URL,
|
||||
API_ENDPOINT,
|
||||
API_REFERER,
|
||||
DEFAULT_COUNTRY_OF_ORIGIN,
|
||||
DEFAULT_NSFW,
|
||||
DEFAULT_PAGE,
|
||||
DEFAULT_PER_PAGE,
|
||||
DEFAULT_UNKNOWN,
|
||||
MP4_SERVER_JUICY_STREAM_REGEX,
|
||||
)
|
||||
from .gql_queries import EPISODES_GQL, SEARCH_GQL, SHOW_GQL
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .types import AllAnimeEpisode
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# TODO: create tests for the api
|
||||
#
|
||||
# ** Based on ani-cli **
|
||||
class AllAnimeAPI(AnimeProvider):
|
||||
class AllAnime(AnimeProvider):
|
||||
"""
|
||||
Provides a fast and effective interface to AllAnime site.
|
||||
AllAnime is a provider class for fetching anime data from the AllAnime API.
|
||||
Attributes:
|
||||
HEADERS (dict): Default headers for API requests.
|
||||
Methods:
|
||||
_execute_graphql_query(query: str, variables: dict) -> dict:
|
||||
Executes a GraphQL query and returns the response data.
|
||||
search_for_anime(
|
||||
**kwargs
|
||||
) -> dict:
|
||||
Searches for anime based on the provided keywords and other parameters.
|
||||
get_anime(show_id: str) -> dict:
|
||||
Retrieves detailed information about a specific anime by its ID.
|
||||
_get_anime_episode(
|
||||
show_id: str, episode, translation_type: str = "sub"
|
||||
Retrieves information about a specific episode of an anime.
|
||||
get_episode_streams(
|
||||
) -> generator:
|
||||
Retrieves streaming links for a specific episode of an anime.
|
||||
"""
|
||||
|
||||
PROVIDER = "allanime"
|
||||
api_endpoint = ALLANIME_API_ENDPOINT
|
||||
HEADERS = {
|
||||
"Referer": ALLANIME_REFERER,
|
||||
"Referer": API_REFERER,
|
||||
}
|
||||
|
||||
def _fetch_gql(self, query: str, variables: dict):
|
||||
"""main abstraction over all requests to the allanime api
|
||||
def _execute_graphql_query(self, query: str, variables: dict):
|
||||
"""
|
||||
Executes a GraphQL query using the provided query string and variables.
|
||||
|
||||
Args:
|
||||
query: [TODO:description]
|
||||
variables: [TODO:description]
|
||||
query (str): The GraphQL query string to be executed.
|
||||
variables (dict): A dictionary of variables to be used in the query.
|
||||
|
||||
Returns:
|
||||
[TODO:return]
|
||||
dict: The JSON response data from the GraphQL API.
|
||||
|
||||
Raises:
|
||||
requests.exceptions.HTTPError: If the HTTP request returned an unsuccessful status code.
|
||||
"""
|
||||
|
||||
response = self.session.get(
|
||||
self.api_endpoint,
|
||||
API_ENDPOINT,
|
||||
params={
|
||||
"variables": json.dumps(variables),
|
||||
"query": query,
|
||||
},
|
||||
timeout=10,
|
||||
)
|
||||
if response.ok:
|
||||
response.raise_for_status()
|
||||
return response.json()["data"]
|
||||
else:
|
||||
logger.error("[ALLANIME-ERROR]: ", response.text)
|
||||
return {}
|
||||
|
||||
@debug_provider(PROVIDER.upper())
|
||||
@debug_provider
|
||||
def search_for_anime(
|
||||
self,
|
||||
user_query: str,
|
||||
translation_type: str = "sub",
|
||||
nsfw=True,
|
||||
unknown=True,
|
||||
search_keywords: str,
|
||||
translation_type: str,
|
||||
*,
|
||||
nsfw=DEFAULT_NSFW,
|
||||
unknown=DEFAULT_UNKNOWN,
|
||||
limit=DEFAULT_PER_PAGE,
|
||||
page=DEFAULT_PAGE,
|
||||
country_of_origin=DEFAULT_COUNTRY_OF_ORIGIN,
|
||||
**kwargs,
|
||||
):
|
||||
"""search for an anime title using allanime provider
|
||||
|
||||
Args:
|
||||
nsfw ([TODO:parameter]): [TODO:description]
|
||||
unknown ([TODO:parameter]): [TODO:description]
|
||||
user_query: [TODO:description]
|
||||
translation_type: [TODO:description]
|
||||
**kwargs: [TODO:args]
|
||||
|
||||
Returns:
|
||||
[TODO:return]
|
||||
"""
|
||||
search = {"allowAdult": nsfw, "allowUnknown": unknown, "query": user_query}
|
||||
limit = 40
|
||||
translationtype = translation_type
|
||||
countryorigin = "all"
|
||||
page = 1
|
||||
variables = {
|
||||
"search": search,
|
||||
Search for anime based on given keywords and filters.
|
||||
Args:
|
||||
search_keywords (str): The keywords to search for.
|
||||
translation_type (str, optional): The type of translation to search for (e.g., "sub" or "dub"). Defaults to "sub".
|
||||
limit (int, optional): The maximum number of results to return. Defaults to 40.
|
||||
page (int, optional): The page number to return. Defaults to 1.
|
||||
country_of_origin (str, optional): The country of origin filter. Defaults to "all".
|
||||
nsfw (bool, optional): Whether to include adult content in the search results. Defaults to True.
|
||||
unknown (bool, optional): Whether to include unknown content in the search results. Defaults to True.
|
||||
**kwargs: Additional keyword arguments.
|
||||
Returns:
|
||||
dict: A dictionary containing the page information and a list of search results. Each result includes:
|
||||
- id (str): The ID of the anime.
|
||||
- title (str): The title of the anime.
|
||||
- type (str): The type of the anime.
|
||||
- availableEpisodes (int): The number of available episodes.
|
||||
"""
|
||||
search_results = self._execute_graphql_query(
|
||||
SEARCH_GQL,
|
||||
variables={
|
||||
"search": {
|
||||
"allowAdult": nsfw,
|
||||
"allowUnknown": unknown,
|
||||
"query": search_keywords,
|
||||
},
|
||||
"limit": limit,
|
||||
"page": page,
|
||||
"translationtype": translationtype,
|
||||
"countryorigin": countryorigin,
|
||||
}
|
||||
search_results = self._fetch_gql(ALLANIME_SEARCH_GQL, variables)
|
||||
page_info = search_results["shows"]["pageInfo"]
|
||||
results = []
|
||||
for result in search_results["shows"]["edges"]:
|
||||
normalized_result = {
|
||||
"translationtype": translation_type,
|
||||
"countryorigin": country_of_origin,
|
||||
},
|
||||
)
|
||||
return {
|
||||
"pageInfo": search_results["shows"]["pageInfo"],
|
||||
"results": [
|
||||
{
|
||||
"id": result["_id"],
|
||||
"title": result["name"],
|
||||
"type": result["__typename"],
|
||||
"availableEpisodes": result["availableEpisodes"],
|
||||
}
|
||||
results.append(normalized_result)
|
||||
|
||||
normalized_search_results = {
|
||||
"pageInfo": page_info,
|
||||
"results": results,
|
||||
for result in search_results["shows"]["edges"]
|
||||
],
|
||||
}
|
||||
return normalized_search_results
|
||||
|
||||
@debug_provider(PROVIDER.upper())
|
||||
def get_anime(self, allanime_show_id: str):
|
||||
"""get an anime details given its id
|
||||
|
||||
Args:
|
||||
allanime_show_id: [TODO:description]
|
||||
|
||||
Returns:
|
||||
[TODO:return]
|
||||
@debug_provider
|
||||
def get_anime(self, id: str, **kwargs):
|
||||
"""
|
||||
Fetches anime details using the provided show ID.
|
||||
Args:
|
||||
id (str): The ID of the anime show to fetch details for.
|
||||
Returns:
|
||||
dict: A dictionary containing the anime details, including:
|
||||
- id (str): The unique identifier of the anime show.
|
||||
- title (str): The title of the anime show.
|
||||
- availableEpisodesDetail (list): A list of available episodes details.
|
||||
- type (str, optional): The type of the anime show.
|
||||
"""
|
||||
variables = {"showId": allanime_show_id}
|
||||
anime = self._fetch_gql(ALLANIME_SHOW_GQL, variables)
|
||||
id: str = anime["show"]["_id"]
|
||||
title: str = anime["show"]["name"]
|
||||
availableEpisodesDetail = anime["show"]["availableEpisodesDetail"]
|
||||
self.store.set(allanime_show_id, "anime_info", {"title": title})
|
||||
type = anime.get("__typename")
|
||||
normalized_anime = {
|
||||
"id": id,
|
||||
"title": title,
|
||||
"availableEpisodesDetail": availableEpisodesDetail,
|
||||
"type": type,
|
||||
}
|
||||
return normalized_anime
|
||||
|
||||
@debug_provider(PROVIDER.upper())
|
||||
anime = self._execute_graphql_query(SHOW_GQL, variables={"showId": id})
|
||||
self.store.set(id, "anime_info", {"title": anime["show"]["name"]})
|
||||
return {
|
||||
"id": anime["show"]["_id"],
|
||||
"title": anime["show"]["name"],
|
||||
"availableEpisodesDetail": anime["show"]["availableEpisodesDetail"],
|
||||
"type": anime.get("__typename"),
|
||||
}
|
||||
|
||||
@debug_provider
|
||||
def _get_anime_episode(
|
||||
self, allanime_show_id: str, episode, translation_type: str = "sub"
|
||||
) -> "AllAnimeEpisode | dict":
|
||||
"""get the episode details and sources info
|
||||
|
||||
Args:
|
||||
allanime_show_id: [TODO:description]
|
||||
episode_string: [TODO:description]
|
||||
translation_type: [TODO:description]
|
||||
|
||||
Returns:
|
||||
[TODO:return]
|
||||
self, anime_id: str, episode, translation_type: str = "sub"
|
||||
) -> "AllAnimeEpisode":
|
||||
"""
|
||||
variables = {
|
||||
"showId": allanime_show_id,
|
||||
Fetches a specific episode of an anime by its ID and episode number.
|
||||
Args:
|
||||
anime_id (str): The unique identifier of the anime.
|
||||
episode (str): The episode number or string identifier.
|
||||
translation_type (str, optional): The type of translation for the episode. Defaults to "sub".
|
||||
Returns:
|
||||
AllAnimeEpisode: The episode details retrieved from the GraphQL query.
|
||||
"""
|
||||
return self._execute_graphql_query(
|
||||
EPISODES_GQL,
|
||||
variables={
|
||||
"showId": anime_id,
|
||||
"translationType": translation_type,
|
||||
"episodeString": episode,
|
||||
}
|
||||
episode = self._fetch_gql(ALLANIME_EPISODES_GQL, variables)
|
||||
return episode["episode"]
|
||||
},
|
||||
)["episode"]
|
||||
|
||||
@debug_provider(PROVIDER.upper())
|
||||
def get_episode_streams(
|
||||
self, anime_id, episode_number: str, translation_type="sub"
|
||||
@debug_provider
|
||||
def _get_server(
|
||||
self,
|
||||
embed,
|
||||
anime_title: str,
|
||||
allanime_episode: "AllAnimeEpisode",
|
||||
episode_number,
|
||||
):
|
||||
"""get the streams of an episode
|
||||
|
||||
"""
|
||||
Retrieves the streaming server information for a given anime episode based on the provided embed data.
|
||||
Args:
|
||||
translation_type ([TODO:parameter]): [TODO:description]
|
||||
anime: [TODO:description]
|
||||
episode_number: [TODO:description]
|
||||
|
||||
Yields:
|
||||
[TODO:description]
|
||||
embed (dict): A dictionary containing the embed data, including the source URL and source name.
|
||||
anime_title (str): The title of the anime.
|
||||
allanime_episode (AllAnimeEpisode): An object representing the episode details.
|
||||
Returns:
|
||||
dict: A dictionary containing server information, headers, subtitles, episode title, and links to the stream.
|
||||
Returns None if no valid URL or stream is found.
|
||||
Raises:
|
||||
requests.exceptions.RequestException: If there is an issue with the HTTP request.
|
||||
"""
|
||||
|
||||
anime_title = (self.store.get(anime_id, "anime_info", "") or {"title": ""})[
|
||||
"title"
|
||||
]
|
||||
allanime_episode = self._get_anime_episode(
|
||||
anime_id, episode_number, translation_type
|
||||
)
|
||||
if not allanime_episode:
|
||||
return []
|
||||
|
||||
embeds = allanime_episode["sourceUrls"]
|
||||
|
||||
@debug_provider(self.PROVIDER.upper())
|
||||
def _get_server(embed):
|
||||
# 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 (
|
||||
# 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
|
||||
):
|
||||
return
|
||||
url = embed.get("sourceUrl")
|
||||
#
|
||||
if not url:
|
||||
return
|
||||
if url.startswith("--"):
|
||||
url = url[2:]
|
||||
url = one_digit_symmetric_xor(56, url)
|
||||
url = one_digit_symmetric_xor(56, url[2:])
|
||||
|
||||
if "tools.fast4speed.rsvp" in url:
|
||||
# FIRST CASE
|
||||
match embed["sourceName"]:
|
||||
case "Yt-mp4":
|
||||
logger.debug("Found streams from Yt")
|
||||
return {
|
||||
"server": "Yt",
|
||||
"episode_title": f"{anime_title}; Episode {episode_number}",
|
||||
"headers": {"Referer": f"https://{ALLANIME_BASE}/"},
|
||||
"headers": {"Referer": f"https://{API_BASE_URL}/"},
|
||||
"subtitles": [],
|
||||
"links": [
|
||||
{
|
||||
@@ -219,77 +222,280 @@ class AllAnimeAPI(AnimeProvider):
|
||||
}
|
||||
],
|
||||
}
|
||||
case "Mp4":
|
||||
logger.debug("Found streams from Mp4")
|
||||
response = self.session.get(
|
||||
url,
|
||||
fresh=1, # pyright: ignore
|
||||
timeout=10,
|
||||
)
|
||||
response.raise_for_status()
|
||||
embed_html = response.text.replace(" ", "").replace("\n", "")
|
||||
vid = MP4_SERVER_JUICY_STREAM_REGEX.search(embed_html)
|
||||
if not vid:
|
||||
return
|
||||
return {
|
||||
"server": "mp4-upload",
|
||||
"headers": {"Referer": "https://www.mp4upload.com/"},
|
||||
"subtitles": [],
|
||||
"episode_title": (allanime_episode["notes"] or f"{anime_title}")
|
||||
+ f"; Episode {episode_number}",
|
||||
"links": [{"link": vid.group(1), "quality": "1080"}],
|
||||
}
|
||||
case "Fm-Hls":
|
||||
# TODO: requires decoding obsfucated js (filemoon)
|
||||
logger.debug("Found streams from Fm-Hls")
|
||||
response = self.session.get(
|
||||
url,
|
||||
timeout=10,
|
||||
)
|
||||
response.raise_for_status()
|
||||
embed_html = response.text.replace(" ", "").replace("\n", "")
|
||||
vid = MP4_SERVER_JUICY_STREAM_REGEX.search(embed_html)
|
||||
if not vid:
|
||||
return
|
||||
return {
|
||||
"server": "filemoon",
|
||||
"headers": {"Referer": "https://www.mp4upload.com/"},
|
||||
"subtitles": [],
|
||||
"episode_title": (allanime_episode["notes"] or f"{anime_title}")
|
||||
+ f"; Episode {episode_number}",
|
||||
"links": [{"link": vid.group(1), "quality": "1080"}],
|
||||
}
|
||||
case "Ok":
|
||||
# TODO: requires decoding the obsfucated js (filemoon)
|
||||
response = self.session.get(
|
||||
url,
|
||||
timeout=10,
|
||||
)
|
||||
response.raise_for_status()
|
||||
embed_html = response.text.replace(" ", "").replace("\n", "")
|
||||
vid = MP4_SERVER_JUICY_STREAM_REGEX.search(embed_html)
|
||||
logger.debug("Found streams from Ok")
|
||||
return {
|
||||
"server": "filemoon",
|
||||
"headers": {"Referer": f"https://{API_BASE_URL}/"},
|
||||
"subtitles": [],
|
||||
"episode_title": (allanime_episode["notes"] or f"{anime_title}")
|
||||
+ f"; Episode {episode_number}",
|
||||
"links": give_random_quality(response.json()["links"]),
|
||||
}
|
||||
case "Vid-mp4":
|
||||
# TODO: requires some serious work i think : )
|
||||
response = self.session.get(
|
||||
url,
|
||||
timeout=10,
|
||||
)
|
||||
response.raise_for_status()
|
||||
embed_html = response.text.replace(" ", "").replace("\n", "")
|
||||
logger.debug("Found streams from vid-mp4")
|
||||
return {
|
||||
"server": "Vid-mp4",
|
||||
"headers": {"Referer": f"https://{API_BASE_URL}/"},
|
||||
"subtitles": [],
|
||||
"episode_title": (allanime_episode["notes"] or f"{anime_title}")
|
||||
+ f"; Episode {episode_number}",
|
||||
"links": give_random_quality(response.json()["links"]),
|
||||
}
|
||||
case "Ss-Hls":
|
||||
# TODO: requires some serious work i think : )
|
||||
response = self.session.get(
|
||||
url,
|
||||
timeout=10,
|
||||
)
|
||||
response.raise_for_status()
|
||||
embed_html = response.text.replace(" ", "").replace("\n", "")
|
||||
logger.debug("Found streams from Ss-Hls")
|
||||
return {
|
||||
"server": "StreamSb",
|
||||
"headers": {"Referer": f"https://{API_BASE_URL}/"},
|
||||
"subtitles": [],
|
||||
"episode_title": (allanime_episode["notes"] or f"{anime_title}")
|
||||
+ f"; Episode {episode_number}",
|
||||
"links": give_random_quality(response.json()["links"]),
|
||||
}
|
||||
|
||||
# get the stream url for an episode of the defined source names
|
||||
embed_url = f"https://{ALLANIME_BASE}{url.replace('clock', 'clock.json')}"
|
||||
resp = self.session.get(
|
||||
embed_url,
|
||||
response = self.session.get(
|
||||
f"https://{API_BASE_URL}{url.replace('clock', 'clock.json')}",
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
if resp.ok:
|
||||
response.raise_for_status()
|
||||
|
||||
# SECOND CASE
|
||||
match embed["sourceName"]:
|
||||
case "Luf-mp4":
|
||||
logger.debug("allanime:Found streams from gogoanime")
|
||||
logger.debug("Found streams from gogoanime")
|
||||
return {
|
||||
"server": "gogoanime",
|
||||
"headers": {"Referer": f"https://{ALLANIME_BASE}/"},
|
||||
"headers": {"Referer": f"https://{API_BASE_URL}/"},
|
||||
"subtitles": [],
|
||||
"episode_title": (
|
||||
allanime_episode["notes"] or f"{anime_title}"
|
||||
)
|
||||
"episode_title": (allanime_episode["notes"] or f"{anime_title}")
|
||||
+ f"; Episode {episode_number}",
|
||||
"links": give_random_quality(resp.json()["links"]),
|
||||
"links": give_random_quality(response.json()["links"]),
|
||||
}
|
||||
case "Kir":
|
||||
logger.debug("allanime:Found streams from wetransfer")
|
||||
logger.debug("Found streams from wetransfer")
|
||||
return {
|
||||
"server": "wetransfer",
|
||||
"headers": {"Referer": f"https://{ALLANIME_BASE}/"},
|
||||
"server": "weTransfer",
|
||||
"headers": {"Referer": f"https://{API_BASE_URL}/"},
|
||||
"subtitles": [],
|
||||
"episode_title": (
|
||||
allanime_episode["notes"] or f"{anime_title}"
|
||||
)
|
||||
"episode_title": (allanime_episode["notes"] or f"{anime_title}")
|
||||
+ f"; Episode {episode_number}",
|
||||
"links": give_random_quality(resp.json()["links"]),
|
||||
"links": give_random_quality(response.json()["links"]),
|
||||
}
|
||||
case "S-mp4":
|
||||
logger.debug("allanime:Found streams from sharepoint")
|
||||
logger.debug("Found streams from sharepoint")
|
||||
return {
|
||||
"server": "sharepoint",
|
||||
"headers": {"Referer": f"https://{ALLANIME_BASE}/"},
|
||||
"headers": {"Referer": f"https://{API_BASE_URL}/"},
|
||||
"subtitles": [],
|
||||
"episode_title": (
|
||||
allanime_episode["notes"] or f"{anime_title}"
|
||||
)
|
||||
"episode_title": (allanime_episode["notes"] or f"{anime_title}")
|
||||
+ f"; Episode {episode_number}",
|
||||
"links": give_random_quality(resp.json()["links"]),
|
||||
"links": give_random_quality(response.json()["links"]),
|
||||
}
|
||||
case "Sak":
|
||||
logger.debug("allanime:Found streams from dropbox")
|
||||
logger.debug("Found streams from dropbox")
|
||||
return {
|
||||
"server": "dropbox",
|
||||
"headers": {"Referer": f"https://{ALLANIME_BASE}/"},
|
||||
"headers": {"Referer": f"https://{API_BASE_URL}/"},
|
||||
"subtitles": [],
|
||||
"episode_title": (
|
||||
allanime_episode["notes"] or f"{anime_title}"
|
||||
)
|
||||
"episode_title": (allanime_episode["notes"] or f"{anime_title}")
|
||||
+ f"; Episode {episode_number}",
|
||||
"links": give_random_quality(resp.json()["links"]),
|
||||
"links": give_random_quality(response.json()["links"]),
|
||||
}
|
||||
case "Default":
|
||||
logger.debug("allanime:Found streams from wixmp")
|
||||
logger.debug("Found streams from wixmp")
|
||||
return {
|
||||
"server": "wixmp",
|
||||
"headers": {"Referer": f"https://{ALLANIME_BASE}/"},
|
||||
"headers": {"Referer": f"https://{API_BASE_URL}/"},
|
||||
"subtitles": [],
|
||||
"episode_title": (
|
||||
allanime_episode["notes"] or f"{anime_title}"
|
||||
)
|
||||
"episode_title": (allanime_episode["notes"] or f"{anime_title}")
|
||||
+ f"; Episode {episode_number}",
|
||||
"links": give_random_quality(resp.json()["links"]),
|
||||
"links": give_random_quality(response.json()["links"]),
|
||||
}
|
||||
|
||||
for embed in embeds:
|
||||
if server := _get_server(embed):
|
||||
case "Ak":
|
||||
# TODO: works but needs further probing
|
||||
logger.debug("Found streams from Ak")
|
||||
return {
|
||||
"server": "Ak",
|
||||
"headers": {"Referer": f"https://{API_BASE_URL}/"},
|
||||
"subtitles": [],
|
||||
"episode_title": (allanime_episode["notes"] or f"{anime_title}")
|
||||
+ f"; Episode {episode_number}",
|
||||
"links": give_random_quality(response.json()["links"]),
|
||||
}
|
||||
|
||||
@debug_provider
|
||||
def get_episode_streams(
|
||||
self, anime_id, episode_number: str, translation_type="sub", **kwargs
|
||||
):
|
||||
"""
|
||||
Retrieve streaming information for a specific episode of an anime.
|
||||
Args:
|
||||
anime_id (str): The unique identifier for the anime.
|
||||
episode_number (str): The episode number to retrieve streams for.
|
||||
translation_type (str, optional): The type of translation for the episode (e.g., "sub" for subtitles). Defaults to "sub".
|
||||
Yields:
|
||||
dict: A dictionary containing streaming information for the episode, including:
|
||||
- server (str): The name of the streaming server.
|
||||
- episode_title (str): The title of the episode.
|
||||
- headers (dict): HTTP headers required for accessing the stream.
|
||||
- subtitles (list): A list of subtitles available for the episode.
|
||||
- links (list): A list of dictionaries containing streaming links and their quality.
|
||||
"""
|
||||
anime_title = (self.store.get(anime_id, "anime_info", "") or {"title": ""})[
|
||||
"title"
|
||||
]
|
||||
allanime_episode = self._get_anime_episode(
|
||||
anime_id, episode_number, translation_type
|
||||
)
|
||||
|
||||
for embed in allanime_episode["sourceUrls"]:
|
||||
if embed.get("sourceName", "") not in (
|
||||
# priorities based on death note
|
||||
"Sak", # 7
|
||||
"S-mp4", # 7.9
|
||||
"Luf-mp4", # 7.7
|
||||
"Default", # 8.5
|
||||
"Yt-mp4", # 7.9
|
||||
"Kir", # NA
|
||||
"Mp4", # 4
|
||||
# "Ak",#
|
||||
# "Vid-mp4", # 4
|
||||
# "Ok", # 3.5
|
||||
# "Ss-Hls", # 5.5
|
||||
# "Fm-Hls",#
|
||||
):
|
||||
logger.debug(f"Found {embed['sourceName']} but ignoring")
|
||||
continue
|
||||
if server := self._get_server(
|
||||
embed, anime_title, allanime_episode, episode_number
|
||||
):
|
||||
yield server
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import subprocess
|
||||
|
||||
allanime = AllAnime(cache_requests="True", use_persistent_provider_store="False")
|
||||
search_term = input("Enter the search term for the anime: ")
|
||||
translation_type = input("Enter the translation type (sub/dub): ")
|
||||
|
||||
search_results = allanime.search_for_anime(
|
||||
search_keywords=search_term, translation_type=translation_type
|
||||
)
|
||||
|
||||
if not search_results["results"]:
|
||||
print("No results found.")
|
||||
exit()
|
||||
|
||||
print("Search Results:")
|
||||
for idx, result in enumerate(search_results["results"], start=1):
|
||||
print(f"{idx}. {result['title']} (ID: {result['id']})")
|
||||
|
||||
anime_choice = int(input("Enter the number of the anime you want to watch: ")) - 1
|
||||
anime_id = search_results["results"][anime_choice]["id"]
|
||||
|
||||
anime_details = allanime.get_anime(anime_id)
|
||||
print(f"Selected Anime: {anime_details['title']}")
|
||||
|
||||
print("Available Episodes:")
|
||||
for idx, episode in enumerate(
|
||||
sorted(anime_details["availableEpisodesDetail"][translation_type], key=float),
|
||||
start=1,
|
||||
):
|
||||
print(f"{idx}. Episode {episode}")
|
||||
|
||||
episode_choice = (
|
||||
int(input("Enter the number of the episode you want to watch: ")) - 1
|
||||
)
|
||||
episode_number = anime_details["availableEpisodesDetail"][translation_type][
|
||||
episode_choice
|
||||
]
|
||||
|
||||
streams = list(
|
||||
allanime.get_episode_streams(anime_id, episode_number, translation_type)
|
||||
)
|
||||
if not streams:
|
||||
print("No streams available.")
|
||||
exit()
|
||||
|
||||
print("Available Streams:")
|
||||
for idx, stream in enumerate(streams, start=1):
|
||||
print(f"{idx}. Server: {stream['server']}")
|
||||
|
||||
server_choice = int(input("Enter the number of the server you want to use: ")) - 1
|
||||
selected_stream = streams[server_choice]
|
||||
|
||||
stream_link = selected_stream["links"][0]["link"]
|
||||
mpv_args = ["mpv", stream_link]
|
||||
headers = selected_stream["headers"]
|
||||
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)
|
||||
subprocess.run(mpv_args)
|
||||
|
||||
@@ -1,4 +1,27 @@
|
||||
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)
|
||||
import re
|
||||
|
||||
SERVERS_AVAILABLE = [
|
||||
"sharepoint",
|
||||
"dropbox",
|
||||
"gogoanime",
|
||||
"weTransfer",
|
||||
"wixmp",
|
||||
"Yt",
|
||||
"mp4-upload",
|
||||
]
|
||||
API_BASE_URL = "allanime.day"
|
||||
API_REFERER = "https://allanime.to/"
|
||||
API_ENDPOINT = f"https://api.{API_BASE_URL}/api/"
|
||||
|
||||
# search constants
|
||||
DEFAULT_COUNTRY_OF_ORIGIN = "all"
|
||||
DEFAULT_NSFW = True
|
||||
DEFAULT_UNKNOWN = True
|
||||
DEFAULT_PER_PAGE = 40
|
||||
DEFAULT_PAGE = 1
|
||||
|
||||
# regex stuff
|
||||
|
||||
MP4_SERVER_JUICY_STREAM_REGEX = re.compile(
|
||||
r"video/mp4\",src:\"(https?://.*/video\.mp4)\""
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
ALLANIME_SEARCH_GQL = """
|
||||
SEARCH_GQL = """
|
||||
query (
|
||||
$search: SearchInput
|
||||
$limit: Int
|
||||
@@ -27,7 +27,7 @@ query (
|
||||
"""
|
||||
|
||||
|
||||
ALLANIME_EPISODES_GQL = """\
|
||||
EPISODES_GQL = """\
|
||||
query (
|
||||
$showId: String!
|
||||
$translationType: VaildTranslationTypeEnumType!
|
||||
@@ -45,7 +45,7 @@ query (
|
||||
}
|
||||
"""
|
||||
|
||||
ALLANIME_SHOW_GQL = """
|
||||
SHOW_GQL = """
|
||||
query ($showId: String!) {
|
||||
show(_id: $showId) {
|
||||
_id
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import logging
|
||||
import random
|
||||
import re
|
||||
import time
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
@@ -15,49 +14,33 @@ from ..decorators import debug_provider
|
||||
from .constants import (
|
||||
ANIMEPAHE_BASE,
|
||||
ANIMEPAHE_ENDPOINT,
|
||||
JUICY_STREAM_REGEX,
|
||||
REQUEST_HEADERS,
|
||||
SERVER_HEADERS,
|
||||
)
|
||||
from .utils import process_animepahe_embed_page
|
||||
from .extractors import process_animepahe_embed_page
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .types import AnimePaheAnimePage, AnimePaheSearchPage, AnimePaheSearchResult
|
||||
JUICY_STREAM_REGEX = re.compile(r"source='(.*)';")
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
KWIK_RE = re.compile(r"Player\|(.+?)'")
|
||||
|
||||
|
||||
class AnimePaheApi(AnimeProvider):
|
||||
class AnimePahe(AnimeProvider):
|
||||
search_page: "AnimePaheSearchPage"
|
||||
anime: "AnimePaheAnimePage"
|
||||
HEADERS = REQUEST_HEADERS
|
||||
PROVIDER = "animepahe"
|
||||
|
||||
@debug_provider(PROVIDER.upper())
|
||||
def search_for_anime(self, user_query: str, *args):
|
||||
url = f"{ANIMEPAHE_ENDPOINT}m=search&q={user_query}"
|
||||
@debug_provider
|
||||
def search_for_anime(self, search_keywords: str, translation_type, **kwargs):
|
||||
response = self.session.get(
|
||||
url,
|
||||
ANIMEPAHE_ENDPOINT, params={"m": "search", "q": search_keywords}
|
||||
)
|
||||
if not response.ok:
|
||||
return
|
||||
response.raise_for_status()
|
||||
data: "AnimePaheSearchPage" = response.json()
|
||||
self.search_page = data
|
||||
for animepahe_search_result in data["data"]:
|
||||
self.store.set(
|
||||
str(animepahe_search_result["session"]),
|
||||
"search_result",
|
||||
animepahe_search_result,
|
||||
)
|
||||
|
||||
return {
|
||||
"pageInfo": {
|
||||
"total": data["total"],
|
||||
"perPage": data["per_page"],
|
||||
"currentPage": data["current_page"],
|
||||
},
|
||||
"results": [
|
||||
results = []
|
||||
for result in data["data"]:
|
||||
results.append(
|
||||
{
|
||||
"availableEpisodes": list(range(result["episodes"])),
|
||||
"id": result["session"],
|
||||
@@ -69,27 +52,32 @@ class AnimePaheApi(AnimeProvider):
|
||||
"season": result["season"],
|
||||
"poster": result["poster"],
|
||||
}
|
||||
for result in data["data"]
|
||||
],
|
||||
)
|
||||
self.store.set(
|
||||
str(result["session"]),
|
||||
"search_result",
|
||||
result,
|
||||
)
|
||||
|
||||
return {
|
||||
"pageInfo": {
|
||||
"total": data["total"],
|
||||
"perPage": data["per_page"],
|
||||
"currentPage": data["current_page"],
|
||||
},
|
||||
"results": results,
|
||||
}
|
||||
|
||||
@debug_provider(PROVIDER.upper())
|
||||
def get_anime(self, session_id: str, *args):
|
||||
page = 1
|
||||
if d := self.store.get(str(session_id), "search_result"):
|
||||
anime_result: "AnimePaheSearchResult" = d
|
||||
data: "AnimePaheAnimePage" = {} # pyright:ignore
|
||||
|
||||
url = f"{ANIMEPAHE_ENDPOINT}m=release&id={session_id}&sort=episode_asc&page={page}"
|
||||
|
||||
@debug_provider
|
||||
def _pages_loader(
|
||||
url,
|
||||
self,
|
||||
data,
|
||||
session_id,
|
||||
params,
|
||||
page,
|
||||
):
|
||||
response = self.session.get(
|
||||
url,
|
||||
)
|
||||
if response.ok:
|
||||
response = self.session.get(ANIMEPAHE_ENDPOINT, params=params)
|
||||
response.raise_for_status()
|
||||
if not data:
|
||||
data.update(response.json())
|
||||
else:
|
||||
@@ -109,15 +97,36 @@ class AnimePaheApi(AnimeProvider):
|
||||
)
|
||||
)
|
||||
page += 1
|
||||
url = f"{ANIMEPAHE_ENDPOINT}m=release&id={session_id}&sort=episode_asc&page={page}"
|
||||
_pages_loader(
|
||||
url,
|
||||
page,
|
||||
self._pages_loader(
|
||||
data,
|
||||
session_id,
|
||||
params={
|
||||
"m": "release",
|
||||
"page": page,
|
||||
"id": session_id,
|
||||
"sort": "episode_asc",
|
||||
},
|
||||
page=page,
|
||||
)
|
||||
return data
|
||||
|
||||
_pages_loader(
|
||||
url,
|
||||
page,
|
||||
@debug_provider
|
||||
def get_anime(self, session_id: str, **kwargs):
|
||||
page = 1
|
||||
if d := self.store.get(str(session_id), "search_result"):
|
||||
anime_result: "AnimePaheSearchResult" = d
|
||||
data: "AnimePaheAnimePage" = {} # pyright:ignore
|
||||
|
||||
data = self._pages_loader(
|
||||
data,
|
||||
session_id,
|
||||
params={
|
||||
"m": "release",
|
||||
"id": session_id,
|
||||
"sort": "episode_asc",
|
||||
"page": page,
|
||||
},
|
||||
page=page,
|
||||
)
|
||||
|
||||
if not data:
|
||||
@@ -151,47 +160,13 @@ class AnimePaheApi(AnimeProvider):
|
||||
],
|
||||
}
|
||||
|
||||
@debug_provider(PROVIDER.upper())
|
||||
def get_episode_streams(
|
||||
self, anime_id, episode_number: str, translation_type, *args
|
||||
):
|
||||
anime_title = ""
|
||||
episode = None
|
||||
# extract episode details from memory
|
||||
if d := self.store.get(str(anime_id), "anime_info"):
|
||||
anime_title = d["title"]
|
||||
episode = [
|
||||
episode
|
||||
for episode in d["data"]
|
||||
if float(episode["episode"]) == float(episode_number)
|
||||
]
|
||||
|
||||
if not episode:
|
||||
logger.error(f"[ANIMEPAHE-ERROR]: episode {episode_number} doesn't exist")
|
||||
return []
|
||||
episode = episode[0]
|
||||
|
||||
# fetch the episode page
|
||||
url = f"{ANIMEPAHE_BASE}/play/{anime_id}/{episode['session']}"
|
||||
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)
|
||||
# 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']}"
|
||||
)
|
||||
@debug_provider
|
||||
def _get_server(self, episode, res_dicts, anime_title, translation_type):
|
||||
# get all links
|
||||
streams = {
|
||||
"server": "kwik",
|
||||
"links": [],
|
||||
"episode_title": episode_title,
|
||||
"episode_title": f"{episode['title'] or anime_title}; Episode {episode['episode']}",
|
||||
"subtitles": [],
|
||||
"headers": {},
|
||||
}
|
||||
@@ -207,23 +182,22 @@ class AnimePaheApi(AnimeProvider):
|
||||
logger.warning(
|
||||
"[ANIMEPAHE-WARN]: embed url not found please report to the developers"
|
||||
)
|
||||
return []
|
||||
continue
|
||||
# get embed page
|
||||
embed_response = self.session.get(
|
||||
embed_url, headers={"User-Agent": self.USER_AGENT, **SERVER_HEADERS}
|
||||
)
|
||||
if not response.ok:
|
||||
continue
|
||||
embed_response.raise_for_status()
|
||||
embed_page = embed_response.text
|
||||
|
||||
decoded_js = process_animepahe_embed_page(embed_page)
|
||||
if not decoded_js:
|
||||
logger.error("[ANIMEPAHE-ERROR]: failed to decode embed page")
|
||||
return
|
||||
continue
|
||||
juicy_stream = JUICY_STREAM_REGEX.search(decoded_js)
|
||||
if not juicy_stream:
|
||||
logger.error("[ANIMEPAHE-ERROR]: failed to find juicy stream")
|
||||
return
|
||||
continue
|
||||
juicy_stream = juicy_stream.group(1)
|
||||
# add the link
|
||||
streams["links"].append(
|
||||
@@ -233,4 +207,119 @@ class AnimePaheApi(AnimeProvider):
|
||||
"link": juicy_stream,
|
||||
}
|
||||
)
|
||||
yield streams
|
||||
return streams
|
||||
|
||||
@debug_provider
|
||||
def get_episode_streams(
|
||||
self, anime_id, episode_number: str, translation_type, **kwargs
|
||||
):
|
||||
anime_title = ""
|
||||
# extract episode details from memory
|
||||
anime_info = self.store.get(str(anime_id), "anime_info")
|
||||
if not anime_info:
|
||||
logger.error(
|
||||
f"[ANIMEPAHE-ERROR]: Anime with ID {anime_id} not found in store"
|
||||
)
|
||||
return
|
||||
|
||||
anime_title = anime_info["title"]
|
||||
episode = next(
|
||||
(
|
||||
ep
|
||||
for ep in anime_info["data"]
|
||||
if float(ep["episode"]) == float(episode_number)
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
if not episode:
|
||||
logger.error(
|
||||
f"[ANIMEPAHE-ERROR]: Episode {episode_number} doesn't exist for anime {anime_title}"
|
||||
)
|
||||
return
|
||||
|
||||
# fetch the episode page
|
||||
url = f"{ANIMEPAHE_BASE}/play/{anime_id}/{episode['session']}"
|
||||
response = self.session.get(url)
|
||||
|
||||
response.raise_for_status()
|
||||
# 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]
|
||||
if _server := self._get_server(
|
||||
episode, res_dicts, anime_title, translation_type
|
||||
):
|
||||
yield _server
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import subprocess
|
||||
|
||||
animepahe = AnimePahe(cache_requests="True", use_persistent_provider_store="False")
|
||||
search_term = input("Enter the search term for the anime: ")
|
||||
translation_type = input("Enter the translation type (sub/dub): ")
|
||||
|
||||
search_results = animepahe.search_for_anime(
|
||||
search_keywords=search_term, translation_type=translation_type
|
||||
)
|
||||
|
||||
if not search_results or not search_results["results"]:
|
||||
print("No results found.")
|
||||
exit()
|
||||
|
||||
print("Search Results:")
|
||||
for idx, result in enumerate(search_results["results"], start=1):
|
||||
print(f"{idx}. {result['title']} (ID: {result['id']})")
|
||||
|
||||
anime_choice = int(input("Enter the number of the anime you want to watch: ")) - 1
|
||||
anime_id = search_results["results"][anime_choice]["id"]
|
||||
|
||||
anime_details = animepahe.get_anime(anime_id)
|
||||
|
||||
if anime_details is None:
|
||||
print("Failed to get anime details.")
|
||||
exit()
|
||||
print(f"Selected Anime: {anime_details['title']}")
|
||||
|
||||
print("Available Episodes:")
|
||||
for idx, episode in enumerate(
|
||||
sorted(anime_details["availableEpisodesDetail"][translation_type], key=float),
|
||||
start=1,
|
||||
):
|
||||
print(f"{idx}. Episode {episode}")
|
||||
|
||||
episode_choice = (
|
||||
int(input("Enter the number of the episode you want to watch: ")) - 1
|
||||
)
|
||||
episode_number = anime_details["availableEpisodesDetail"][translation_type][
|
||||
episode_choice
|
||||
]
|
||||
|
||||
streams = list(
|
||||
animepahe.get_episode_streams(anime_id, episode_number, translation_type)
|
||||
)
|
||||
if not streams:
|
||||
print("No streams available.")
|
||||
exit()
|
||||
|
||||
print("Available Streams:")
|
||||
for idx, stream in enumerate(streams, start=1):
|
||||
print(f"{idx}. Server: {stream['server']}")
|
||||
|
||||
server_choice = int(input("Enter the number of the server you want to use: ")) - 1
|
||||
selected_stream = streams[server_choice]
|
||||
|
||||
stream_link = selected_stream["links"][0]["link"]
|
||||
mpv_args = ["mpv", stream_link]
|
||||
headers = selected_stream["headers"]
|
||||
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)
|
||||
subprocess.run(mpv_args)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import re
|
||||
|
||||
ANIMEPAHE = "animepahe.ru"
|
||||
ANIMEPAHE_BASE = f"https://{ANIMEPAHE}"
|
||||
ANIMEPAHE_ENDPOINT = f"{ANIMEPAHE_BASE}/api?"
|
||||
ANIMEPAHE_ENDPOINT = f"{ANIMEPAHE_BASE}/api"
|
||||
|
||||
SERVERS_AVAILABLE = ["kwik"]
|
||||
REQUEST_HEADERS = {
|
||||
@@ -31,3 +33,5 @@ SERVER_HEADERS = {
|
||||
"Priority": "u=4",
|
||||
"TE": "trailers",
|
||||
}
|
||||
JUICY_STREAM_REGEX = re.compile(r"source='(.*)';")
|
||||
KWIK_RE = re.compile(r"Player\|(.+?)'")
|
||||
|
||||
@@ -10,7 +10,6 @@ from .providers_store import ProviderStore
|
||||
class AnimeProvider:
|
||||
session: requests.Session
|
||||
|
||||
PROVIDER = ""
|
||||
USER_AGENT = random_user_agent()
|
||||
HEADERS = {}
|
||||
|
||||
@@ -19,7 +18,10 @@ class AnimeProvider:
|
||||
from ..common.requests_cacher import CachedRequestsSession
|
||||
|
||||
self.session = CachedRequestsSession(
|
||||
os.path.join(APP_CACHE_DIR, "cached_requests.db")
|
||||
os.path.join(APP_CACHE_DIR, "cached_requests.db"),
|
||||
max_lifetime=int(
|
||||
os.environ.get("FASTANIME_MAX_CACHE_LIFETIME", 259200)
|
||||
),
|
||||
)
|
||||
else:
|
||||
self.session = requests.session()
|
||||
@@ -27,7 +29,7 @@ class AnimeProvider:
|
||||
if use_persistent_provider_store.lower() == "true":
|
||||
self.store = ProviderStore(
|
||||
"persistent",
|
||||
self.PROVIDER,
|
||||
self.__class__.__name__,
|
||||
os.path.join(APP_CACHE_DIR, "anime_providers_store.db"),
|
||||
)
|
||||
else:
|
||||
|
||||
@@ -5,22 +5,20 @@ import os
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def debug_provider(provider_name: str):
|
||||
def _provider_function_decorator(provider_function):
|
||||
def debug_provider(provider_function):
|
||||
@functools.wraps(provider_function)
|
||||
def _provider_function_wrapper(*args, **kwargs):
|
||||
def _provider_function_wrapper(self, *args, **kwargs):
|
||||
provider_name = self.__class__.__name__.upper()
|
||||
if not os.environ.get("FASTANIME_DEBUG"):
|
||||
try:
|
||||
return provider_function(*args, **kwargs)
|
||||
return provider_function(self, *args, **kwargs)
|
||||
except Exception as e:
|
||||
logger.error(f"[{provider_name}@{provider_function.__name__}]: {e}")
|
||||
else:
|
||||
return provider_function(*args, **kwargs)
|
||||
return provider_function(self, *args, **kwargs)
|
||||
|
||||
return _provider_function_wrapper
|
||||
|
||||
return _provider_function_decorator
|
||||
|
||||
|
||||
def ensure_internet_connection(provider_function):
|
||||
@functools.wraps(provider_function)
|
||||
|
||||
@@ -17,6 +17,7 @@ from ..base_provider import AnimeProvider
|
||||
from ..decorators import debug_provider
|
||||
from ..utils import give_random_quality
|
||||
from .constants import SERVERS_AVAILABLE
|
||||
from .extractors import MegaCloud
|
||||
from .types import HiAnimeStream
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -38,13 +39,11 @@ class ParseAnchorAndImgTag(HTMLParser):
|
||||
self.a_tag = {attr[0]: attr[1] for attr in attrs}
|
||||
|
||||
|
||||
class HiAnimeApi(AnimeProvider):
|
||||
class HiAnime(AnimeProvider):
|
||||
# HEADERS = {"Referer": "https://hianime.to/home"}
|
||||
|
||||
PROVIDER = "hianime"
|
||||
|
||||
@debug_provider(PROVIDER.upper())
|
||||
def search_for_anime(self, anime_title: str, *args):
|
||||
@debug_provider
|
||||
def search_for_anime(self, anime_title: str, translation_type, **kwargs):
|
||||
query = quote_plus(anime_title)
|
||||
url = f"https://hianime.to/search?keyword={query}"
|
||||
response = self.session.get(url)
|
||||
@@ -91,8 +90,8 @@ class HiAnimeApi(AnimeProvider):
|
||||
self.store.set(result["id"], "search_result", result)
|
||||
return {"pageInfo": {}, "results": results}
|
||||
|
||||
@debug_provider(PROVIDER.upper())
|
||||
def get_anime(self, hianime_id, *args):
|
||||
@debug_provider
|
||||
def get_anime(self, hianime_id, **kwargs):
|
||||
anime_result = {}
|
||||
if d := self.store.get(str(hianime_id), "search_result"):
|
||||
anime_result = d
|
||||
@@ -144,8 +143,8 @@ class HiAnimeApi(AnimeProvider):
|
||||
"episodes_info": episodes_info,
|
||||
}
|
||||
|
||||
@debug_provider(PROVIDER.upper())
|
||||
def get_episode_streams(self, anime_id, episode, translation_type, *args):
|
||||
@debug_provider
|
||||
def get_episode_streams(self, anime_id, episode, translation_type, **kwargs):
|
||||
if d := self.store.get(str(anime_id), "anime_info"):
|
||||
episodes_info = d
|
||||
episode_details = [
|
||||
@@ -191,15 +190,45 @@ class HiAnimeApi(AnimeProvider):
|
||||
if not servers_html:
|
||||
return
|
||||
|
||||
@debug_provider(self.PROVIDER.upper())
|
||||
@debug_provider
|
||||
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)
|
||||
embed_url = f"https://hianime.to/ajax/v2/episode/sources?id={servers_info['data-id']}"
|
||||
server_id = servers_info["data-id"]
|
||||
embed_url = (
|
||||
f"https://hianime.to/ajax/v2/episode/sources?id={server_id}"
|
||||
)
|
||||
embed_response = self.session.get(embed_url)
|
||||
if embed_response.ok:
|
||||
embed_json = embed_response.json()
|
||||
raw_link_to_streams = embed_json["link"]
|
||||
match server_name:
|
||||
# TODO: Finish the other servers
|
||||
case "HD2":
|
||||
data = MegaCloud(self.session).extract(
|
||||
raw_link_to_streams
|
||||
)
|
||||
return {
|
||||
"headers": {},
|
||||
"subtitles": [
|
||||
{
|
||||
"url": track["file"],
|
||||
"language": track["label"],
|
||||
}
|
||||
for track in data["tracks"]
|
||||
if track["kind"] == "captions"
|
||||
],
|
||||
"server": server_name,
|
||||
"episode_title": episode_details["title"],
|
||||
"links": give_random_quality(
|
||||
[
|
||||
{"link": link["url"]}
|
||||
for link in data["sources"]
|
||||
]
|
||||
),
|
||||
}
|
||||
case _:
|
||||
# NOTE: THIS METHOD DOES'NT WORK will get the other servers later
|
||||
match = LINK_TO_STREAMS_REGEX.match(raw_link_to_streams)
|
||||
if not match:
|
||||
return
|
||||
@@ -209,13 +238,14 @@ class HiAnimeApi(AnimeProvider):
|
||||
source_id = match.group(4)
|
||||
|
||||
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)
|
||||
link_to_streams_response = self.session.get(
|
||||
link_to_streams
|
||||
)
|
||||
if link_to_streams_response.ok:
|
||||
juicy_streams_json: "HiAnimeStream" = (
|
||||
link_to_streams_response.json()
|
||||
)
|
||||
# TODO: Hianime decided to fucking encrypt shit
|
||||
# so got to fix it later
|
||||
|
||||
return {
|
||||
"headers": {},
|
||||
"subtitles": [
|
||||
@@ -239,5 +269,6 @@ class HiAnimeApi(AnimeProvider):
|
||||
for server_name, server_html in zip(
|
||||
cycle(SERVERS_AVAILABLE), servers_html
|
||||
):
|
||||
if server_name == "HD2":
|
||||
if server := _get_server(server_name, server_html):
|
||||
yield server
|
||||
|
||||
@@ -1 +1,26 @@
|
||||
SERVERS_AVAILABLE = ["HD1", "HD2", "StreamSB", "StreamTape"]
|
||||
""""
|
||||
| "hd-1"
|
||||
| "hd-2"
|
||||
| "megacloud"
|
||||
| "streamsb"
|
||||
| "streamtape";
|
||||
|
||||
"""
|
||||
|
||||
|
||||
"""
|
||||
VidStreaming = "hd-1",
|
||||
MegaCloud = "megacloud",
|
||||
StreamSB = "streamsb",
|
||||
StreamTape = "streamtape",
|
||||
VidCloud = "hd-2",
|
||||
AsianLoad = "asianload",
|
||||
GogoCDN = "gogocdn",
|
||||
MixDrop = "mixdrop",
|
||||
UpCloud = "upcloud",
|
||||
VizCloud = "vizcloud",
|
||||
MyCloud = "mycloud",
|
||||
Filemoon = "filemoon",
|
||||
|
||||
"""
|
||||
|
||||
191
fastanime/libs/anime_provider/hianime/extractors.py
Normal file
191
fastanime/libs/anime_provider/hianime/extractors.py
Normal file
@@ -0,0 +1,191 @@
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
from base64 import b64decode
|
||||
from typing import TYPE_CHECKING, Dict, List
|
||||
|
||||
from Crypto.Cipher import AES
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ...common.requests_cacher import CachedRequestsSession
|
||||
|
||||
|
||||
# Constants
|
||||
megacloud = {
|
||||
"script": "https://megacloud.tv/js/player/a/prod/e1-player.min.js?v=",
|
||||
"sources": "https://megacloud.tv/embed-2/ajax/e-1/getSources?id=",
|
||||
}
|
||||
|
||||
|
||||
class HiAnimeError(Exception):
|
||||
def __init__(self, message, context, status_code):
|
||||
super().__init__(f"{context}: {message} (Status: {status_code})")
|
||||
self.context = context
|
||||
self.status_code = status_code
|
||||
|
||||
|
||||
# Adapted from https://github.com/ghoshRitesh12/aniwatch
|
||||
class MegaCloud:
|
||||
def __init__(self, session):
|
||||
self.session: "CachedRequestsSession" = session
|
||||
|
||||
def extract(self, video_url: str) -> Dict:
|
||||
try:
|
||||
extracted_data = {
|
||||
"tracks": [],
|
||||
"intro": {"start": 0, "end": 0},
|
||||
"outro": {"start": 0, "end": 0},
|
||||
"sources": [],
|
||||
}
|
||||
|
||||
video_id = video_url.split("/")[-1].split("?")[0]
|
||||
response = self.session.get(
|
||||
megacloud["sources"] + video_id,
|
||||
headers={
|
||||
"Accept": "*/*",
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
"Referer": video_url,
|
||||
},
|
||||
fresh=1, # pyright: ignore
|
||||
)
|
||||
srcs_data = response.json()
|
||||
|
||||
if not srcs_data:
|
||||
raise HiAnimeError(
|
||||
"Url may have an invalid video id", "getAnimeEpisodeSources", 400
|
||||
)
|
||||
|
||||
encrypted_string = srcs_data["sources"]
|
||||
if not srcs_data["encrypted"] and isinstance(encrypted_string, list):
|
||||
extracted_data.update(
|
||||
{
|
||||
"intro": srcs_data["intro"],
|
||||
"outro": srcs_data["outro"],
|
||||
"tracks": srcs_data["tracks"],
|
||||
"sources": [
|
||||
{"url": s["file"], "type": s["type"]}
|
||||
for s in encrypted_string
|
||||
],
|
||||
}
|
||||
)
|
||||
return extracted_data
|
||||
|
||||
# Fetch decryption script
|
||||
script_response = self.session.get(
|
||||
megacloud["script"] + str(int(time.time() * 1000)),
|
||||
fresh=1, # pyright: ignore
|
||||
)
|
||||
script_text = script_response.text
|
||||
if not script_text:
|
||||
raise HiAnimeError(
|
||||
"Couldn't fetch script to decrypt resource",
|
||||
"getAnimeEpisodeSources",
|
||||
500,
|
||||
)
|
||||
|
||||
vars_ = self.extract_variables(script_text)
|
||||
if not vars_:
|
||||
raise Exception(
|
||||
"Can't find variables. Perhaps the extractor is outdated."
|
||||
)
|
||||
|
||||
secret, encrypted_source = self.get_secret(encrypted_string, vars_)
|
||||
decrypted = self.decrypt(encrypted_source, secret)
|
||||
|
||||
try:
|
||||
sources = json.loads(decrypted)
|
||||
extracted_data.update(
|
||||
{
|
||||
"intro": srcs_data["intro"],
|
||||
"outro": srcs_data["outro"],
|
||||
"tracks": srcs_data["tracks"],
|
||||
"sources": [
|
||||
{"url": s["file"], "type": s["type"]} for s in sources
|
||||
],
|
||||
}
|
||||
)
|
||||
return extracted_data
|
||||
except Exception:
|
||||
raise HiAnimeError(
|
||||
"Failed to decrypt resource", "getAnimeEpisodeSources", 500
|
||||
)
|
||||
except Exception as err:
|
||||
raise err
|
||||
|
||||
def extract_variables(self, text: str) -> List[List[int]]:
|
||||
regex = r"case\s*0x[0-9a-f]+:(?![^;]*=partKey)\s*\w+\s*=\s*(\w+)\s*,\s*\w+\s*=\s*(\w+);"
|
||||
matches = re.finditer(regex, text)
|
||||
vars_ = []
|
||||
for match in matches:
|
||||
key1 = self.matching_key(match[1], text)
|
||||
key2 = self.matching_key(match[2], text)
|
||||
try:
|
||||
vars_.append([int(key1, 16), int(key2, 16)])
|
||||
except ValueError:
|
||||
continue
|
||||
return vars_
|
||||
|
||||
def get_secret(
|
||||
self, encrypted_string: str, values: List[List[int]]
|
||||
) -> tuple[str, str]:
|
||||
secret = []
|
||||
encrypted_source_array = list(encrypted_string)
|
||||
current_index = 0
|
||||
|
||||
for start, length in values:
|
||||
start += current_index
|
||||
end = start + length
|
||||
secret.extend(encrypted_string[start:end])
|
||||
encrypted_source_array[start:end] = [""] * length
|
||||
current_index += length
|
||||
|
||||
encrypted_source = "".join(encrypted_source_array) # .replace("\x00", "")
|
||||
return ("".join(secret), encrypted_source)
|
||||
|
||||
def decrypt(self, encrypted: str, key_or_secret: str, maybe_iv: str = "") -> str:
|
||||
if maybe_iv:
|
||||
key = key_or_secret.encode()
|
||||
iv = maybe_iv.encode()
|
||||
contents = encrypted
|
||||
else:
|
||||
# Decode the Base64 string
|
||||
cypher = b64decode(encrypted)
|
||||
|
||||
# Extract the salt from the cypher text
|
||||
salt = cypher[8:16]
|
||||
|
||||
# Combine the key_or_secret with the salt
|
||||
password = key_or_secret.encode() + salt
|
||||
|
||||
# Generate MD5 hashes
|
||||
md5_hashes = []
|
||||
digest = password
|
||||
for _ in range(3):
|
||||
md5 = hashlib.md5()
|
||||
md5.update(digest)
|
||||
md5_hashes.append(md5.digest())
|
||||
digest = md5_hashes[-1] + password
|
||||
|
||||
# Derive the key and IV
|
||||
key = md5_hashes[0] + md5_hashes[1]
|
||||
iv = md5_hashes[2]
|
||||
|
||||
# Extract the encrypted contents
|
||||
contents = cypher[16:]
|
||||
|
||||
# Initialize the AES decipher
|
||||
decipher = AES.new(key, AES.MODE_CBC, iv)
|
||||
|
||||
# Decrypt and decode
|
||||
decrypted = decipher.decrypt(contents).decode("utf-8") # pyright: ignore
|
||||
|
||||
# Remove any padding (PKCS#7)
|
||||
pad = ord(decrypted[-1])
|
||||
return decrypted[:-pad]
|
||||
|
||||
def matching_key(self, value: str, script: str) -> str:
|
||||
match = re.search(rf",{value}=((?:0x)?[0-9a-fA-F]+)", script)
|
||||
if match:
|
||||
return match.group(1).replace("0x", "")
|
||||
raise Exception("Failed to match the key")
|
||||
0
fastanime/libs/anime_provider/nyaa/__init__.py
Normal file
0
fastanime/libs/anime_provider/nyaa/__init__.py
Normal file
@@ -27,11 +27,10 @@ EXTRACT_USEFUL_INFO_PATTERN_2 = re.compile(
|
||||
)
|
||||
|
||||
|
||||
class NyaaApi(AnimeProvider):
|
||||
class Nyaa(AnimeProvider):
|
||||
search_results: SearchResults
|
||||
PROVIDER = "nyaa"
|
||||
|
||||
@debug_provider(PROVIDER.upper())
|
||||
@debug_provider
|
||||
def search_for_anime(self, user_query: str, *args, **_):
|
||||
self.search_results = search_for_anime_with_anilist(
|
||||
user_query, True
|
||||
@@ -39,7 +38,7 @@ class NyaaApi(AnimeProvider):
|
||||
self.user_query = user_query
|
||||
return self.search_results
|
||||
|
||||
@debug_provider(PROVIDER.upper())
|
||||
@debug_provider
|
||||
def get_anime(self, anilist_id: str, *_):
|
||||
for anime in self.search_results["results"]:
|
||||
if anime["id"] == anilist_id:
|
||||
@@ -55,7 +54,7 @@ class NyaaApi(AnimeProvider):
|
||||
},
|
||||
}
|
||||
|
||||
@debug_provider(PROVIDER.upper())
|
||||
@debug_provider
|
||||
def get_episode_streams(
|
||||
self,
|
||||
anime_id: str,
|
||||
|
||||
0
fastanime/libs/anime_provider/yugen/__init__.py
Normal file
0
fastanime/libs/anime_provider/yugen/__init__.py
Normal file
@@ -1,32 +1,32 @@
|
||||
import base64
|
||||
import re
|
||||
from itertools import cycle
|
||||
|
||||
from yt_dlp.utils import (
|
||||
get_element_text_and_html_by_tag,
|
||||
get_elements_text_and_html_by_attribute,
|
||||
extract_attributes,
|
||||
get_element_by_attribute,
|
||||
get_element_text_and_html_by_tag,
|
||||
get_elements_text_and_html_by_attribute,
|
||||
)
|
||||
import re
|
||||
|
||||
from yt_dlp.utils.traversal import get_element_html_by_attribute
|
||||
from .constants import YUGEN_ENDPOINT, SEARCH_URL
|
||||
from ..decorators import debug_provider
|
||||
|
||||
from ..base_provider import AnimeProvider
|
||||
from ..decorators import debug_provider
|
||||
from .constants import SEARCH_URL, YUGEN_ENDPOINT
|
||||
|
||||
|
||||
# ** Adapted from anipy-cli **
|
||||
class YugenApi(AnimeProvider):
|
||||
class Yugen(AnimeProvider):
|
||||
"""
|
||||
Provides a fast and effective interface to YugenApi site.
|
||||
"""
|
||||
|
||||
PROVIDER = "yugen"
|
||||
api_endpoint = YUGEN_ENDPOINT
|
||||
# HEADERS = {
|
||||
# "Referer": ALLANIME_REFERER,
|
||||
# }
|
||||
|
||||
@debug_provider(PROVIDER.upper())
|
||||
@debug_provider
|
||||
def search_for_anime(
|
||||
self,
|
||||
user_query: str,
|
||||
@@ -94,7 +94,7 @@ class YugenApi(AnimeProvider):
|
||||
"results": results,
|
||||
}
|
||||
|
||||
@debug_provider(PROVIDER.upper())
|
||||
@debug_provider
|
||||
def get_anime(self, anime_id: str, **kwargs):
|
||||
identifier = base64.b64decode(anime_id).decode()
|
||||
response = self.session.get(f"{YUGEN_ENDPOINT}/anime/{identifier}")
|
||||
@@ -118,7 +118,9 @@ class YugenApi(AnimeProvider):
|
||||
|
||||
if sub_match:
|
||||
eps = int(sub_match.group(1))
|
||||
data_map["availableEpisodesDetail"]["sub"] = list(map(str,range(1, eps + 1)))
|
||||
data_map["availableEpisodesDetail"]["sub"] = list(
|
||||
map(str, range(1, eps + 1))
|
||||
)
|
||||
|
||||
dub_match = re.search(
|
||||
r'<div class="ap-.+?">Episodes \(Dub\)</div><span class="description" .+?>(\d+)</span></div>',
|
||||
@@ -127,7 +129,9 @@ class YugenApi(AnimeProvider):
|
||||
|
||||
if dub_match:
|
||||
eps = int(dub_match.group(1))
|
||||
data_map["availableEpisodesDetail"]["dub"] = list(map(str,range(1, eps + 1)))
|
||||
data_map["availableEpisodesDetail"]["dub"] = list(
|
||||
map(str, range(1, eps + 1))
|
||||
)
|
||||
|
||||
name = get_element_text_and_html_by_tag("h1", html_page)
|
||||
if name is not None:
|
||||
@@ -174,7 +178,7 @@ class YugenApi(AnimeProvider):
|
||||
|
||||
return data_map
|
||||
|
||||
@debug_provider(PROVIDER.upper())
|
||||
@debug_provider
|
||||
def get_episode_streams(
|
||||
self, anime_id, episode_number: str, translation_type="sub"
|
||||
):
|
||||
@@ -212,5 +216,10 @@ class YugenApi(AnimeProvider):
|
||||
"episode_title": f"{anime_title}; Episode {episode_number}",
|
||||
"headers": {},
|
||||
"subtitles": [],
|
||||
"links": [{"quality": quality, "link": link} for quality,link in zip(cycle(["1080","720","480","360"]),res["hls"])],
|
||||
"links": [
|
||||
{"quality": quality, "link": link}
|
||||
for quality, link in zip(
|
||||
cycle(["1080", "720", "480", "360"]), res["hls"]
|
||||
)
|
||||
],
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
YUGEN_ENDPOINT: str = "https://yugenanime.tv"
|
||||
|
||||
SEARCH_URL = YUGEN_ENDPOINT + "/api/discover/"
|
||||
|
||||
@@ -80,7 +80,8 @@ class CachedRequestsSession(requests.Session):
|
||||
response_headers TEXT,
|
||||
data BLOB,
|
||||
redirection_policy INT,
|
||||
cache_expiry INTEGER
|
||||
cache_expiry INTEGER,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)"""
|
||||
)
|
||||
|
||||
@@ -117,6 +118,8 @@ class CachedRequestsSession(requests.Session):
|
||||
url = ?
|
||||
AND redirection_policy = ?
|
||||
AND cache_expiry > ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
""",
|
||||
(url, redirection_policy, int(time.time())),
|
||||
)
|
||||
@@ -162,8 +165,15 @@ class CachedRequestsSession(requests.Session):
|
||||
logger.debug("Caching the current request")
|
||||
cursor.execute(
|
||||
f"""
|
||||
INSERT INTO {self.table_name}
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO {self.table_name} (
|
||||
url,
|
||||
status_code,
|
||||
request_headers,
|
||||
response_headers,
|
||||
data,
|
||||
redirection_policy,
|
||||
cache_expiry
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
url,
|
||||
|
||||
11
fastanime/libs/discord/discord.py
Normal file
11
fastanime/libs/discord/discord.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from pypresence import Presence
|
||||
import time
|
||||
|
||||
def discord_connect(show, episode, switch):
|
||||
presence = Presence(client_id = '1292070065583165512')
|
||||
presence.connect()
|
||||
if not switch.is_set():
|
||||
presence.update(details = show, state = "Watching episode "+episode)
|
||||
time.sleep(10)
|
||||
else:
|
||||
presence.close()
|
||||
@@ -14,12 +14,13 @@
|
||||
pythonPackages = python.pkgs;
|
||||
fastanimeEnv = pythonPackages.buildPythonApplication {
|
||||
pname = "fastanime";
|
||||
version = "2.8.0";
|
||||
version = "v2.8.6";
|
||||
|
||||
src = ./.;
|
||||
|
||||
preBuild = ''
|
||||
sed -i 's/rich>=13.9.2/rich>=13.8.1/' pyproject.toml
|
||||
sed -i 's/pycryptodome>=3.21.0/pycryptodome>=3.20.0/' pyproject.toml
|
||||
'';
|
||||
|
||||
# Add runtime dependencies
|
||||
@@ -35,6 +36,8 @@
|
||||
plyer
|
||||
mpv
|
||||
fastapi
|
||||
pycryptodome
|
||||
pypresence
|
||||
];
|
||||
|
||||
# Ensure compatibility with the pyproject.toml
|
||||
|
||||
@@ -5,7 +5,12 @@ VERSION=$1
|
||||
[ "$VERSION" = "current" ] && fastanime --version && exit 0
|
||||
sed -i "s/^version.*/version = \"$VERSION\"/" "$CLI_DIR/pyproject.toml" &&
|
||||
sed -i "s/__version__.*/__version__ = \"v$VERSION\"/" "$CLI_DIR/fastanime/__init__.py" &&
|
||||
git stage "$CLI_DIR/pyproject.toml" "$CLI_DIR/fastanime/__init__.py" &&
|
||||
sed -i "s/version = .*/version = \"$VERSION\";/" "$CLI_DIR/flake.nix" &&
|
||||
git stage "$CLI_DIR/pyproject.toml" "$CLI_DIR/fastanime/__init__.py" "$CLI_DIR/flake.nix" &&
|
||||
git commit -m "chore: bump version (v$VERSION)" &&
|
||||
nix flake lock &&
|
||||
uv lock &&
|
||||
git stage "$CLI_DIR/flake.lock" "$CLI_DIR/uv.lock" &&
|
||||
git commit -m "chore: update lock files" &&
|
||||
git push &&
|
||||
gh release create "v$VERSION"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "fastanime"
|
||||
version = "2.8.1"
|
||||
version = "v2.8.6"
|
||||
description = "A browser anime site experience from the terminal"
|
||||
license = "UNLICENSE"
|
||||
readme = "README.md"
|
||||
@@ -8,10 +8,12 @@ requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"click>=8.1.7",
|
||||
"inquirerpy>=0.3.4",
|
||||
"pycryptodome>=3.21.0",
|
||||
"pypresence>=4.3.0",
|
||||
"requests>=2.32.3",
|
||||
"rich>=13.9.2",
|
||||
"thefuzz>=0.22.1",
|
||||
"yt-dlp>=2024.10.7",
|
||||
"yt-dlp[default]>=2024.10.7",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
@@ -29,6 +31,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[tool.uv]
|
||||
dev-dependencies = [
|
||||
"pre-commit>=4.0.1",
|
||||
"pyinstaller>=6.11.1",
|
||||
"pyright>=1.1.384",
|
||||
"pytest>=8.3.3",
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from click.testing import CliRunner
|
||||
|
||||
@@ -147,3 +149,10 @@ def test_anilist_upcoming_help(runner: CliRunner):
|
||||
def test_anilist_watching_help(runner: CliRunner):
|
||||
result = runner.invoke(run_cli, ["anilist", "watching", "--help"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
def test_check_for_updates_not_called_on_completions(runner):
|
||||
with patch("fastanime.cli.app_updater.check_for_updates") as mock_check_for_updates:
|
||||
result = runner.invoke(run_cli, ["completions"])
|
||||
assert result.exit_code == 0
|
||||
mock_check_for_updates.assert_not_called()
|
||||
|
||||
323
uv.lock
generated
323
uv.lock
generated
@@ -38,6 +38,98 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/f5/f2b75d2fc6f1a260f340f0e7c6a060f4dd2961cc16884ed851b0d18da06a/anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d", size = 90377 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "brotli"
|
||||
version = "1.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2f/c2/f9e977608bdf958650638c3f1e28f85a1b075f075ebbe77db8555463787b/Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724", size = 7372270 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/3a/dbf4fb970c1019a57b5e492e1e0eae745d32e59ba4d6161ab5422b08eefe/Brotli-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752", size = 873045 },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/11/afc14026ea7f44bd6eb9316d800d439d092c8d508752055ce8d03086079a/Brotli-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9", size = 446218 },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/83/7545a6e7729db43cb36c4287ae388d6885c85a86dd251768a47015dfde32/Brotli-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ae56aca0402a0f9a3431cddda62ad71666ca9d4dc3a10a142b9dce2e3c0cda3", size = 2903872 },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/23/35331c4d9391fcc0f29fd9bec2c76e4b4eeab769afbc4b11dd2e1098fb13/Brotli-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43ce1b9935bfa1ede40028054d7f48b5469cd02733a365eec8a329ffd342915d", size = 2941254 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/24/1671acb450c902edb64bd765d73603797c6c7280a9ada85a195f6b78c6e5/Brotli-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7c4855522edb2e6ae7fdb58e07c3ba9111e7621a8956f481c68d5d979c93032e", size = 2857293 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/00/40f760cc27007912b327fe15bf6bfd8eaecbe451687f72a8abc587d503b3/Brotli-1.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:38025d9f30cf4634f8309c6874ef871b841eb3c347e90b0851f63d1ded5212da", size = 3002385 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/cb/8aaa83f7a4caa131757668c0fb0c4b6384b09ffa77f2fba9570d87ab587d/Brotli-1.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e6a904cb26bfefc2f0a6f240bdf5233be78cd2488900a2f846f3c3ac8489ab80", size = 2911104 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/c4/65456561d89d3c49f46b7fbeb8fe6e449f13bdc8ea7791832c5d476b2faf/Brotli-1.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d", size = 2809981 },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/1b/cf49528437bae28abce5f6e059f0d0be6fecdcc1d3e33e7c54b3ca498425/Brotli-1.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0", size = 2935297 },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/ff/190d4af610680bf0c5a09eb5d1eac6e99c7c8e216440f9c7cfd42b7adab5/Brotli-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e", size = 2930735 },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/7d/f1abbc0c98f6e09abd3cad63ec34af17abc4c44f308a7a539010f79aae7a/Brotli-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5dab0844f2cf82be357a0eb11a9087f70c5430b2c241493fc122bb6f2bb0917c", size = 2933107 },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/ce/5a5020ba48f2b5a4ad1c0522d095ad5847a0be508e7d7569c8630ce25062/Brotli-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e4fe605b917c70283db7dfe5ada75e04561479075761a0b3866c081d035b01c1", size = 2845400 },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/89/fa2c4355ab1eecf3994e5a0a7f5492c6ff81dfcb5f9ba7859bd534bb5c1a/Brotli-1.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1e9a65b5736232e7a7f91ff3d02277f11d339bf34099a56cdab6a8b3410a02b2", size = 3031985 },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/a4/79196b4a1674143d19dca400866b1a4d1a089040df7b93b88ebae81f3447/Brotli-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:58d4b711689366d4a03ac7957ab8c28890415e267f9b6589969e74b6e42225ec", size = 2927099 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/54/1c0278556a097f9651e657b873ab08f01b9a9ae4cac128ceb66427d7cd20/Brotli-1.1.0-cp310-cp310-win32.whl", hash = "sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2", size = 333172 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/65/b785722e941193fd8b571afd9edbec2a9b838ddec4375d8af33a50b8dab9/Brotli-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128", size = 357255 },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/12/ad41e7fadd5db55459c4c401842b47f7fee51068f86dd2894dd0dcfc2d2a/Brotli-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc", size = 873068 },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/4e/5afab7b2b4b61a84e9c75b17814198ce515343a44e2ed4488fac314cd0a9/Brotli-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6", size = 446244 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/e6/f305eb61fb9a8580c525478a4a34c5ae1a9bcb12c3aee619114940bc513d/Brotli-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd", size = 2906500 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/4f/af6846cfbc1550a3024e5d3775ede1e00474c40882c7bf5b37a43ca35e91/Brotli-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf", size = 2943950 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/e7/ca2993c7682d8629b62630ebf0d1f3bb3d579e667ce8e7ca03a0a0576a2d/Brotli-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a469274ad18dc0e4d316eefa616d1d0c2ff9da369af19fa6f3daa4f09671fd61", size = 2918527 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/96/da98e7bedc4c51104d29cc61e5f449a502dd3dbc211944546a4cc65500d3/Brotli-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327", size = 2845489 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/ef/ccbc16947d6ce943a7f57e1a40596c75859eeb6d279c6994eddd69615265/Brotli-1.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd", size = 2914080 },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/d6/0bd38d758d1afa62a5524172f0b18626bb2392d717ff94806f741fcd5ee9/Brotli-1.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9", size = 2813051 },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/56/48859dd5d129d7519e001f06dcfbb6e2cf6db92b2702c0c2ce7d97e086c1/Brotli-1.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265", size = 2938172 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/77/a236d5f8cd9e9f4348da5acc75ab032ab1ab2c03cc8f430d24eea2672888/Brotli-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8", size = 2933023 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/87/3b283efc0f5cb35f7f84c0c240b1e1a1003a5e47141a4881bf87c86d0ce2/Brotli-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c247dd99d39e0338a604f8c2b3bc7061d5c2e9e2ac7ba9cc1be5a69cb6cd832f", size = 2935871 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/eb/2be4cc3e2141dc1a43ad4ca1875a72088229de38c68e842746b342667b2a/Brotli-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1b2c248cd517c222d89e74669a4adfa5577e06ab68771a529060cf5a156e9757", size = 2847784 },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/13/b58ddebfd35edde572ccefe6890cf7c493f0c319aad2a5badee134b4d8ec/Brotli-1.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2a24c50840d89ded6c9a8fdc7b6ed3692ed4e86f1c4a4a938e1e92def92933e0", size = 3034905 },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/9c/bc96b6c7db824998a49ed3b38e441a2cae9234da6fa11f6ed17e8cf4f147/Brotli-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f31859074d57b4639318523d6ffdca586ace54271a73ad23ad021acd807eb14b", size = 2929467 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/71/8f161dee223c7ff7fea9d44893fba953ce97cf2c3c33f78ba260a91bcff5/Brotli-1.1.0-cp311-cp311-win32.whl", hash = "sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50", size = 333169 },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/8a/fece0ee1057643cb2a5bbf59682de13f1725f8482b2c057d4e799d7ade75/Brotli-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1", size = 357253 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/d0/5373ae13b93fe00095a58efcbce837fd470ca39f703a235d2a999baadfbc/Brotli-1.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:32d95b80260d79926f5fab3c41701dbb818fde1c9da590e77e571eefd14abe28", size = 815693 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/48/f6e1cdf86751300c288c1459724bfa6917a80e30dbfc326f92cea5d3683a/Brotli-1.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b760c65308ff1e462f65d69c12e4ae085cff3b332d894637f6273a12a482d09f", size = 422489 },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/88/564958cedce636d0f1bed313381dfc4b4e3d3f6015a63dae6146e1b8c65c/Brotli-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409", size = 873081 },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/79/b7026a8bb65da9a6bb7d14329fd2bd48d2b7f86d7329d5cc8ddc6a90526f/Brotli-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2", size = 446244 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/18/c18c32ecea41b6c0004e15606e274006366fe19436b6adccc1ae7b2e50c2/Brotli-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451", size = 2906505 },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/c8/69ec0496b1ada7569b62d85893d928e865df29b90736558d6c98c2031208/Brotli-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91", size = 2944152 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/fb/0517cea182219d6768113a38167ef6d4eb157a033178cc938033a552ed6d/Brotli-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408", size = 2919252 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/53/73a3431662e33ae61a5c80b1b9d2d18f58dfa910ae8dd696e57d39f1a2f5/Brotli-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0", size = 2845955 },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/ac/bd280708d9c5ebdbf9de01459e625a3e3803cce0784f47d633562cf40e83/Brotli-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc", size = 2914304 },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/58/5c391b41ecfc4527d2cc3350719b02e87cb424ef8ba2023fb662f9bf743c/Brotli-1.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180", size = 2814452 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/4e/91b8256dfe99c407f174924b65a01f5305e303f486cc7a2e8a5d43c8bec3/Brotli-1.1.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248", size = 2938751 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/a6/e2a39a5d3b412938362bbbeba5af904092bf3f95b867b4a3eb856104074e/Brotli-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966", size = 2933757 },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/f0/358354786280a509482e0e77c1a5459e439766597d280f28cb097642fc26/Brotli-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:87a3044c3a35055527ac75e419dfa9f4f3667a1e887ee80360589eb8c90aabb9", size = 2936146 },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/f7/daf538c1060d3a88266b80ecc1d1c98b79553b3f117a485653f17070ea2a/Brotli-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c5529b34c1c9d937168297f2c1fde7ebe9ebdd5e121297ff9c043bdb2ae3d6fb", size = 2848055 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/cf/0eaa0585c4077d3c2d1edf322d8e97aabf317941d3a72d7b3ad8bce004b0/Brotli-1.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ca63e1890ede90b2e4454f9a65135a4d387a4585ff8282bb72964fab893f2111", size = 3035102 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/63/1c1585b2aa554fe6dbce30f0c18bdbc877fa9a1bf5ff17677d9cca0ac122/Brotli-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e79e6520141d792237c70bcd7a3b122d00f2613769ae0cb61c52e89fd3443839", size = 2930029 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/3b/4e3fd1893eb3bbfef8e5a80d4508bec17a57bb92d586c85c12d28666bb13/Brotli-1.1.0-cp312-cp312-win32.whl", hash = "sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0", size = 333276 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/d5/942051b45a9e883b5b6e98c041698b1eb2012d25e5948c58d6bf85b1bb43/Brotli-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951", size = 357255 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/9f/fb37bb8ffc52a8da37b1c03c459a8cd55df7a57bdccd8831d500e994a0ca/Brotli-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8bf32b98b75c13ec7cf774164172683d6e7891088f6316e54425fde1efc276d5", size = 815681 },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/b3/dbd332a988586fefb0aa49c779f59f47cae76855c2d00f450364bb574cac/Brotli-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7bc37c4d6b87fb1017ea28c9508b36bbcb0c3d18b4260fcdf08b200c74a6aee8", size = 422475 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/80/6aaddc2f63dbcf2d93c2d204e49c11a9ec93a8c7c63261e2b4bd35198283/Brotli-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c0ef38c7a7014ffac184db9e04debe495d317cc9c6fb10071f7fefd93100a4f", size = 2906173 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/1d/e6ca79c96ff5b641df6097d299347507d39a9604bde8915e76bf026d6c77/Brotli-1.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91d7cc2a76b5567591d12c01f019dd7afce6ba8cba6571187e21e2fc418ae648", size = 2943803 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/a3/d98d2472e0130b7dd3acdbb7f390d478123dbf62b7d32bda5c830a96116d/Brotli-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a93dde851926f4f2678e704fadeb39e16c35d8baebd5252c9fd94ce8ce68c4a0", size = 2918946 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/a5/c69e6d272aee3e1423ed005d8915a7eaa0384c7de503da987f2d224d0721/Brotli-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0db75f47be8b8abc8d9e31bc7aad0547ca26f24a54e6fd10231d623f183d089", size = 2845707 },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/9f/4149d38b52725afa39067350696c09526de0125ebfbaab5acc5af28b42ea/Brotli-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6967ced6730aed543b8673008b5a391c3b1076d834ca438bbd70635c73775368", size = 2936231 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/5a/145de884285611838a16bebfdb060c231c52b8f84dfbe52b852a15780386/Brotli-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7eedaa5d036d9336c95915035fb57422054014ebdeb6f3b42eac809928e40d0c", size = 2848157 },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/ae/408b6bfb8525dadebd3b3dd5b19d631da4f7d46420321db44cd99dcf2f2c/Brotli-1.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d487f5432bf35b60ed625d7e1b448e2dc855422e87469e3f450aa5552b0eb284", size = 3035122 },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/85/a94e5cfaa0ca449d8f91c3d6f78313ebf919a0dbd55a100c711c6e9655bc/Brotli-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:832436e59afb93e1836081a20f324cb185836c617659b07b129141a8426973c7", size = 2930206 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/f0/a61d9262cd01351df22e57ad7c34f66794709acab13f34be2675f45bf89d/Brotli-1.1.0-cp313-cp313-win32.whl", hash = "sha256:43395e90523f9c23a3d5bdf004733246fba087f2948f87ab28015f12359ca6a0", size = 333804 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/c1/ec214e9c94000d1c1974ec67ced1c970c148aa6b8d8373066123fc3dbf06/Brotli-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:9011560a466d2eb3f5a6e4929cf4a09be405c64154e12df0dd72713f6500e32b", size = 358517 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "brotlicffi"
|
||||
version = "1.1.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cffi" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/95/9d/70caa61192f570fcf0352766331b735afa931b4c6bc9a348a0925cc13288/brotlicffi-1.1.0.0.tar.gz", hash = "sha256:b77827a689905143f87915310b93b273ab17888fd43ef350d4832c4a71083c13", size = 465192 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/11/7b96009d3dcc2c931e828ce1e157f03824a69fb728d06bfd7b2fc6f93718/brotlicffi-1.1.0.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9b7ae6bd1a3f0df532b6d67ff674099a96d22bc0948955cb338488c31bfb8851", size = 453786 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/e6/a8f46f4a4ee7856fbd6ac0c6fb0dc65ed181ba46cd77875b8d9bbe494d9e/brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19ffc919fa4fc6ace69286e0a23b3789b4219058313cf9b45625016bf7ff996b", size = 2911165 },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/20/201559dff14e83ba345a5ec03335607e47467b6633c210607e693aefac40/brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9feb210d932ffe7798ee62e6145d3a757eb6233aa9a4e7db78dd3690d7755814", size = 2927895 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/15/695b1409264143be3c933f708a3f81d53c4a1e1ebbc06f46331decbf6563/brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84763dbdef5dd5c24b75597a77e1b30c66604725707565188ba54bab4f114820", size = 2851834 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/40/b961a702463b6005baf952794c2e9e0099bde657d0d7e007f923883b907f/brotlicffi-1.1.0.0-cp37-abi3-win32.whl", hash = "sha256:1b12b50e07c3911e1efa3a8971543e7648100713d4e0971b13631cce22c587eb", size = 341731 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/fa/5408a03c041114ceab628ce21766a4ea882aa6f6f0a800e04ee3a30ec6b9/brotlicffi-1.1.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:994a4f0681bb6c6c3b0925530a1926b7a189d878e6e5e38fae8efa47c5d9c613", size = 366783 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/3b/bd4f3d2bcf2306ae66b0346f5b42af1962480b200096ffc7abc3bd130eca/brotlicffi-1.1.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2e4aeb0bd2540cb91b069dbdd54d458da8c4334ceaf2d25df2f4af576d6766ca", size = 397397 },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/10/1fd57864449360852c535c2381ee7120ba8f390aa3869df967c44ca7eba1/brotlicffi-1.1.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b7b0033b0d37bb33009fb2fef73310e432e76f688af76c156b3594389d81391", size = 379698 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/95/15aa422aa6450e6556e54a5fd1650ff59f470aed77ac739aa90ab63dc611/brotlicffi-1.1.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54a07bb2374a1eba8ebb52b6fafffa2afd3c4df85ddd38fcc0511f2bb387c2a8", size = 378635 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/a7/f254e13b2cb43337d6d99a4ec10394c134e41bfda8a2eff15b75627f4a3d/brotlicffi-1.1.0.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7901a7dc4b88f1c1475de59ae9be59799db1007b7d059817948d8e4f12e24e35", size = 385719 },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/a9/0971251c4427c14b2a827dba3d910d4d3330dabf23d4278bf6d06a978847/brotlicffi-1.1.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ce01c7316aebc7fce59da734286148b1d1b9455f89cf2c8a4dfce7d41db55c2d", size = 361760 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2024.8.30"
|
||||
@@ -47,6 +139,72 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cffi"
|
||||
version = "1.17.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pycparser" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191 },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592 },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571 },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325 },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784 },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651 },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235 },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999 },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604 },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfgv"
|
||||
version = "3.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.0"
|
||||
@@ -137,6 +295,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "distlib"
|
||||
version = "0.3.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dnspython"
|
||||
version = "2.7.0"
|
||||
@@ -170,15 +337,17 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "fastanime"
|
||||
version = "2.8.0"
|
||||
version = "2.8.4"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "inquirerpy" },
|
||||
{ name = "pycryptodome" },
|
||||
{ name = "pypresence" },
|
||||
{ name = "requests" },
|
||||
{ name = "rich" },
|
||||
{ name = "thefuzz" },
|
||||
{ name = "yt-dlp" },
|
||||
{ name = "yt-dlp", extra = ["default"] },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
@@ -199,6 +368,7 @@ standard = [
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "pre-commit" },
|
||||
{ name = "pyinstaller" },
|
||||
{ name = "pyright" },
|
||||
{ name = "pytest" },
|
||||
@@ -215,14 +385,17 @@ requires-dist = [
|
||||
{ name = "mpv", marker = "extra == 'standard'", specifier = ">=1.0.7" },
|
||||
{ name = "plyer", marker = "extra == 'notifications'", specifier = ">=2.1.0" },
|
||||
{ name = "plyer", marker = "extra == 'standard'", specifier = ">=2.1.0" },
|
||||
{ name = "pycryptodome", specifier = ">=3.21.0" },
|
||||
{ name = "pypresence", specifier = ">=4.3.0" },
|
||||
{ name = "requests", specifier = ">=2.32.3" },
|
||||
{ name = "rich", specifier = ">=13.9.2" },
|
||||
{ name = "thefuzz", specifier = ">=0.22.1" },
|
||||
{ name = "yt-dlp", specifier = ">=2024.10.7" },
|
||||
{ name = "yt-dlp", extras = ["default"], specifier = ">=2024.10.7" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
{ name = "pre-commit", specifier = ">=4.0.1" },
|
||||
{ name = "pyinstaller", specifier = ">=6.11.1" },
|
||||
{ name = "pyright", specifier = ">=1.1.384" },
|
||||
{ name = "pytest", specifier = ">=8.3.3" },
|
||||
@@ -271,6 +444,15 @@ standard = [
|
||||
{ name = "uvicorn", extra = ["standard"] },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filelock"
|
||||
version = "3.16.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9d/db/3ef5bb276dae18d6ec2124224403d1d67bccdbefc17af4cc8f553e341ab1/filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435", size = 18037 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/f8/feced7779d755758a52d1f6635d990b8d98dc0a29fa568bbe0625f18fdf3/filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", size = 16163 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.14.0"
|
||||
@@ -345,6 +527,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/56/95/9377bcb415797e44274b51d46e3249eba641711cf3348050f76ee7b15ffc/httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0", size = 76395 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "identify"
|
||||
version = "2.6.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1a/5f/05f0d167be94585d502b4adf8c7af31f1dc0b1c7e14f9938a88fdbbcf4a7/identify-2.6.3.tar.gz", hash = "sha256:62f5dae9b5fef52c84cc188514e9ea4f3f636b1d8799ab5ebc475471f9e47a02", size = 99179 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/f5/09644a3ad803fae9eca8efa17e1f2aef380c7f0b02f7ec4e8d446e51d64a/identify-2.6.3-py2.py3-none-any.whl", hash = "sha256:9edba65473324c2ea9684b1f944fe3191db3345e50b6d04571d10ed164f8d7bd", size = 99049 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.10"
|
||||
@@ -488,6 +679,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/3f/d835556e34804cd0078507ed0f8a550f15d2861b875656193dd3451b720b/mpv-1.0.7-py3-none-any.whl", hash = "sha256:520fb134c18185b69c7fce4aa3514f14371028022d92eb193818e9fefb1e9fe8", size = 45257 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mutagen"
|
||||
version = "1.47.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/81/e6/64bc71b74eef4b68e61eb921dcf72dabd9e4ec4af1e11891bbd312ccbb77/mutagen-1.47.0.tar.gz", hash = "sha256:719fadef0a978c31b4cf3c956261b3c58b6948b32023078a2117b1de09f0fc99", size = 1274186 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/7a/620f945b96be1f6ee357d211d5bf74ab1b7fe72a9f1525aafbfe3aee6875/mutagen-1.47.0-py3-none-any.whl", hash = "sha256:edd96f50c5907a9539d8e5bba7245f62c9f520aef333d13392a79a4f70aca719", size = 194391 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nodeenv"
|
||||
version = "1.9.1"
|
||||
@@ -524,6 +724,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/d7/8ff98376b1acc4503253b685ea09981697385ce344d4e3935c2af49e044d/pfzy-0.3.4-py3-none-any.whl", hash = "sha256:5f50d5b2b3207fa72e7ec0ef08372ef652685470974a107d0d4999fc5a903a96", size = 8537 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "4.3.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.5.0"
|
||||
@@ -542,6 +751,22 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/89/a41c2643fc8eabeb84791acb9d0e4d139b1e4b53473cc4dae947b5fa33ed/plyer-2.1.0-py2.py3-none-any.whl", hash = "sha256:1b1772060df8b3045ed4f08231690ec8f7de30f5a004aa1724665a9074eed113", size = 142266 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pre-commit"
|
||||
version = "4.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cfgv" },
|
||||
{ name = "identify" },
|
||||
{ name = "nodeenv" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "virtualenv" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2e/c8/e22c292035f1bac8b9f5237a2622305bc0304e776080b246f3df57c4ff9f/pre_commit-4.0.1.tar.gz", hash = "sha256:80905ac375958c0444c65e9cebebd948b3cdb518f335a091a670a89d652139d2", size = 191678 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/16/8f/496e10d51edd6671ebe0432e33ff800aa86775d2d147ce7d43389324a525/pre_commit-4.0.1-py2.py3-none-any.whl", hash = "sha256:efde913840816312445dc98787724647c65473daefe420785f885e8ed9a06878", size = 218713 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prompt-toolkit"
|
||||
version = "3.0.48"
|
||||
@@ -554,6 +779,63 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/6a/fd08d94654f7e67c52ca30523a178b3f8ccc4237fce4be90d39c938a831a/prompt_toolkit-3.0.48-py3-none-any.whl", hash = "sha256:f49a827f90062e411f1ce1f854f2aedb3c23353244f8108b89283587397ac10e", size = 386595 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pycparser"
|
||||
version = "2.22"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pycryptodome"
|
||||
version = "3.21.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/13/52/13b9db4a913eee948152a079fe58d035bd3d1a519584155da8e786f767e6/pycryptodome-3.21.0.tar.gz", hash = "sha256:f7787e0d469bdae763b876174cf2e6c0f7be79808af26b1da96f1a64bcf47297", size = 4818071 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/88/5e83de10450027c96c79dc65ac45e9d0d7a7fef334f39d3789a191f33602/pycryptodome-3.21.0-cp36-abi3-macosx_10_9_universal2.whl", hash = "sha256:2480ec2c72438430da9f601ebc12c518c093c13111a5c1644c82cdfc2e50b1e4", size = 2495937 },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/e1/8f28cd8cf7f7563319819d1e172879ccce2333781ae38da61c28fe22d6ff/pycryptodome-3.21.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:de18954104667f565e2fbb4783b56667f30fb49c4d79b346f52a29cb198d5b6b", size = 1634629 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/c1/f75a1aaff0c20c11df8dc8e2bf8057e7f73296af7dfd8cbb40077d1c930d/pycryptodome-3.21.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de4b7263a33947ff440412339cb72b28a5a4c769b5c1ca19e33dd6cd1dcec6e", size = 2168708 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/66/6f2b7ddb457b19f73b82053ecc83ba768680609d56dd457dbc7e902c41aa/pycryptodome-3.21.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0714206d467fc911042d01ea3a1847c847bc10884cf674c82e12915cfe1649f8", size = 2254555 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/2b/152c330732a887a86cbf591ed69bd1b489439b5464806adb270f169ec139/pycryptodome-3.21.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d85c1b613121ed3dbaa5a97369b3b757909531a959d229406a75b912dd51dd1", size = 2294143 },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/92/517c5c498c2980c1b6d6b9965dffbe31f3cd7f20f40d00ec4069559c5902/pycryptodome-3.21.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:8898a66425a57bcf15e25fc19c12490b87bd939800f39a03ea2de2aea5e3611a", size = 2160509 },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/1f/c74288f54d80a20a78da87df1818c6464ac1041d10988bb7d982c4153fbc/pycryptodome-3.21.0-cp36-abi3-musllinux_1_2_i686.whl", hash = "sha256:932c905b71a56474bff8a9c014030bc3c882cee696b448af920399f730a650c2", size = 2329480 },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/1b/d0b013bf7d1af7cf0a6a4fce13f5fe5813ab225313755367b36e714a63f8/pycryptodome-3.21.0-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:18caa8cfbc676eaaf28613637a89980ad2fd96e00c564135bf90bc3f0b34dd93", size = 2254397 },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/71/4cbd3870d3e926c34706f705d6793159ac49d9a213e3ababcdade5864663/pycryptodome-3.21.0-cp36-abi3-win32.whl", hash = "sha256:280b67d20e33bb63171d55b1067f61fbd932e0b1ad976b3a184303a3dad22764", size = 1775641 },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/1d/81d59d228381576b92ecede5cd7239762c14001a828bdba30d64896e9778/pycryptodome-3.21.0-cp36-abi3-win_amd64.whl", hash = "sha256:b7aa25fc0baa5b1d95b7633af4f5f1838467f1815442b22487426f94e0d66c53", size = 1812863 },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/b3/09ff7072e6d96c9939c24cf51d3c389d7c345bf675420355c22402f71b68/pycryptodome-3.21.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:2cb635b67011bc147c257e61ce864879ffe6d03342dc74b6045059dfbdedafca", size = 1691593 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/91/38e43628148f68ba9b68dedbc323cf409e537fd11264031961fd7c744034/pycryptodome-3.21.0-pp27-pypy_73-win32.whl", hash = "sha256:4c26a2f0dc15f81ea3afa3b0c87b87e501f235d332b7f27e2225ecb80c0b1cdd", size = 1765997 },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/16/ae464d4ac338c1dd41f89c41f9488e54f7d2a3acf93bb920bb193b99f8e3/pycryptodome-3.21.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:d5ebe0763c982f069d3877832254f64974139f4f9655058452603ff559c482e8", size = 1615855 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/8c/b0cee957eee1950ce7655006b26a8894cee1dc4b8747ae913684352786eb/pycryptodome-3.21.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ee86cbde706be13f2dec5a42b52b1c1d1cbb90c8e405c68d0755134735c8dc6", size = 1650018 },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/4d/d7138068089b99f6b0368622e60f97a577c936d75f533552a82613060c58/pycryptodome-3.21.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0fd54003ec3ce4e0f16c484a10bc5d8b9bd77fa662a12b85779a2d2d85d67ee0", size = 1687977 },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/02/90ae1ac9f28be4df0ed88c127bf4acc1b102b40053e172759d4d1c54d937/pycryptodome-3.21.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5dfafca172933506773482b0e18f0cd766fd3920bd03ec85a283df90d8a17bc6", size = 1788273 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pycryptodomex"
|
||||
version = "3.21.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/11/dc/e66551683ade663b5f07d7b3bc46434bf703491dbd22ee12d1f979ca828f/pycryptodomex-3.21.0.tar.gz", hash = "sha256:222d0bd05381dd25c32dd6065c071ebf084212ab79bab4599ba9e6a3e0009e6c", size = 4818543 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/22/5e/99f217d9881eead69607a2248dd7bbdf610837d7f5ad53f45a6cb71bbbfb/pycryptodomex-3.21.0-cp36-abi3-macosx_10_9_universal2.whl", hash = "sha256:34325b84c8b380675fd2320d0649cdcbc9cf1e0d1526edbe8fce43ed858cdc7e", size = 2499490 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/8f/4d0e2a859a6470289d64e39b419f01d2494dfa2e4995342d50f6c2834237/pycryptodomex-3.21.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:103c133d6cd832ae7266feb0a65b69e3a5e4dbbd6f3a3ae3211a557fd653f516", size = 1638037 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/9e/6e748c1fa814c956d356f93cf7192b19487ca56fc9e2a0bcde2bbc057601/pycryptodomex-3.21.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77ac2ea80bcb4b4e1c6a596734c775a1615d23e31794967416afc14852a639d3", size = 2172279 },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/3f/f5bef92b11750af9e3516d4e69736eeeff20a2818d34611508bef5a7b381/pycryptodomex-3.21.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9aa0cf13a1a1128b3e964dc667e5fe5c6235f7d7cfb0277213f0e2a783837cc2", size = 2258130 },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/4d/f0c65afd64ce435fd0547187ce6f99dfb37cdde16b05b57bca9f5c06966e/pycryptodomex-3.21.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:46eb1f0c8d309da63a2064c28de54e5e614ad17b7e2f88df0faef58ce192fc7b", size = 2297719 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/6a/2a1a101b0345ee70376ba93df8de6c8c01aac8341fda02970800873456a7/pycryptodomex-3.21.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:cc7e111e66c274b0df5f4efa679eb31e23c7545d702333dfd2df10ab02c2a2ce", size = 2164079 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/00/90a15f16c234815b660303c2d7266b41b401ea2605f3a90373e9d425e39f/pycryptodomex-3.21.0-cp36-abi3-musllinux_1_2_i686.whl", hash = "sha256:770d630a5c46605ec83393feaa73a9635a60e55b112e1fb0c3cea84c2897aa0a", size = 2333060 },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/74/49f5d20c514ccc631b940cc9dfec45dcce418dc84a98463a2e2ebec33904/pycryptodomex-3.21.0-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:52e23a0a6e61691134aa8c8beba89de420602541afaae70f66e16060fdcd677e", size = 2257982 },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/4b/d33ef74e2cc0025a259936661bb53432c5bbbadc561c5f2e023bcd73ce4c/pycryptodomex-3.21.0-cp36-abi3-win32.whl", hash = "sha256:a3d77919e6ff56d89aada1bd009b727b874d464cb0e2e3f00a49f7d2e709d76e", size = 1779052 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/be/7c991840af1184009fc86267160948350d1bf875f153c97bb471ad944e40/pycryptodomex-3.21.0-cp36-abi3-win_amd64.whl", hash = "sha256:b0e9765f93fe4890f39875e6c90c96cb341767833cfa767f41b490b506fa9ec0", size = 1816307 },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/ac/24125ad36778914a36f08d61ba5338cb9159382c638d9761ee19c8de822c/pycryptodomex-3.21.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:feaecdce4e5c0045e7a287de0c4351284391fe170729aa9182f6bd967631b3a8", size = 1694999 },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/73/be7a54a5903508070e5508925ba94493a1f326cfeecfff750e3eb250ea28/pycryptodomex-3.21.0-pp27-pypy_73-win32.whl", hash = "sha256:365aa5a66d52fd1f9e0530ea97f392c48c409c2f01ff8b9a39c73ed6f527d36c", size = 1769437 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/9f/39a6187f3986841fa6a9f35c6fdca5030ef73ff708b45a993813a51d7d10/pycryptodomex-3.21.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3efddfc50ac0ca143364042324046800c126a1d63816d532f2e19e6f2d8c0c31", size = 1619607 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/70/60bb08e9e9841b18d4669fb69d84b64ce900aacd7eb0ebebd4c7b9bdecd3/pycryptodomex-3.21.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0df2608682db8279a9ebbaf05a72f62a321433522ed0e499bc486a6889b96bf3", size = 1653571 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/6f/191b73509291c5ff0dddec9cc54797b1d73303c12b2e4017b24678e57099/pycryptodomex-3.21.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5823d03e904ea3e53aebd6799d6b8ec63b7675b5d2f4a4bd5e3adcb512d03b37", size = 1691548 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/c7/a0d3356f3074ac548afefa515ff46f3bea011deca607faf1c09b26dd5330/pycryptodomex-3.21.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:27e84eeff24250ffec32722334749ac2a57a5fd60332cd6a0680090e7c42877e", size = 1792099 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.9.2"
|
||||
@@ -685,6 +967,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/64/445861ee7a5fd32874c0f6cfe8222aacc8feda22539332e0d8ff50dadec6/pyinstaller_hooks_contrib-2024.10-py3-none-any.whl", hash = "sha256:ad47db0e153683b4151e10d231cb91f2d93c85079e78d76d9e0f57ac6c8a5e10", size = 338417 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pypresence"
|
||||
version = "4.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f4/2e/d110f862720b5e3ba1b0b719657385fc4151929befa2c6981f48360aa480/pypresence-4.3.0.tar.gz", hash = "sha256:a6191a3af33a9667f2a4ef0185577c86b962ee70aa82643c472768a6fed1fbf3", size = 10696 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/40/1d30b30e18f81eb71365681223971a9822a89b3d6ee5269dd2aa955bc228/pypresence-4.3.0-py2.py3-none-any.whl", hash = "sha256:af878c6d49315084f1b108aec86b31915080614d9421d6dd3a44737aba9ff13f", size = 11778 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyright"
|
||||
version = "1.1.388"
|
||||
@@ -1064,6 +1355,20 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/63/9a/0962b05b308494e3202d3f794a6e85abe471fe3cafdbcf95c2e8c713aabd/uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553", size = 4660018 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "virtualenv"
|
||||
version = "20.28.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "distlib" },
|
||||
{ name = "filelock" },
|
||||
{ name = "platformdirs" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bf/75/53316a5a8050069228a2f6d11f32046cfa94fbb6cc3f08703f59b873de2e/virtualenv-20.28.0.tar.gz", hash = "sha256:2c9c3262bb8e7b87ea801d715fae4495e6032450c71d2309be9550e7364049aa", size = 7650368 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/10/f9/0919cf6f1432a8c4baa62511f8f8da8225432d22e83e3476f5be1a1edc6e/virtualenv-20.28.0-py3-none-any.whl", hash = "sha256:23eae1b4516ecd610481eda647f3a7c09aea295055337331bb4e6892ecce47b0", size = 4276702 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "watchfiles"
|
||||
version = "0.24.0"
|
||||
@@ -1205,3 +1510,15 @@ sdist = { url = "https://files.pythonhosted.org/packages/52/50/0014e9099a9dc3dec
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/e4/e45c5067a79780954b905db4a42aa83d7aaefd91e32b18ab91c77600e668/yt_dlp-2024.11.4-py3-none-any.whl", hash = "sha256:589d51ed9f154624a45c1f0ceb3d68d0d1e2031460e8dbc62139be631c20b388", size = 3165645 },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
default = [
|
||||
{ name = "brotli", marker = "implementation_name == 'cpython'" },
|
||||
{ name = "brotlicffi", marker = "implementation_name != 'cpython'" },
|
||||
{ name = "certifi" },
|
||||
{ name = "mutagen" },
|
||||
{ name = "pycryptodomex" },
|
||||
{ name = "requests" },
|
||||
{ name = "urllib3" },
|
||||
{ name = "websockets" },
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user