diff --git a/fastanime/Utility/downloader/downloader.py b/fastanime/Utility/downloader/downloader.py index 4abc854..98d00e7 100644 --- a/fastanime/Utility/downloader/downloader.py +++ b/fastanime/Utility/downloader/downloader.py @@ -53,7 +53,8 @@ class YtDLPDownloader: anime_title = sanitize_filename(title[0]) episode_title = sanitize_filename(title[1]) ydl_opts = { - "outtmpl": f"{download_dir}/{anime_title}/{episode_title}.%(ext)s", # Specify the output path and template + # Specify the output path and template + "outtmpl": f"{download_dir}/{anime_title}/{episode_title}.%(ext)s", "progress_hooks": [ main_progress_hook, ], # Progress hook diff --git a/fastanime/__init__.py b/fastanime/__init__.py index d0a54d7..85a37cb 100644 --- a/fastanime/__init__.py +++ b/fastanime/__init__.py @@ -54,7 +54,8 @@ def FastAnime(): logging.getLogger(__name__) logging.basicConfig( level=logging.DEBUG, # Set the logging level to DEBUG - format="%(asctime)s%(levelname)s: %(message)s", # Use a simple message format + # Use a simple message format + format="%(asctime)s%(levelname)s: %(message)s", datefmt="[%d/%m/%Y@%H:%M:%S]", # Use a custom date format filename=NOTIFIER_LOG_FILE_PATH, filemode="a", # Use RichHandler to format the logs diff --git a/fastanime/cli/__init__.py b/fastanime/cli/__init__.py index 5dbf77c..744c134 100644 --- a/fastanime/cli/__init__.py +++ b/fastanime/cli/__init__.py @@ -66,6 +66,11 @@ signal.signal(signal.SIGINT, handle_exit) type=bool, help="Continue from last episode?", ) +@click.option( + "--skip/--no-skip", + type=bool, + help="Skip opening and ending theme songs?", +) @click.option( "-q", "--quality", @@ -112,6 +117,7 @@ def run_cli( server, format, continue_, + skip, translation_type, quality, auto_next, @@ -134,6 +140,9 @@ def run_cli( ctx.obj.format = format if ctx.get_parameter_source("continue_") == click.core.ParameterSource.COMMANDLINE: ctx.obj.continue_from_history = continue_ + if ctx.get_parameter_source("skip") == click.core.ParameterSource.COMMANDLINE: + ctx.obj.skip = skip + if quality: ctx.obj.quality = quality if ctx.get_parameter_source("auto-next") == click.core.ParameterSource.COMMANDLINE: diff --git a/fastanime/cli/commands/anilist/login.py b/fastanime/cli/commands/anilist/login.py index e331841..94b4264 100644 --- a/fastanime/cli/commands/anilist/login.py +++ b/fastanime/cli/commands/anilist/login.py @@ -27,7 +27,8 @@ def login(config: Config, status): exit_app() # ---- new loggin ----- print( - f"A browser session will be opened ( [link]{config.fastanime_anilist_app_login_url}[/link] )", + f"A browser session will be opened ( [link]{ + config.fastanime_anilist_app_login_url}[/link] )", ) webbrowser.open(config.fastanime_anilist_app_login_url) print("Please paste the token provided here") diff --git a/fastanime/cli/commands/anilist/notifier.py b/fastanime/cli/commands/anilist/notifier.py index 75619c0..3dea575 100644 --- a/fastanime/cli/commands/anilist/notifier.py +++ b/fastanime/cli/commands/anilist/notifier.py @@ -49,7 +49,8 @@ def notifier(config: Config): time.sleep(timeout * 60) continue data = result[1] - notifications = data["data"]["Page"]["notifications"] # pyright:ignore + # pyright:ignore + notifications = data["data"]["Page"]["notifications"] if not notifications: logger.info("Nothing to notify") else: @@ -59,13 +60,15 @@ def notifier(config: Config): anime_title = notification_["media"]["title"][ config.preferred_language ] - message = f"{anime_title}\nBe sure to watch so you are not left out of the loop." # pyright:ignore + # pyright:ignore + message = f"{anime_title}\nBe sure to watch so you are not left out of the loop." # message = str(textwrap.wrap(message, width=50)) id = notification_["media"]["id"] if past_notifications.get(str(id)) == notification_["episode"]: logger.info( - f"skipping id={id} title={anime_title} episode={anime_episode} already notified" + f"skipping id={id} title={anime_title} episode={ + anime_episode} already notified" ) else: diff --git a/fastanime/cli/config.py b/fastanime/cli/config.py index 45c8e88..4592762 100644 --- a/fastanime/cli/config.py +++ b/fastanime/cli/config.py @@ -37,6 +37,7 @@ class Config(object): "error": "3", "icons": "false", "notification_duration": "2", + "skip": "false", } ) self.configparser.add_section("stream") @@ -51,6 +52,7 @@ class Config(object): self.downloads_dir = self.get_downloads_dir() self.provider = self.get_provider() self.use_fzf = self.get_use_fzf() + self.skip = self.get_skip() self.icons = self.get_icons() self.preview = self.get_preview() self.translation_type = self.get_translation_type() @@ -112,6 +114,9 @@ class Config(object): def get_use_fzf(self): return self.configparser.getboolean("general", "use_fzf") + def get_skip(self): + return self.configparser.getboolean("stream", "skip") + def get_icons(self): return self.configparser.getboolean("general", "icons") diff --git a/fastanime/cli/interfaces/anilist_interfaces.py b/fastanime/cli/interfaces/anilist_interfaces.py index 0bc8866..ff8c15f 100644 --- a/fastanime/cli/interfaces/anilist_interfaces.py +++ b/fastanime/cli/interfaces/anilist_interfaces.py @@ -4,9 +4,11 @@ import os import random from datetime import datetime +from InquirerPy import inquirer +from InquirerPy.validator import EmptyInputValidator from rich import print from rich.progress import Progress -from rich.prompt import Prompt +from rich.prompt import Confirm, Prompt from ...anilist import AniList from ...constants import USER_CONFIG_PATH @@ -19,6 +21,7 @@ from ..config import Config from ..utils.mpv import mpv from ..utils.tools import QueryDict, exit_app from ..utils.utils import clear, fuzzy_inquirer +from .utils import aniskip def calculate_time_delta(start_time, end_time): @@ -62,8 +65,17 @@ def player_controls(config: Config, anilist_config: QueryDict): start_time = config.watch_history[str(anime_id)]["start_time"] print("[green]Continuing from:[/] ", start_time) + custom_args = [] + if config.skip: + if args := aniskip( + anilist_config.selected_anime_anilist["idMal"], current_episode + ): + custom_args = args stop_time, total_time = mpv( - current_link, selected_server["episode_title"], start_time=start_time + current_link, + selected_server["episode_title"], + start_time=start_time, + custom_args=custom_args, ) if stop_time == "0": episode = str(int(current_episode) + 1) @@ -263,8 +275,18 @@ def fetch_streams(config: Config, anilist_config: QueryDict): start_time = config.watch_history.get(str(anime_id), {}).get("start_time", "0") if start_time != "0": print("[green]Continuing from:[/] ", start_time) + custom_args = [] + if config.skip: + if args := aniskip( + anilist_config.selected_anime_anilist["idMal"], episode_number + ): + custom_args = args + stop_time, total_time = mpv( - stream_link, selected_server["episode_title"], start_time=start_time + stream_link, + selected_server["episode_title"], + start_time=start_time, + custom_args=custom_args, ) print("Finished at: ", stop_time) @@ -441,7 +463,7 @@ def anilist_options(config, anilist_config: QueryDict): "Paused": "PAUSED", "Planning": "PLANNING", "Dropped": "DROPPED", - "Repeating": "REPEATING", + "Rewatching": "REPEATING", "Completed": "COMPLETED", } if config.use_fzf: @@ -454,15 +476,52 @@ def anilist_options(config, anilist_config: QueryDict): anime_list = fuzzy_inquirer( "Choose the list you want to add to", list(anime_lists.keys()) ) - AniList.update_anime_list( + result = AniList.update_anime_list( {"status": anime_lists[anime_list], "mediaId": selected_anime["id"]} ) - print("Successfully updated your list") + if not result[0]: + print("Failed to update", result) + else: + print( + f"Successfully added {selected_anime_title} to your {anime_list} list :smile:" + ) + input("Enter to continue...") + anilist_options(config, anilist_config) + + def _score_anime(config: Config, anilist_config: QueryDict): + score = inquirer.number( + message="Enter the score:", + min_allowed=0, + max_allowed=100, + validate=EmptyInputValidator(), + ).execute() + + result = AniList.update_anime_list( + {"scoreRaw": score, "mediaId": selected_anime["id"]} + ) + if not result[0]: + print("Failed to update", result) + else: + print(f"Successfully scored {selected_anime_title}; score: {score}") input("Enter to continue...") anilist_options(config, anilist_config) def _remove_from_list(config: Config, anilist_config: QueryDict): - config.update_anime_list(anilist_config.anime_id, True) + if Confirm.ask( + f"Are you sure you want to procede, the folowing action will permanently remove { + selected_anime_title} from your list and your progress will be erased", + default=False, + ): + success, data = AniList.delete_medialist_entry(selected_anime["id"]) + if not success: + print("Failed to delete", data) + elif not data.get("deleted"): + print("Failed to delete", data) + else: + print("Successfully deleted :cry:", selected_anime_title) + else: + print(selected_anime_title, ":relieved:") + input("Enter to continue...") anilist_options(config, anilist_config) def _change_translation_type(config: Config, anilist_config: QueryDict): @@ -537,6 +596,7 @@ def anilist_options(config, anilist_config: QueryDict): options = { f"{'📽️ ' if icons else ''}Stream": provide_anime, f"{'📼 ' if icons else ''}Watch Trailer": _watch_trailer, + f"{'✨ ' if icons else ''}Score Anime": _score_anime, f"{'📥 ' if icons else ''}Add to List": _add_to_list, f"{'📤 ' if icons else ''}Remove from List": _remove_from_list, f"{'📖 ' if icons else ''}View Info": _view_info, @@ -683,7 +743,7 @@ def anilist(config: Config, anilist_config: QueryDict): f"{'✅ ' if icons else ''}Completed": lambda x="Completed": handle_animelist( anilist_config, config, x ), - f"{'🔁 ' if icons else ''}Repeating": lambda x="Repeating": handle_animelist( + f"{'🔁 ' if icons else ''}Rewatching": lambda x="Repeating": handle_animelist( anilist_config, config, x ), f"{'🔔 ' if icons else ''}Recently Updated Anime": AniList.get_most_recently_updated, diff --git a/fastanime/cli/interfaces/utils.py b/fastanime/cli/interfaces/utils.py index d62be1b..c855657 100644 --- a/fastanime/cli/interfaces/utils.py +++ b/fastanime/cli/interfaces/utils.py @@ -1,5 +1,6 @@ import os import shutil +import subprocess import textwrap from threading import Thread @@ -92,6 +93,19 @@ fzf-preview(){ SEARCH_RESULTS_CACHE = os.path.join(APP_CACHE_DIR, "search_results") +def aniskip(mal_id, episode): + ANISKIP = shutil.which("ani-skip") + if not ANISKIP: + print("Aniskip not found, please install and try again") + return + args = [ANISKIP, "-q", str(mal_id), "-e", str(episode)] + aniskip_result = subprocess.run(args, text=True, stdout=subprocess.PIPE) + if aniskip_result.returncode != 0: + return + mpv_skip_args = aniskip_result.stdout.strip() + return mpv_skip_args.split(" ") + + def write_search_results( search_results: list[AnilistBaseMediaDataSchema], config: Config ): @@ -126,7 +140,8 @@ def write_search_results( Favourites: {anime['favourites']} Status: {anime['status']} Episodes: {anime['episodes']} - Genres: {anilist_data_helper.format_list_data_with_comma(anime['genres'])} + Genres: {anilist_data_helper.format_list_data_with_comma( + anime['genres'])} Next Episode: {anilist_data_helper.extract_next_airing_episode(anime['nextAiringEpisode'])} Start Date: {anilist_data_helper.format_anilist_date_object(anime['startDate'])} End Date: {anilist_data_helper.format_anilist_date_object(anime['endDate'])} @@ -136,7 +151,8 @@ def write_search_results( template = textwrap.dedent(template) template = f""" {template} - {textwrap.fill(remove_html_tags(str(anime['description'])),width=45)} + {textwrap.fill(remove_html_tags( + str(anime['description'])), width=45)} """ f.write(template) diff --git a/fastanime/cli/utils/mpv.py b/fastanime/cli/utils/mpv.py index 7723b31..2dbee63 100644 --- a/fastanime/cli/utils/mpv.py +++ b/fastanime/cli/utils/mpv.py @@ -26,9 +26,9 @@ from typing import Optional # -def stream_video(url, mpv_args): +def stream_video(url, mpv_args, custom_args): process = subprocess.Popen( - ["mpv", url, *mpv_args], + ["mpv", url, *mpv_args, *custom_args], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, @@ -67,7 +67,13 @@ def stream_video(url, mpv_args): return last_time, total_time -def mpv(link: str, title: Optional[str] = "", start_time: str = "0", ytdl_format=""): +def mpv( + link: str, + title: Optional[str] = "", + start_time: str = "0", + ytdl_format="", + custom_args=[], +): # Determine if mpv is available MPV = shutil.which("mpv") @@ -121,7 +127,7 @@ def mpv(link: str, title: Optional[str] = "", start_time: str = "0", ytdl_format mpv_args.append(f"--title={title}") if ytdl_format: mpv_args.append(f"--ytdl-format={ytdl_format}") - stop_time, total_time = stream_video(link, mpv_args) + stop_time, total_time = stream_video(link, mpv_args, custom_args) return stop_time, total_time diff --git a/fastanime/libs/anilist/api.py b/fastanime/libs/anilist/api.py index 607d768..60bfa3d 100644 --- a/fastanime/libs/anilist/api.py +++ b/fastanime/libs/anilist/api.py @@ -13,7 +13,9 @@ from .queries_graphql import ( anime_characters_query, anime_query, anime_relations_query, + delete_list_entry_query, get_logged_in_user_query, + get_medialist_item_query, mark_as_read_mutation, media_list_mutation, media_list_query, @@ -79,6 +81,18 @@ class AniListApi: variables = {"status": status, "userId": self.user_id} return self._make_authenticated_request(media_list_query, variables) + def get_medialist_entry(self, mediaId: int): + variables = {"mediaId": mediaId} + return self._make_authenticated_request(get_medialist_item_query, variables) + + def delete_medialist_entry(self, mediaId: int): + result = self.get_medialist_entry(mediaId) + if not result[0]: + return result + id = result[1]["data"]["MediaList"]["id"] + variables = {"id": id} + return self._make_authenticated_request(delete_list_entry_query, variables) + def _make_authenticated_request(self, query: str, variables: dict = {}): """ The core abstraction for getting authenticated data from the anilist api diff --git a/fastanime/libs/anime_provider/allanime/api.py b/fastanime/libs/anime_provider/allanime/api.py index 3c698e1..0567ccb 100644 --- a/fastanime/libs/anime_provider/allanime/api.py +++ b/fastanime/libs/anime_provider/allanime/api.py @@ -139,7 +139,7 @@ class AllAnimeAPI: # get the stream url for an episode of the defined source names parsed_url = decode_hex_string(url) - embed_url = f"https://{ALLANIME_BASE}{parsed_url.replace('clock','clock.json')}" + embed_url = f"https://{ALLANIME_BASE}{parsed_url.replace('clock', 'clock.json')}" resp = requests.get( embed_url, headers={ @@ -155,7 +155,9 @@ class AllAnimeAPI: yield { "server": "gogoanime", "episode_title": ( - allanime_episode["notes"] or f'{anime["title"]}' + allanime_episode["notes"] + or f'{ + anime["title"]}' ) + f"; Episode {episode_number}", "links": resp.json()["links"], @@ -165,7 +167,9 @@ class AllAnimeAPI: yield { "server": "wetransfer", "episode_title": ( - allanime_episode["notes"] or f'{anime["title"]}' + allanime_episode["notes"] + or f'{ + anime["title"]}' ) + f"; Episode {episode_number}", "links": resp.json()["links"], @@ -175,7 +179,9 @@ class AllAnimeAPI: yield { "server": "sharepoint", "episode_title": ( - allanime_episode["notes"] or f'{anime["title"]}' + allanime_episode["notes"] + or f'{ + anime["title"]}' ) + f"; Episode {episode_number}", "links": resp.json()["links"], @@ -185,7 +191,9 @@ class AllAnimeAPI: yield { "server": "dropbox", "episode_title": ( - allanime_episode["notes"] or f'{anime["title"]}' + allanime_episode["notes"] + or f'{ + anime["title"]}' ) + f"; Episode {episode_number}", "links": resp.json()["links"], @@ -195,7 +203,9 @@ class AllAnimeAPI: yield { "server": "wixmp", "episode_title": ( - allanime_episode["notes"] or f'{anime["title"]}' + allanime_episode["notes"] + or f'{ + anime["title"]}' ) + f"; Episode {episode_number}", "links": resp.json()["links"], diff --git a/fastanime/libs/anime_provider/animepahe/api.py b/fastanime/libs/anime_provider/animepahe/api.py index cfab326..a26cc2b 100644 --- a/fastanime/libs/anime_provider/animepahe/api.py +++ b/fastanime/libs/anime_provider/animepahe/api.py @@ -32,7 +32,8 @@ class AnimePaheApi: def get_anime(self, session_id: str, *args): url = "https://animepahe.ru/api?m=release&id=&sort=episode_asc&page=1" - url = f"{ANIMEPAHE_ENDPOINT}m=release&id={session_id}&sort=episode_asc&page=1" + url = f"{ANIMEPAHE_ENDPOINT}m=release&id={ + session_id}&sort=episode_asc&page=1" response = requests.get(url, headers=REQUEST_HEADERS) if not response.status_code == 200: return