Files
FastAnime/fastanime/cli/interactive/menu/media/media_characters.py
Benexl e8849940e1 feat: Add media airing schedule and character selection features
- Implemented media airing schedule functionality in `media_airing_schedule.py` to fetch and display upcoming episodes with air dates and countdown timers.
- Created character selection feature in `media_characters.py` to fetch and display a list of characters, showing details upon selection.
- Updated state management to include new menu options for characters and airing schedules.
- Enhanced preview functionality to support character and airing schedule previews.
- Added necessary API methods and data models for handling character and airing schedule data.
2025-07-28 13:37:27 +03:00

174 lines
5.7 KiB
Python

import re
from typing import Dict, List, Optional, Union
from .....libs.media_api.types import Character, CharacterSearchResult
from ...session import Context, session
from ...state import InternalDirective, State
@session.menu
def media_characters(ctx: Context, state: State) -> Union[State, InternalDirective]:
"""
Fetches and displays a list of characters for the user to select from.
Shows character details upon selection or in the preview pane.
"""
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
from rich.text import Text
feedback = ctx.feedback
selector = ctx.selector
console = Console()
config = ctx.config
media_item = state.media_api.media_item
if not media_item:
feedback.error("Media item is not in state.")
return InternalDirective.BACK
from .....libs.media_api.params import MediaCharactersParams
loading_message = (
f"Fetching characters for {media_item.title.english or media_item.title.romaji}..."
)
characters_result: Optional[CharacterSearchResult] = None
with feedback.progress(loading_message):
characters_result = ctx.media_api.get_characters_of(
MediaCharactersParams(id=media_item.id)
)
if not characters_result or not characters_result.characters:
feedback.error("No characters found for this anime.")
return InternalDirective.BACK
characters = characters_result.characters
choice_map: Dict[str, Character] = {}
# Create display names for characters
for character in characters:
display_name = character.name.full or character.name.first or "Unknown"
if character.gender:
display_name += f" ({character.gender})"
if character.age:
display_name += f" - Age {character.age}"
choice_map[display_name] = character
choices = list(choice_map.keys()) + ["Back"]
preview_command = None
if config.general.preview != "none":
from ....utils.preview import create_preview_context
with create_preview_context() as preview_ctx:
preview_command = preview_ctx.get_character_preview(choice_map, ctx.config)
while True:
chosen_title = selector.choose(
prompt="Select a character to view details",
choices=choices,
preview=preview_command,
)
if not chosen_title or chosen_title == "Back":
return InternalDirective.BACK
selected_character = choice_map[chosen_title]
console.clear()
# Display character details
anime_title = media_item.title.english or media_item.title.romaji or "Unknown"
_display_character_details(console, selected_character, anime_title)
selector.ask("\nPress Enter to return to the character list...")
def _display_character_details(console, character: Character, anime_title: str):
"""Display detailed character information in a formatted panel."""
from rich.columns import Columns
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
from rich.text import Text
import re
# Character name panel
name_text = Text()
if character.name.full:
name_text.append(character.name.full, style="bold cyan")
elif character.name.first:
full_name = character.name.first
if character.name.last:
full_name += f" {character.name.last}"
name_text.append(full_name, style="bold cyan")
else:
name_text.append("Unknown Character", style="bold dim")
if character.name.native:
name_text.append(f"\n{character.name.native}", style="green")
name_panel = Panel(
name_text,
title=f"[bold]Character from {anime_title}[/bold]",
border_style="cyan",
expand=False,
)
# Basic info table
info_table = Table(show_header=False, box=None, padding=(0, 1))
info_table.add_column("Field", style="bold yellow", min_width=12)
info_table.add_column("Value", style="white")
if character.gender:
info_table.add_row("Gender", character.gender)
if character.age:
info_table.add_row("Age", str(character.age))
if character.blood_type:
info_table.add_row("Blood Type", character.blood_type)
if character.favourites:
info_table.add_row("Favorites", f"{character.favourites:,}")
if character.date_of_birth:
birth_date = character.date_of_birth.strftime("%B %d, %Y")
info_table.add_row("Birthday", birth_date)
info_panel = Panel(
info_table,
title="[bold]Basic Information[/bold]",
border_style="blue",
)
# Description panel
description = character.description or "No description available"
# Clean HTML tags from description
clean_description = re.sub(r"<[^>]+>", "", description)
# Replace common HTML entities
clean_description = (
clean_description.replace("&quot;", '"')
.replace("&amp;", "&")
.replace("&lt;", "<")
.replace("&gt;", ">")
.replace("&#039;", "'")
.replace("&nbsp;", " ")
)
# Limit length for display
if len(clean_description) > 500:
clean_description = clean_description[:497] + "..."
description_panel = Panel(
Text(clean_description, style="white"),
title="[bold]Description[/bold]",
border_style="green",
)
# Display everything
console.print(name_panel)
console.print()
# Show panels side by side if there's basic info
if info_table.rows:
console.print(Columns([info_panel, description_panel], equal=True, expand=True))
else:
console.print(description_panel)