feat: pagination

This commit is contained in:
Benexl
2025-07-14 22:44:27 +03:00
parent c882691412
commit be4cc58e47
3 changed files with 214 additions and 39 deletions

View File

@@ -10,7 +10,7 @@ from ...utils.auth_utils import format_auth_menu_header, check_authentication_re
from ..session import Context, session
from ..state import ControlFlow, MediaApiState, State
MenuAction = Callable[[], Tuple[str, MediaSearchResult | None]]
MenuAction = Callable[[], Tuple[str, MediaSearchResult | None, ApiSearchParams | None, UserListParams | None]]
@session.menu
@@ -59,13 +59,13 @@ def main(ctx: Context, state: State) -> State | ControlFlow:
ctx, "REPEATING"
),
# --- Local Watch History ---
f"{'📖 ' if icons else ''}Local Watch History": lambda: ("WATCH_HISTORY", None),
f"{'📖 ' if icons else ''}Local Watch History": lambda: ("WATCH_HISTORY", None, None, None),
# --- Authentication and Account Management ---
f"{'🔐 ' if icons else ''}Authentication": lambda: ("AUTH", None),
f"{'🔐 ' if icons else ''}Authentication": lambda: ("AUTH", None, None, None),
# --- Control Flow and Utility Options ---
f"{'🔧 ' if icons else ''}Session Management": lambda: ("SESSION_MANAGEMENT", None),
f"{'📝 ' if icons else ''}Edit Config": lambda: ("RELOAD_CONFIG", None),
f"{'' if icons else ''}Exit": lambda: ("EXIT", None),
f"{'🔧 ' if icons else ''}Session Management": lambda: ("SESSION_MANAGEMENT", None, None, None),
f"{'📝 ' if icons else ''}Edit Config": lambda: ("RELOAD_CONFIG", None, None, None),
f"{'' if icons else ''}Exit": lambda: ("EXIT", None, None, None),
}
choice_str = ctx.selector.choose(
@@ -80,7 +80,7 @@ def main(ctx: Context, state: State) -> State | ControlFlow:
# --- Action Handling ---
selected_action = options[choice_str]
next_menu_name, result_data = selected_action()
next_menu_name, result_data, api_params, user_list_params = selected_action()
if next_menu_name == "EXIT":
return ControlFlow.EXIT
@@ -105,7 +105,11 @@ def main(ctx: Context, state: State) -> State | ControlFlow:
# On success, transition to the RESULTS menu state.
return State(
menu_name="RESULTS",
media_api=MediaApiState(search_results=result_data),
media_api=MediaApiState(
search_results=result_data,
original_api_params=api_params,
original_user_list_params=user_list_params,
),
)
@@ -117,12 +121,13 @@ def _create_media_list_action(
def action():
feedback = create_feedback_manager(ctx.config.general.icons)
# Create the search parameters
search_params = ApiSearchParams(
sort=sort, per_page=ctx.config.anilist.per_page, status=status
)
def fetch_data():
return ctx.media_api.search_media(
ApiSearchParams(
sort=sort, per_page=ctx.config.anilist.per_page, status=status
)
)
return ctx.media_api.search_media(search_params)
success, result = execute_with_feedback(
fetch_data,
@@ -132,7 +137,8 @@ def _create_media_list_action(
success_msg="Anime list loaded successfully",
)
return "RESULTS" if success else "CONTINUE", result
# Return the search parameters along with the result for pagination
return ("RESULTS", result, search_params, None) if success else ("CONTINUE", None, None, None)
return action
@@ -141,13 +147,14 @@ def _create_random_media_list(ctx: Context) -> MenuAction:
def action():
feedback = create_feedback_manager(ctx.config.general.icons)
# Create the search parameters
search_params = ApiSearchParams(
id_in=random.sample(range(1, 160000), k=50),
per_page=ctx.config.anilist.per_page,
)
def fetch_data():
return ctx.media_api.search_media(
ApiSearchParams(
id_in=random.sample(range(1, 160000), k=50),
per_page=ctx.config.anilist.per_page,
)
)
return ctx.media_api.search_media(search_params)
success, result = execute_with_feedback(
fetch_data,
@@ -157,7 +164,8 @@ def _create_random_media_list(ctx: Context) -> MenuAction:
success_msg="Random anime loaded successfully",
)
return "RESULTS" if success else "CONTINUE", result
# Return the search parameters along with the result for pagination
return ("RESULTS", result, search_params, None) if success else ("CONTINUE", None, None, None)
return action
@@ -168,10 +176,13 @@ def _create_search_media_list(ctx: Context) -> MenuAction:
query = ctx.selector.ask("Search for Anime")
if not query:
return "CONTINUE", None
return "CONTINUE", None, None, None
# Create the search parameters
search_params = ApiSearchParams(query=query)
def fetch_data():
return ctx.media_api.search_media(ApiSearchParams(query=query))
return ctx.media_api.search_media(search_params)
success, result = execute_with_feedback(
fetch_data,
@@ -181,7 +192,8 @@ def _create_search_media_list(ctx: Context) -> MenuAction:
success_msg=f"Search results for '{query}' loaded successfully",
)
return "RESULTS" if success else "CONTINUE", result
# Return the search parameters along with the result for pagination
return ("RESULTS", result, search_params, None) if success else ("CONTINUE", None, None, None)
return action
@@ -196,12 +208,13 @@ def _create_user_list_action(ctx: Context, status: UserListStatusType) -> MenuAc
if not check_authentication_required(
ctx.media_api, feedback, f"view your {status.lower()} list"
):
return "CONTINUE", None
return "CONTINUE", None, None, None
# Create the user list parameters
user_list_params = UserListParams(status=status, per_page=ctx.config.anilist.per_page)
def fetch_data():
return ctx.media_api.fetch_user_list(
UserListParams(status=status, per_page=ctx.config.anilist.per_page)
)
return ctx.media_api.fetch_user_list(user_list_params)
success, result = execute_with_feedback(
fetch_data,
@@ -211,6 +224,7 @@ def _create_user_list_action(ctx: Context, status: UserListStatusType) -> MenuAc
success_msg=f"Your {status.lower()} list loaded successfully",
)
return "RESULTS" if success else "CONTINUE", result
# Return the user list parameters along with the result for pagination
return ("RESULTS", result, None, user_list_params) if success else ("CONTINUE", None, None, None)
return action

View File

@@ -1,7 +1,9 @@
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
@@ -41,16 +43,21 @@ def results(ctx: Context, state: State) -> State | ControlFlow:
choices = formatted_titles
page_info = search_results.page_info
# Add pagination controls if available
# Add pagination controls if available with more descriptive text
if page_info.has_next_page:
choices.append("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("Previous Page")
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
# Create header with auth status and pagination info
auth_status, _ = get_auth_status_indicator(ctx.media_api, ctx.config.general.icons)
header = f"Search Results ({len(anime_items)} anime)\n{auth_status}"
pagination_info = f"Page {page_info.current_page}"
if page_info.total > 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(
@@ -67,11 +74,13 @@ def results(ctx: Context, state: State) -> State | ControlFlow:
if choice_str == "Back":
return ControlFlow.BACK
if choice_str == "Next Page" or choice_str == "Previous Page":
page_delta = 1 if choice_str == "Next Page" else -1
# TODO: implement next page logic
return ControlFlow.CONTINUE
# 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)
@@ -114,3 +123,150 @@ def _format_anime_choice(anime: MediaItem, config) -> str:
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
)

View File

@@ -9,6 +9,7 @@ from ...libs.api.types import (
MediaStatus,
UserListStatusType,
)
from ...libs.api.params import ApiSearchParams, UserListParams # Add this import
from ...libs.players.types import PlayerResult
from ...libs.providers.anime.types import Anime, SearchResults, Server
@@ -76,6 +77,10 @@ class MediaApiState(BaseModel):
user_media_status: Optional[UserListStatusType] = None
media_status: Optional[MediaStatus] = None
anime: Optional[MediaItem] = None
# Add pagination support: store original search parameters to enable page navigation
original_api_params: Optional[ApiSearchParams] = None
original_user_list_params: Optional[UserListParams] = None
model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True)