feat: implement authentication utilities and integrate with menus

This commit is contained in:
Benexl
2025-07-14 21:07:47 +03:00
parent a079f9919c
commit 064401f8e8
6 changed files with 268 additions and 18 deletions
+7 -8
View File
@@ -6,6 +6,7 @@ from rich.console import Console
from ....libs.api.params import ApiSearchParams, UserListParams
from ....libs.api.types import MediaSearchResult, MediaStatus, UserListStatusType
from ...utils.feedback import create_feedback_manager, execute_with_feedback
from ...utils.auth_utils import format_auth_menu_header, check_authentication_required
from ..session import Context, session
from ..state import ControlFlow, MediaApiState, State
@@ -65,7 +66,7 @@ def main(ctx: Context, state: State) -> State | ControlFlow:
choice_str = ctx.selector.choose(
prompt="Select Category",
choices=list(options.keys()),
header="FastAnime Main Menu",
header=format_auth_menu_header(ctx.media_api, "FastAnime Main Menu", icons),
)
if not choice_str:
@@ -180,13 +181,11 @@ def _create_user_list_action(ctx: Context, status: UserListStatusType) -> MenuAc
def action():
feedback = create_feedback_manager(ctx.config.general.icons)
# Check authentication (commented code from original)
# if not ctx.media_api.user_profile:
# feedback.warning(
# f"Please log in to view your '{status.title()}' list",
# "You need to authenticate with AniList to access your personal lists"
# )
# return "CONTINUE", None
# Check authentication
if not check_authentication_required(
ctx.media_api, feedback, f"view your {status.lower()} list"
):
return "CONTINUE", None
def fetch_data():
return ctx.media_api.fetch_user_list(
@@ -7,6 +7,7 @@ from ....libs.api.params import UpdateListEntryParams
from ....libs.api.types import MediaItem
from ....libs.players.params import PlayerParams
from ...utils.feedback import create_feedback_manager, execute_with_feedback
from ...utils.auth_utils import check_authentication_required, get_auth_status_indicator
from ..session import Context, session
from ..state import ControlFlow, ProviderState, State
@@ -21,6 +22,14 @@ def media_actions(ctx: Context, state: State) -> State | ControlFlow:
"""
icons = ctx.config.general.icons
# Get authentication status for display
auth_status, user_profile = get_auth_status_indicator(ctx.media_api, icons)
# Create header with auth status
anime = state.media_api.anime
anime_title = anime.title.english or anime.title.romaji if anime else "Unknown"
header = f"Actions for: {anime_title}\n{auth_status}"
# TODO: Add 'Recommendations' and 'Relations' here later.
options: Dict[str, MenuAction] = {
f"{'▶️ ' if icons else ''}Stream": _stream(ctx, state),
@@ -33,7 +42,7 @@ def media_actions(ctx: Context, state: State) -> State | ControlFlow:
# --- Prompt and Execute ---
choice_str = ctx.selector.choose(
prompt="Select Action", choices=list(options.keys())
prompt="Select Action", choices=list(options.keys()), header=header
)
if choice_str and choice_str in options:
@@ -90,13 +99,21 @@ def _add_to_list(ctx: Context, state: State) -> MenuAction:
anime = state.media_api.anime
if not anime:
return ControlFlow.CONTINUE
# Check authentication before proceeding
if not check_authentication_required(
ctx.media_api, feedback, "add anime to your list"
):
return ControlFlow.CONTINUE
choices = ["CURRENT", "PLANNING", "COMPLETED", "DROPPED", "PAUSED", "REPEATING"]
status = ctx.selector.choose("Select list status:", choices=choices)
if status:
# status is now guaranteed to be one of the valid choices
_update_user_list_with_feedback(
ctx,
anime,
UpdateListEntryParams(media_id=anime.id, status=status),
UpdateListEntryParams(media_id=anime.id, status=status), # type: ignore
feedback,
)
return ControlFlow.CONTINUE
@@ -110,6 +127,11 @@ def _score_anime(ctx: Context, state: State) -> MenuAction:
anime = state.media_api.anime
if not anime:
return ControlFlow.CONTINUE
# Check authentication before proceeding
if not check_authentication_required(ctx.media_api, feedback, "score anime"):
return ControlFlow.CONTINUE
score_str = ctx.selector.ask("Enter score (0.0 - 10.0):")
try:
score = float(score_str) if score_str else 0.0
@@ -180,13 +202,8 @@ def _update_user_list_with_feedback(
ctx: Context, anime: MediaItem, params: UpdateListEntryParams, feedback
):
"""Helper to call the API to update a user's list with comprehensive feedback."""
# Check authentication (commented code from original)
# if not ctx.media_api.user_profile:
# feedback.warning(
# "You must be logged in to modify your list",
# "Please authenticate with AniList to manage your anime lists"
# )
# return
# Authentication check is handled by the calling functions now
# This function assumes authentication has already been verified
def update_operation():
return ctx.media_api.update_list_entry(params)
@@ -1,6 +1,7 @@
from rich.console import Console
from ....libs.api.types import MediaItem
from ...utils.auth_utils import get_auth_status_indicator
from ..session import Context, session
from ..state import ControlFlow, MediaApiState, State
@@ -47,11 +48,16 @@ def results(ctx: Context, state: State) -> State | ControlFlow:
choices.append("Previous Page")
choices.append("Back")
# Create header with auth status
auth_status, _ = get_auth_status_indicator(ctx.media_api, ctx.config.general.icons)
header = f"Search Results ({len(anime_items)} anime)\n{auth_status}"
# --- Prompt User ---
choice_str = ctx.selector.choose(
prompt="Select Anime",
choices=choices,
preview=preview_command,
header=header,
)
if not choice_str:
+29 -1
View File
@@ -67,15 +67,43 @@ class Session:
from ...libs.providers.anime.provider import create_provider
from ...libs.selectors import create_selector
# Create API client
media_api = create_api_client(config.general.api_client, config)
# Attempt to load saved user authentication
self._load_saved_authentication(media_api)
self._context = Context(
config=config,
provider=create_provider(config.general.provider),
selector=create_selector(config),
player=create_player(config),
media_api=create_api_client(config.general.api_client, config),
media_api=media_api,
)
logger.info("Application context reloaded.")
def _load_saved_authentication(self, media_api):
"""Attempt to load saved user authentication."""
try:
from ..auth.manager import AuthManager
auth_manager = AuthManager()
user_data = auth_manager.load_user_profile()
if user_data and user_data.get("token"):
# Try to authenticate with the saved token
profile = media_api.authenticate(user_data["token"])
if profile:
logger.info(f"Successfully authenticated as {profile.name}")
else:
logger.warning("Saved authentication token is invalid or expired")
else:
logger.debug("No saved authentication found")
except Exception as e:
logger.error(f"Failed to load saved authentication: {e}")
# Continue without authentication rather than failing completely
def _edit_config(self):
"""Handles the logic for editing the config file and reloading the context."""
from ..utils.feedback import create_feedback_manager
+116
View File
@@ -0,0 +1,116 @@
"""
Authentication utilities for the interactive CLI.
Provides functions to check authentication status and display user information.
"""
from typing import Optional
from ...libs.api.base import BaseApiClient
from ...libs.api.types import UserProfile
from .feedback import FeedbackManager
def get_auth_status_indicator(
api_client: BaseApiClient, icons_enabled: bool = True
) -> tuple[str, Optional[UserProfile]]:
"""
Get authentication status indicator for display in menus.
Returns:
tuple of (status_text, user_profile or None)
"""
user_profile = getattr(api_client, "user_profile", None)
if user_profile:
# User is authenticated
icon = "🟢 " if icons_enabled else ""
status_text = f"{icon}Logged in as {user_profile.name}"
return status_text, user_profile
else:
# User is not authenticated
icon = "🔴 " if icons_enabled else ""
status_text = f"{icon}Not logged in"
return status_text, None
def format_user_info_header(
user_profile: Optional[UserProfile], icons_enabled: bool = True
) -> str:
"""
Format user information for display in menu headers.
Returns:
Formatted string with user info or empty string if not authenticated
"""
if not user_profile:
return ""
icon = "👤 " if icons_enabled else ""
return f"{icon}User: {user_profile.name} (ID: {user_profile.id})"
def check_authentication_required(
api_client: BaseApiClient,
feedback: FeedbackManager,
operation_name: str = "this action",
) -> bool:
"""
Check if user is authenticated and show appropriate feedback if not.
Returns:
True if authenticated, False if not (with feedback shown)
"""
user_profile = getattr(api_client, "user_profile", None)
if not user_profile:
feedback.warning(
f"Authentication required for {operation_name}",
"Please log in to your AniList account using 'fastanime anilist auth' to access this feature",
)
return False
return True
def format_auth_menu_header(
api_client: BaseApiClient, base_header: str, icons_enabled: bool = True
) -> str:
"""
Format menu header with authentication status.
Args:
api_client: The API client to check authentication status
base_header: Base header text (e.g., "FastAnime Main Menu")
icons_enabled: Whether to show icons
Returns:
Formatted header with authentication status
"""
status_text, user_profile = get_auth_status_indicator(api_client, icons_enabled)
if user_profile:
return f"{base_header}\n{status_text}"
else:
return f"{base_header}\n{status_text} - Some features require authentication"
def prompt_for_authentication(
feedback: FeedbackManager, operation_name: str = "continue"
) -> bool:
"""
Prompt user about authentication requirement and offer guidance.
Returns:
True if user wants to continue anyway, False if they want to stop
"""
feedback.info(
"Authentication Required",
f"To {operation_name}, you need to log in to your AniList account",
)
feedback.info(
"How to authenticate:",
"Run 'fastanime anilist auth' in your terminal to log in",
)
return feedback.confirm("Continue without authentication?", default=False)
+84
View File
@@ -0,0 +1,84 @@
"""
Test script to verify the authentication system works correctly.
This tests the auth utilities and their integration with the feedback system.
"""
import sys
from pathlib import Path
# Add the project root to the path so we can import fastanime modules
project_root = Path(__file__).parent.parent.parent
sys.path.insert(0, str(project_root))
from fastanime.cli.utils.auth_utils import (
get_auth_status_indicator,
format_user_info_header,
check_authentication_required,
format_auth_menu_header,
prompt_for_authentication,
)
from fastanime.cli.utils.feedback import create_feedback_manager
from fastanime.libs.api.types import UserProfile
class MockApiClient:
"""Mock API client for testing authentication utilities."""
def __init__(self, authenticated=False):
if authenticated:
self.user_profile = UserProfile(
id=12345,
name="TestUser",
avatar_url="https://example.com/avatar.jpg",
banner_url="https://example.com/banner.jpg",
)
else:
self.user_profile = None
def test_auth_status_display():
"""Test authentication status display functionality."""
print("=== Testing Authentication Status Display ===\n")
feedback = create_feedback_manager(icons_enabled=True)
print("1. Testing authentication status when NOT logged in:")
mock_api_not_auth = MockApiClient(authenticated=False)
status_text, user_profile = get_auth_status_indicator(mock_api_not_auth, True)
print(f" Status: {status_text}")
print(f" User Profile: {user_profile}")
print("\n2. Testing authentication status when logged in:")
mock_api_auth = MockApiClient(authenticated=True)
status_text, user_profile = get_auth_status_indicator(mock_api_auth, True)
print(f" Status: {status_text}")
print(f" User Profile: {user_profile}")
print("\n3. Testing user info header formatting:")
header = format_user_info_header(user_profile, True)
print(f" Header: {header}")
print("\n4. Testing menu header formatting:")
auth_header = format_auth_menu_header(mock_api_auth, "Test Menu", True)
print(f" Auth Header:\n{auth_header}")
print("\n5. Testing authentication check (not authenticated):")
is_auth = check_authentication_required(
mock_api_not_auth, feedback, "test operation"
)
print(f" Authentication passed: {is_auth}")
print("\n6. Testing authentication check (authenticated):")
is_auth = check_authentication_required(mock_api_auth, feedback, "test operation")
print(f" Authentication passed: {is_auth}")
print("\n7. Testing authentication prompt:")
# Note: This will show interactive prompts if run in a terminal
# prompt_for_authentication(feedback, "access your anime list")
print(" Skipped interactive prompt test - uncomment to test manually")
print("\n=== Authentication Tests Completed! ===")
if __name__ == "__main__":
test_auth_status_display()