mirror of
https://github.com/Benexl/FastAnime.git
synced 2026-01-04 08:47:14 -08:00
273 lines
10 KiB
Python
273 lines
10 KiB
Python
from rich.console import Console
|
|
|
|
from ....libs.api.types import MediaItem
|
|
from ....libs.api.params import ApiSearchParams, UserListParams
|
|
from ...utils.auth_utils import get_auth_status_indicator
|
|
from ...utils.feedback import create_feedback_manager, execute_with_feedback
|
|
from ..session import Context, session
|
|
from ..state import ControlFlow, MediaApiState, State
|
|
|
|
|
|
@session.menu
|
|
def results(ctx: Context, state: State) -> State | ControlFlow:
|
|
"""
|
|
Displays a paginated list of anime from a search or category query.
|
|
Allows the user to select an anime to view its actions or navigate pages.
|
|
"""
|
|
search_results = state.media_api.search_results
|
|
console = Console()
|
|
console.clear()
|
|
if not search_results or not search_results.media:
|
|
console.print(
|
|
"[bold yellow]No anime found for the given criteria.[/bold yellow]"
|
|
)
|
|
return ControlFlow.BACK
|
|
|
|
# --- Prepare choices and previews ---
|
|
anime_items = search_results.media
|
|
formatted_titles = [
|
|
_format_anime_choice(anime, ctx.config) for anime in anime_items
|
|
]
|
|
|
|
# Map formatted titles back to the original MediaItem objects
|
|
anime_map = dict(zip(formatted_titles, anime_items))
|
|
|
|
preview_command = None
|
|
if ctx.config.general.preview != "none":
|
|
# This function will start background jobs to cache preview data
|
|
from ...utils.previews import get_anime_preview
|
|
|
|
preview_command = get_anime_preview(anime_items, formatted_titles, ctx.config)
|
|
|
|
# --- Build Navigation and Final Choice List ---
|
|
choices = formatted_titles
|
|
page_info = search_results.page_info
|
|
|
|
# Add pagination controls if available with more descriptive text
|
|
if page_info.has_next_page:
|
|
choices.append(f"{'➡️ ' if ctx.config.general.icons else ''}Next Page (Page {page_info.current_page + 1})")
|
|
if page_info.current_page > 1:
|
|
choices.append(f"{'⬅️ ' if ctx.config.general.icons else ''}Previous Page (Page {page_info.current_page - 1})")
|
|
choices.append("Back")
|
|
|
|
# Create header with auth status and pagination info
|
|
auth_status, _ = get_auth_status_indicator(ctx.media_api, ctx.config.general.icons)
|
|
pagination_info = f"Page {page_info.current_page}"
|
|
if page_info.total > 0 and page_info.per_page > 0:
|
|
total_pages = (page_info.total + page_info.per_page - 1) // page_info.per_page
|
|
pagination_info += f" of ~{total_pages}"
|
|
|
|
header = f"Search Results ({len(anime_items)} anime) - {pagination_info}\n{auth_status}"
|
|
|
|
# --- Prompt User ---
|
|
choice_str = ctx.selector.choose(
|
|
prompt="Select Anime",
|
|
choices=choices,
|
|
preview=preview_command,
|
|
header=header,
|
|
)
|
|
|
|
if not choice_str:
|
|
return ControlFlow.EXIT
|
|
|
|
# --- Handle User Selection ---
|
|
if choice_str == "Back":
|
|
return ControlFlow.BACK
|
|
|
|
# Handle pagination - check for both old and new formats
|
|
if (choice_str == "Next Page" or choice_str == "Previous Page" or
|
|
choice_str.startswith("Next Page (") or choice_str.startswith("Previous Page (")):
|
|
page_delta = 1 if choice_str.startswith("Next Page") else -1
|
|
|
|
# Implement pagination logic
|
|
return _handle_pagination(ctx, state, page_delta)
|
|
|
|
# If an anime was selected, transition to the MEDIA_ACTIONS state
|
|
selected_anime = anime_map.get(choice_str)
|
|
if selected_anime:
|
|
return State(
|
|
menu_name="MEDIA_ACTIONS",
|
|
media_api=MediaApiState(
|
|
search_results=state.media_api.search_results, # Carry over the list
|
|
anime=selected_anime, # Set the newly selected item
|
|
),
|
|
# Persist provider state if it exists
|
|
provider=state.provider,
|
|
)
|
|
|
|
# Fallback
|
|
return ControlFlow.CONTINUE
|
|
|
|
|
|
def _format_anime_choice(anime: MediaItem, config) -> str:
|
|
"""Creates a display string for a single anime item for the selector."""
|
|
title = anime.title.english or anime.title.romaji
|
|
progress = "0"
|
|
if anime.user_status:
|
|
progress = str(anime.user_status.progress or 0)
|
|
|
|
episodes_total = str(anime.episodes or "??")
|
|
display_title = f"{title} ({progress} of {episodes_total})"
|
|
|
|
# Add a visual indicator for new episodes if applicable
|
|
if (
|
|
anime.status == "RELEASING"
|
|
and anime.next_airing
|
|
and anime.user_status
|
|
and anime.user_status.status == "CURRENT"
|
|
):
|
|
last_aired = anime.next_airing.episode - 1
|
|
unwatched = last_aired - (anime.user_status.progress or 0)
|
|
if unwatched > 0:
|
|
icon = "🔹" if config.general.icons else "!"
|
|
display_title += f" {icon}{unwatched} new{icon}"
|
|
|
|
return display_title
|
|
|
|
|
|
def _handle_pagination(ctx: Context, state: State, page_delta: int) -> State | ControlFlow:
|
|
"""
|
|
Handle pagination by fetching the next or previous page of results.
|
|
|
|
Args:
|
|
ctx: The application context
|
|
state: Current state containing search results and original parameters
|
|
page_delta: +1 for next page, -1 for previous page
|
|
|
|
Returns:
|
|
New State with updated search results or ControlFlow.CONTINUE on error
|
|
"""
|
|
feedback = create_feedback_manager(ctx.config.general.icons)
|
|
|
|
if not state.media_api.search_results:
|
|
feedback.error("No search results available for pagination")
|
|
return ControlFlow.CONTINUE
|
|
|
|
current_page = state.media_api.search_results.page_info.current_page
|
|
new_page = current_page + page_delta
|
|
|
|
# Validate page bounds
|
|
if new_page < 1:
|
|
feedback.warning("Already at the first page")
|
|
return ControlFlow.CONTINUE
|
|
|
|
if page_delta > 0 and not state.media_api.search_results.page_info.has_next_page:
|
|
feedback.warning("No more pages available")
|
|
return ControlFlow.CONTINUE
|
|
|
|
# Determine which type of search to perform based on stored parameters
|
|
if state.media_api.original_api_params:
|
|
# Media search (trending, popular, search, etc.)
|
|
return _fetch_media_page(ctx, state, new_page, feedback)
|
|
elif state.media_api.original_user_list_params:
|
|
# User list search (watching, completed, etc.)
|
|
return _fetch_user_list_page(ctx, state, new_page, feedback)
|
|
else:
|
|
feedback.error("No original search parameters found for pagination")
|
|
return ControlFlow.CONTINUE
|
|
|
|
|
|
def _fetch_media_page(ctx: Context, state: State, page: int, feedback) -> State | ControlFlow:
|
|
"""Fetch a specific page for media search results."""
|
|
original_params = state.media_api.original_api_params
|
|
if not original_params:
|
|
feedback.error("No original API parameters found")
|
|
return ControlFlow.CONTINUE
|
|
|
|
# Create new parameters with updated page number
|
|
new_params = ApiSearchParams(
|
|
query=original_params.query,
|
|
page=page,
|
|
per_page=original_params.per_page,
|
|
sort=original_params.sort,
|
|
id_in=original_params.id_in,
|
|
genre_in=original_params.genre_in,
|
|
genre_not_in=original_params.genre_not_in,
|
|
tag_in=original_params.tag_in,
|
|
tag_not_in=original_params.tag_not_in,
|
|
status_in=original_params.status_in,
|
|
status=original_params.status,
|
|
status_not_in=original_params.status_not_in,
|
|
popularity_greater=original_params.popularity_greater,
|
|
popularity_lesser=original_params.popularity_lesser,
|
|
averageScore_greater=original_params.averageScore_greater,
|
|
averageScore_lesser=original_params.averageScore_lesser,
|
|
seasonYear=original_params.seasonYear,
|
|
season=original_params.season,
|
|
startDate_greater=original_params.startDate_greater,
|
|
startDate_lesser=original_params.startDate_lesser,
|
|
startDate=original_params.startDate,
|
|
endDate_greater=original_params.endDate_greater,
|
|
endDate_lesser=original_params.endDate_lesser,
|
|
format_in=original_params.format_in,
|
|
type=original_params.type,
|
|
on_list=original_params.on_list,
|
|
)
|
|
|
|
def fetch_data():
|
|
return ctx.media_api.search_media(new_params)
|
|
|
|
success, result = execute_with_feedback(
|
|
fetch_data,
|
|
feedback,
|
|
f"fetch page {page}",
|
|
loading_msg=f"Loading page {page}",
|
|
success_msg=f"Page {page} loaded successfully",
|
|
show_loading=False,
|
|
)
|
|
|
|
if not success or not result:
|
|
return ControlFlow.CONTINUE
|
|
|
|
# Return new state with updated results
|
|
return State(
|
|
menu_name="RESULTS",
|
|
media_api=MediaApiState(
|
|
search_results=result,
|
|
original_api_params=original_params, # Keep original params for further pagination
|
|
original_user_list_params=state.media_api.original_user_list_params,
|
|
),
|
|
provider=state.provider, # Preserve provider state if it exists
|
|
)
|
|
|
|
|
|
def _fetch_user_list_page(ctx: Context, state: State, page: int, feedback) -> State | ControlFlow:
|
|
"""Fetch a specific page for user list results."""
|
|
original_params = state.media_api.original_user_list_params
|
|
if not original_params:
|
|
feedback.error("No original user list parameters found")
|
|
return ControlFlow.CONTINUE
|
|
|
|
# Create new parameters with updated page number
|
|
new_params = UserListParams(
|
|
status=original_params.status,
|
|
page=page,
|
|
per_page=original_params.per_page,
|
|
)
|
|
|
|
def fetch_data():
|
|
return ctx.media_api.fetch_user_list(new_params)
|
|
|
|
success, result = execute_with_feedback(
|
|
fetch_data,
|
|
feedback,
|
|
f"fetch page {page} of {original_params.status.lower()} list",
|
|
loading_msg=f"Loading page {page}",
|
|
success_msg=f"Page {page} loaded successfully",
|
|
show_loading=False,
|
|
)
|
|
|
|
if not success or not result:
|
|
return ControlFlow.CONTINUE
|
|
|
|
# Return new state with updated results
|
|
return State(
|
|
menu_name="RESULTS",
|
|
media_api=MediaApiState(
|
|
search_results=result,
|
|
original_api_params=state.media_api.original_api_params,
|
|
original_user_list_params=original_params, # Keep original params for further pagination
|
|
),
|
|
provider=state.provider, # Preserve provider state if it exists
|
|
)
|