feat: mass refactor

This commit is contained in:
Benexl
2025-07-06 14:15:13 +03:00
parent 5a50e79216
commit 2bd02c7e99
30 changed files with 120 additions and 163 deletions

View File

@@ -12,15 +12,15 @@ from ...core.constants import (
ROFI_THEME_PREVIEW,
)
from ...libs.anilist.constants import SORTS_AVAILABLE
from ...libs.anime_provider import PROVIDERS_AVAILABLE, SERVERS_AVAILABLE
from ...libs.providers.anime import PROVIDERS_AVAILABLE, SERVERS_AVAILABLE
from ..constants import APP_ASCII_ART, USER_VIDEOS_DIR
class External(BaseModel):
class OtherConfig(BaseModel):
pass
class FzfConfig(External):
class FzfConfig(OtherConfig):
"""Configuration specific to the FZF selector."""
opts: str = Field(
@@ -48,7 +48,7 @@ class FzfConfig(External):
)
class RofiConfig(External):
class RofiConfig(OtherConfig):
"""Configuration specific to the Rofi selector."""
theme_main: Path = Field(
@@ -69,7 +69,7 @@ class RofiConfig(External):
)
class MpvConfig(External):
class MpvConfig(OtherConfig):
"""Configuration specific to the MPV player integration."""
args: str = Field(
@@ -92,7 +92,7 @@ class MpvConfig(External):
)
class AnilistConfig(External):
class AnilistConfig(OtherConfig):
"""Configuration for interacting with the AniList API."""
per_page: int = Field(
@@ -182,10 +182,10 @@ class GeneralConfig(BaseModel):
@field_validator("provider")
@classmethod
def validate_server(cls, v: str) -> str:
if v.lower() != "top" and v not in PROVIDERS_AVAILABLE:
def validate_provider(cls, v: str) -> str:
if v not in PROVIDERS_AVAILABLE:
raise ValueError(
f"'{v}' is not a valid server. Must be 'top' or one of: {PROVIDERS_AVAILABLE}"
f"'{v}' is not a valid provider. Must be one of: {PROVIDERS_AVAILABLE}"
)
return v

View File

@@ -1,13 +1,13 @@
from collections.abc import Callable
from pathlib import Path
from typing import Any, Literal, get_origin, get_args
from typing import Any, Literal, get_args, get_origin
import click
from pydantic import BaseModel
from pydantic.fields import FieldInfo
from pydantic_core import PydanticUndefined
from .config.model import External
from .config.model import OtherConfig
# Mapping from Python/Pydantic types to Click types
TYPE_MAP = {
@@ -50,7 +50,7 @@ def options_from_model(model: type[BaseModel], parent_name: str = "") -> Callabl
decorators = []
# Check if this model inherits from ExternalTool
is_external_tool = issubclass(model, External)
is_external_tool = issubclass(model, OtherConfig)
model_name = model.__name__.lower().replace("config", "")
# Introspect the model's fields

View File

View File

View File

View File

View File

View File

View File

@@ -1,12 +1,7 @@
import logging
from typing import TYPE_CHECKING
from fastanime.libs.anime_provider.allanime.parser import (
map_to_anime_result,
map_to_search_results,
)
from ....core.utils.graphql import execute_graphql_query
from .....core.utils.graphql import execute_graphql_query
from ..base import AnimeProvider
from ..utils.decorators import debug_provider
from .constants import (
@@ -18,6 +13,10 @@ from .constants import (
SEARCH_GQL,
)
from .extractors import extract_server
from .parser import (
map_to_anime_result,
map_to_search_results,
)
if TYPE_CHECKING:
from .types import AllAnimeEpisode
@@ -25,7 +24,7 @@ logger = logging.getLogger(__name__)
class AllAnime(AnimeProvider):
DEFAULT_HEADERS = {"Referer": API_GRAPHQL_REFERER}
HEADERS = {"Referer": API_GRAPHQL_REFERER}
@debug_provider
def search_for_anime(self, params):

View File

@@ -1,8 +1,9 @@
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import TYPE_CHECKING, Literal
from typing import TYPE_CHECKING, ClassVar, Dict
from httpx import AsyncClient, Client
from httpx import Client
from .params import AnimeParams, EpisodeStreamsParams, SearchParams
if TYPE_CHECKING:
from collections.abc import Iterator
@@ -10,61 +11,29 @@ if TYPE_CHECKING:
from .types import Anime, SearchResults, Server
@dataclass
class SearchParams:
"""Parameters for searching anime."""
query: str
# pagination and sorting
current_page: int = 1
page_limit: int = 20
sort_by: str = "relevance"
order: Literal["asc", "desc"] = "desc"
# filters
translation_type: Literal["sub", "dub"] = "sub"
genre: str | None = None
year: int | None = None
status: str | None = None
allow_nsfw: bool = True
allow_unknown: bool = True
country_of_origin: str | None = None
@dataclass
class EpisodeStreamsParams:
"""Parameters for fetching episode streams."""
anime_id: str
episode: str
translation_type: Literal["sub", "dub"] = "sub"
server: str | None = None
quality: Literal["1080", "720", "480", "360"] = "720"
subtitles: bool = True
@dataclass
class AnimeParams:
"""Parameters for fetching anime details."""
anime_id: str
class AnimeProvider(ABC):
HEADERS: ClassVar[Dict[str, str]]
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
if not hasattr(cls, "HEADERS"):
raise TypeError(
f"Subclasses of AnimeProvider must define a 'HEADERS' class attribute."
)
def __init__(self, client: Client) -> None:
self.client = client
@abstractmethod
def search_for_anime(self, params: SearchParams) -> "SearchResults | None":
def search(self, params: SearchParams) -> "SearchResults | None":
pass
@abstractmethod
def get_anime(self, params: AnimeParams) -> "Anime | None":
def get(self, params: AnimeParams) -> "Anime | None":
pass
@abstractmethod
def get_episode_streams(
def episode_streams(
self, params: EpisodeStreamsParams
) -> "Iterator[Server] | None":
pass

View File

@@ -0,0 +1,43 @@
from dataclasses import dataclass
from typing import Literal
@dataclass
class SearchParams:
"""Parameters for searching anime."""
query: str
# pagination and sorting
current_page: int = 1
page_limit: int = 20
sort_by: str = "relevance"
order: Literal["asc", "desc"] = "desc"
# filters
translation_type: Literal["sub", "dub"] = "sub"
genre: str | None = None
year: int | None = None
status: str | None = None
allow_nsfw: bool = True
allow_unknown: bool = True
country_of_origin: str | None = None
@dataclass
class EpisodeStreamsParams:
"""Parameters for fetching episode streams."""
anime_id: str
episode: str
translation_type: Literal["sub", "dub"] = "sub"
server: str | None = None
quality: Literal["1080", "720", "480", "360"] = "720"
subtitles: bool = True
@dataclass
class AnimeParams:
"""Parameters for fetching anime details."""
anime_id: str

View File

@@ -1,141 +1,86 @@
"""An abstraction over all providers offering added features with a simple and well typed api"""
import importlib
import logging
import os
from typing import TYPE_CHECKING
from yt_dlp.utils.networking import random_user_agent
from .allanime.constants import SERVERS_AVAILABLE as ALLANIME_SERVERS
from .animepahe.constants import SERVERS_AVAILABLE as ANIMEPAHE_SERVERS
from .base import AnimeProvider as Base
from .hianime.constants import SERVERS_AVAILABLE as HIANIME_SERVERS
from httpx import Client, AsyncClient
from yt_dlp.utils.networking import random_user_agent
from .params import AnimeParams, EpisodeStreamsParams, SearchParams
if TYPE_CHECKING:
from collections.abc import Iterator
from httpx import AsyncClient, Client
from .types import Anime, SearchResults, Server
logger = logging.getLogger(__name__)
PROVIDERS_AVAILABLE = {
"allanime": "api.AllAnime",
"animepahe": "api.AnimePahe",
"hianime": "api.HiAnime",
"nyaa": "api.Nyaa",
"yugen": "api.Yugen",
"allanime": "provider.AllAnime",
"animepahe": "provider.AnimePahe",
"hianime": "provider.HiAnime",
"nyaa": "provider.Nyaa",
"yugen": "provider.Yugen",
}
SERVERS_AVAILABLE = ["top", *ALLANIME_SERVERS, *ANIMEPAHE_SERVERS, *HIANIME_SERVERS]
SERVERS_AVAILABLE = ["TOP", *ALLANIME_SERVERS, *ANIMEPAHE_SERVERS, *HIANIME_SERVERS]
class AnimeProvider:
"""An abstraction over all anime providers"""
PROVIDERS = list(PROVIDERS_AVAILABLE.keys())
provider = PROVIDERS[0]
current_provider_name = PROVIDERS[0]
current_provider: Base
def __init__(
self,
provider,
cache_requests=os.environ.get("FASTANIME_CACHE_REQUESTS", "false"),
use_persistent_provider_store=os.environ.get(
"FASTANIME_USE_PERSISTENT_PROVIDER_STORE", "false"
),
provider: str,
cache_requests=False,
use_persistent_provider_store=False,
dynamic=False,
retries=0,
) -> None:
self.provider = provider
self.current_provider_name = provider
self.dynamic = dynamic
self.retries = retries
self.cache_requests = cache_requests
self.use_persistent_provider_store = use_persistent_provider_store
self.lazyload_provider(self.provider)
self.lazyload(self.current_provider_name)
def setup_httpx_client(self) -> Client:
def search(self, params: SearchParams) -> "SearchResults | None":
results = self.current_provider.search(params)
return results
def get(self, params: AnimeParams) -> "Anime | None":
results = self.current_provider.get(params)
return results
def episode_streams(
self, params: EpisodeStreamsParams
) -> "Iterator[Server] | None":
results = self.current_provider.episode_streams(params)
return results
def setup_httpx_client(self, headers) -> "Client":
"""Sets up a httpx client with a random user agent"""
client = Client(headers={"User-Agent": random_user_agent()})
client = Client(headers={"User-Agent": random_user_agent(), **headers})
return client
def setup_httpx_async_client(self) -> AsyncClient:
def setup_httpx_async_client(self) -> "AsyncClient":
"""Sets up a httpx client with a random user agent"""
client = AsyncClient(headers={"User-Agent": random_user_agent()})
return client
def lazyload_provider(self, provider):
"""updates the current provider being used"""
try:
self.anime_provider.session.kill_connection_to_db()
except Exception:
pass
def lazyload(self, provider):
_, anime_provider_cls_name = PROVIDERS_AVAILABLE[provider].split(".", 1)
package = f"fastanime.libs.anime_provider.{provider}"
package = f"fastanime.libs.providers.anime.{provider}"
provider_api = importlib.import_module(".api", package)
anime_provider = getattr(provider_api, anime_provider_cls_name)
self.anime_provider = anime_provider(
self.cache_requests, self.use_persistent_provider_store
)
def search_for_anime(
self, search_keywords, translation_type, **kwargs
) -> "SearchResults | None":
"""core abstraction over all providers search functionality
Args:
user_query ([TODO:parameter]): [TODO:description]
translation_type ([TODO:parameter]): [TODO:description]
nsfw ([TODO:parameter]): [TODO:description]
unknown ([TODO:parameter]): [TODO:description]
anilist_obj: [TODO:description]
Returns:
[TODO:return]
"""
anime_provider = self.anime_provider
results = anime_provider.search_for_anime(
search_keywords, translation_type, **kwargs
)
return results
def get_anime(
self,
anime_id: str,
**kwargs,
) -> "Anime | None":
"""core abstraction over getting info of an anime from all providers
Args:
anime_id: [TODO:description]
anilist_obj: [TODO:description]
Returns:
[TODO:return]
"""
anime_provider = self.anime_provider
results = anime_provider.get_anime(anime_id, **kwargs)
return results
def get_episode_streams(
self,
anime_id,
episode: str,
translation_type: str,
**kwargs,
) -> "Iterator[Server] | None":
"""core abstractions for getting juicy streams from all providers
Args:
anime ([TODO:parameter]): [TODO:description]
episode: [TODO:description]
translation_type: [TODO:description]
anilist_obj: [TODO:description]
Returns:
[TODO:return]
"""
anime_provider = self.anime_provider
results = anime_provider.get_episode_streams(
anime_id, episode, translation_type, **kwargs
)
return results
client = self.setup_httpx_client(anime_provider.HEADERS)
self.current_provider = anime_provider(client)