From b86c1a04796e374c400f164359fa15ebddfb2b33 Mon Sep 17 00:00:00 2001 From: benex Date: Thu, 21 Nov 2024 16:40:09 +0300 Subject: [PATCH] feat: add fastanime anilist download beta --- fastanime/cli/commands/anilist/data.py | 476 +++++++++++++++++ fastanime/cli/commands/anilist/download.py | 595 +++++++++++---------- fastanime/cli/commands/anilist/search.py | 485 +---------------- fastanime/libs/anilist/api.py | 1 + fastanime/libs/anilist/queries_graphql.py | 3 +- 5 files changed, 793 insertions(+), 767 deletions(-) create mode 100644 fastanime/cli/commands/anilist/data.py diff --git a/fastanime/cli/commands/anilist/data.py b/fastanime/cli/commands/anilist/data.py new file mode 100644 index 0000000..e799d30 --- /dev/null +++ b/fastanime/cli/commands/anilist/data.py @@ -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) diff --git a/fastanime/cli/commands/anilist/download.py b/fastanime/cli/commands/anilist/download.py index b3bfceb..afcaa59 100644 --- a/fastanime/cli/commands/anilist/download.py +++ b/fastanime/cli/commands/anilist/download.py @@ -1,84 +1,79 @@ -from typing import TYPE_CHECKING - import click -from ...completion_functions import anime_titles_shell_complete -if TYPE_CHECKING: - from ..config import Config +from ...completion_functions import anime_titles_shell_complete +from .data import ( + tags_available_list, + sorts_available, + media_statuses_available, + seasons_available, + genres_available, + media_formats_available, + years_available, +) @click.command( - help="Download anime using the anime provider for a specified range", - short_help="Download anime", - epilog=""" -\b -\b\bExamples: - # Download all available episodes - # multiple titles can be specified with -t option - fastanime download -t -t - # -- or -- - fastanime download -t -t -r ':' -\b - # download latest episode for the two anime titles - # the number can be any no of latest episodes but a minus sign - # must be present - fastanime download -t -t -r '-1' -\b - # latest 5 - fastanime download -t -t -r '-5' -\b - # Download specific episode range - # be sure to observe the range Syntax - fastanime download -t -r '::' -\b - fastanime download -t -r ':' -\b - fastanime download -t -r ':' -\b - fastanime download -t -r ':' -\b - # download specific episode - # remember python indexing starts at 0 - fastanime download -t -r ':' -\b - # merge subtitles with ffmpeg to mkv format; hianime tends to give subs as separate files - # and dont prompt for anything - # eg existing file in destination instead remove - # and clean - # ie remove original files (sub file and vid file) - # only keep merged files - fastanime download -t --merge --clean --no-prompt -\b - # EOF is used since -t always expects a title - # you can supply anime titles from file or -t at the same time - # from stdin - echo -e "\\n\\n" | fastanime download -t "EOF" -r -f - -\b - # from file - fastanime download -t "EOF" -r -f -""", + 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( - "--anime-titles", - "--anime_title", - "-t", - required=True, - shell_complete=anime_titles_shell_complete, + "--status", + "-S", + help="The media status of the anime", multiple=True, - help="Specify which anime to download", + 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( - "--file", - "-f", - type=click.File(), - help="A file to read from all anime to download", -) @click.option( "--force-unknown-ext", "-F", @@ -114,12 +109,22 @@ if TYPE_CHECKING: help="Whether to prompt for anything instead just do the best thing", default=True, ) +@click.option( + "--max-results", "-M", type=int, help="The maximum number of results to show" +) @click.pass_obj def download( - config: "Config", - anime_titles: tuple, + config, + title, + season, + status, + sort, + genres, + tags, + media_format, + year, + on_list, episode_range, - file, force_unknown_ext, silent, verbose, @@ -127,253 +132,251 @@ def download( clean, wait_time, prompt, + max_results, ): - import time - + from ....anilist import AniList from rich import print - 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, + 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 - anime_provider = AnimeProvider(config.provider) - anilist_anime_info = None + from rich.progress import Progress + from thefuzz import fuzz - translation_type = config.translation_type - download_dir = config.downloads_dir - if file: - contents = file.read() - anime_titles_from_file = tuple( - [title for title in contents.split("\n") if title] + 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, ) - file.close() - anime_titles = (*anime_titles_from_file, *anime_titles) - print(f"[green bold]Queued:[/] {anime_titles}") - for anime_title in anime_titles: - if anime_title == "EOF": - break - 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("Search results failed") - input("Enter to retry") - download( - config, - anime_title, - episode_range, - file, - force_unknown_ext, - silent, - verbose, - merge, - clean, - wait_time, - prompt, - ) - return - 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 - } + anime_provider = AnimeProvider(config.provider) + anilist_anime_info = None - if config.auto_select: - selected_anime_title = max( - search_results_.keys(), - key=lambda title: fuzz.ratio( - anime_normalizer.get(title, title), anime_title - ), + translation_type = config.translation_type + download_dir = config.downloads_dir + anime_titles = [ + ( + anime["title"][config.preferred_language] + or anime["title"]["english"] + or anime["title"]["romaji"] ) - 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" + 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 ) - else: - selected_anime_title = fuzzy_inquirer( - choices, - "Please Select title", + 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 + } - # ---- 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("Sth went wring anime no found") - input("Enter to continue...") - download( - config, - anime_title, - episode_range, - file, - force_unknown_ext, - silent, - verbose, - merge, - clean, - wait_time, - prompt, - ) - return - - episodes = sorted( - anime["availableEpisodesDetail"][config.translation_type], key=float - ) - # where the magic happens - if episode_range: - if ":" in episode_range: - ep_range_tuple = episode_range.split(":") - 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 + 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: - episodes_range = episodes[int(episode_range) :] - print(f"[green bold]Downloading: [/] {episodes_range}") - - else: - episodes_range = sorted(episodes, key=float) - - if config.normalize_titles: - from ....libs.common.mini_anilist import get_basic_anime_info_by_title - - anilist_anime_info = get_basic_anime_info_by_title(anime["title"]) - - # 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 + choices = list(search_results_.keys()) + if config.use_fzf: + selected_anime_title = fzf.run( + choices, "Please Select title", "FastAnime" ) - 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 + 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: - if config.use_fzf: - server_name = fzf.run(servers_names, "Select an link") + 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: - 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...") + 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 - link = stream_link["link"] - provider_headers = servers[server_name]["headers"] + 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"] + 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"] + 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 ) - import re + 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, + ) + 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 - for episode_detail in anilist_anime_info["episodes"]: - 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, - ) - except Exception as e: - print(e) - time.sleep(1) - print("Continuing...") - print("Done Downloading") - time.sleep(wait_time) - exit_app() + print("Failed to search for anime", anilist_search_results) + exit(1) diff --git a/fastanime/cli/commands/anilist/search.py b/fastanime/cli/commands/anilist/search.py index 7adb4fb..207876d 100644 --- a/fastanime/cli/commands/anilist/search.py +++ b/fastanime/cli/commands/anilist/search.py @@ -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 ( + tags_available_list, + sorts_available, + media_statuses_available, + seasons_available, + genres_available, + media_formats_available, + 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( diff --git a/fastanime/libs/anilist/api.py b/fastanime/libs/anilist/api.py index 1b4f5e9..541f436 100644 --- a/fastanime/libs/anilist/api.py +++ b/fastanime/libs/anilist/api.py @@ -306,6 +306,7 @@ class AniListApi: def search( self, + max_results=50, query: str | None = None, sort: str | None = None, genre_in: list[str] | None = None, diff --git a/fastanime/libs/anilist/queries_graphql.py b/fastanime/libs/anilist/queries_graphql.py index 68d65fe..6131055 100644 --- a/fastanime/libs/anilist/queries_graphql.py +++ b/fastanime/libs/anilist/queries_graphql.py @@ -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