mirror of
https://github.com/Benexl/FastAnime.git
synced 2025-12-12 15:50:01 -08:00
feat: results menu
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ class UserMediaListSearchParams:
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class UpdateListEntryParams:
|
||||
class UpdateUserMediaListEntryParams:
|
||||
media_id: int
|
||||
status: Optional[UserMediaListStatus] = None
|
||||
progress: Optional[str] = None
|
||||
|
||||
Reference in New Issue
Block a user