mirror of
https://github.com/Benexl/FastAnime.git
synced 2025-12-05 20:40:09 -08:00
feat: improve api types
This commit is contained in:
@@ -18,7 +18,7 @@ from rich.table import Table
|
||||
from rich.text import Text
|
||||
|
||||
from ....libs.api.params import UpdateListEntryParams, UserListParams
|
||||
from ....libs.api.types import MediaItem, MediaSearchResult, UserListStatusType
|
||||
from ....libs.api.types import MediaItem, MediaSearchResult, UserListItem
|
||||
from ...utils.feedback import create_feedback_manager, execute_with_feedback
|
||||
from ..session import Context, session
|
||||
from ..state import ControlFlow, MediaApiState, State
|
||||
@@ -373,7 +373,7 @@ def _display_anime_list_details(console: Console, anime: MediaItem, icons: bool)
|
||||
console.print(panel)
|
||||
|
||||
|
||||
def _navigate_to_list(ctx: Context, list_status: UserListStatusType) -> State:
|
||||
def _navigate_to_list(ctx: Context, list_status: UserListItem) -> State:
|
||||
"""Navigate to a specific list view."""
|
||||
return State(
|
||||
menu_name="ANILIST_LIST_VIEW", data={"list_status": list_status, "page": 1}
|
||||
|
||||
@@ -3,7 +3,12 @@ import random
|
||||
from typing import Callable, Dict, Tuple
|
||||
|
||||
from ....libs.api.params import ApiSearchParams, UserListParams
|
||||
from ....libs.api.types import MediaSearchResult, MediaStatus, UserListStatusType
|
||||
from ....libs.api.types import (
|
||||
MediaSearchResult,
|
||||
MediaSort,
|
||||
MediaStatus,
|
||||
UserMediaListStatus,
|
||||
)
|
||||
from ..session import Context, session
|
||||
from ..state import ControlFlow, MediaApiState, State
|
||||
|
||||
@@ -28,36 +33,44 @@ def main(ctx: Context, state: State) -> State | ControlFlow:
|
||||
options: Dict[str, MenuAction] = {
|
||||
# --- Search-based Actions ---
|
||||
f"{'🔥 ' if icons else ''}Trending": _create_media_list_action(
|
||||
ctx, "TRENDING_DESC"
|
||||
ctx, MediaSort.TRENDING_DESC
|
||||
),
|
||||
f"{'✨ ' if icons else ''}Popular": _create_media_list_action(
|
||||
ctx, "POPULARITY_DESC"
|
||||
ctx, MediaSort.POPULARITY_DESC
|
||||
),
|
||||
f"{'💖 ' if icons else ''}Favourites": _create_media_list_action(
|
||||
ctx, "FAVOURITES_DESC"
|
||||
ctx, MediaSort.FAVOURITES_DESC
|
||||
),
|
||||
f"{'💯 ' if icons else ''}Top Scored": _create_media_list_action(
|
||||
ctx, "SCORE_DESC"
|
||||
ctx, MediaSort.SCORE_DESC
|
||||
),
|
||||
f"{'🎬 ' if icons else ''}Upcoming": _create_media_list_action(
|
||||
ctx, "POPULARITY_DESC", "NOT_YET_RELEASED"
|
||||
ctx, MediaSort.POPULARITY_DESC, MediaStatus.NOT_YET_RELEASED
|
||||
),
|
||||
f"{'🔔 ' if icons else ''}Recently Updated": _create_media_list_action(
|
||||
ctx, "UPDATED_AT_DESC"
|
||||
ctx, MediaSort.UPDATED_AT_DESC
|
||||
),
|
||||
# --- special case media list --
|
||||
f"{'🎲 ' if icons else ''}Random": _create_random_media_list(ctx),
|
||||
f"{'🔎 ' if icons else ''}Search": _create_search_media_list(ctx),
|
||||
# --- Authenticated User List Actions ---
|
||||
f"{'📺 ' if icons else ''}Watching": _create_user_list_action(ctx, "watching"),
|
||||
f"{'📑 ' if icons else ''}Planned": _create_user_list_action(ctx, "planning"),
|
||||
f"{'✅ ' if icons else ''}Completed": _create_user_list_action(
|
||||
ctx, "completed"
|
||||
f"{'📺 ' if icons else ''}Watching": _create_user_list_action(
|
||||
ctx, UserMediaListStatus.WATCHING
|
||||
),
|
||||
f"{'📑 ' if icons else ''}Planned": _create_user_list_action(
|
||||
ctx, UserMediaListStatus.PLANNING
|
||||
),
|
||||
f"{'✅ ' if icons else ''}Completed": _create_user_list_action(
|
||||
ctx, UserMediaListStatus.COMPLETED
|
||||
),
|
||||
f"{'⏸️ ' if icons else ''}Paused": _create_user_list_action(
|
||||
ctx, UserMediaListStatus.PAUSED
|
||||
),
|
||||
f"{'🚮 ' if icons else ''}Dropped": _create_user_list_action(
|
||||
ctx, UserMediaListStatus.DROPPED
|
||||
),
|
||||
f"{'⏸️ ' if icons else ''}Paused": _create_user_list_action(ctx, "paused"),
|
||||
f"{'🚮 ' if icons else ''}Dropped": _create_user_list_action(ctx, "dropped"),
|
||||
f"{'🔁 ' if icons else ''}Rewatching": _create_user_list_action(
|
||||
ctx, "repeating"
|
||||
ctx, UserMediaListStatus.REPEATING
|
||||
),
|
||||
f"{'🔁 ' if icons else ''}Recent": lambda: (
|
||||
"RESULTS",
|
||||
@@ -123,7 +136,7 @@ def main(ctx: Context, state: State) -> State | ControlFlow:
|
||||
|
||||
|
||||
def _create_media_list_action(
|
||||
ctx: Context, sort, status: MediaStatus | None = None
|
||||
ctx: Context, sort: MediaSort, status: MediaStatus | None = None
|
||||
) -> MenuAction:
|
||||
"""A factory to create menu actions for fetching media lists"""
|
||||
|
||||
@@ -163,7 +176,7 @@ def _create_search_media_list(ctx: Context) -> MenuAction:
|
||||
return action
|
||||
|
||||
|
||||
def _create_user_list_action(ctx: Context, status: UserListStatusType) -> MenuAction:
|
||||
def _create_user_list_action(ctx: Context, status: UserMediaListStatus) -> MenuAction:
|
||||
"""A factory to create menu actions for fetching user lists, handling authentication."""
|
||||
|
||||
def action():
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from typing import Callable, Dict
|
||||
|
||||
import click
|
||||
from rich.console import Console
|
||||
|
||||
from ....libs.api.params import UpdateListEntryParams
|
||||
@@ -152,7 +151,7 @@ def _view_info(ctx: Context, state: State) -> MenuAction:
|
||||
console = Console()
|
||||
title = Text(anime.title.english or anime.title.romaji or "", style="bold cyan")
|
||||
description = Text(anime.description or "NO description")
|
||||
genres = Text(f"Genres: {', '.join(anime.genres)}")
|
||||
genres = Text(f"Genres: {', '.join([v.value for v in anime.genres])}")
|
||||
|
||||
panel_content = f"{genres}\n\n{description}"
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from ....libs.api.params import ApiSearchParams, UserListParams
|
||||
from ....libs.api.types import MediaItem
|
||||
from ....libs.api.types import MediaItem, MediaStatus, UserMediaListStatus
|
||||
from ..session import Context, session
|
||||
from ..state import ControlFlow, MediaApiState, State
|
||||
|
||||
@@ -96,10 +96,10 @@ def _format_anime_choice(anime: MediaItem, config) -> str:
|
||||
|
||||
# Add a visual indicator for new episodes if applicable
|
||||
if (
|
||||
anime.status == "RELEASING"
|
||||
anime.status == MediaStatus.RELEASING
|
||||
and anime.next_airing
|
||||
and anime.user_status
|
||||
and anime.user_status.status == "CURRENT"
|
||||
and anime.user_status.status == UserMediaListStatus.WATCHING
|
||||
):
|
||||
last_aired = anime.next_airing.episode - 1
|
||||
unwatched = last_aired - (anime.user_status.progress or 0)
|
||||
|
||||
@@ -8,7 +8,7 @@ from ...libs.api.types import (
|
||||
MediaItem,
|
||||
MediaSearchResult,
|
||||
MediaStatus,
|
||||
UserListStatusType,
|
||||
UserListItem,
|
||||
)
|
||||
from ...libs.players.types import PlayerResult
|
||||
from ...libs.providers.anime.types import Anime, SearchResults, Server
|
||||
@@ -80,7 +80,7 @@ class MediaApiState(BaseModel):
|
||||
search_results_type: Optional[Literal["MEDIA_LIST", "USER_MEDIA_LIST"]] = None
|
||||
sort: Optional[str] = None
|
||||
query: Optional[str] = None
|
||||
user_media_status: Optional[UserListStatusType] = None
|
||||
user_media_status: Optional[UserListItem] = None
|
||||
media_status: Optional[MediaStatus] = None
|
||||
anime: Optional[MediaItem] = None
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from collections.abc import Callable
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Any, Literal, get_args, get_origin
|
||||
|
||||
@@ -130,6 +131,16 @@ def _get_click_type(field_info: FieldInfo) -> Any:
|
||||
"""Maps a Pydantic field's type to a corresponding click type."""
|
||||
field_type = field_info.annotation
|
||||
|
||||
# check if type is enum
|
||||
if (
|
||||
field_type is not None
|
||||
and isinstance(field_type, type)
|
||||
and issubclass(field_type, Enum)
|
||||
):
|
||||
# Get the string values of the enum members
|
||||
enum_choices = [member.value for member in field_type]
|
||||
return click.Choice(enum_choices)
|
||||
|
||||
# Check if the type is a Literal
|
||||
if (
|
||||
field_type is not None
|
||||
|
||||
@@ -1,27 +1,33 @@
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Dict, Literal, Optional
|
||||
|
||||
from pydantic import BaseModel, Field, computed_field
|
||||
|
||||
from ....libs.api.types import MediaItem, UserListStatusType
|
||||
from ....libs.api.types import MediaItem, UserMediaListStatus
|
||||
from ...utils import converters
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Type aliases
|
||||
DownloadStatus = Literal[
|
||||
"not_downloaded", "queued", "downloading", "completed", "failed", "paused"
|
||||
]
|
||||
|
||||
class DownloadStatus(Enum):
|
||||
NOT_DOWNLOADED = "not_downloaded"
|
||||
QUEUED = "queued"
|
||||
DOWNLOADING = "downloading"
|
||||
COMPLETED = "completed"
|
||||
FAILED = "failed"
|
||||
PAUSED = "paused"
|
||||
|
||||
|
||||
REGISTRY_VERSION = "1.0"
|
||||
|
||||
|
||||
class MediaEpisode(BaseModel):
|
||||
episode_number: str
|
||||
|
||||
# Download tracking
|
||||
download_status: DownloadStatus = "not_downloaded"
|
||||
download_status: DownloadStatus = DownloadStatus.NOT_DOWNLOADED
|
||||
file_path: Path
|
||||
download_date: datetime = Field(default_factory=datetime.now)
|
||||
|
||||
@@ -35,7 +41,7 @@ class MediaRegistryIndexEntry(BaseModel):
|
||||
media_id: int
|
||||
media_api: Literal["anilist", "NONE", "jikan"] = "NONE"
|
||||
|
||||
status: UserListStatusType = "watching"
|
||||
status: UserMediaListStatus = UserMediaListStatus.WATCHING
|
||||
progress: str = "0"
|
||||
last_watch_position: Optional[str] = None
|
||||
last_watched: datetime = Field(default_factory=datetime.now)
|
||||
|
||||
@@ -12,7 +12,7 @@ from ....libs.api.types import (
|
||||
MediaItem,
|
||||
MediaSearchResult,
|
||||
PageInfo,
|
||||
UserListStatusType,
|
||||
UserMediaListStatus,
|
||||
)
|
||||
from .filters import MediaFilter
|
||||
from .models import (
|
||||
@@ -150,7 +150,7 @@ class MediaRegistryService:
|
||||
watched: bool = False,
|
||||
media_item: Optional[MediaItem] = None,
|
||||
progress: Optional[str] = None,
|
||||
status: Optional[UserListStatusType] = None,
|
||||
status: Optional[UserMediaListStatus] = None,
|
||||
last_watch_position: Optional[str] = None,
|
||||
total_duration: Optional[str] = None,
|
||||
score: Optional[float] = None,
|
||||
@@ -171,7 +171,7 @@ class MediaRegistryService:
|
||||
if status:
|
||||
index_entry.status = status
|
||||
else:
|
||||
index_entry.status = "watching"
|
||||
index_entry.status = UserMediaListStatus.WATCHING
|
||||
|
||||
if last_watch_position:
|
||||
index_entry.last_watch_position = last_watch_position
|
||||
|
||||
@@ -4,7 +4,7 @@ from typing import Optional
|
||||
from ....core.config.model import AppConfig
|
||||
from ....libs.api.base import BaseApiClient
|
||||
from ....libs.api.params import UpdateListEntryParams
|
||||
from ....libs.api.types import MediaItem, UserListStatusType
|
||||
from ....libs.api.types import MediaItem, UserMediaListStatus
|
||||
from ....libs.players.types import PlayerResult
|
||||
from ..registry import MediaRegistryService
|
||||
|
||||
@@ -48,7 +48,7 @@ class WatchHistoryService:
|
||||
self,
|
||||
media_item: MediaItem,
|
||||
progress: Optional[str] = None,
|
||||
status: Optional[UserListStatusType] = None,
|
||||
status: Optional[UserMediaListStatus] = None,
|
||||
score: Optional[float] = None,
|
||||
notes: Optional[str] = None,
|
||||
):
|
||||
|
||||
@@ -1,21 +1,16 @@
|
||||
import concurrent.futures
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
from hashlib import sha256
|
||||
from io import StringIO
|
||||
from threading import Thread
|
||||
from typing import List
|
||||
|
||||
import httpx
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
|
||||
from ...core.config import AppConfig
|
||||
from ...core.constants import APP_CACHE_DIR, APP_DIR, PLATFORM
|
||||
from ...core.utils.file import AtomicWriter
|
||||
from ...libs.api.types import MediaItem, StreamingEpisode
|
||||
from ...libs.api.types import MediaItem
|
||||
from . import ansi, formatters
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -76,8 +71,8 @@ def _populate_info_template(item: MediaItem, config: AppConfig) -> str:
|
||||
# plain text
|
||||
#
|
||||
"TITLE": formatters.shell_safe(item.title.english or item.title.romaji),
|
||||
"STATUS": formatters.shell_safe(item.status),
|
||||
"FORMAT": formatters.shell_safe(item.format),
|
||||
"STATUS": formatters.shell_safe(item.status.value),
|
||||
"FORMAT": formatters.shell_safe(item.format.value),
|
||||
#
|
||||
# numerical
|
||||
#
|
||||
@@ -100,10 +95,10 @@ def _populate_info_template(item: MediaItem, config: AppConfig) -> str:
|
||||
# list
|
||||
#
|
||||
"GENRES": formatters.shell_safe(
|
||||
formatters.format_list_with_commas(item.genres)
|
||||
formatters.format_list_with_commas([v.value for v in item.genres])
|
||||
),
|
||||
"TAGS": formatters.shell_safe(
|
||||
formatters.format_list_with_commas([t.name for t in item.tags])
|
||||
formatters.format_list_with_commas([t.name.value for t in item.tags])
|
||||
),
|
||||
"STUDIOS": formatters.shell_safe(
|
||||
formatters.format_list_with_commas([t.name for t in item.studios if t.name])
|
||||
@@ -115,7 +110,9 @@ def _populate_info_template(item: MediaItem, config: AppConfig) -> str:
|
||||
# user
|
||||
#
|
||||
"USER_STATUS": formatters.shell_safe(
|
||||
item.user_status.status if item.user_status else "NOT_ON_LIST"
|
||||
item.user_status.status.value
|
||||
if item.user_status and item.user_status.status
|
||||
else "NOT_ON_LIST"
|
||||
),
|
||||
"USER_PROGRESS": formatters.shell_safe(
|
||||
f"Episode {item.user_status.progress}" if item.user_status else "0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import List, Literal
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, Field, PrivateAttr, computed_field, field_validator
|
||||
|
||||
@@ -11,7 +11,7 @@ from ...core.constants import (
|
||||
ROFI_THEME_MAIN,
|
||||
ROFI_THEME_PREVIEW,
|
||||
)
|
||||
from ...libs.api.anilist.constants import MEDIA_LIST_SORTS, SORTS_AVAILABLE
|
||||
from ...libs.api.types import MediaSort, UserMediaListSort
|
||||
from ...libs.providers.anime import PROVIDERS_AVAILABLE, SERVERS_AVAILABLE
|
||||
from ..constants import APP_ASCII_ART
|
||||
from . import defaults
|
||||
@@ -21,12 +21,6 @@ from . import descriptions as desc
|
||||
class GeneralConfig(BaseModel):
|
||||
"""Configuration for general application behavior and integrations."""
|
||||
|
||||
per_page: int = Field(
|
||||
default=defaults.ANILIST_PER_PAGE,
|
||||
gt=0,
|
||||
le=50,
|
||||
description=desc.ANILIST_PER_PAGE,
|
||||
)
|
||||
pygment_style: str = Field(
|
||||
default=defaults.GENERAL_PYGMENT_STYLE, description=desc.GENERAL_PYGMENT_STYLE
|
||||
)
|
||||
@@ -311,44 +305,41 @@ class AnilistConfig(OtherConfig):
|
||||
le=50,
|
||||
description=desc.ANILIST_PER_PAGE,
|
||||
)
|
||||
sort_by: str = Field(
|
||||
default=defaults.ANILIST_SORT_BY,
|
||||
sort_by: MediaSort = Field(
|
||||
default=MediaSort.SEARCH_MATCH,
|
||||
description=desc.ANILIST_SORT_BY,
|
||||
examples=SORTS_AVAILABLE,
|
||||
)
|
||||
media_list_sort_by: str = Field(
|
||||
default=defaults.ANILIST_MEDIA_LIST_SORT_BY,
|
||||
media_list_sort_by: UserMediaListSort = Field(
|
||||
default=UserMediaListSort.MEDIA_POPULARITY_DESC,
|
||||
description=desc.ANILIST_MEDIA_LIST_SORT_BY,
|
||||
examples=MEDIA_LIST_SORTS,
|
||||
)
|
||||
preferred_language: Literal["english", "romaji"] = Field(
|
||||
default=defaults.ANILIST_PREFERRED_LANGUAGE,
|
||||
description=desc.ANILIST_PREFERRED_LANGUAGE,
|
||||
)
|
||||
|
||||
@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
|
||||
|
||||
@field_validator("media_list_sort_by")
|
||||
@classmethod
|
||||
def validate_media_list_sort_by(cls, v: str) -> str:
|
||||
if v not in MEDIA_LIST_SORTS:
|
||||
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
|
||||
per_page: int = Field(
|
||||
default=defaults.ANILIST_PER_PAGE,
|
||||
gt=0,
|
||||
le=50,
|
||||
description=desc.ANILIST_PER_PAGE,
|
||||
)
|
||||
sort_by: MediaSort = Field(
|
||||
default=MediaSort.SEARCH_MATCH,
|
||||
description=desc.ANILIST_SORT_BY,
|
||||
)
|
||||
media_list_sort_by: UserMediaListSort = Field(
|
||||
default=UserMediaListSort.MEDIA_POPULARITY_DESC,
|
||||
description=desc.ANILIST_MEDIA_LIST_SORT_BY,
|
||||
)
|
||||
preferred_language: Literal["english", "romaji"] = Field(
|
||||
default=defaults.ANILIST_PREFERRED_LANGUAGE,
|
||||
description=desc.ANILIST_PREFERRED_LANGUAGE,
|
||||
)
|
||||
|
||||
|
||||
class DownloadsConfig(OtherConfig):
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import logging
|
||||
from typing import Optional
|
||||
from enum import Enum
|
||||
from typing import List, Optional
|
||||
|
||||
from httpx import Client
|
||||
|
||||
@@ -8,20 +9,20 @@ from ....core.utils.graphql import (
|
||||
execute_graphql,
|
||||
)
|
||||
from ..base import ApiSearchParams, BaseApiClient, UpdateListEntryParams, UserListParams
|
||||
from ..types import MediaSearchResult, UserProfile
|
||||
from ..types import MediaSearchResult, UserMediaListStatus, UserProfile
|
||||
from . import gql, mapper
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
ANILIST_ENDPOINT = "https://graphql.anilist.co"
|
||||
|
||||
|
||||
status_map = {
|
||||
"watching": "CURRENT",
|
||||
"planning": "PLANNING",
|
||||
"completed": "COMPLETED",
|
||||
"dropped": "DROPPED",
|
||||
"paused": "PAUSED",
|
||||
"repeating": "REPEATING",
|
||||
user_list_status_map = {
|
||||
UserMediaListStatus.WATCHING: "CURRENT",
|
||||
UserMediaListStatus.PLANNING: "PLANNING",
|
||||
UserMediaListStatus.COMPLETED: "COMPLETED",
|
||||
UserMediaListStatus.DROPPED: "DROPPED",
|
||||
UserMediaListStatus.PAUSED: "PAUSED",
|
||||
UserMediaListStatus.REPEATING: "REPEATING",
|
||||
}
|
||||
|
||||
# TODO: Just remove and have consistent variable naming between the two
|
||||
@@ -86,15 +87,40 @@ class AniListApi(BaseApiClient):
|
||||
|
||||
def search_media(self, params: ApiSearchParams) -> Optional[MediaSearchResult]:
|
||||
variables = {
|
||||
search_params_map[k]: v for k, v in params.__dict__.items() if v is not None
|
||||
search_params_map[k]: v
|
||||
for k, v in params.__dict__.items()
|
||||
if v is not None and not isinstance(v, Enum)
|
||||
}
|
||||
|
||||
# handle case where value is an enum
|
||||
variables.update(
|
||||
{
|
||||
search_params_map[k]: v.value
|
||||
for k, v in params.__dict__.items()
|
||||
if v is not None and isinstance(v, Enum)
|
||||
}
|
||||
)
|
||||
|
||||
# handle case where is a list of enums
|
||||
variables.update(
|
||||
{
|
||||
search_params_map[k]: list(map(lambda item: item.value, v))
|
||||
for k, v in params.__dict__.items()
|
||||
if v is not None and isinstance(v, list)
|
||||
}
|
||||
)
|
||||
|
||||
variables["per_page"] = params.per_page or self.config.per_page
|
||||
|
||||
# ignore hentai by default
|
||||
variables["genre_not_in"] = params.genre_not_in or ["Hentai"]
|
||||
variables["genre_not_in"] = (
|
||||
list(map(lambda item: item.value, params.genre_not_in))
|
||||
if params.genre_not_in
|
||||
else ["Hentai"]
|
||||
)
|
||||
|
||||
# anime by default
|
||||
variables["type"] = params.type or "ANIME"
|
||||
variables["type"] = params.type.value if params.type else "ANIME"
|
||||
response = execute_graphql(
|
||||
ANILIST_ENDPOINT, self.http_client, gql.SEARCH_MEDIA, variables
|
||||
)
|
||||
@@ -108,12 +134,14 @@ class AniListApi(BaseApiClient):
|
||||
# TODO: use consistent variable naming btw graphql and params
|
||||
# so variables can be dynamically filled
|
||||
variables = {
|
||||
"sort": params.sort or self.config.media_list_sort_by,
|
||||
"sort": params.sort.value
|
||||
if params.sort
|
||||
else self.config.media_list_sort_by,
|
||||
"userId": self.user_profile.id,
|
||||
"status": status_map[params.status] if params.status else None,
|
||||
"status": user_list_status_map[params.status] if params.status else None,
|
||||
"page": params.page,
|
||||
"perPage": params.per_page or self.config.per_page,
|
||||
"type": params.type or "ANIME",
|
||||
"type": params.type.value if params.type else "ANIME",
|
||||
}
|
||||
response = execute_graphql(
|
||||
ANILIST_ENDPOINT, self.http_client, gql.GET_USER_MEDIA_LIST, variables
|
||||
@@ -126,7 +154,7 @@ class AniListApi(BaseApiClient):
|
||||
score_raw = int(params.score * 10) if params.score is not None else None
|
||||
variables = {
|
||||
"mediaId": params.media_id,
|
||||
"status": status_map[params.status] if params.status else None,
|
||||
"status": user_list_status_map[params.status] if params.status else None,
|
||||
"progress": int(float(params.progress)) if params.progress else None,
|
||||
"scoreRaw": score_raw,
|
||||
}
|
||||
|
||||
@@ -1,515 +0,0 @@
|
||||
SORTS_AVAILABLE = [
|
||||
"ID",
|
||||
"ID_DESC",
|
||||
"TITLE_ROMAJI",
|
||||
"TITLE_ROMAJI_DESC",
|
||||
"TITLE_ENGLISH",
|
||||
"TITLE_ENGLISH_DESC",
|
||||
"TITLE_NATIVE",
|
||||
"TITLE_NATIVE_DESC",
|
||||
"TYPE",
|
||||
"TYPE_DESC",
|
||||
"FORMAT",
|
||||
"FORMAT_DESC",
|
||||
"START_DATE",
|
||||
"START_DATE_DESC",
|
||||
"END_DATE",
|
||||
"END_DATE_DESC",
|
||||
"SCORE",
|
||||
"SCORE_DESC",
|
||||
"POPULARITY",
|
||||
"POPULARITY_DESC",
|
||||
"TRENDING",
|
||||
"TRENDING_DESC",
|
||||
"EPISODES",
|
||||
"EPISODES_DESC",
|
||||
"DURATION",
|
||||
"DURATION_DESC",
|
||||
"STATUS",
|
||||
"STATUS_DESC",
|
||||
"CHAPTERS",
|
||||
"CHAPTERS_DESC",
|
||||
"VOLUMES",
|
||||
"VOLUMES_DESC",
|
||||
"UPDATED_AT",
|
||||
"UPDATED_AT_DESC",
|
||||
"SEARCH_MATCH",
|
||||
"FAVOURITES",
|
||||
"FAVOURITES_DESC",
|
||||
]
|
||||
|
||||
MEDIA_LIST_SORTS = [
|
||||
"MEDIA_ID",
|
||||
"MEDIA_ID_DESC",
|
||||
"SCORE",
|
||||
"SCORE_DESC",
|
||||
"STATUS",
|
||||
"STATUS_DESC",
|
||||
"PROGRESS",
|
||||
"PROGRESS_DESC",
|
||||
"PROGRESS_VOLUMES",
|
||||
"PROGRESS_VOLUMES_DESC",
|
||||
"REPEAT",
|
||||
"REPEAT_DESC",
|
||||
"PRIORITY",
|
||||
"PRIORITY_DESC",
|
||||
"STARTED_ON",
|
||||
"STARTED_ON_DESC",
|
||||
"FINISHED_ON",
|
||||
"FINISHED_ON_DESC",
|
||||
"ADDED_TIME",
|
||||
"ADDED_TIME_DESC",
|
||||
"UPDATED_TIME",
|
||||
"UPDATED_TIME_DESC",
|
||||
"MEDIA_TITLE_ROMAJI",
|
||||
"MEDIA_TITLE_ROMAJI_DESC",
|
||||
"MEDIA_TITLE_ENGLISH",
|
||||
"MEDIA_TITLE_ENGLISH_DESC",
|
||||
"MEDIA_TITLE_NATIVE",
|
||||
"MEDIA_TITLE_NATIVE_DESC",
|
||||
"MEDIA_POPULARITY",
|
||||
"MEDIA_POPULARITY_DESC",
|
||||
"MEDIA_SCORE",
|
||||
"MEDIA_SCORE_DESC",
|
||||
"MEDIA_START_DATE",
|
||||
"MEDIA_START_DATE_DESC",
|
||||
"MEDIA_RATING",
|
||||
"MEDIA_RATING_DESC",
|
||||
]
|
||||
media_statuses_available = [
|
||||
"FINISHED",
|
||||
"RELEASING",
|
||||
"NOT_YET_RELEASED",
|
||||
"CANCELLED",
|
||||
"HIATUS",
|
||||
]
|
||||
seasons_available = ["WINTER", "SPRING", "SUMMER", "FALL"]
|
||||
genres_available = [
|
||||
"Action",
|
||||
"Adventure",
|
||||
"Comedy",
|
||||
"Drama",
|
||||
"Ecchi",
|
||||
"Fantasy",
|
||||
"Horror",
|
||||
"Mahou Shoujo",
|
||||
"Mecha",
|
||||
"Music",
|
||||
"Mystery",
|
||||
"Psychological",
|
||||
"Romance",
|
||||
"Sci-Fi",
|
||||
"Slice of Life",
|
||||
"Sports",
|
||||
"Supernatural",
|
||||
"Thriller",
|
||||
"Hentai",
|
||||
]
|
||||
media_formats_available = [
|
||||
"TV",
|
||||
"TV_SHORT",
|
||||
"MOVIE",
|
||||
"SPECIAL",
|
||||
"OVA",
|
||||
"MUSIC",
|
||||
"NOVEL",
|
||||
"ONE_SHOT",
|
||||
]
|
||||
years_available = [
|
||||
"1900",
|
||||
"1910",
|
||||
"1920",
|
||||
"1930",
|
||||
"1940",
|
||||
"1950",
|
||||
"1960",
|
||||
"1970",
|
||||
"1980",
|
||||
"1990",
|
||||
"2000",
|
||||
"2004",
|
||||
"2005",
|
||||
"2006",
|
||||
"2007",
|
||||
"2008",
|
||||
"2009",
|
||||
"2010",
|
||||
"2011",
|
||||
"2012",
|
||||
"2013",
|
||||
"2014",
|
||||
"2015",
|
||||
"2016",
|
||||
"2017",
|
||||
"2018",
|
||||
"2019",
|
||||
"2020",
|
||||
"2021",
|
||||
"2022",
|
||||
"2023",
|
||||
"2024",
|
||||
"2025",
|
||||
]
|
||||
|
||||
tags_available = {
|
||||
"Cast": ["Polyamorous"],
|
||||
"Cast Main Cast": [
|
||||
"Anti-Hero",
|
||||
"Elderly Protagonist",
|
||||
"Ensemble Cast",
|
||||
"Estranged Family",
|
||||
"Female Protagonist",
|
||||
"Male Protagonist",
|
||||
"Primarily Adult Cast",
|
||||
"Primarily Animal Cast",
|
||||
"Primarily Child Cast",
|
||||
"Primarily Female Cast",
|
||||
"Primarily Male Cast",
|
||||
"Primarily Teen Cast",
|
||||
],
|
||||
"Cast Traits": [
|
||||
"Age Regression",
|
||||
"Agender",
|
||||
"Aliens",
|
||||
"Amnesia",
|
||||
"Angels",
|
||||
"Anthropomorphism",
|
||||
"Aromantic",
|
||||
"Arranged Marriage",
|
||||
"Artificial Intelligence",
|
||||
"Asexual",
|
||||
"Butler",
|
||||
"Centaur",
|
||||
"Chimera",
|
||||
"Chuunibyou",
|
||||
"Clone",
|
||||
"Cosplay",
|
||||
"Cowboys",
|
||||
"Crossdressing",
|
||||
"Cyborg",
|
||||
"Delinquents",
|
||||
"Demons",
|
||||
"Detective",
|
||||
"Dinosaurs",
|
||||
"Disability",
|
||||
"Dissociative Identities",
|
||||
"Dragons",
|
||||
"Dullahan",
|
||||
"Elf",
|
||||
"Fairy",
|
||||
"Femboy",
|
||||
"Ghost",
|
||||
"Goblin",
|
||||
"Gods",
|
||||
"Gyaru",
|
||||
"Hikikomori",
|
||||
"Homeless",
|
||||
"Idol",
|
||||
"Kemonomimi",
|
||||
"Kuudere",
|
||||
"Maids",
|
||||
"Mermaid",
|
||||
"Monster Boy",
|
||||
"Monster Girl",
|
||||
"Nekomimi",
|
||||
"Ninja",
|
||||
"Nudity",
|
||||
"Nun",
|
||||
"Office Lady",
|
||||
"Oiran",
|
||||
"Ojou-sama",
|
||||
"Orphan",
|
||||
"Pirates",
|
||||
"Robots",
|
||||
"Samurai",
|
||||
"Shrine Maiden",
|
||||
"Skeleton",
|
||||
"Succubus",
|
||||
"Tanned Skin",
|
||||
"Teacher",
|
||||
"Tomboy",
|
||||
"Transgender",
|
||||
"Tsundere",
|
||||
"Twins",
|
||||
"Vampire",
|
||||
"Veterinarian",
|
||||
"Vikings",
|
||||
"Villainess",
|
||||
"VTuber",
|
||||
"Werewolf",
|
||||
"Witch",
|
||||
"Yandere",
|
||||
"Zombie",
|
||||
],
|
||||
"Demographic": ["Josei", "Kids", "Seinen", "Shoujo", "Shounen"],
|
||||
"Setting": ["Matriarchy"],
|
||||
"Setting Scene": [
|
||||
"Bar",
|
||||
"Boarding School",
|
||||
"Circus",
|
||||
"Coastal",
|
||||
"College",
|
||||
"Desert",
|
||||
"Dungeon",
|
||||
"Foreign",
|
||||
"Inn",
|
||||
"Konbini",
|
||||
"Natural Disaster",
|
||||
"Office",
|
||||
"Outdoor",
|
||||
"Prison",
|
||||
"Restaurant",
|
||||
"Rural",
|
||||
"School",
|
||||
"School Club",
|
||||
"Snowscape",
|
||||
"Urban",
|
||||
"Work",
|
||||
],
|
||||
"Setting Time": [
|
||||
"Achronological Order",
|
||||
"Anachronism",
|
||||
"Ancient China",
|
||||
"Dystopian",
|
||||
"Historical",
|
||||
"Time Skip",
|
||||
],
|
||||
"Setting Universe": [
|
||||
"Afterlife",
|
||||
"Alternate Universe",
|
||||
"Augmented Reality",
|
||||
"Omegaverse",
|
||||
"Post-Apocalyptic",
|
||||
"Space",
|
||||
"Urban Fantasy",
|
||||
"Virtual World",
|
||||
],
|
||||
"Technical": [
|
||||
"4-koma",
|
||||
"Achromatic",
|
||||
"Advertisement",
|
||||
"Anthology",
|
||||
"CGI",
|
||||
"Episodic",
|
||||
"Flash",
|
||||
"Full CGI",
|
||||
"Full Color",
|
||||
"No Dialogue",
|
||||
"Non-fiction",
|
||||
"POV",
|
||||
"Puppetry",
|
||||
"Rotoscoping",
|
||||
"Stop Motion",
|
||||
],
|
||||
"Theme Action": [
|
||||
"Archery",
|
||||
"Battle Royale",
|
||||
"Espionage",
|
||||
"Fugitive",
|
||||
"Guns",
|
||||
"Martial Arts",
|
||||
"Spearplay",
|
||||
"Swordplay",
|
||||
],
|
||||
"Theme Arts": [
|
||||
"Acting",
|
||||
"Calligraphy",
|
||||
"Classic Literature",
|
||||
"Drawing",
|
||||
"Fashion",
|
||||
"Food",
|
||||
"Makeup",
|
||||
"Photography",
|
||||
"Rakugo",
|
||||
"Writing",
|
||||
],
|
||||
"Theme Arts-Music": [
|
||||
"Band",
|
||||
"Classical Music",
|
||||
"Dancing",
|
||||
"Hip-hop Music",
|
||||
"Jazz Music",
|
||||
"Metal Music",
|
||||
"Musical Theater",
|
||||
"Rock Music",
|
||||
],
|
||||
"Theme Comedy": ["Parody", "Satire", "Slapstick", "Surreal Comedy"],
|
||||
"Theme Drama": [
|
||||
"Bullying",
|
||||
"Class Struggle",
|
||||
"Coming of Age",
|
||||
"Conspiracy",
|
||||
"Eco-Horror",
|
||||
"Fake Relationship",
|
||||
"Kingdom Management",
|
||||
"Rehabilitation",
|
||||
"Revenge",
|
||||
"Suicide",
|
||||
"Tragedy",
|
||||
],
|
||||
"Theme Fantasy": [
|
||||
"Alchemy",
|
||||
"Body Swapping",
|
||||
"Cultivation",
|
||||
"Fairy Tale",
|
||||
"Henshin",
|
||||
"Isekai",
|
||||
"Kaiju",
|
||||
"Magic",
|
||||
"Mythology",
|
||||
"Necromancy",
|
||||
"Shapeshifting",
|
||||
"Steampunk",
|
||||
"Super Power",
|
||||
"Superhero",
|
||||
"Wuxia",
|
||||
"Youkai",
|
||||
],
|
||||
"Theme Game": ["Board Game", "E-Sports", "Video Games"],
|
||||
"Theme Game-Card & Board Game": [
|
||||
"Card Battle",
|
||||
"Go",
|
||||
"Karuta",
|
||||
"Mahjong",
|
||||
"Poker",
|
||||
"Shogi",
|
||||
],
|
||||
"Theme Game-Sport": [
|
||||
"Acrobatics",
|
||||
"Airsoft",
|
||||
"American Football",
|
||||
"Athletics",
|
||||
"Badminton",
|
||||
"Baseball",
|
||||
"Basketball",
|
||||
"Bowling",
|
||||
"Boxing",
|
||||
"Cheerleading",
|
||||
"Cycling",
|
||||
"Fencing",
|
||||
"Fishing",
|
||||
"Fitness",
|
||||
"Football",
|
||||
"Golf",
|
||||
"Handball",
|
||||
"Ice Skating",
|
||||
"Judo",
|
||||
"Lacrosse",
|
||||
"Parkour",
|
||||
"Rugby",
|
||||
"Scuba Diving",
|
||||
"Skateboarding",
|
||||
"Sumo",
|
||||
"Surfing",
|
||||
"Swimming",
|
||||
"Table Tennis",
|
||||
"Tennis",
|
||||
"Volleyball",
|
||||
"Wrestling",
|
||||
],
|
||||
"Theme Other": [
|
||||
"Adoption",
|
||||
"Animals",
|
||||
"Astronomy",
|
||||
"Autobiographical",
|
||||
"Biographical",
|
||||
"Body Horror",
|
||||
"Cannibalism",
|
||||
"Chibi",
|
||||
"Cosmic Horror",
|
||||
"Crime",
|
||||
"Crossover",
|
||||
"Death Game",
|
||||
"Denpa",
|
||||
"Drugs",
|
||||
"Economics",
|
||||
"Educational",
|
||||
"Environmental",
|
||||
"Ero Guro",
|
||||
"Filmmaking",
|
||||
"Found Family",
|
||||
"Gambling",
|
||||
"Gender Bending",
|
||||
"Gore",
|
||||
"Language Barrier",
|
||||
"LGBTQ+ Themes",
|
||||
"Lost Civilization",
|
||||
"Marriage",
|
||||
"Medicine",
|
||||
"Memory Manipulation",
|
||||
"Meta",
|
||||
"Mountaineering",
|
||||
"Noir",
|
||||
"Otaku Culture",
|
||||
"Pandemic",
|
||||
"Philosophy",
|
||||
"Politics",
|
||||
"Proxy Battle",
|
||||
"Psychosexual",
|
||||
"Reincarnation",
|
||||
"Religion",
|
||||
"Royal Affairs",
|
||||
"Slavery",
|
||||
"Software Development",
|
||||
"Survival",
|
||||
"Terrorism",
|
||||
"Torture",
|
||||
"Travel",
|
||||
"War",
|
||||
],
|
||||
"Theme Other-Organisations": [
|
||||
"Assassins",
|
||||
"Criminal Organization",
|
||||
"Cult",
|
||||
"Firefighters",
|
||||
"Gangs",
|
||||
"Mafia",
|
||||
"Military",
|
||||
"Police",
|
||||
"Triads",
|
||||
"Yakuza",
|
||||
],
|
||||
"Theme Other-Vehicle": [
|
||||
"Aviation",
|
||||
"Cars",
|
||||
"Mopeds",
|
||||
"Motorcycles",
|
||||
"Ships",
|
||||
"Tanks",
|
||||
"Trains",
|
||||
],
|
||||
"Theme Romance": [
|
||||
"Age Gap",
|
||||
"Bisexual",
|
||||
"Boys' Love",
|
||||
"Female Harem",
|
||||
"Heterosexual",
|
||||
"Love Triangle",
|
||||
"Male Harem",
|
||||
"Matchmaking",
|
||||
"Mixed Gender Harem",
|
||||
"Teens' Love",
|
||||
"Unrequited Love",
|
||||
"Yuri",
|
||||
],
|
||||
"Theme Sci Fi": [
|
||||
"Cyberpunk",
|
||||
"Space Opera",
|
||||
"Time Loop",
|
||||
"Time Manipulation",
|
||||
"Tokusatsu",
|
||||
],
|
||||
"Theme Sci Fi-Mecha": ["Real Robot", "Super Robot"],
|
||||
"Theme Slice of Life": [
|
||||
"Agriculture",
|
||||
"Cute Boys Doing Cute Things",
|
||||
"Cute Girls Doing Cute Things",
|
||||
"Family Life",
|
||||
"Horticulture",
|
||||
"Iyashikei",
|
||||
"Parenthood",
|
||||
],
|
||||
}
|
||||
tags_available_list = []
|
||||
for tag_category, tags_in_category in tags_available.items():
|
||||
tags_available_list.extend(tags_in_category)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional
|
||||
from typing import List, Optional
|
||||
|
||||
from ....core.utils.formatting import renumber_titles, strip_original_episode_prefix
|
||||
from ..types import (
|
||||
@@ -8,13 +8,15 @@ from ..types import (
|
||||
MediaImage,
|
||||
MediaItem,
|
||||
MediaSearchResult,
|
||||
MediaTag,
|
||||
MediaStatus,
|
||||
MediaTagItem,
|
||||
MediaTitle,
|
||||
MediaTrailer,
|
||||
PageInfo,
|
||||
StreamingEpisode,
|
||||
Studio,
|
||||
UserListStatus,
|
||||
UserListItem,
|
||||
UserMediaListStatus,
|
||||
UserProfile,
|
||||
)
|
||||
from .types import (
|
||||
@@ -25,7 +27,6 @@ from .types import (
|
||||
AnilistImage,
|
||||
AnilistMediaList,
|
||||
AnilistMediaLists,
|
||||
AnilistMediaListStatus,
|
||||
AnilistMediaNextAiringEpisode,
|
||||
AnilistMediaTag,
|
||||
AnilistMediaTitle,
|
||||
@@ -40,13 +41,19 @@ from .types import (
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
user_list_status_map = {
|
||||
"CURRENT": UserMediaListStatus.WATCHING,
|
||||
"PLANNING": UserMediaListStatus.PLANNING,
|
||||
"COMPLETED": UserMediaListStatus.COMPLETED,
|
||||
"PAUSED": UserMediaListStatus.PAUSED,
|
||||
"REPEATING": UserMediaListStatus.REPEATING,
|
||||
}
|
||||
status_map = {
|
||||
"CURRENT": "watching",
|
||||
"PLANNING": "planning",
|
||||
"COMPLETED": "completed",
|
||||
"DROPPED": "dropped",
|
||||
"PAUSED": "paused",
|
||||
"REPEATING": "repeating",
|
||||
"FINISHED": MediaStatus.FINISHED,
|
||||
"RELEASING": MediaStatus.RELEASING,
|
||||
"NOT_YET_RELEASED": MediaStatus.NOT_YET_RELEASED,
|
||||
"CANCELLED": MediaStatus.CANCELLED,
|
||||
"HIATUS": MediaStatus.HIATUS,
|
||||
}
|
||||
|
||||
|
||||
@@ -123,10 +130,10 @@ def _to_generic_studios(anilist_studios: AnilistStudioNodes) -> List[Studio]:
|
||||
]
|
||||
|
||||
|
||||
def _to_generic_tags(anilist_tags: list[AnilistMediaTag]) -> List[MediaTag]:
|
||||
def _to_generic_tags(anilist_tags: list[AnilistMediaTag]) -> List[MediaTagItem]:
|
||||
"""Maps a list of AniList tags to generic MediaTag objects."""
|
||||
return [
|
||||
MediaTag(name=t["name"], rank=t.get("rank"))
|
||||
MediaTagItem(name=t["name"], rank=t.get("rank"))
|
||||
for t in anilist_tags
|
||||
if t.get("name")
|
||||
]
|
||||
@@ -200,11 +207,11 @@ def _to_generic_streaming_episodes(
|
||||
def _to_generic_user_status(
|
||||
anilist_media: AnilistBaseMediaDataSchema,
|
||||
anilist_list_entry: Optional[AnilistMediaList],
|
||||
) -> Optional[UserListStatus]:
|
||||
) -> Optional[UserListItem]:
|
||||
"""Maps an AniList mediaListEntry to a generic UserListStatus."""
|
||||
if anilist_list_entry:
|
||||
return UserListStatus(
|
||||
status=status_map[anilist_list_entry["status"]], # pyright: ignore
|
||||
return UserListItem(
|
||||
status=user_list_status_map[anilist_list_entry["status"]],
|
||||
progress=anilist_list_entry["progress"],
|
||||
score=anilist_list_entry["score"],
|
||||
repeat=anilist_list_entry["repeat"],
|
||||
@@ -218,9 +225,9 @@ def _to_generic_user_status(
|
||||
if not anilist_media["mediaListEntry"]:
|
||||
return
|
||||
|
||||
return UserListStatus(
|
||||
return UserListItem(
|
||||
id=anilist_media["mediaListEntry"]["id"],
|
||||
status=status_map[anilist_media["mediaListEntry"]["status"]] # pyright: ignore
|
||||
status=user_list_status_map[anilist_media["mediaListEntry"]["status"]]
|
||||
if anilist_media["mediaListEntry"]["status"]
|
||||
else None,
|
||||
progress=anilist_media["mediaListEntry"]["progress"],
|
||||
@@ -236,7 +243,7 @@ def _to_generic_media_item(
|
||||
id_mal=data.get("idMal"),
|
||||
type=data.get("type", "ANIME"),
|
||||
title=_to_generic_media_title(data["title"]),
|
||||
status=data["status"],
|
||||
status=status_map[data["status"]],
|
||||
format=data.get("format"),
|
||||
cover_image=_to_generic_media_image(data["coverImage"]),
|
||||
banner_image=data.get("bannerImage"),
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
"""
|
||||
This module defines the shape of the anilist data that can be received in order to enhance dev experience
|
||||
"""
|
||||
|
||||
# TODO: rename this module to types
|
||||
|
||||
from typing import Literal, TypedDict
|
||||
|
||||
|
||||
|
||||
@@ -9,12 +9,12 @@ from ..types import (
|
||||
MediaItem,
|
||||
MediaSearchResult,
|
||||
MediaStatus,
|
||||
MediaTag,
|
||||
MediaTagItem,
|
||||
MediaTitle,
|
||||
PageInfo,
|
||||
StreamingEpisode,
|
||||
Studio,
|
||||
UserListStatus,
|
||||
UserListItem,
|
||||
UserProfile,
|
||||
)
|
||||
|
||||
@@ -36,7 +36,7 @@ def _to_generic_title(jikan_titles: list[dict]) -> MediaTitle:
|
||||
romaji = None
|
||||
english = None
|
||||
native = None
|
||||
|
||||
|
||||
# Jikan's default title is often the romaji one.
|
||||
# We prioritize specific types if available.
|
||||
for t in jikan_titles:
|
||||
@@ -48,12 +48,8 @@ def _to_generic_title(jikan_titles: list[dict]) -> MediaTitle:
|
||||
english = title_
|
||||
elif type_ == "Japanese":
|
||||
native = title_
|
||||
|
||||
return MediaTitle(
|
||||
romaji=romaji,
|
||||
english=english,
|
||||
native=native
|
||||
)
|
||||
|
||||
return MediaTitle(romaji=romaji, english=english, native=native)
|
||||
|
||||
|
||||
def _to_generic_image(jikan_images: dict) -> MediaImage:
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Literal, Optional, Union
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from .types import UserListStatusType
|
||||
from .types import (
|
||||
MediaFormat,
|
||||
MediaGenre,
|
||||
MediaSeason,
|
||||
MediaSort,
|
||||
MediaStatus,
|
||||
MediaTag,
|
||||
MediaType,
|
||||
UserMediaListSort,
|
||||
UserMediaListStatus,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -9,23 +19,23 @@ class ApiSearchParams:
|
||||
query: Optional[str] = None
|
||||
page: int = 1
|
||||
per_page: Optional[int] = None
|
||||
sort: Optional[Union[str, List[str]]] = None
|
||||
sort: Optional[Union[MediaSort, List[MediaSort]]] = None
|
||||
|
||||
# IDs
|
||||
id_in: Optional[List[int]] = None
|
||||
|
||||
# Genres
|
||||
genre_in: Optional[List[str]] = None
|
||||
genre_not_in: Optional[List[str]] = None
|
||||
genre_in: Optional[List[MediaGenre]] = None
|
||||
genre_not_in: Optional[List[MediaGenre]] = None
|
||||
|
||||
# Tags
|
||||
tag_in: Optional[List[str]] = None
|
||||
tag_not_in: Optional[List[str]] = None
|
||||
tag_in: Optional[List[MediaTag]] = None
|
||||
tag_not_in: Optional[List[MediaTag]] = None
|
||||
|
||||
# Status
|
||||
status_in: Optional[List[str]] = None # Corresponds to [MediaStatus]
|
||||
status: Optional[str] = None # Corresponds to MediaStatus
|
||||
status_not_in: Optional[List[str]] = None # Corresponds to [MediaStatus]
|
||||
status_in: Optional[List[MediaStatus]] = None # Corresponds to [MediaStatus]
|
||||
status: Optional[MediaStatus] = None # Corresponds to MediaStatus
|
||||
status_not_in: Optional[List[MediaStatus]] = None # Corresponds to [MediaStatus]
|
||||
|
||||
# Popularity
|
||||
popularity_greater: Optional[int] = None
|
||||
@@ -37,7 +47,7 @@ class ApiSearchParams:
|
||||
|
||||
# Season and Year
|
||||
seasonYear: Optional[int] = None
|
||||
season: Optional[str] = None
|
||||
season: Optional[MediaSeason] = None
|
||||
|
||||
# Start Date (FuzzyDateInt is often an integer representation like YYYYMMDD)
|
||||
startDate_greater: Optional[int] = None
|
||||
@@ -49,8 +59,8 @@ class ApiSearchParams:
|
||||
endDate_lesser: Optional[int] = None
|
||||
|
||||
# Format and Type
|
||||
format_in: Optional[List[str]] = None # Corresponds to [MediaFormat]
|
||||
type: Optional[str] = None # Corresponds to MediaType (e.g., "ANIME", "MANGA")
|
||||
format_in: Optional[List[MediaFormat]] = None
|
||||
type: Optional[MediaType] = None
|
||||
|
||||
# On List
|
||||
on_list: Optional[bool] = None
|
||||
@@ -58,16 +68,16 @@ class ApiSearchParams:
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class UserListParams:
|
||||
status: UserListStatusType
|
||||
status: UserMediaListStatus
|
||||
page: int = 1
|
||||
type: Optional[str] = None
|
||||
sort: Optional[str] = None
|
||||
type: Optional[MediaType] = None
|
||||
sort: Optional[UserMediaListSort] = None
|
||||
per_page: Optional[int] = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class UpdateListEntryParams:
|
||||
media_id: int
|
||||
status: Optional[UserListStatusType] = None
|
||||
status: Optional[UserMediaListStatus] = None
|
||||
progress: Optional[str] = None
|
||||
score: Optional[float] = None
|
||||
|
||||
@@ -1,25 +1,474 @@
|
||||
from datetime import datetime
|
||||
from typing import List, Literal, Optional
|
||||
from enum import Enum
|
||||
from typing import List, Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
# --- Generic Enums and Type Aliases ---
|
||||
|
||||
MediaType = Literal["ANIME", "MANGA"]
|
||||
MediaStatus = Literal[
|
||||
"FINISHED", "RELEASING", "NOT_YET_RELEASED", "CANCELLED", "HIATUS"
|
||||
]
|
||||
|
||||
UserListStatusType = Literal[
|
||||
"planning", "watching", "completed", "dropped", "paused", "repeating"
|
||||
]
|
||||
# --- Generic Data Models ---
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
# ENUMS
|
||||
class MediaStatus(Enum):
|
||||
FINISHED = "FINISHED"
|
||||
RELEASING = "RELEASING"
|
||||
NOT_YET_RELEASED = "NOT_YET_RELEASED"
|
||||
CANCELLED = "CANCELLED"
|
||||
HIATUS = "HIATUS"
|
||||
|
||||
|
||||
class MediaType(Enum):
|
||||
ANIME = "ANIME"
|
||||
MANGA = "MANGA"
|
||||
|
||||
|
||||
class UserMediaListStatus(Enum):
|
||||
PLANNING = "planning"
|
||||
WATCHING = "watching"
|
||||
COMPLETED = "completed"
|
||||
DROPPED = "dropped"
|
||||
PAUSED = "paused"
|
||||
REPEATING = "repeating"
|
||||
|
||||
|
||||
class MediaGenre(Enum):
|
||||
ACTION = "Action"
|
||||
ADVENTURE = "Adventure"
|
||||
COMEDY = "Comedy"
|
||||
DRAMA = "Drama"
|
||||
ECCHI = "Ecchi"
|
||||
FANTASY = "Fantasy"
|
||||
HORROR = "Horror"
|
||||
MAHOU_SHOUJO = "Mahou Shoujo"
|
||||
MECHA = "Mecha"
|
||||
MUSIC = "Music"
|
||||
MYSTERY = "Mystery"
|
||||
PSYCHOLOGICAL = "Psychological"
|
||||
ROMANCE = "Romance"
|
||||
SCI_FI = "Sci-Fi"
|
||||
SLICE_OF_LIFE = "Slice of Life"
|
||||
SPORTS = "Sports"
|
||||
SUPERNATURAL = "Supernatural"
|
||||
THRILLER = "Thriller"
|
||||
HENTAI = "Hentai"
|
||||
|
||||
|
||||
class MediaFormat(Enum):
|
||||
TV = "TV"
|
||||
TV_SHORT = "TV_SHORT"
|
||||
MOVIE = "MOVIE"
|
||||
SPECIAL = "SPECIAL"
|
||||
OVA = "OVA"
|
||||
ONA = "ONA"
|
||||
MUSIC = "MUSIC"
|
||||
NOVEL = "NOVEL"
|
||||
ONE_SHOT = "ONE_SHOT"
|
||||
|
||||
|
||||
class MediaTag(Enum):
|
||||
# Cast
|
||||
POLYAMOROUS = "Polyamorous"
|
||||
|
||||
# Cast Main Cast
|
||||
ANTI_HERO = "Anti-Hero"
|
||||
ELDERLY_PROTAGONIST = "Elderly Protagonist"
|
||||
ENSEMBLE_CAST = "Ensemble Cast"
|
||||
ESTRANGED_FAMILY = "Estranged Family"
|
||||
FEMALE_PROTAGONIST = "Female Protagonist"
|
||||
MALE_PROTAGONIST = "Male Protagonist"
|
||||
PRIMARILY_ADULT_CAST = "Primarily Adult Cast"
|
||||
PRIMARILY_ANIMAL_CAST = "Primarily Animal Cast"
|
||||
PRIMARILY_CHILD_CAST = "Primarily Child Cast"
|
||||
PRIMARILY_FEMALE_CAST = "Primarily Female Cast"
|
||||
PRIMARILY_MALE_CAST = "Primarily Male Cast"
|
||||
PRIMARILY_TEEN_CAST = "Primarily Teen Cast"
|
||||
|
||||
# Cast Traits
|
||||
AGE_REGRESSION = "Age Regression"
|
||||
AGENDER = "Agender"
|
||||
ALIENS = "Aliens"
|
||||
AMNESIA = "Amnesia"
|
||||
ANGELS = "Angels"
|
||||
ANTHROPOMORPHISM = "Anthropomorphism"
|
||||
AROMANTIC = "Aromantic"
|
||||
ARRANGED_MARRIAGE = "Arranged Marriage"
|
||||
ARTIFICIAL_INTELLIGENCE = "Artificial Intelligence"
|
||||
ASEXUAL = "Asexual"
|
||||
BUTLER = "Butler"
|
||||
CENTAUR = "Centaur"
|
||||
CHIMERA = "Chimera"
|
||||
CHUUNIBYOU = "Chuunibyou"
|
||||
CLONE = "Clone"
|
||||
COSPLAY = "Cosplay"
|
||||
COWBOYS = "Cowboys"
|
||||
CROSSDRESSING = "Crossdressing"
|
||||
CYBORG = "Cyborg"
|
||||
DELINQUENTS = "Delinquents"
|
||||
DEMONS = "Demons"
|
||||
DETECTIVE = "Detective"
|
||||
DINOSAURS = "Dinosaurs"
|
||||
DISABILITY = "Disability"
|
||||
DISSOCIATIVE_IDENTITIES = "Dissociative Identities"
|
||||
DRAGONS = "Dragons"
|
||||
DULLAHAN = "Dullahan"
|
||||
ELF = "Elf"
|
||||
EXHIBITIONISM = "Exhibitionism"
|
||||
FAIRY = "Fairy"
|
||||
FEMBOY = "Femboy"
|
||||
GHOST = "Ghost"
|
||||
GOBLIN = "Goblin"
|
||||
GODS = "Gods"
|
||||
GYARU = "Gyaru"
|
||||
HIKIKOMORI = "Hikikomori"
|
||||
HOMELESS = "Homeless"
|
||||
IDOL = "Idol"
|
||||
INSEKI = "Inseki"
|
||||
KEMONOMIMI = "Kemonomimi"
|
||||
KUUDERE = "Kuudere"
|
||||
MAIDS = "Maids"
|
||||
MERMAID = "Mermaid"
|
||||
MONSTER_BOY = "Monster Boy"
|
||||
MONSTER_GIRL = "Monster Girl"
|
||||
NEKOMIMI = "Nekomimi"
|
||||
NINJA = "Ninja"
|
||||
NUDITY = "Nudity"
|
||||
NUN = "Nun"
|
||||
OFFICE_LADY = "Office Lady"
|
||||
OIRAN = "Oiran"
|
||||
OJOU_SAMA = "Ojou-sama"
|
||||
ORPHAN = "Orphan"
|
||||
PIRATES = "Pirates"
|
||||
ROBOTS = "Robots"
|
||||
SAMURAI = "Samurai"
|
||||
SHRINE_MAIDEN = "Shrine Maiden"
|
||||
SKELETON = "Skeleton"
|
||||
SUCCUBUS = "Succubus"
|
||||
TANNED_SKIN = "Tanned Skin"
|
||||
TEACHER = "Teacher"
|
||||
TOMBOY = "Tomboy"
|
||||
TRANSGENDER = "Transgender"
|
||||
TSUNDERE = "Tsundere"
|
||||
TWINS = "Twins"
|
||||
VAMPIRE = "Vampire"
|
||||
VETERINARIAN = "Veterinarian"
|
||||
VIKINGS = "Vikings"
|
||||
VILLAINESS = "Villainess"
|
||||
VIRGINITY = "Virginity"
|
||||
VTUBER = "VTuber"
|
||||
WEREWOLF = "Werewolf"
|
||||
WITCH = "Witch"
|
||||
YANDERE = "Yandere"
|
||||
ZOMBIE = "Zombie"
|
||||
YOUKAI = "Youkai" # Added
|
||||
|
||||
# Demographic
|
||||
JOSEI = "Josei"
|
||||
KIDS = "Kids"
|
||||
SEINEN = "Seinen"
|
||||
SHOUJO = "Shoujo"
|
||||
SHOUNEN = "Shounen"
|
||||
|
||||
# Setting
|
||||
MATRIARCHY = "Matriarchy"
|
||||
|
||||
# Setting Scene
|
||||
BAR = "Bar"
|
||||
BOARDING_SCHOOL = "Boarding School"
|
||||
CIRCUS = "Circus"
|
||||
COASTAL = "Coastal"
|
||||
COLLEGE = "College"
|
||||
DESERT = "Desert"
|
||||
DUNGEON = "Dungeon"
|
||||
FOREIGN = "Foreign"
|
||||
INN = "Inn"
|
||||
KONBINI = "Konbini"
|
||||
NATURAL_DISASTER = "Natural Disaster"
|
||||
OFFICE = "Office"
|
||||
OUTDOOR = "Outdoor"
|
||||
PRISON = "Prison"
|
||||
RESTAURANT = "Restaurant"
|
||||
RURAL = "Rural"
|
||||
SCHOOL = "School"
|
||||
SCHOOL_CLUB = "School Club"
|
||||
SNOWSCAPE = "Snowscape"
|
||||
URBAN = "Urban"
|
||||
WORK = "Work"
|
||||
|
||||
# Setting Time
|
||||
ACHRONOLOGICAL_ORDER = "Achronological Order"
|
||||
ANACHRONISM = "Anachronism"
|
||||
ANCIENT_CHINA = "Ancient China"
|
||||
DYSTOPIAN = "Dystopian"
|
||||
HISTORICAL = "Historical"
|
||||
TIME_SKIP = "Time Skip"
|
||||
|
||||
# Setting Universe
|
||||
AFTERLIFE = "Afterlife"
|
||||
ALTERNATE_UNIVERSE = "Alternate Universe"
|
||||
AUGMENTED_REALITY = "Augmented Reality"
|
||||
OMEGAVERSE = "Omegaverse"
|
||||
POST_APOCALYPTIC = "Post-Apocalyptic"
|
||||
SPACE = "Space"
|
||||
URBAN_FANTASY = "Urban Fantasy"
|
||||
VIRTUAL_WORLD = "Virtual World"
|
||||
|
||||
# Technical
|
||||
_4_KOMA = "4-koma"
|
||||
ACHROMATIC = "Achromatic"
|
||||
ADVERTISEMENT = "Advertisement"
|
||||
ANTHOLOGY = "Anthology"
|
||||
CGI = "CGI"
|
||||
EPISODIC = "Episodic"
|
||||
FLASH = "Flash"
|
||||
FULL_CGI = "Full CGI"
|
||||
FULL_COLOR = "Full Color"
|
||||
NO_DIALOGUE = "No Dialogue"
|
||||
NON_FICTION = "Non-fiction"
|
||||
POV = "POV"
|
||||
PUPPETRY = "Puppetry"
|
||||
ROTOSCOPING = "Rotoscoping"
|
||||
STOP_MOTION = "Stop Motion"
|
||||
|
||||
# Theme Action
|
||||
ARCHERY = "Archery"
|
||||
BATTLE_ROYALE = "Battle Royale"
|
||||
ESPIONAGE = "Espionage"
|
||||
FUGITIVE = "Fugitive"
|
||||
GUNS = "Guns"
|
||||
MARTIAL_ARTS = "Martial Arts"
|
||||
SPEARPLAY = "Spearplay"
|
||||
SWORDPLAY = "Swordplay"
|
||||
|
||||
# Theme Arts
|
||||
ACTING = "Acting"
|
||||
CALLIGRAPHY = "Calligraphy"
|
||||
CLASSIC_LITERATURE = "Classic Literature"
|
||||
DRAWING = "Drawing"
|
||||
FASHION = "Fashion"
|
||||
FOOD = "Food"
|
||||
MAKEUP = "Makeup"
|
||||
PHOTOGRAPHY = "Photography"
|
||||
RAKUGO = "Rakugo"
|
||||
WRITING = "Writing"
|
||||
|
||||
# Theme Arts-Music
|
||||
BAND = "Band"
|
||||
CLASSICAL_MUSIC = "Classical Music"
|
||||
DANCING = "Dancing"
|
||||
HIP_HOP_MUSIC = "Hip-hop Music"
|
||||
JAZZ_MUSIC = "Jazz Music"
|
||||
METAL_MUSIC = "Metal Music"
|
||||
MUSICAL_THEATER = "Musical Theater"
|
||||
ROCK_MUSIC = "Rock Music"
|
||||
|
||||
# Theme Comedy
|
||||
PARODY = "Parody"
|
||||
SATIRE = "Satire"
|
||||
SLAPSTICK = "Slapstick"
|
||||
SURREAL_COMEDY = "Surreal Comedy"
|
||||
|
||||
# Theme Drama
|
||||
BULLYING = "Bullying"
|
||||
CLASS_STRUGGLE = "Class Struggle"
|
||||
COMING_OF_AGE = "Coming of Age"
|
||||
CONSPIRACY = "Conspiracy"
|
||||
ECO_HORROR = "Eco-Horror"
|
||||
FAKE_RELATIONSHIP = "Fake Relationship"
|
||||
KINGDOM_MANAGEMENT = "Kingdom Management"
|
||||
MASTURBATION = "Masturbation"
|
||||
PREGNANCY = "Pregnancy"
|
||||
RAPE = "Rape"
|
||||
REHABILITATION = "Rehabilitation"
|
||||
REVENGE = "Revenge"
|
||||
SUICIDE = "Suicide"
|
||||
TRAGEDY = "Tragedy"
|
||||
|
||||
# Theme Fantasy
|
||||
ALCHEMY = "Alchemy"
|
||||
BODY_SWAPPING = "Body Swapping"
|
||||
CURSES = "Curses"
|
||||
CULTIVATION = "Cultivation"
|
||||
EXORCISM = "Exorcism"
|
||||
FAIRY_TALE = "Fairy Tale"
|
||||
HENSHIN = "Henshin"
|
||||
ISEKAI = "Isekai"
|
||||
KAIJU = "Kaiju"
|
||||
MAGIC = "Magic"
|
||||
MYTHOLOGY = "Mythology"
|
||||
MEDIEVAL = "Medieval"
|
||||
NECROMANCY = "Necromancy"
|
||||
SHAPESHIFTING = "Shapeshifting"
|
||||
STEAMPUNK = "Steampunk"
|
||||
SUPER_POWER = "Super Power"
|
||||
SUPERHERO = "Superhero"
|
||||
WUXIA = "Wuxia"
|
||||
|
||||
# Theme Game
|
||||
BOARD_GAME = "Board Game"
|
||||
E_SPORTS = "E-Sports"
|
||||
VIDEO_GAMES = "Video Games"
|
||||
|
||||
# Theme Game-Card & Board Game
|
||||
CARD_BATTLE = "Card Battle"
|
||||
GO = "Go"
|
||||
KARUTA = "Karuta"
|
||||
MAHJONG = "Mahjong"
|
||||
POKER = "Poker"
|
||||
SHOGI = "Shogi"
|
||||
|
||||
# Theme Game-Sport
|
||||
ACROBATICS = "Acrobatics"
|
||||
AIRSOFT = "Airsoft"
|
||||
AMERICAN_FOOTBALL = "American Football"
|
||||
ATHLETICS = "Athletics"
|
||||
BADMINTON = "Badminton"
|
||||
BASEBALL = "Baseball"
|
||||
BASKETBALL = "Basketball"
|
||||
BOWLING = "Bowling"
|
||||
BOXING = "Boxing"
|
||||
CHEERLEADING = "Cheerleading"
|
||||
CYCLING = "Cycling"
|
||||
FENCING = "Fencing"
|
||||
FISHING = "Fishing"
|
||||
FITNESS = "Fitness"
|
||||
FOOTBALL = "Football"
|
||||
GOLF = "Golf"
|
||||
HANDBALL = "Handball"
|
||||
ICE_SKATING = "Ice Skating"
|
||||
JUDO = "Judo"
|
||||
LACROSSE = "Lacrosse"
|
||||
PARKOUR = "Parkour"
|
||||
RUGBY = "Rugby"
|
||||
SCUBA_DIVING = "Scuba Diving"
|
||||
SKATEBOARDING = "Skateboarding"
|
||||
SUMO = "Sumo"
|
||||
SURFING = "Surfing"
|
||||
SWIMMING = "Swimming"
|
||||
TABLE_TENNIS = "Table Tennis"
|
||||
TENNIS = "Tennis"
|
||||
VOLLEYBALL = "Volleyball"
|
||||
WRESTLING = "Wrestling"
|
||||
|
||||
# Theme Other
|
||||
ADOPTION = "Adoption"
|
||||
ANIMALS = "Animals"
|
||||
ASTRONOMY = "Astronomy"
|
||||
AUTOBIOGRAPHICAL = "Autobiographical"
|
||||
BIOGRAPHICAL = "Biographical"
|
||||
BODY_HORROR = "Body Horror"
|
||||
BODY_IMAGE = "Body Image"
|
||||
CANNIBALISM = "Cannibalism"
|
||||
CHIBI = "Chibi"
|
||||
COHABITATION = "Cohabitation"
|
||||
COSMIC_HORROR = "Cosmic Horror"
|
||||
CREATURE_TAMING = "Creature Taming"
|
||||
CRIME = "Crime"
|
||||
CROSSOVER = "Crossover"
|
||||
DEATH_GAME = "Death Game"
|
||||
DENPA = "Denpa"
|
||||
DEFLORATION = "Defloration"
|
||||
DRUGS = "Drugs"
|
||||
ECONOMICS = "Economics"
|
||||
EDUCATIONAL = "Educational"
|
||||
ENVIRONMENTAL = "Environmental"
|
||||
ERO_GURO = "Ero Guro"
|
||||
FILMMAKING = "Filmmaking"
|
||||
FOUND_FAMILY = "Found Family"
|
||||
GAMBLING = "Gambling"
|
||||
GENDER_BENDING = "Gender Bending"
|
||||
GORE = "Gore"
|
||||
HYPERSEXUALITY = "Hypersexuality"
|
||||
LANGUAGE_BARRIER = "Language Barrier"
|
||||
LARGE_BREASTS = "Large Breasts"
|
||||
LGBTQ_PLUS_THEMES = "LGBTQ+ Themes"
|
||||
LOST_CIVILIZATION = "Lost Civilization"
|
||||
MARRIAGE = "Marriage"
|
||||
MEDICINE = "Medicine"
|
||||
MEMORY_MANIPULATION = "Memory Manipulation"
|
||||
META = "Meta"
|
||||
MIXED_MEDIA = "Mixed Media"
|
||||
MOUNTAINEERING = "Mountaineering"
|
||||
NOIR = "Noir"
|
||||
OTAKU_CULTURE = "Otaku Culture"
|
||||
OUTDOOR_ACTIVITIES = "Outdoor Activities"
|
||||
PANDEMIC = "Pandemic"
|
||||
PHILOSOPHY = "Philosophy"
|
||||
POLITICS = "Politics"
|
||||
PROXY_BATTLE = "Proxy Battle"
|
||||
PSYCHOSEXUAL = "Psychosexual"
|
||||
REINCARNATION = "Reincarnation"
|
||||
RELIGION = "Religion"
|
||||
RESCUE = "Rescue"
|
||||
ROYAL_AFFAIRS = "Royal Affairs"
|
||||
SLAVERY = "Slavery"
|
||||
SOFTWARE_DEVELOPMENT = "Software Development"
|
||||
SURVIVAL = "Survival"
|
||||
TERRORISM = "Terrorism"
|
||||
THREESOME = "Threesome"
|
||||
TORTURE = "Torture"
|
||||
TRAVEL = "Travel"
|
||||
WAR = "War"
|
||||
WILDERNESS = "Wilderness"
|
||||
VORE = "Vore" # Added
|
||||
|
||||
# Theme Other-Organisations
|
||||
ASSASSINS = "Assassins"
|
||||
CRIMINAL_ORGANIZATION = "Criminal Organization"
|
||||
CULT = "Cult"
|
||||
FIREFIGHTERS = "Firefighters"
|
||||
GANGS = "Gangs"
|
||||
MAFIA = "Mafia"
|
||||
MILITARY = "Military"
|
||||
POLICE = "Police"
|
||||
TRIADS = "Triads"
|
||||
YAKUZA = "Yakuza"
|
||||
|
||||
# Theme Other-Vehicle
|
||||
AVIATION = "Aviation"
|
||||
CARS = "Cars"
|
||||
MOPEDS = "Mopeds"
|
||||
MOTORCYCLES = "Motorcycles"
|
||||
SHIPS = "Ships"
|
||||
TANKS = "Tanks"
|
||||
TRAINS = "Trains"
|
||||
|
||||
# Theme Romance
|
||||
AGE_GAP = "Age Gap"
|
||||
BISEXUAL = "Bisexual"
|
||||
BOYS_LOVE = "Boys' Love"
|
||||
FEMALE_HAREM = "Female Harem"
|
||||
HETEROSEXUAL = "Heterosexual"
|
||||
INCEST = "Incest"
|
||||
LOVE_TRIANGLE = "Love Triangle"
|
||||
MALE_HAREM = "Male Harem"
|
||||
MATCHMAKING = "Matchmaking"
|
||||
MIXED_GENDER_HAREM = "Mixed Gender Harem"
|
||||
PUBLIC_SEX = "Public Sex"
|
||||
TEENS_LOVE = "Teens' Love"
|
||||
UNREQUITED_LOVE = "Unrequited Love"
|
||||
YURI = "Yuri"
|
||||
|
||||
# Theme Sci Fi
|
||||
CYBERPUNK = "Cyberpunk"
|
||||
SPACE_OPERA = "Space Opera"
|
||||
TIME_LOOP = "Time Loop"
|
||||
TIME_MANIPULATION = "Time Manipulation"
|
||||
TOKUSATSU = "Tokusatsu"
|
||||
|
||||
# Theme Sci Fi-Mecha
|
||||
REAL_ROBOT = "Real Robot"
|
||||
SUPER_ROBOT = "Super Robot"
|
||||
|
||||
# Theme Slice of Life
|
||||
AGRICULTURE = "Agriculture"
|
||||
CUTE_BOYS_DOING_CUTE_THINGS = "Cute Boys Doing Cute Things"
|
||||
CUTE_GIRLS_DOING_CUTE_THINGS = "Cute Girls Doing Cute Things"
|
||||
FAMILY_LIFE = "Family Life"
|
||||
HORTICULTURE = "Horticulture"
|
||||
IYASHIKEI = "Iyashikei"
|
||||
PARENTHOOD = "Parenthood"
|
||||
|
||||
|
||||
# MODELS
|
||||
class BaseApiModel(BaseModel):
|
||||
"""Base model for all API types."""
|
||||
|
||||
pass
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
|
||||
class MediaImage(BaseApiModel):
|
||||
@@ -50,22 +499,22 @@ class AiringSchedule(BaseApiModel):
|
||||
"""A generic representation of the next airing episode."""
|
||||
|
||||
episode: int
|
||||
airing_at: datetime | None = None
|
||||
airing_at: Optional[datetime] = None
|
||||
|
||||
|
||||
class Studio(BaseApiModel):
|
||||
"""A generic representation of an animation studio."""
|
||||
|
||||
id: int | None = None
|
||||
name: str | None = None
|
||||
favourites: int | None = None
|
||||
is_animation_studio: bool | None = None
|
||||
id: Optional[int] = None
|
||||
name: Optional[str] = None
|
||||
favourites: Optional[int] = None
|
||||
is_animation_studio: Optional[bool] = None
|
||||
|
||||
|
||||
class MediaTag(BaseApiModel):
|
||||
class MediaTagItem(BaseApiModel):
|
||||
"""A generic representation of a descriptive tag."""
|
||||
|
||||
name: str
|
||||
name: MediaTag
|
||||
rank: Optional[int] = None # Percentage relevance from 0-100
|
||||
|
||||
|
||||
@@ -76,12 +525,11 @@ class StreamingEpisode(BaseApiModel):
|
||||
thumbnail: Optional[str] = None
|
||||
|
||||
|
||||
class UserListStatus(BaseApiModel):
|
||||
class UserListItem(BaseApiModel):
|
||||
"""Generic representation of a user's list status for a media item."""
|
||||
|
||||
id: int | None = None
|
||||
|
||||
status: Optional[UserListStatusType] = None
|
||||
id: Optional[int] = None
|
||||
status: Optional[UserMediaListStatus] = None
|
||||
progress: Optional[int] = None
|
||||
score: Optional[float] = None
|
||||
repeat: Optional[int] = None
|
||||
@@ -95,9 +543,9 @@ class MediaItem(BaseApiModel):
|
||||
id: int
|
||||
title: MediaTitle
|
||||
id_mal: Optional[int] = None
|
||||
type: MediaType = "ANIME"
|
||||
status: Optional[str] = None
|
||||
format: Optional[str] = None # e.g., TV, MOVIE, OVA
|
||||
type: MediaType = MediaType.ANIME
|
||||
status: MediaStatus = MediaStatus.FINISHED
|
||||
format: MediaFormat = MediaFormat.TV
|
||||
|
||||
cover_image: Optional[MediaImage] = None
|
||||
banner_image: Optional[str] = None
|
||||
@@ -106,8 +554,8 @@ class MediaItem(BaseApiModel):
|
||||
description: Optional[str] = None
|
||||
episodes: Optional[int] = None
|
||||
duration: Optional[int] = None # In minutes
|
||||
genres: List[str] = Field(default_factory=list)
|
||||
tags: List[MediaTag] = Field(default_factory=list)
|
||||
genres: List[MediaGenre] = Field(default_factory=list)
|
||||
tags: List[MediaTagItem] = Field(default_factory=list)
|
||||
studios: List[Studio] = Field(default_factory=list)
|
||||
synonymns: List[str] = Field(default_factory=list)
|
||||
|
||||
@@ -124,7 +572,7 @@ class MediaItem(BaseApiModel):
|
||||
streaming_episodes: List[StreamingEpisode] = Field(default_factory=list)
|
||||
|
||||
# user related
|
||||
user_status: Optional[UserListStatus] = None
|
||||
user_status: Optional[UserListItem] = None
|
||||
|
||||
|
||||
class PageInfo(BaseApiModel):
|
||||
@@ -150,3 +598,126 @@ class UserProfile(BaseApiModel):
|
||||
name: str
|
||||
avatar_url: Optional[str] = None
|
||||
banner_url: Optional[str] = None
|
||||
|
||||
|
||||
# ENUMS
|
||||
class MediaSort(Enum):
|
||||
ID = "ID"
|
||||
ID_DESC = "ID_DESC"
|
||||
TITLE_ROMAJI = "TITLE_ROMAJI"
|
||||
TITLE_ROMAJI_DESC = "TITLE_ROMAJI_DESC"
|
||||
TITLE_ENGLISH = "TITLE_ENGLISH"
|
||||
TITLE_ENGLISH_DESC = "TITLE_ENGLISH_DESC"
|
||||
TITLE_NATIVE = "TITLE_NATIVE"
|
||||
TITLE_NATIVE_DESC = "TITLE_NATIVE_DESC"
|
||||
TYPE = "TYPE"
|
||||
TYPE_DESC = "TYPE_DESC"
|
||||
FORMAT = "FORMAT"
|
||||
FORMAT_DESC = "FORMAT_DESC"
|
||||
START_DATE = "START_DATE"
|
||||
START_DATE_DESC = "START_DATE_DESC"
|
||||
END_DATE = "END_DATE"
|
||||
END_DATE_DESC = "END_DATE_DESC"
|
||||
SCORE = "SCORE"
|
||||
SCORE_DESC = "SCORE_DESC"
|
||||
POPULARITY = "POPULARITY"
|
||||
POPULARITY_DESC = "POPULARITY_DESC"
|
||||
TRENDING = "TRENDING"
|
||||
TRENDING_DESC = "TRENDING_DESC"
|
||||
EPISODES = "EPISODES"
|
||||
EPISODES_DESC = "EPISODES_DESC"
|
||||
DURATION = "DURATION"
|
||||
DURATION_DESC = "DURATION_DESC"
|
||||
STATUS = "STATUS"
|
||||
STATUS_DESC = "STATUS_DESC"
|
||||
CHAPTERS = "CHAPTERS"
|
||||
CHAPTERS_DESC = "CHAPTERS_DESC"
|
||||
VOLUMES = "VOLUMES"
|
||||
VOLUMES_DESC = "VOLUMES_DESC"
|
||||
UPDATED_AT = "UPDATED_AT"
|
||||
UPDATED_AT_DESC = "UPDATED_AT_DESC"
|
||||
SEARCH_MATCH = "SEARCH_MATCH"
|
||||
FAVOURITES = "FAVOURITES"
|
||||
FAVOURITES_DESC = "FAVOURITES_DESC"
|
||||
|
||||
|
||||
class UserMediaListSort(Enum):
|
||||
MEDIA_ID = "MEDIA_ID"
|
||||
MEDIA_ID_DESC = "MEDIA_ID_DESC"
|
||||
SCORE = "SCORE"
|
||||
SCORE_DESC = "SCORE_DESC"
|
||||
STATUS = "STATUS"
|
||||
STATUS_DESC = "STATUS_DESC"
|
||||
PROGRESS = "PROGRESS"
|
||||
PROGRESS_DESC = "PROGRESS_DESC"
|
||||
PROGRESS_VOLUMES = "PROGRESS_VOLUMES"
|
||||
PROGRESS_VOLUMES_DESC = "PROGRESS_VOLUMES_DESC"
|
||||
REPEAT = "REPEAT"
|
||||
REPEAT_DESC = "REPEAT_DESC"
|
||||
PRIORITY = "PRIORITY"
|
||||
PRIORITY_DESC = "PRIORITY_DESC"
|
||||
STARTED_ON = "STARTED_ON"
|
||||
STARTED_ON_DESC = "STARTED_ON_DESC"
|
||||
FINISHED_ON = "FINISHED_ON"
|
||||
FINISHED_ON_DESC = "FINISHED_ON_DESC"
|
||||
ADDED_TIME = "ADDED_TIME"
|
||||
ADDED_TIME_DESC = "ADDED_TIME_DESC"
|
||||
UPDATED_TIME = "UPDATED_TIME"
|
||||
UPDATED_TIME_DESC = "UPDATED_TIME_DESC"
|
||||
MEDIA_TITLE_ROMAJI = "MEDIA_TITLE_ROMAJI"
|
||||
MEDIA_TITLE_ROMAJI_DESC = "MEDIA_TITLE_ROMAJI_DESC"
|
||||
MEDIA_TITLE_ENGLISH = "MEDIA_TITLE_ENGLISH"
|
||||
MEDIA_TITLE_ENGLISH_DESC = "MEDIA_TITLE_ENGLISH_DESC"
|
||||
MEDIA_TITLE_NATIVE = "MEDIA_TITLE_NATIVE"
|
||||
MEDIA_TITLE_NATIVE_DESC = "MEDIA_TITLE_NATIVE_DESC"
|
||||
MEDIA_POPULARITY = "MEDIA_POPULARITY"
|
||||
MEDIA_POPULARITY_DESC = "MEDIA_POPULARITY_DESC"
|
||||
MEDIA_SCORE = "MEDIA_SCORE"
|
||||
MEDIA_SCORE_DESC = "MEDIA_SCORE_DESC"
|
||||
MEDIA_START_DATE = "MEDIA_START_DATE"
|
||||
MEDIA_START_DATE_DESC = "MEDIA_START_DATE_DESC"
|
||||
MEDIA_RATING = "MEDIA_RATING"
|
||||
MEDIA_RATING_DESC = "MEDIA_RATING_DESC"
|
||||
|
||||
|
||||
class MediaSeason(Enum):
|
||||
WINTER = "WINTER"
|
||||
SPRING = "SPRING"
|
||||
SUMMER = "SUMMER"
|
||||
FALL = "FALL"
|
||||
|
||||
|
||||
class MediaYear(Enum):
|
||||
_1900 = "1900"
|
||||
_1910 = "1910"
|
||||
_1920 = "1920"
|
||||
_1930 = "1930"
|
||||
_1940 = "1940"
|
||||
_1950 = "1950"
|
||||
_1960 = "1960"
|
||||
_1970 = "1970"
|
||||
_1980 = "1980"
|
||||
_1990 = "1990"
|
||||
_2000 = "2000"
|
||||
_2004 = "2004"
|
||||
_2005 = "2005"
|
||||
_2006 = "2006"
|
||||
_2007 = "2007"
|
||||
_2008 = "2008"
|
||||
_2009 = "2009"
|
||||
_2010 = "2010"
|
||||
_2011 = "2011"
|
||||
_2012 = "2012"
|
||||
_2013 = "2013"
|
||||
_2014 = "2014"
|
||||
_2015 = "2015"
|
||||
_2016 = "2016"
|
||||
_2017 = "2017"
|
||||
_2018 = "2018"
|
||||
_2019 = "2019"
|
||||
_2020 = "2020"
|
||||
_2021 = "2021"
|
||||
_2022 = "2022"
|
||||
_2023 = "2023"
|
||||
_2024 = "2024"
|
||||
_2025 = "2025"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
from typing import Literal, Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
|
||||
class BaseAnimeProviderModel(BaseModel):
|
||||
pass
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
|
||||
class PageInfo(BaseAnimeProviderModel):
|
||||
@@ -35,7 +35,6 @@ class SearchResult(BaseAnimeProviderModel):
|
||||
class SearchResults(BaseAnimeProviderModel):
|
||||
page_info: PageInfo
|
||||
results: list[SearchResult]
|
||||
model_config = {"frozen": True}
|
||||
|
||||
|
||||
class AnimeEpisodeInfo(BaseAnimeProviderModel):
|
||||
|
||||
Reference in New Issue
Block a user