mirror of
https://github.com/Benexl/FastAnime.git
synced 2025-12-26 20:53:34 -08:00
310 lines
10 KiB
Python
310 lines
10 KiB
Python
import os
|
|
from pathlib import Path
|
|
from typing import Literal
|
|
|
|
from pydantic import BaseModel, Field, PrivateAttr, computed_field, field_validator
|
|
|
|
from ...core.constants import (
|
|
FZF_DEFAULT_OPTS,
|
|
ROFI_THEME_CONFIRM,
|
|
ROFI_THEME_INPUT,
|
|
ROFI_THEME_MAIN,
|
|
ROFI_THEME_PREVIEW,
|
|
)
|
|
from ...libs.api.anilist.constants import SORTS_AVAILABLE
|
|
from ...libs.providers.anime import PROVIDERS_AVAILABLE, SERVERS_AVAILABLE
|
|
from ..constants import APP_ASCII_ART, USER_VIDEOS_DIR
|
|
|
|
|
|
class OtherConfig(BaseModel):
|
|
pass
|
|
|
|
|
|
class FzfConfig(OtherConfig):
|
|
"""Configuration specific to the FZF selector."""
|
|
|
|
_opts: str = PrivateAttr(default=FZF_DEFAULT_OPTS.read_text(encoding="utf-8"))
|
|
header_color: str = Field(
|
|
default="95,135,175", description="RGB color for the main TUI header."
|
|
)
|
|
_header_ascii_art: str = PrivateAttr(default=APP_ASCII_ART)
|
|
preview_header_color: str = Field(
|
|
default="215,0,95", description="RGB color for preview pane headers."
|
|
)
|
|
preview_separator_color: str = Field(
|
|
default="208,208,208", description="RGB color for preview pane separators."
|
|
)
|
|
|
|
def __init__(self, **kwargs):
|
|
opts = kwargs.pop("opts", None)
|
|
header_ascii_art = kwargs.pop("header_ascii_art", None)
|
|
|
|
super().__init__(**kwargs)
|
|
if opts:
|
|
self._opts = opts
|
|
if header_ascii_art:
|
|
self._header_ascii_art = header_ascii_art
|
|
|
|
@computed_field(
|
|
description="The FZF options, formatted with leading tabs for the config file."
|
|
)
|
|
@property
|
|
def opts(self) -> str:
|
|
return "\n" + "\n".join([f"\t{line}" for line in self._opts.split()])
|
|
|
|
@computed_field(
|
|
description="The ASCII art to display as a header in the FZF interface."
|
|
)
|
|
@property
|
|
def header_ascii_art(self) -> str:
|
|
return "\n" + "\n".join(
|
|
[f"\t{line}" for line in self._header_ascii_art.split()]
|
|
)
|
|
|
|
|
|
class RofiConfig(OtherConfig):
|
|
"""Configuration specific to the Rofi selector."""
|
|
|
|
theme_main: Path = Field(
|
|
default=Path(str(ROFI_THEME_MAIN)),
|
|
description="Path to the main Rofi theme file.",
|
|
)
|
|
theme_preview: Path = Field(
|
|
default=Path(str(ROFI_THEME_PREVIEW)),
|
|
description="Path to the Rofi theme file for previews.",
|
|
)
|
|
theme_confirm: Path = Field(
|
|
default=Path(str(ROFI_THEME_CONFIRM)),
|
|
description="Path to the Rofi theme file for confirmation prompts.",
|
|
)
|
|
theme_input: Path = Field(
|
|
default=Path(str(ROFI_THEME_INPUT)),
|
|
description="Path to the Rofi theme file for user input prompts.",
|
|
)
|
|
|
|
|
|
class MpvConfig(OtherConfig):
|
|
"""Configuration specific to the MPV player integration."""
|
|
|
|
args: str = Field(
|
|
default="", description="Comma-separated arguments to pass to the MPV player."
|
|
)
|
|
pre_args: str = Field(
|
|
default="",
|
|
description="Comma-separated arguments to prepend before the MPV command.",
|
|
)
|
|
disable_popen: bool = Field(
|
|
default=True,
|
|
description="Disable using subprocess.Popen for MPV, which can be unstable on some systems.",
|
|
)
|
|
force_window: str = Field(
|
|
default="immediate", description="Value for MPV's --force-window option."
|
|
)
|
|
use_python_mpv: bool = Field(
|
|
default=False,
|
|
description="Use the python-mpv library for enhanced player control.",
|
|
)
|
|
|
|
|
|
class AnilistConfig(OtherConfig):
|
|
"""Configuration for interacting with the AniList API."""
|
|
|
|
per_page: int = Field(
|
|
default=15,
|
|
gt=0,
|
|
le=50,
|
|
description="Number of items to fetch per page from AniList.",
|
|
)
|
|
sort_by: str = Field(
|
|
default="SEARCH_MATCH",
|
|
description="Default sort order for AniList search results.",
|
|
examples=SORTS_AVAILABLE,
|
|
)
|
|
preferred_language: Literal["english", "romaji"] = Field(
|
|
default="english",
|
|
description="Preferred language for anime titles from AniList.",
|
|
)
|
|
|
|
@field_validator("sort_by")
|
|
@classmethod
|
|
def validate_sort_by(cls, v: str) -> str:
|
|
if v not in SORTS_AVAILABLE:
|
|
raise ValueError(
|
|
f"'{v}' is not a valid sort option. See documentation for available options."
|
|
)
|
|
return v
|
|
|
|
|
|
class JikanConfig(OtherConfig):
|
|
"""Configuration for the Jikan API (currently none)."""
|
|
|
|
pass
|
|
|
|
|
|
class GeneralConfig(BaseModel):
|
|
"""Configuration for general application behavior and integrations."""
|
|
|
|
api_client: Literal["anilist", "jikan"] = Field(
|
|
default="anilist",
|
|
description="The media database API to use (e.g., 'anilist', 'jikan').",
|
|
)
|
|
provider: str = Field(
|
|
default="allanime",
|
|
description="The default anime provider to use for scraping.",
|
|
examples=list(PROVIDERS_AVAILABLE.keys()),
|
|
)
|
|
selector: Literal["default", "fzf", "rofi"] = Field(
|
|
default="default", description="The interactive selector tool to use for menus."
|
|
)
|
|
auto_select_anime_result: bool = Field(
|
|
default=True,
|
|
description="Automatically select the best-matching search result from a provider.",
|
|
)
|
|
icons: bool = Field(
|
|
default=False, description="Display emoji icons in the user interface."
|
|
)
|
|
preview: Literal["full", "text", "image", "none"] = Field(
|
|
default="none", description="Type of preview to display in selectors."
|
|
)
|
|
image_renderer: Literal["icat", "chafa", "imgcat"] = Field(
|
|
default="icat" if os.environ.get("KITTY_WINDOW_ID") else "chafa",
|
|
description="The command-line tool to use for rendering images in the terminal.",
|
|
)
|
|
manga_viewer: Literal["feh", "icat"] = Field(
|
|
default="feh",
|
|
description="The external application to use for viewing manga pages.",
|
|
)
|
|
downloads_dir: Path = Field(
|
|
default_factory=lambda: USER_VIDEOS_DIR,
|
|
description="The default directory to save downloaded anime.",
|
|
)
|
|
check_for_updates: bool = Field(
|
|
default=True,
|
|
description="Automatically check for new versions of FastAnime on startup.",
|
|
)
|
|
cache_requests: bool = Field(
|
|
default=True,
|
|
description="Enable caching of network requests to speed up subsequent operations.",
|
|
)
|
|
max_cache_lifetime: str = Field(
|
|
default="03:00:00",
|
|
description="Maximum lifetime for a cached request in DD:HH:MM format.",
|
|
)
|
|
normalize_titles: bool = Field(
|
|
default=True,
|
|
description="Attempt to normalize provider titles to match AniList titles.",
|
|
)
|
|
discord: bool = Field(
|
|
default=False,
|
|
description="Enable Discord Rich Presence to show your current activity.",
|
|
)
|
|
recent: int = Field(
|
|
default=50,
|
|
ge=0,
|
|
description="Number of recently watched anime to keep in history.",
|
|
)
|
|
|
|
@field_validator("provider")
|
|
@classmethod
|
|
def validate_provider(cls, v: str) -> str:
|
|
if v not in PROVIDERS_AVAILABLE:
|
|
raise ValueError(
|
|
f"'{v}' is not a valid provider. Must be one of: {PROVIDERS_AVAILABLE}"
|
|
)
|
|
return v
|
|
|
|
|
|
class StreamConfig(BaseModel):
|
|
"""Configuration specific to video streaming and playback."""
|
|
|
|
player: Literal["mpv", "vlc"] = Field(
|
|
default="mpv", description="The media player to use for streaming."
|
|
)
|
|
quality: Literal["360", "480", "720", "1080"] = Field(
|
|
default="1080", description="Preferred stream quality."
|
|
)
|
|
translation_type: Literal["sub", "dub"] = Field(
|
|
default="sub", description="Preferred audio/subtitle language type."
|
|
)
|
|
server: str = Field(
|
|
default="TOP",
|
|
description="The default server to use from a provider. 'top' uses the first available.",
|
|
examples=SERVERS_AVAILABLE,
|
|
)
|
|
auto_next: bool = Field(
|
|
default=False,
|
|
description="Automatically play the next episode when the current one finishes.",
|
|
)
|
|
continue_from_watch_history: bool = Field(
|
|
default=True,
|
|
description="Automatically resume playback from the last known episode and position.",
|
|
)
|
|
preferred_watch_history: Literal["local", "remote"] = Field(
|
|
default="local",
|
|
description="Which watch history to prioritize: local file or remote AniList progress.",
|
|
)
|
|
auto_skip: bool = Field(
|
|
default=False,
|
|
description="Automatically skip openings/endings if skip data is available.",
|
|
)
|
|
episode_complete_at: int = Field(
|
|
default=80,
|
|
ge=0,
|
|
le=100,
|
|
description="Percentage of an episode to watch before it's marked as complete.",
|
|
)
|
|
ytdlp_format: str = Field(
|
|
default="best[height<=1080]/bestvideo[height<=1080]+bestaudio/best",
|
|
description="The format selection string for yt-dlp.",
|
|
)
|
|
force_forward_tracking: bool = Field(
|
|
default=True,
|
|
description="Prevent updating AniList progress to a lower episode number.",
|
|
)
|
|
default_media_list_tracking: Literal["track", "disabled", "prompt"] = Field(
|
|
default="prompt",
|
|
description="Default behavior for tracking progress on AniList.",
|
|
)
|
|
sub_lang: str = Field(
|
|
default="eng",
|
|
description="Preferred language code for subtitles (e.g., 'en', 'es').",
|
|
)
|
|
|
|
@field_validator("server")
|
|
@classmethod
|
|
def validate_server(cls, v: str) -> str:
|
|
if v.lower() != "top" and v not in SERVERS_AVAILABLE:
|
|
raise ValueError(
|
|
f"'{v}' is not a valid server. Must be 'top' or one of: {SERVERS_AVAILABLE}"
|
|
)
|
|
return v
|
|
|
|
|
|
class AppConfig(BaseModel):
|
|
"""The root configuration model for the FastAnime application."""
|
|
|
|
general: GeneralConfig = Field(
|
|
default_factory=GeneralConfig,
|
|
description="General configuration settings for application behavior.",
|
|
)
|
|
stream: StreamConfig = Field(
|
|
default_factory=StreamConfig,
|
|
description="Settings related to video streaming and playback.",
|
|
)
|
|
anilist: AnilistConfig = Field(
|
|
default_factory=AnilistConfig,
|
|
description="Configuration for AniList API integration.",
|
|
)
|
|
|
|
fzf: FzfConfig = Field(
|
|
default_factory=FzfConfig,
|
|
description="Settings for the FZF selector interface.",
|
|
)
|
|
rofi: RofiConfig = Field(
|
|
default_factory=RofiConfig,
|
|
description="Settings for the Rofi selector interface.",
|
|
)
|
|
mpv: MpvConfig = Field(
|
|
default_factory=MpvConfig, description="Configuration for the MPV media player."
|
|
)
|