mirror of
https://github.com/Benexl/FastAnime.git
synced 2025-12-25 04:15:19 -08:00
214 lines
7.2 KiB
Python
214 lines
7.2 KiB
Python
import importlib.util
|
|
import logging
|
|
import os
|
|
from dataclasses import dataclass
|
|
from typing import Callable, List, Optional, Union
|
|
|
|
import click
|
|
|
|
from ...core.config import AppConfig
|
|
from ...core.constants import APP_DIR, USER_CONFIG_PATH
|
|
from ...libs.media_api.base import BaseApiClient
|
|
from ...libs.player.base import BasePlayer
|
|
from ...libs.provider.anime.base import BaseAnimeProvider
|
|
from ...libs.selectors.base import BaseSelector
|
|
from ..service.auth import AuthService
|
|
from ..service.feedback import FeedbackService
|
|
from ..service.registry import MediaRegistryService
|
|
from ..service.session import SessionsService
|
|
from ..service.watch_history import WatchHistoryService
|
|
from .state import InternalDirective, MenuName, State
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# A type alias for the signature all menu functions must follow.
|
|
|
|
MENUS_DIR = APP_DIR / "cli" / "interactive" / "menu"
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Services:
|
|
feedback: FeedbackService
|
|
media_registry: MediaRegistryService
|
|
watch_history: WatchHistoryService
|
|
session: SessionsService
|
|
auth: AuthService
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Context:
|
|
config: AppConfig
|
|
provider: BaseAnimeProvider
|
|
selector: BaseSelector
|
|
player: BasePlayer
|
|
media_api: BaseApiClient
|
|
service: Services
|
|
|
|
|
|
MenuFunction = Callable[[Context, State], Union[State, InternalDirective]]
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Menu:
|
|
name: MenuName
|
|
execute: MenuFunction
|
|
|
|
|
|
class Session:
|
|
_context: Context
|
|
_history: List[State] = []
|
|
_menus: dict[MenuName, Menu] = {}
|
|
|
|
def _load_context(self, config: AppConfig):
|
|
"""Initializes all shared service based on the provided configuration."""
|
|
from ...libs.media_api.api import create_api_client
|
|
from ...libs.player import create_player
|
|
from ...libs.provider.anime.provider import create_provider
|
|
from ...libs.selectors import create_selector
|
|
|
|
media_registry = MediaRegistryService(
|
|
media_api=config.general.media_api, config=config.media_registry
|
|
)
|
|
auth = AuthService(config.general.media_api)
|
|
services = Services(
|
|
feedback=FeedbackService(config.general.icons),
|
|
media_registry=media_registry,
|
|
watch_history=WatchHistoryService(config, media_registry),
|
|
session=SessionsService(config.sessions),
|
|
auth=auth,
|
|
)
|
|
|
|
media_api = create_api_client(config.general.media_api, config)
|
|
|
|
if auth_profile := auth.get_auth():
|
|
p = media_api.authenticate(auth_profile.token)
|
|
if p:
|
|
logger.debug(f"Authenticated as {p.name}")
|
|
else:
|
|
logger.warning(f"Failed to authenticate with {auth_profile.token}")
|
|
else:
|
|
logger.debug("Not authenticated")
|
|
|
|
self._context = Context(
|
|
config=config,
|
|
provider=create_provider(config.general.provider),
|
|
selector=create_selector(config),
|
|
player=create_player(config),
|
|
media_api=media_api,
|
|
service=services,
|
|
)
|
|
logger.info("Application context reloaded.")
|
|
|
|
def _edit_config(self):
|
|
from ..config import ConfigLoader
|
|
|
|
click.edit(filename=str(USER_CONFIG_PATH))
|
|
logger.debug("Config changed; Reloading context")
|
|
loader = ConfigLoader()
|
|
config = loader.load()
|
|
self._load_context(config)
|
|
|
|
def run(
|
|
self,
|
|
config: AppConfig,
|
|
resume: bool = False,
|
|
history: Optional[List[State]] = None,
|
|
):
|
|
self._load_context(config)
|
|
if resume:
|
|
if (
|
|
history
|
|
:= self._context.service.session.get_most_recent_session_history()
|
|
):
|
|
self._history = history
|
|
else:
|
|
logger.warning("Failed to continue from history. No sessions found")
|
|
|
|
if history:
|
|
self._history = history
|
|
else:
|
|
self._history.append(State(menu_name=MenuName.MAIN))
|
|
|
|
try:
|
|
self._run_main_loop()
|
|
except Exception:
|
|
self._context.service.session.create_crash_backup(self._history)
|
|
raise
|
|
self._context.service.session.save_session(self._history)
|
|
|
|
def _run_main_loop(self):
|
|
"""Run the main session loop."""
|
|
while self._history:
|
|
current_state = self._history[-1]
|
|
|
|
next_step = self._menus[current_state.menu_name].execute(
|
|
self._context, current_state
|
|
)
|
|
|
|
if isinstance(next_step, InternalDirective):
|
|
if next_step == InternalDirective.MAIN:
|
|
self._history = [self._history[0]]
|
|
if next_step == InternalDirective.RELOAD:
|
|
continue
|
|
elif next_step == InternalDirective.CONFIG_EDIT:
|
|
self._edit_config()
|
|
elif next_step == InternalDirective.BACK:
|
|
if len(self._history) > 1:
|
|
self._history.pop()
|
|
elif next_step == InternalDirective.BACKX2:
|
|
if len(self._history) > 2:
|
|
self._history.pop()
|
|
self._history.pop()
|
|
elif next_step == InternalDirective.BACKX3:
|
|
if len(self._history) > 3:
|
|
self._history.pop()
|
|
self._history.pop()
|
|
self._history.pop()
|
|
elif next_step == InternalDirective.EXIT:
|
|
break
|
|
else:
|
|
self._history.append(next_step)
|
|
|
|
@property
|
|
def menu(self) -> Callable[[MenuFunction], MenuFunction]:
|
|
"""A decorator to register a function as a menu."""
|
|
|
|
def decorator(func: MenuFunction) -> MenuFunction:
|
|
menu_name = MenuName(func.__name__.upper())
|
|
if menu_name in self._menus:
|
|
logger.warning(f"Menu '{menu_name}' is being redefined.")
|
|
self._menus[menu_name] = Menu(name=menu_name, execute=func)
|
|
return func
|
|
|
|
return decorator
|
|
|
|
def load_menus_from_folder(self, package: str):
|
|
package_path = MENUS_DIR / package
|
|
package_name = package_path.name
|
|
logger.debug(f"Loading menus from '{package_path}'...")
|
|
|
|
for filename in os.listdir(package_path):
|
|
if filename.endswith(".py") and not filename.startswith("__"):
|
|
module_name = filename[:-3]
|
|
full_module_name = (
|
|
f"fastanime.cli.interactive.menu.{package_name}.{module_name}"
|
|
)
|
|
file_path = package_path / filename
|
|
|
|
try:
|
|
spec = importlib.util.spec_from_file_location(
|
|
full_module_name, file_path
|
|
)
|
|
if spec and spec.loader:
|
|
module = importlib.util.module_from_spec(spec)
|
|
# The act of executing the module runs the @session.menu decorators
|
|
spec.loader.exec_module(module)
|
|
except Exception as e:
|
|
logger.error(
|
|
f"Failed to load menu module '{full_module_name}': {e}"
|
|
)
|
|
|
|
|
|
# Create a single, global instance of the Session to be imported by menu modules.
|
|
session = Session()
|