From 83933f7a63f06bb7e81221fe6d57012d054459b9 Mon Sep 17 00:00:00 2001 From: Benexl Date: Thu, 24 Jul 2025 01:04:45 +0300 Subject: [PATCH] feat: results menu --- .../cli/interactive/menus/media_actions.py | 123 +++++++++--------- fastanime/cli/interactive/menus/results.py | 19 +-- .../cli/interactive/menus/user_media_list.py | 8 +- .../cli/services/watch_history/service.py | 6 +- fastanime/libs/api/anilist/api.py | 4 +- fastanime/libs/api/base.py | 8 +- fastanime/libs/api/jikan/api.py | 4 +- fastanime/libs/api/params.py | 2 +- 8 files changed, 92 insertions(+), 82 deletions(-) diff --git a/fastanime/cli/interactive/menus/media_actions.py b/fastanime/cli/interactive/menus/media_actions.py index 99bd11f..6205612 100644 --- a/fastanime/cli/interactive/menus/media_actions.py +++ b/fastanime/cli/interactive/menus/media_actions.py @@ -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) diff --git a/fastanime/cli/interactive/menus/results.py b/fastanime/cli/interactive/menus/results.py index 42fd4f6..4491346 100644 --- a/fastanime/cli/interactive/menus/results.py +++ b/fastanime/cli/interactive/menus/results.py @@ -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 diff --git a/fastanime/cli/interactive/menus/user_media_list.py b/fastanime/cli/interactive/menus/user_media_list.py index f8782e6..9b59b1d 100644 --- a/fastanime/cli/interactive/menus/user_media_list.py +++ b/fastanime/cli/interactive/menus/user_media_list.py @@ -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( diff --git a/fastanime/cli/services/watch_history/service.py b/fastanime/cli/services/watch_history/service.py index e4dc6a7..923c71b 100644 --- a/fastanime/cli/services/watch_history/service.py +++ b/fastanime/cli/services/watch_history/service.py @@ -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, diff --git a/fastanime/libs/api/anilist/api.py b/fastanime/libs/api/anilist/api.py index 47a3bcd..663ce66 100644 --- a/fastanime/libs/api/anilist/api.py +++ b/fastanime/libs/api/anilist/api.py @@ -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 diff --git a/fastanime/libs/api/base.py b/fastanime/libs/api/base.py index c268da8..e4f0aed 100644 --- a/fastanime/libs/api/base.py +++ b/fastanime/libs/api/base.py @@ -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 diff --git a/fastanime/libs/api/jikan/api.py b/fastanime/libs/api/jikan/api.py index 96238a2..9cff22c 100644 --- a/fastanime/libs/api/jikan/api.py +++ b/fastanime/libs/api/jikan/api.py @@ -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 diff --git a/fastanime/libs/api/params.py b/fastanime/libs/api/params.py index 576c186..dfb64c0 100644 --- a/fastanime/libs/api/params.py +++ b/fastanime/libs/api/params.py @@ -76,7 +76,7 @@ class UserMediaListSearchParams: @dataclass(frozen=True) -class UpdateListEntryParams: +class UpdateUserMediaListEntryParams: media_id: int status: Optional[UserMediaListStatus] = None progress: Optional[str] = None