feat: results menu

This commit is contained in:
Benexl
2025-07-24 01:04:45 +03:00
parent afe1cb68f6
commit 83933f7a63
8 changed files with 92 additions and 82 deletions

View File

@@ -2,20 +2,26 @@ from typing import Callable, Dict
from rich.console import Console
from ....libs.api.params import UpdateListEntryParams
from ....libs.api.types import MediaItem
from ....libs.api.params import UpdateUserMediaListEntryParams
from ....libs.api.types import MediaItem, UserMediaListStatus
from ....libs.players.params import PlayerParams
from ..session import Context, session
from ..state import InternalDirective, ProviderState, State
from ..state import InternalDirective, MenuName, State
MenuAction = Callable[[], State | InternalDirective]
@session.menu
def media_actions(ctx: Context, state: State) -> State | InternalDirective:
feedback = ctx.services.feedback
icons = ctx.config.general.icons
anime = state.media_api.anime
anime_title = anime.title.english or anime.title.romaji if anime else "Unknown"
media_item = state.media_api.media_item
if not media_item:
feedback.error("Media item is not in state")
return InternalDirective.BACK
# TODO: Add 'Recommendations' and 'Relations' here later.
# TODO: Add media list management
@@ -23,31 +29,26 @@ def media_actions(ctx: Context, state: State) -> State | InternalDirective:
options: Dict[str, MenuAction] = {
f"{'▶️ ' if icons else ''}Stream": _stream(ctx, state),
f"{'📼 ' if icons else ''}Watch Trailer": _watch_trailer(ctx, state),
f"{' ' if icons else ''}Add/Update List": _add_to_list(ctx, state),
f"{' ' if icons else ''}Add/Update List": _manage_user_media_list(ctx, state),
f"{'' if icons else ''}Score Anime": _score_anime(ctx, state),
f"{' ' if icons else ''}View Info": _view_info(ctx, state),
f"{'🔙 ' if icons else ''}Back to Results": lambda: InternalDirective.BACK,
}
choice_str = ctx.selector.choose(
choice = ctx.selector.choose(
prompt="Select Action",
choices=list(options.keys()),
)
if choice_str and choice_str in options:
return options[choice_str]()
if choice and choice in options:
return options[choice]()
return InternalDirective.BACK
# --- Action Implementations ---
def _stream(ctx: Context, state: State) -> MenuAction:
def action():
return State(
menu_name="PROVIDER_SEARCH",
media_api=state.media_api, # Carry over the existing api state
provider=ProviderState(), # Initialize a fresh provider state
)
return State(menu_name=MenuName.PROVIDER_SEARCH, media_api=state.media_api)
return action
@@ -55,16 +56,18 @@ def _stream(ctx: Context, state: State) -> MenuAction:
def _watch_trailer(ctx: Context, state: State) -> MenuAction:
def action():
feedback = ctx.services.feedback
anime = state.media_api.anime
if not anime:
media_item = state.media_api.media_item
if not media_item:
return InternalDirective.RELOAD
if not anime.trailer or not anime.trailer.id:
if not media_item.trailer or not media_item.trailer.id:
feedback.warning(
"No trailer available for this anime",
"This anime doesn't have a trailer link in the database",
)
else:
trailer_url = f"https://www.youtube.com/watch?v={anime.trailer.id}"
trailer_url = f"https://www.youtube.com/watch?v={media_item.trailer.id}"
ctx.player.play(PlayerParams(url=trailer_url, title=""))
@@ -73,31 +76,35 @@ def _watch_trailer(ctx: Context, state: State) -> MenuAction:
return action
def _add_to_list(ctx: Context, state: State) -> MenuAction:
def _manage_user_media_list(ctx: Context, state: State) -> MenuAction:
def action():
feedback = ctx.services.feedback
anime = state.media_api.anime
if not anime:
media_item = state.media_api.media_item
if not media_item:
return InternalDirective.RELOAD
if not ctx.media_api.is_authenticated():
feedback.warning(
"You are not authenticated",
)
return InternalDirective.RELOAD
choices = [
"watching",
"planning",
"completed",
"dropped",
"paused",
"repeating",
]
status = ctx.selector.choose("Select list status:", choices=choices)
status = ctx.selector.choose(
"Select list status:", choices=[t.value for t in UserMediaListStatus]
)
if status:
_update_user_list(
ctx,
anime,
UpdateListEntryParams(media_id=anime.id, status=status), # pyright:ignore
feedback,
# local
ctx.services.media_registry.update_media_index_entry(
media_id=media_item.id,
media_item=media_item,
status=UserMediaListStatus(status),
)
# remote
ctx.media_api.update_list_entry(
UpdateUserMediaListEntryParams(
media_item.id, status=UserMediaListStatus(status)
)
)
return InternalDirective.RELOAD
@@ -107,11 +114,11 @@ def _add_to_list(ctx: Context, state: State) -> MenuAction:
def _score_anime(ctx: Context, state: State) -> MenuAction:
def action():
feedback = ctx.services.feedback
anime = state.media_api.anime
if not anime:
media_item = state.media_api.media_item
if not media_item:
return InternalDirective.RELOAD
# Check authentication before proceeding
if not ctx.media_api.is_authenticated():
return InternalDirective.RELOAD
@@ -120,11 +127,13 @@ def _score_anime(ctx: Context, state: State) -> MenuAction:
score = float(score_str) if score_str else 0.0
if not 0.0 <= score <= 10.0:
raise ValueError("Score out of range.")
_update_user_list(
ctx,
anime,
UpdateListEntryParams(media_id=anime.id, score=score),
feedback,
# local
ctx.services.media_registry.update_media_index_entry(
media_id=media_item.id, media_item=media_item, score=score
)
# remote
ctx.media_api.update_list_entry(
UpdateUserMediaListEntryParams(media_id=media_item.id, score=score)
)
except (ValueError, TypeError):
feedback.error(
@@ -137,26 +146,29 @@ def _score_anime(ctx: Context, state: State) -> MenuAction:
def _view_info(ctx: Context, state: State) -> MenuAction:
def action():
anime = state.media_api.anime
if not anime:
media_item = state.media_api.media_item
if not media_item:
return InternalDirective.RELOAD
# TODO: Make this nice and include all other media item fields
from rich import box
from rich.panel import Panel
from rich.text import Text
from ...utils import image
# TODO: make this look nicer plus add other fields
console = Console()
title = Text(anime.title.english or anime.title.romaji or "", style="bold cyan")
description = Text(anime.description or "NO description")
genres = Text(f"Genres: {', '.join([v.value for v in anime.genres])}")
title = Text(
media_item.title.english or media_item.title.romaji or "", style="bold cyan"
)
description = Text(media_item.description or "NO description")
genres = Text(f"Genres: {', '.join([v.value for v in media_item.genres])}")
panel_content = f"{genres}\n\n{description}"
console.clear()
if cover_image := anime.cover_image:
if cover_image := media_item.cover_image:
image.render_image(cover_image.large)
console.print(Panel(panel_content, title=title, box=box.ROUNDED, expand=True))
@@ -164,12 +176,3 @@ def _view_info(ctx: Context, state: State) -> MenuAction:
return InternalDirective.RELOAD
return action
def _update_user_list(
ctx: Context, anime: MediaItem, params: UpdateListEntryParams, feedback
):
if ctx.media_api.is_authenticated():
return InternalDirective.RELOAD
ctx.media_api.update_list_entry(params)

View File

@@ -19,20 +19,25 @@ def results(ctx: Context, state: State) -> State | InternalDirective:
feedback.info("No anime found for the given criteria")
return InternalDirective.BACK
_formatted_titles = [_format_title(ctx, anime) for anime in search_result.values()]
search_result_dict = {
_format_title(ctx, media_item): media_item
for media_item in search_result.values()
}
preview_command = None
if ctx.config.general.preview != "none":
from ...utils.previews import get_anime_preview
preview_command = get_anime_preview(
list(search_result.values()), _formatted_titles, ctx.config
list(search_result_dict.values()),
list(search_result_dict.keys()),
ctx.config,
)
choices: Dict[str, Callable[[], Union[int, State, InternalDirective]]] = dict(
zip(_formatted_titles, [lambda: item for item in search_result.keys()])
)
choices: Dict[str, Callable[[], Union[int, State, InternalDirective]]] = {
title: lambda media_id=item.id: media_id
for title, item in search_result_dict.items()
}
if page_info:
if page_info.has_next_page:
choices.update(
@@ -184,7 +189,5 @@ def _handle_pagination(
),
)
# print(new_search_params)
# print(result)
feedback.warning("Failed to load page")
return InternalDirective.RELOAD

View File

@@ -17,7 +17,7 @@ from rich.panel import Panel
from rich.table import Table
from rich.text import Text
from ....libs.api.params import UpdateListEntryParams, UserListParams
from ....libs.api.params import UpdateUserMediaListEntryParams, UserListParams
from ....libs.api.types import MediaItem, MediaSearchResult, UserListItem
from ...utils.feedback import create_feedback_manager, execute_with_feedback
from ..session import Context, session
@@ -451,7 +451,7 @@ def _edit_anime_progress(
# Update via API
def update_progress():
return ctx.media_api.update_list_entry(
UpdateListEntryParams(media_id=anime.id, progress=new_progress)
UpdateUserMediaListEntryParams(media_id=anime.id, progress=new_progress)
)
success, _ = execute_with_feedback(
@@ -509,7 +509,7 @@ def _edit_anime_rating(
# Update via API
def update_score():
return ctx.media_api.update_list_entry(
UpdateListEntryParams(media_id=anime.id, score=new_score)
UpdateUserMediaListEntryParams(media_id=anime.id, score=new_score)
)
success, _ = execute_with_feedback(
@@ -571,7 +571,7 @@ def _edit_anime_status(
# Update via API
def update_status():
return ctx.media_api.update_list_entry(
UpdateListEntryParams(media_id=anime.id, status=new_status)
UpdateUserMediaListEntryParams(media_id=anime.id, status=new_status)
)
success, _ = execute_with_feedback(

View File

@@ -3,7 +3,7 @@ from typing import Optional
from ....core.config.model import AppConfig
from ....libs.api.base import BaseApiClient
from ....libs.api.params import UpdateListEntryParams
from ....libs.api.params import UpdateUserMediaListEntryParams
from ....libs.api.types import MediaItem, UserMediaListStatus
from ....libs.players.types import PlayerResult
from ..registry import MediaRegistryService
@@ -37,7 +37,7 @@ class WatchHistoryService:
if self.media_api and self.media_api.is_authenticated():
self.media_api.update_list_entry(
UpdateListEntryParams(
UpdateUserMediaListEntryParams(
media_id=media_item.id,
progress=episode,
status=status,
@@ -63,7 +63,7 @@ class WatchHistoryService:
if self.media_api and self.media_api.is_authenticated():
self.media_api.update_list_entry(
UpdateListEntryParams(
UpdateUserMediaListEntryParams(
media_id=media_item.id,
status=status,
score=score,

View File

@@ -11,7 +11,7 @@ from ....core.utils.graphql import (
from ..base import (
BaseApiClient,
MediaSearchParams,
UpdateListEntryParams,
UpdateUserMediaListEntryParams,
UserMediaListSearchParams,
)
from ..types import MediaSearchResult, UserMediaListStatus, UserProfile
@@ -155,7 +155,7 @@ class AniListApi(BaseApiClient):
)
return mapper.to_generic_user_list_result(response.json()) if response else None
def update_list_entry(self, params: UpdateListEntryParams) -> bool:
def update_list_entry(self, params: UpdateUserMediaListEntryParams) -> bool:
if not self.token:
return False
score_raw = int(params.score * 10) if params.score is not None else None

View File

@@ -4,7 +4,11 @@ from typing import Any, Optional
from httpx import Client
from ...core.config import AnilistConfig
from .params import MediaSearchParams, UpdateListEntryParams, UserMediaListSearchParams
from .params import (
MediaSearchParams,
UpdateUserMediaListEntryParams,
UserMediaListSearchParams,
)
from .types import MediaSearchResult, UserProfile
@@ -41,7 +45,7 @@ class BaseApiClient(abc.ABC):
pass
@abc.abstractmethod
def update_list_entry(self, params: UpdateListEntryParams) -> bool:
def update_list_entry(self, params: UpdateUserMediaListEntryParams) -> bool:
pass
@abc.abstractmethod

View File

@@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, List, Optional
from ..base import (
BaseApiClient,
MediaSearchParams,
UpdateListEntryParams,
UpdateUserMediaListEntryParams,
UserMediaListSearchParams,
)
from ..types import MediaItem, MediaSearchResult, UserProfile
@@ -93,7 +93,7 @@ class JikanApi(BaseApiClient):
logger.warning("Jikan API does not support fetching user lists.")
return None
def update_list_entry(self, params: UpdateListEntryParams) -> bool:
def update_list_entry(self, params: UpdateUserMediaListEntryParams) -> bool:
logger.warning("Jikan API does not support updating list entries.")
return False

View File

@@ -76,7 +76,7 @@ class UserMediaListSearchParams:
@dataclass(frozen=True)
class UpdateListEntryParams:
class UpdateUserMediaListEntryParams:
media_id: int
status: Optional[UserMediaListStatus] = None
progress: Optional[str] = None