mirror of
https://github.com/Benexl/FastAnime.git
synced 2026-02-04 11:07:48 -08:00
chore: format with ruff
This commit is contained in:
@@ -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()
|
||||
raise click.Abort()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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
|
||||
"""
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -25,7 +25,6 @@ Requirements:
|
||||
import json
|
||||
import logging
|
||||
import random
|
||||
import re
|
||||
import socket
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -128,7 +128,7 @@ class FzfSelector(BaseSelector):
|
||||
f"change:reload({search_command})",
|
||||
"--ansi",
|
||||
]
|
||||
|
||||
|
||||
if preview:
|
||||
commands.extend(["--preview", preview])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user