feat: mass refactor

This commit is contained in:
Benexl
2025-07-06 23:59:18 +03:00
parent 32f4d9271f
commit 0737c5c14b
38 changed files with 966 additions and 1800 deletions

View File

@@ -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()

View File

@@ -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