mirror of
https://github.com/Benexl/FastAnime.git
synced 2026-01-06 01:37:19 -08:00
feat: mass refactor
This commit is contained in:
@@ -1,17 +1,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from typing import TYPE_CHECKING, Callable, Optional, Tuple
|
||||
|
||||
from .....libs.api.base import ApiSearchParams
|
||||
from .base import GoBack, State
|
||||
from .task_states import (
|
||||
AnimeActionsState,
|
||||
EpisodeSelectionState,
|
||||
ProviderSearchState,
|
||||
StreamPlaybackState,
|
||||
)
|
||||
from .task_states import AnimeActionsState
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .....libs.api.types import MediaSearchResult
|
||||
from ...session import Session
|
||||
from .. import ui
|
||||
|
||||
@@ -24,46 +21,72 @@ class MainMenuState(State):
|
||||
def run(self, session: Session) -> Optional[State | type[GoBack]]:
|
||||
from .. import ui
|
||||
|
||||
menu_actions = {
|
||||
"🔥 Trending": (session.anilist.get_trending, ResultsState()),
|
||||
"🔎 Search": (
|
||||
lambda: session.anilist.search(query=ui.prompt_for_search(session)),
|
||||
# Define actions as tuples: (Display Name, SearchParams, Next State)
|
||||
# This centralizes the "business logic" of what each menu item means.
|
||||
menu_actions: List[
|
||||
Tuple[str, Callable[[], Optional[ApiSearchParams]], Optional[State]]
|
||||
] = [
|
||||
(
|
||||
"🔥 Trending",
|
||||
lambda: ApiSearchParams(sort="TRENDING_DESC"),
|
||||
ResultsState(),
|
||||
),
|
||||
"📺 Watching": (
|
||||
lambda: session.anilist.get_anime_list("CURRENT"),
|
||||
(
|
||||
"🌟 Most Popular",
|
||||
lambda: ApiSearchParams(sort="POPULARITY_DESC"),
|
||||
ResultsState(),
|
||||
),
|
||||
"🌟 Most Popular": (session.anilist.get_most_popular, ResultsState()),
|
||||
"💖 Most Favourite": (session.anilist.get_most_favourite, ResultsState()),
|
||||
"❌ Exit": (lambda: (True, None), None),
|
||||
}
|
||||
(
|
||||
"💖 Most Favourite",
|
||||
lambda: ApiSearchParams(sort="FAVOURITES_DESC"),
|
||||
ResultsState(),
|
||||
),
|
||||
(
|
||||
"🔎 Search",
|
||||
lambda: ApiSearchParams(query=ui.prompt_for_search(session)),
|
||||
ResultsState(),
|
||||
),
|
||||
(
|
||||
"📺 Watching",
|
||||
lambda: session.api_client.fetch_user_list,
|
||||
ResultsState(),
|
||||
), # Direct method call
|
||||
("❌ Exit", lambda: None, None),
|
||||
]
|
||||
|
||||
choice = ui.prompt_main_menu(session, list(menu_actions.keys()))
|
||||
display_choices = [action[0] for action in menu_actions]
|
||||
choice_str = ui.prompt_main_menu(session, display_choices)
|
||||
|
||||
if not choice:
|
||||
if not choice_str:
|
||||
return None
|
||||
|
||||
data_loader, next_state = menu_actions[choice]
|
||||
if not next_state:
|
||||
# Find the chosen action
|
||||
chosen_action = next(
|
||||
(action for action in menu_actions if action[0] == choice_str), None
|
||||
)
|
||||
if not chosen_action:
|
||||
return self # Should not happen
|
||||
|
||||
_, param_creator, next_state = chosen_action
|
||||
|
||||
if not next_state: # Exit case
|
||||
return None
|
||||
|
||||
with ui.progress_spinner(f"Fetching {choice.strip('🔥🔎📺🌟💖❌ ')}..."):
|
||||
success, data = data_loader()
|
||||
# Execute the data fetch
|
||||
with ui.progress_spinner(f"Fetching {choice_str.strip('🔥🔎📺🌟💖❌ ')}..."):
|
||||
if choice_str == "📺 Watching": # Special case for user list
|
||||
result_data = param_creator(status="CURRENT")
|
||||
else:
|
||||
search_params = param_creator()
|
||||
if search_params is None: # User cancelled search prompt
|
||||
return self
|
||||
result_data = session.api_client.search_media(search_params)
|
||||
|
||||
if not success or not data:
|
||||
ui.display_error(f"Failed to fetch data. Reason: {data}")
|
||||
if not result_data:
|
||||
ui.display_error(f"Failed to fetch data for '{choice_str}'.")
|
||||
return self
|
||||
|
||||
if "mediaList" in data.get("data", {}).get("Page", {}):
|
||||
data["data"]["Page"]["media"] = [
|
||||
item["media"] for item in data["data"]["Page"]["mediaList"]
|
||||
]
|
||||
|
||||
session.state.anilist.results_data = data
|
||||
session.state.navigation.current_page = 1
|
||||
# Store the data loader for pagination
|
||||
session.current_data_loader = data_loader
|
||||
session.state.anilist.results_data = result_data # Store the generic dataclass
|
||||
return next_state
|
||||
|
||||
|
||||
@@ -73,59 +96,20 @@ class ResultsState(State):
|
||||
def run(self, session: Session) -> Optional[State | type[GoBack]]:
|
||||
from .. import ui
|
||||
|
||||
if not session.state.anilist.results_data:
|
||||
search_result = session.state.anilist.results_data
|
||||
if not search_result or not isinstance(search_result, MediaSearchResult):
|
||||
ui.display_error("No results to display.")
|
||||
return GoBack
|
||||
|
||||
media_list = (
|
||||
session.state.anilist.results_data.get("data", {})
|
||||
.get("Page", {})
|
||||
.get("media", [])
|
||||
)
|
||||
selection = ui.prompt_anime_selection(session, media_list)
|
||||
selection = ui.prompt_anime_selection(session, search_result.media)
|
||||
|
||||
if selection == "Back":
|
||||
return GoBack
|
||||
if selection is None:
|
||||
return None # User cancelled prompt
|
||||
return None
|
||||
|
||||
if selection == "Next Page":
|
||||
page_info = (
|
||||
session.state.anilist.results_data.get("data", {})
|
||||
.get("Page", {})
|
||||
.get("pageInfo", {})
|
||||
)
|
||||
if page_info.get("hasNextPage"):
|
||||
session.state.navigation.current_page += 1
|
||||
with ui.progress_spinner("Fetching next page..."):
|
||||
success, data = session.current_data_loader(
|
||||
page=session.state.navigation.current_page
|
||||
)
|
||||
if success:
|
||||
session.state.anilist.results_data = data
|
||||
else:
|
||||
ui.display_error("Failed to fetch next page.")
|
||||
session.state.navigation.current_page -= 1
|
||||
else:
|
||||
ui.display_error("Already on the last page.")
|
||||
return self # Return to the same results state
|
||||
# TODO: Implement pagination logic here by checking selection for "Next Page" etc.
|
||||
# and re-calling the search_media method with an updated page number.
|
||||
|
||||
if selection == "Previous Page":
|
||||
if session.state.navigation.current_page > 1:
|
||||
session.state.navigation.current_page -= 1
|
||||
with ui.progress_spinner("Fetching previous page..."):
|
||||
success, data = session.current_data_loader(
|
||||
page=session.state.navigation.current_page
|
||||
)
|
||||
if success:
|
||||
session.state.anilist.results_data = data
|
||||
else:
|
||||
ui.display_error("Failed to fetch previous page.")
|
||||
session.state.navigation.current_page += 1
|
||||
else:
|
||||
ui.display_error("Already on the first page.")
|
||||
return self
|
||||
|
||||
# If it's a valid anime object
|
||||
session.state.anilist.selected_anime = selection
|
||||
return AnimeActionsState()
|
||||
|
||||
@@ -7,53 +7,42 @@ from pydantic import BaseModel, Field
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ...core.config import AppConfig
|
||||
from ...libs.anilist.api import AniListApi
|
||||
from ...libs.anilist.types import AnilistBaseMediaDataSchema
|
||||
from ...libs.anime.provider import AnimeProvider
|
||||
|
||||
# Import the dataclasses for type hinting
|
||||
from ...libs.anime.types import Anime, SearchResult, SearchResults, Server
|
||||
from ...libs.api.base import BaseApiClient
|
||||
from ...libs.api.types import Anime, SearchResult, Server, UserProfile
|
||||
from ...libs.players.base import BasePlayer
|
||||
from ...libs.selector.base import BaseSelector
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# --- Nested State Models ---
|
||||
# --- Nested State Models (Unchanged) ---
|
||||
class AnilistState(BaseModel):
|
||||
"""Holds state related to AniList data and selections."""
|
||||
|
||||
results_data: dict | None = None
|
||||
selected_anime: Optional[AnilistBaseMediaDataSchema] = None
|
||||
results_data: Optional[dict] = None
|
||||
selected_anime: Optional[dict] = (
|
||||
None # Using dict for AnilistBaseMediaDataSchema for now
|
||||
)
|
||||
|
||||
|
||||
class ProviderState(BaseModel):
|
||||
"""Holds state related to the current anime provider, using specific dataclasses."""
|
||||
|
||||
search_results: Optional[SearchResults] = None
|
||||
selected_search_result: Optional[SearchResult] = None
|
||||
anime_details: Optional[Anime] = None
|
||||
current_episode: Optional[str] = None
|
||||
current_server: Optional[Server] = None
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
|
||||
class NavigationState(BaseModel):
|
||||
"""Holds state related to the UI navigation stack."""
|
||||
|
||||
current_page: int = 1
|
||||
history_stack_class_names: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class TrackingState(BaseModel):
|
||||
"""Holds state for user progress tracking preferences."""
|
||||
|
||||
progress_mode: str = "prompt"
|
||||
|
||||
|
||||
# --- Top-Level SessionState ---
|
||||
class SessionState(BaseModel):
|
||||
"""The root model for all serializable runtime state."""
|
||||
|
||||
anilist: AnilistState = Field(default_factory=AnilistState)
|
||||
provider: ProviderState = Field(default_factory=ProviderState)
|
||||
navigation: NavigationState = Field(default_factory=NavigationState)
|
||||
@@ -64,41 +53,48 @@ class SessionState(BaseModel):
|
||||
|
||||
|
||||
class Session:
|
||||
"""
|
||||
Manages the entire runtime session for the interactive anilist command.
|
||||
"""
|
||||
|
||||
def __init__(self, config: AppConfig, anilist_client: AniListApi) -> None:
|
||||
def __init__(self, config: AppConfig) -> None:
|
||||
self.config: AppConfig = config
|
||||
self.state: SessionState = SessionState()
|
||||
self.is_running: bool = True
|
||||
self.anilist: AniListApi = anilist_client
|
||||
self.user_profile: Optional[UserProfile] = None
|
||||
self._initialize_components()
|
||||
|
||||
def _initialize_components(self) -> None:
|
||||
"""Creates instances of core components based on the current config."""
|
||||
from ...libs.anime.provider import create_provider
|
||||
from ...cli.auth.manager import CredentialsManager
|
||||
from ...libs.api.factory import create_api_client
|
||||
from ...libs.players import create_player
|
||||
from ...libs.selector import create_selector
|
||||
|
||||
logger.debug("Initializing session components from configuration...")
|
||||
logger.debug("Initializing session components...")
|
||||
self.selector: BaseSelector = create_selector(self.config)
|
||||
self.provider: AnimeProvider = create_provider(self.config.general.provider)
|
||||
self.player: BasePlayer = create_player(self.config.stream.player, self.config)
|
||||
|
||||
# Instantiate and use the API factory
|
||||
self.api_client: BaseApiClient = create_api_client("anilist", self.config)
|
||||
|
||||
# Load credentials and authenticate the API client
|
||||
manager = CredentialsManager()
|
||||
user_data = manager.load_user_profile()
|
||||
if user_data and (token := user_data.get("token")):
|
||||
self.user_profile = self.api_client.authenticate(token)
|
||||
if not self.user_profile:
|
||||
logger.warning(
|
||||
"Loaded token is invalid or expired. User is not logged in."
|
||||
)
|
||||
|
||||
def change_provider(self, provider_name: str) -> None:
|
||||
from ...libs.anime.provider import create_provider
|
||||
|
||||
self.config.general.provider = provider_name
|
||||
self.provider = create_provider(provider_name)
|
||||
logger.info(f"Provider changed to: {self.provider.__class__.__name__}")
|
||||
|
||||
def change_player(self, player_name: str) -> None:
|
||||
from ...libs.players import create_player
|
||||
|
||||
self.config.stream.player = player_name
|
||||
self.player = create_player(player_name, self.config)
|
||||
logger.info(f"Player changed to: {self.player.__class__.__name__}")
|
||||
|
||||
def stop(self) -> None:
|
||||
self.is_running = False
|
||||
|
||||
Reference in New Issue
Block a user