feat: improve api types

This commit is contained in:
Benexl
2025-07-23 18:48:57 +03:00
parent 6c30cf808b
commit d78b62fcee
19 changed files with 807 additions and 700 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,
):

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"),

View File

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

View File

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

View File

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

View File

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

View File

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