diff --git a/fastanime/cli/commands/anilist/commands/stats.py b/fastanime/cli/commands/anilist/commands/stats.py index 4840aa5..f6d6d40 100644 --- a/fastanime/cli/commands/anilist/commands/stats.py +++ b/fastanime/cli/commands/anilist/commands/stats.py @@ -40,16 +40,18 @@ def stats(config: "AppConfig"): "Authentication Required", f"You must be logged in to {config.general.media_api} to sync your media list.", ) - feedback.info("Run this command to authenticate:", f"fastanime {config.general.media_api} auth") + feedback.info( + "Run this command to authenticate:", + f"fastanime {config.general.media_api} auth", + ) raise click.Abort() - - - # Check if kitten is available for image display KITTEN_EXECUTABLE = shutil.which("kitten") if not KITTEN_EXECUTABLE: - feedback.warning("Kitten not found - profile image will not be displayed") + feedback.warning( + "Kitten not found - profile image will not be displayed" + ) else: # Display profile image using kitten icat if profile.avatar_url: @@ -92,4 +94,4 @@ def stats(config: "AppConfig"): raise click.Abort() except Exception as e: feedback.error("Unexpected error occurred", str(e)) - raise click.Abort() \ No newline at end of file + raise click.Abort() diff --git a/fastanime/cli/commands/search.py b/fastanime/cli/commands/search.py index 71c7fc0..60a2f1c 100644 --- a/fastanime/cli/commands/search.py +++ b/fastanime/cli/commands/search.py @@ -176,16 +176,12 @@ def stream_anime( f"Failed to get stream link for anime: {anime.title}, episode: {episode}" ) print(f"[green bold]Now Streaming:[/] {anime.title} Episode: {episode}") - + # Check if IPC player should be used if config.mpv.use_ipc: # Get available episodes for current translation type - available_episodes = getattr( - anime.episodes, - config.stream.translation_type, - [] - ) - + available_episodes = getattr(anime.episodes, config.stream.translation_type, []) + # Use IPC player with episode navigation capabilities player.play( PlayerParams( @@ -200,7 +196,7 @@ def stream_anime( current_episode=episode, current_anime_id=anime.id, current_anime_title=anime.title, - current_translation_type=config.stream.translation_type + current_translation_type=config.stream.translation_type, ) ) else: diff --git a/fastanime/cli/interactive/menu/media/downloads.py b/fastanime/cli/interactive/menu/media/downloads.py index 3c4f9e2..c0fe835 100644 --- a/fastanime/cli/interactive/menu/media/downloads.py +++ b/fastanime/cli/interactive/menu/media/downloads.py @@ -237,9 +237,7 @@ def _create_local_recent_media_action(ctx: Context, state: State) -> MenuAction: ), ) else: - ctx.feedback.info( - "No recently watched media found in local registry" - ) + ctx.feedback.info("No recently watched media found in local registry") return InternalDirective.RELOAD return action diff --git a/fastanime/cli/interactive/menu/media/dynamic_search.py b/fastanime/cli/interactive/menu/media/dynamic_search.py index 6cdc507..ad5a8bd 100644 --- a/fastanime/cli/interactive/menu/media/dynamic_search.py +++ b/fastanime/cli/interactive/menu/media/dynamic_search.py @@ -2,7 +2,6 @@ import json import logging import os import tempfile -from pathlib import Path from .....core.constants import APP_CACHE_DIR, SCRIPTS_DIR from .....libs.media_api.params import MediaSearchParams @@ -30,20 +29,22 @@ def dynamic_search(ctx: Context, state: State) -> State | InternalDirective: # Read the GraphQL search query from .....libs.media_api.anilist import gql - + search_query = gql.SEARCH_MEDIA.read_text(encoding="utf-8") # Properly escape the GraphQL query for JSON search_query_escaped = json.dumps(search_query) - + # Prepare the search script auth_header = "" - if ctx.media_api.is_authenticated() and hasattr(ctx.media_api, 'token'): + if ctx.media_api.is_authenticated() and hasattr(ctx.media_api, "token"): auth_header = f"Bearer {ctx.media_api.token}" # Create a temporary search script - with tempfile.NamedTemporaryFile(mode='w', suffix='.sh', delete=False) as temp_script: + with tempfile.NamedTemporaryFile( + mode="w", suffix=".sh", delete=False + ) as temp_script: script_content = SEARCH_TEMPLATE_SCRIPT - + replacements = { "GRAPHQL_ENDPOINT": "https://graphql.anilist.co", "GRAPHQL_QUERY": search_query_escaped, @@ -51,17 +52,17 @@ def dynamic_search(ctx: Context, state: State) -> State | InternalDirective: "SEARCH_RESULTS_FILE": str(SEARCH_RESULTS_FILE), "AUTH_HEADER": auth_header, } - + for key, value in replacements.items(): script_content = script_content.replace(f"{{{key}}}", str(value)) - + temp_script.write(script_content) temp_script_path = temp_script.name try: # Make the script executable os.chmod(temp_script_path, 0o755) - + # Use the selector's search functionality try: # Prepare preview functionality @@ -76,56 +77,69 @@ def dynamic_search(ctx: Context, state: State) -> State | InternalDirective: prompt="Search Anime", search_command=f"bash {temp_script_path} {{q}}", preview=preview_command, - header="Type to search for anime dynamically" + header="Type to search for anime dynamically", ) else: choice = ctx.selector.search( prompt="Search Anime", search_command=f"bash {temp_script_path} {{q}}", - header="Type to search for anime dynamically" + header="Type to search for anime dynamically", ) except NotImplementedError: feedback.error("Dynamic search is not supported by your current selector") - feedback.info("Please use the regular search option or switch to fzf selector") + feedback.info( + "Please use the regular search option or switch to fzf selector" + ) return InternalDirective.MAIN - + if not choice: return InternalDirective.MAIN - + # Read the cached search results if not SEARCH_RESULTS_FILE.exists(): logger.error("Search results file not found") return InternalDirective.MAIN - + try: - with open(SEARCH_RESULTS_FILE, 'r', encoding='utf-8') as f: + with open(SEARCH_RESULTS_FILE, "r", encoding="utf-8") as f: raw_data = json.load(f) - + # Transform the raw data into MediaSearchResult search_result = ctx.media_api.transform_raw_search_data(raw_data) - + if not search_result or not search_result.media: feedback.info("No results found") return InternalDirective.MAIN - + # Find the selected media item by matching the choice with the displayed format selected_media = None for media_item in search_result.media: - title = media_item.title.english or media_item.title.romaji or media_item.title.native or "Unknown" - year = media_item.start_date.year if media_item.start_date else "Unknown" + title = ( + media_item.title.english + or media_item.title.romaji + or media_item.title.native + or "Unknown" + ) + year = ( + media_item.start_date.year if media_item.start_date else "Unknown" + ) status = media_item.status.value if media_item.status else "Unknown" - genres = ", ".join([genre.value for genre in media_item.genres[:3]]) if media_item.genres else "Unknown" - + genres = ( + ", ".join([genre.value for genre in media_item.genres[:3]]) + if media_item.genres + else "Unknown" + ) + display_format = f"{title} ({year}) [{status}] - {genres}" - + if choice.strip() == display_format.strip(): selected_media = media_item break - + if not selected_media: logger.error(f"Could not find selected media for choice: {choice}") return InternalDirective.MAIN - + # Navigate to media actions with the selected item return State( menu_name=MenuName.MEDIA_ACTIONS, @@ -136,12 +150,12 @@ def dynamic_search(ctx: Context, state: State) -> State | InternalDirective: page_info=search_result.page_info, ), ) - + except (json.JSONDecodeError, KeyError, Exception) as e: logger.error(f"Error processing search results: {e}") feedback.error("Failed to process search results") return InternalDirective.MAIN - + finally: # Clean up the temporary script try: diff --git a/fastanime/cli/interactive/menu/media/main.py b/fastanime/cli/interactive/menu/media/main.py index 08ae164..6dd2286 100644 --- a/fastanime/cli/interactive/menu/media/main.py +++ b/fastanime/cli/interactive/menu/media/main.py @@ -39,7 +39,9 @@ def main(ctx: Context, state: State) -> State | InternalDirective: ctx, state, UserMediaListStatus.PLANNING ), f"{'🔎 ' if icons else ''}Search": _create_search_media_list(ctx, state), - f"{'🔍 ' if icons else ''}Dynamic Search": _create_dynamic_search_action(ctx, state), + f"{'🔍 ' if icons else ''}Dynamic Search": _create_dynamic_search_action( + ctx, state + ), f"{'🏠 ' if icons else ''}Downloads": _create_downloads_action(ctx, state), f"{'🔔 ' if icons else ''}Recently Updated": _create_media_list_action( ctx, state, MediaSort.UPDATED_AT_DESC diff --git a/fastanime/cli/interactive/menu/media/servers.py b/fastanime/cli/interactive/menu/media/servers.py index bd95e65..34a16a2 100644 --- a/fastanime/cli/interactive/menu/media/servers.py +++ b/fastanime/cli/interactive/menu/media/servers.py @@ -82,18 +82,17 @@ def servers(ctx: Context, state: State) -> State | InternalDirective: # TODO: Refine implementation mpv ipc player # Check if IPC player should be used and if we have the required data - if (config.mpv.use_ipc and - state.provider.anime and - provider_anime and - episode_number): - + if ( + config.mpv.use_ipc + and state.provider.anime + and provider_anime + and episode_number + ): # Get available episodes for current translation type available_episodes = getattr( - provider_anime.episodes, - config.stream.translation_type, - [] + provider_anime.episodes, config.stream.translation_type, [] ) - + # Create player params with IPC dependencies for episode navigation player_result = ctx.player.play( PlayerParams( @@ -109,7 +108,7 @@ def servers(ctx: Context, state: State) -> State | InternalDirective: current_episode=episode_number, current_anime_id=provider_anime.id, current_anime_title=provider_anime.title, - current_translation_type=config.stream.translation_type + current_translation_type=config.stream.translation_type, ) ) else: diff --git a/fastanime/cli/interactive/state.py b/fastanime/cli/interactive/state.py index 1156a79..0f35d06 100644 --- a/fastanime/cli/interactive/state.py +++ b/fastanime/cli/interactive/state.py @@ -1,4 +1,4 @@ -from enum import Enum, auto +from enum import Enum from typing import Dict, Optional, Union from pydantic import BaseModel, ConfigDict, Field diff --git a/fastanime/cli/utils/preview.py b/fastanime/cli/utils/preview.py index f080c86..5a7fc48 100644 --- a/fastanime/cli/utils/preview.py +++ b/fastanime/cli/utils/preview.py @@ -225,15 +225,15 @@ def get_episode_preview( def get_dynamic_anime_preview(config: AppConfig) -> str: """ Generate dynamic anime preview script for search functionality. - + This is different from regular anime preview because: 1. We don't have media items upfront 2. The preview needs to work with search results as they come in 3. Preview is handled entirely in shell by parsing JSON results - + Args: config: Application configuration - + Returns: Preview script content for fzf dynamic search """ @@ -249,6 +249,7 @@ def get_dynamic_anime_preview(config: AppConfig) -> str: # We need to return the path to the search results file from ...core.constants import APP_CACHE_DIR + search_cache_dir = APP_CACHE_DIR / "search" search_results_file = search_cache_dir / "current_search_results.json" diff --git a/fastanime/core/config/descriptions.py b/fastanime/core/config/descriptions.py index fe37b6c..37c700c 100644 --- a/fastanime/core/config/descriptions.py +++ b/fastanime/core/config/descriptions.py @@ -91,7 +91,9 @@ MPV_DISABLE_POPEN = ( "Disable using subprocess.Popen for MPV, which can be unstable on some systems." ) MPV_USE_PYTHON_MPV = "Use the python-mpv library for enhanced player control." -MPV_USE_IPC = "Use IPC communication with MPV for advanced features like episode navigation." +MPV_USE_IPC = ( + "Use IPC communication with MPV for advanced features like episode navigation." +) # VlcConfig VLC_ARGS = "Comma-separated arguments to pass to the Vlc player." diff --git a/fastanime/libs/media_api/base.py b/fastanime/libs/media_api/base.py index 16dfd5e..c0b0b18 100644 --- a/fastanime/libs/media_api/base.py +++ b/fastanime/libs/media_api/base.py @@ -83,10 +83,10 @@ class BaseApiClient(abc.ABC): def transform_raw_search_data(self, raw_data: Dict) -> Optional[MediaSearchResult]: """ Transform raw API response data into a MediaSearchResult. - + Args: raw_data: Raw response data from the API - + Returns: MediaSearchResult object or None if transformation fails """ diff --git a/fastanime/libs/player/mpv/example_integration.py b/fastanime/libs/player/mpv/example_integration.py index 5091b5a..c4dab50 100644 --- a/fastanime/libs/player/mpv/example_integration.py +++ b/fastanime/libs/player/mpv/example_integration.py @@ -22,11 +22,11 @@ def create_ipc_player_params( translation_type: Literal["sub", "dub"] = "sub", subtitles: Optional[List[str]] = None, headers: Optional[dict] = None, - start_time: Optional[str] = None + start_time: Optional[str] = None, ) -> PlayerParams: """ Create PlayerParams with IPC player dependencies for episode navigation. - + Args: url: Stream URL title: Episode title @@ -37,13 +37,13 @@ def create_ipc_player_params( subtitles: List of subtitle URLs headers: HTTP headers for streaming start_time: Start time for playback - + Returns: PlayerParams configured for IPC player """ # Get available episodes for the translation type available_episodes: List[str] = getattr(anime.episodes, translation_type, []) - + return PlayerParams( url=url, title=title, @@ -57,7 +57,7 @@ def create_ipc_player_params( current_episode=current_episode, current_anime_id=anime.id, current_anime_title=anime.title, - current_translation_type=translation_type + current_translation_type=translation_type, ) @@ -65,7 +65,7 @@ def example_usage(): """Example of how to use the IPC player in an interactive session.""" # This would typically be called from within the servers.py menu # when the IPC player is enabled - + # Updated integration example: """ # In servers.py, around line 82: @@ -112,28 +112,28 @@ def example_usage(): # Key features enabled by IPC player: -# +# # 1. Episode Navigation: # - Shift+N: Next episode # - Shift+P: Previous episode # - Shift+R: Reload current episode -# +# # 2. Quality/Server switching: # - Script message: select-quality 720 # - Script message: select-server gogoanime -# +# # 3. Episode jumping: # - Script message: select-episode 5 -# +# # 4. Translation type switching: # - Shift+T: Toggle between sub/dub -# +# # 5. Auto-next episode (when implemented): # - Automatically plays next episode when current one ends # # To send script messages from MPV console (` key): # script-message select-episode 5 -# script-message select-quality 1080 +# script-message select-quality 1080 # script-message select-server top # # Configuration: diff --git a/fastanime/libs/player/mpv/ipc.py b/fastanime/libs/player/mpv/ipc.py index f895974..67bc898 100644 --- a/fastanime/libs/player/mpv/ipc.py +++ b/fastanime/libs/player/mpv/ipc.py @@ -25,7 +25,6 @@ Requirements: import json import logging import random -import re import socket import subprocess import tempfile diff --git a/fastanime/libs/player/mpv/player.py b/fastanime/libs/player/mpv/player.py index 3070601..70e5c43 100644 --- a/fastanime/libs/player/mpv/player.py +++ b/fastanime/libs/player/mpv/player.py @@ -112,7 +112,7 @@ class MpvPlayer(BasePlayer): def _stream_on_desktop_with_ipc(self, params: PlayerParams) -> PlayerResult: """Stream using IPC player for enhanced features.""" from .ipc import MpvIPCPlayer - + ipc_player = MpvIPCPlayer(self.config) return ipc_player.play(params) diff --git a/fastanime/libs/player/params.py b/fastanime/libs/player/params.py index 4b76e9f..f1d62f2 100644 --- a/fastanime/libs/player/params.py +++ b/fastanime/libs/player/params.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, List, Literal, Optional +from typing import TYPE_CHECKING, List, Literal, Optional if TYPE_CHECKING: from ..provider.anime.base import BaseAnimeProvider @@ -14,7 +14,7 @@ class PlayerParams: subtitles: list[str] | None = None headers: dict[str, str] | None = None start_time: str | None = None - + # IPC player specific parameters for episode navigation anime_provider: Optional["BaseAnimeProvider"] = None current_anime: Optional["Anime"] = None diff --git a/fastanime/libs/selectors/base.py b/fastanime/libs/selectors/base.py index 0fdb1df..465fff5 100644 --- a/fastanime/libs/selectors/base.py +++ b/fastanime/libs/selectors/base.py @@ -115,16 +115,16 @@ class BaseSelector(ABC): ) -> str | None: """ Provides dynamic search functionality that reloads results based on user input. - + Args: prompt: The message to display to the user. search_command: The command to execute for searching/reloading results. preview: An optional command or string for a preview window. header: An optional header to display above the choices. - + Returns: The string of the chosen item. - + Raises: NotImplementedError: If the selector doesn't support dynamic search. """ diff --git a/fastanime/libs/selectors/fzf/selector.py b/fastanime/libs/selectors/fzf/selector.py index 32cfa4c..4453a48 100644 --- a/fastanime/libs/selectors/fzf/selector.py +++ b/fastanime/libs/selectors/fzf/selector.py @@ -128,7 +128,7 @@ class FzfSelector(BaseSelector): f"change:reload({search_command})", "--ansi", ] - + if preview: commands.extend(["--preview", preview])