Files
FastAnime/fastanime/cli/interactive/menu/media/media_actions.py

280 lines
8.9 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from typing import Callable, Dict
from rich.console import Console
from .....libs.media_api.params import (
MediaRecommendationParams,
MediaRelationsParams,
UpdateUserMediaListEntryParams,
)
from .....libs.media_api.types import UserMediaListStatus
from .....libs.player.params import PlayerParams
from ...session import Context, session
from ...state import InternalDirective, MediaApiState, MenuName, State
MenuAction = Callable[[], State | InternalDirective]
@session.menu
def media_actions(ctx: Context, state: State) -> State | InternalDirective:
feedback = ctx.service.feedback
icons = ctx.config.general.icons
media_item = state.media_api.media_item
if not media_item:
feedback.error("Media item is not in state")
return InternalDirective.BACK
# TODO: Add media list management
# TODO: cross reference for none implemented features
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 ''}Recommendations": _view_recommendations(ctx, state),
f"{'🔄 ' if icons else ''}Related Anime": _view_relations(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 = ctx.selector.choose(
prompt="Select Action",
choices=list(options.keys()),
)
if choice and choice in options:
return options[choice]()
return InternalDirective.BACK
def _stream(ctx: Context, state: State) -> MenuAction:
def action():
return State(menu_name=MenuName.PROVIDER_SEARCH, media_api=state.media_api)
return action
def _watch_trailer(ctx: Context, state: State) -> MenuAction:
def action():
feedback = ctx.service.feedback
media_item = state.media_api.media_item
if not media_item:
return InternalDirective.RELOAD
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={media_item.trailer.id}"
ctx.player.play(PlayerParams(url=trailer_url, title=""))
return InternalDirective.RELOAD
return action
def _manage_user_media_list(ctx: Context, state: State) -> MenuAction:
def action():
feedback = ctx.service.feedback
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
status = ctx.selector.choose(
"Select list status:", choices=[t.value for t in UserMediaListStatus]
)
if status:
# local
ctx.service.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
return action
def _score_anime(ctx: Context, state: State) -> MenuAction:
def action():
feedback = ctx.service.feedback
media_item = state.media_api.media_item
if not media_item:
return InternalDirective.RELOAD
if not ctx.media_api.is_authenticated():
return InternalDirective.RELOAD
score_str = ctx.selector.ask("Enter score (0.0 - 10.0):")
try:
score = float(score_str) if score_str else 0.0
if not 0.0 <= score <= 10.0:
raise ValueError("Score out of range.")
# local
ctx.service.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(
"Invalid score entered", "Please enter a number between 0.0 and 10.0"
)
return InternalDirective.RELOAD
return action
def _view_info(ctx: Context, state: State) -> MenuAction:
def action():
media_item = state.media_api.media_item
if not media_item:
return InternalDirective.RELOAD
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(
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 := media_item.cover_image:
image.render_image(cover_image.large)
console.print(Panel(panel_content, title=title, box=box.ROUNDED, expand=True))
ctx.selector.ask("Press Enter to continue...")
return InternalDirective.RELOAD
return action
def _view_recommendations(ctx: Context, state: State) -> MenuAction:
def action():
feedback = ctx.service.feedback
media_item = state.media_api.media_item
if not media_item:
feedback.error("Media item is not in state")
return InternalDirective.RELOAD
loading_message = "Fetching recommendations..."
recommendations = None
with feedback.progress(loading_message):
recommendations = ctx.media_api.get_recommendation_for(
MediaRecommendationParams(id=media_item.id, page=1)
)
if not recommendations:
feedback.warning(
"No recommendations found",
"This anime doesn't have any recommendations available"
)
return InternalDirective.RELOAD
# Convert list of MediaItem to search result format
search_result = {item.id: item for item in recommendations}
# Create a fake page info since recommendations don't have pagination
from .....libs.media_api.types import PageInfo
page_info = PageInfo(
total=len(recommendations),
current_page=1,
has_next_page=False,
per_page=len(recommendations)
)
return State(
menu_name=MenuName.RESULTS,
media_api=MediaApiState(
search_result=search_result,
page_info=page_info,
search_params=None, # No search params for recommendations
),
)
return action
def _view_relations(ctx: Context, state: State) -> MenuAction:
def action():
feedback = ctx.service.feedback
media_item = state.media_api.media_item
if not media_item:
feedback.error("Media item is not in state")
return InternalDirective.RELOAD
loading_message = "Fetching related anime..."
relations = None
with feedback.progress(loading_message):
relations = ctx.media_api.get_related_anime_for(
MediaRelationsParams(id=media_item.id)
)
if not relations:
feedback.warning(
"No related anime found",
"This anime doesn't have any related anime available"
)
return InternalDirective.RELOAD
# Convert list of MediaItem to search result format
search_result = {item.id: item for item in relations}
# Create a fake page info since relations don't have pagination
from .....libs.media_api.types import PageInfo
page_info = PageInfo(
total=len(relations),
current_page=1,
has_next_page=False,
per_page=len(relations)
)
return State(
menu_name=MenuName.RESULTS,
media_api=MediaApiState(
search_result=search_result,
page_info=page_info,
search_params=None, # No search params for relations
),
)
return action