mirror of
https://github.com/Benexl/FastAnime.git
synced 2025-12-26 12:51:15 -08:00
169 lines
6.2 KiB
Python
169 lines
6.2 KiB
Python
from __future__ import annotations
|
|
|
|
import contextlib
|
|
from typing import TYPE_CHECKING, Any, Iterator, List, Optional
|
|
|
|
from rich import print as rprint
|
|
from rich.panel import Panel
|
|
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
from rich.prompt import Confirm
|
|
from yt_dlp.utils import clean_html
|
|
|
|
from ...libs.anime.types import Anime
|
|
|
|
if TYPE_CHECKING:
|
|
from ...core.config import AppConfig
|
|
from ...libs.anilist.types import AnilistBaseMediaDataSchema
|
|
from .session import Session
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def progress_spinner(description: str = "Working...") -> Iterator[None]:
|
|
"""A context manager for showing a rich spinner for long operations."""
|
|
progress = Progress(
|
|
SpinnerColumn(),
|
|
TextColumn("[progress.description]{task.description}"),
|
|
transient=True,
|
|
)
|
|
task = progress.add_task(description=description, total=None)
|
|
with progress:
|
|
yield
|
|
progress.remove_task(task)
|
|
|
|
|
|
def display_error(message: str) -> None:
|
|
"""Displays a formatted error message and waits for user confirmation."""
|
|
rprint(f"[bold red]Error:[/] {message}")
|
|
Confirm.ask("Press Enter to continue...", default=True, show_default=False)
|
|
|
|
|
|
def prompt_main_menu(session: Session, choices: list[str]) -> Optional[str]:
|
|
"""Displays the main menu using the session's selector."""
|
|
header = (
|
|
"🚀 FastAnime Interactive Menu"
|
|
if session.config.general.icons
|
|
else "FastAnime Interactive Menu"
|
|
)
|
|
return session.selector.choose("Select Action", choices, header=header)
|
|
|
|
|
|
def prompt_for_search(session: Session) -> Optional[str]:
|
|
"""Prompts the user for a search query using the session's selector."""
|
|
search_term = session.selector.ask("Enter search term")
|
|
return search_term if search_term and search_term.strip() else None
|
|
|
|
|
|
def prompt_anime_selection(
|
|
session: Session, media_list: list[AnilistBaseMediaDataSchema]
|
|
) -> Optional[AnilistBaseMediaDataSchema]:
|
|
"""Displays anime results using the session's selector."""
|
|
from yt_dlp.utils import sanitize_filename
|
|
|
|
choice_map = {}
|
|
for anime in media_list:
|
|
title = anime.get("title", {}).get("romaji") or anime.get("title", {}).get(
|
|
"english", "Unknown Title"
|
|
)
|
|
progress = anime.get("mediaListEntry", {}).get("progress", 0)
|
|
episodes_total = anime.get("episodes") or "∞"
|
|
display_title = sanitize_filename(f"{title} ({progress}/{episodes_total})")
|
|
choice_map[display_title] = anime
|
|
|
|
choices = list(choice_map.keys()) + ["Next Page", "Previous Page", "Back"]
|
|
selection = session.selector.choose(
|
|
"Select Anime", choices, header="Search Results"
|
|
)
|
|
|
|
if selection in ["Back", "Next Page", "Previous Page"] or selection is None:
|
|
return selection # Let the state handle these special strings
|
|
|
|
return choice_map.get(selection)
|
|
|
|
|
|
def prompt_anime_actions(
|
|
session: Session, anime: AnilistBaseMediaDataSchema
|
|
) -> Optional[str]:
|
|
"""Displays the actions menu for a selected anime."""
|
|
choices = ["Stream", "View Info", "Back"]
|
|
if anime.get("trailer"):
|
|
choices.insert(0, "Watch Trailer")
|
|
if session.config.user:
|
|
choices.insert(1, "Add to List")
|
|
choices.insert(2, "Score Anime")
|
|
|
|
header = anime.get("title", {}).get("romaji", "Anime Actions")
|
|
return session.selector.choose("Select Action", choices, header=header)
|
|
|
|
|
|
def prompt_episode_selection(
|
|
session: Session, episode_list: list[str], anime_details: Anime
|
|
) -> Optional[str]:
|
|
"""Displays the list of available episodes."""
|
|
choices = episode_list + ["Back"]
|
|
header = f"Episodes for {anime_details.title}"
|
|
return session.selector.choose("Select Episode", choices, header=header)
|
|
|
|
|
|
def prompt_add_to_list(session: Session) -> Optional[str]:
|
|
"""Prompts user to select an AniList media list status."""
|
|
statuses = {
|
|
"Watching": "CURRENT",
|
|
"Planning": "PLANNING",
|
|
"Completed": "COMPLETED",
|
|
"Rewatching": "REPEATING",
|
|
"Paused": "PAUSED",
|
|
"Dropped": "DROPPED",
|
|
"Back": None,
|
|
}
|
|
choice = session.selector.choose("Add to which list?", list(statuses.keys()))
|
|
return statuses.get(choice) if choice else None
|
|
|
|
|
|
def display_anime_details(anime: AnilistBaseMediaDataSchema) -> None:
|
|
"""Renders a detailed view of an anime's information."""
|
|
from click import clear
|
|
|
|
from ...cli.utils.anilist import (
|
|
extract_next_airing_episode,
|
|
format_anilist_date_object,
|
|
format_list_data_with_comma,
|
|
format_number_with_commas,
|
|
)
|
|
|
|
clear()
|
|
|
|
title_eng = anime.get("title", {}).get("english", "N/A")
|
|
title_romaji = anime.get("title", {}).get("romaji", "N/A")
|
|
|
|
content = (
|
|
f"[bold cyan]English:[/] {title_eng}\n"
|
|
f"[bold cyan]Romaji:[/] {title_romaji}\n\n"
|
|
f"[bold]Status:[/] {anime.get('status', 'N/A')} "
|
|
f"[bold]Episodes:[/] {anime.get('episodes') or 'N/A'}\n"
|
|
f"[bold]Score:[/] {anime.get('averageScore', 0) / 10.0} / 10\n"
|
|
f"[bold]Popularity:[/] {format_number_with_commas(anime.get('popularity'))}\n\n"
|
|
f"[bold]Genres:[/] {format_list_data_with_comma([g for g in anime.get('genres', [])])}\n"
|
|
f"[bold]Tags:[/] {format_list_data_with_comma([t['name'] for t in anime.get('tags', [])[:5]])}\n\n"
|
|
f"[bold]Airing:[/] {extract_next_airing_episode(anime.get('nextAiringEpisode'))}\n"
|
|
f"[bold]Period:[/] {format_anilist_date_object(anime.get('startDate'))} to {format_anilist_date_object(anime.get('endDate'))}\n\n"
|
|
f"[bold underline]Description[/]\n{clean_html(anime.get('description', 'No description available.'))}"
|
|
)
|
|
|
|
rprint(Panel(content, title="Anime Details", border_style="magenta"))
|
|
Confirm.ask("Press Enter to return...", default=True, show_default=False)
|
|
|
|
|
|
def filter_by_quality(quality: str, stream_links: list, default=True):
|
|
"""(Moved from utils) Filters a list of streams by quality."""
|
|
for stream_link in stream_links:
|
|
q = float(quality)
|
|
try:
|
|
stream_q = float(stream_link.quality)
|
|
except (ValueError, TypeError):
|
|
continue
|
|
if q - 80 <= stream_q <= q + 80:
|
|
return stream_link
|
|
if stream_links and default:
|
|
return stream_links[0]
|
|
return None
|