Source code for animedex.backends.waifu.models

"""Rich Waifu.im dataclasses (one per resource type).

Waifu.im wraps every listing in a ``{items, pageNumber, totalPages,
totalCount, pageSize, hasPreviousPage, hasNextPage}`` envelope. The
high-level API extracts ``items`` and validates each row through a
typed model; per the project's lossless contract every model
inherits from :class:`BackendRichModel` (``extra='allow'``,
``populate_by_name=True``, ``frozen=True``) so any field upstream
adds round-trips through ``model_dump``.

The :meth:`WaifuImage.to_common` projection maps image records onto
the cross-source :class:`~animedex.models.art.ArtPost` shape.
``WaifuTag`` and ``WaifuArtist`` have no cross-source common type
and surface as their rich shape only.
"""

from __future__ import annotations

from typing import List, Optional

from animedex.models.art import ArtPost
from animedex.models.common import BackendRichModel, SourceTag


# ---------- nested sub-blocks ----------


[docs] class WaifuTag(BackendRichModel): """A single tag record (also nested inside :class:`WaifuImage`). :ivar id: Numeric tag ID. :vartype id: int :ivar name: Display name (capitalised, e.g. ``"Waifu"``). :vartype name: str or None :ivar slug: URL-safe slug (lowercased, e.g. ``"waifu"``). :vartype slug: str or None :ivar description: Short description. :vartype description: str or None :ivar imageCount: Number of images carrying this tag. :vartype imageCount: int or None :ivar reviewStatus: Upstream's review state (``"Accepted"``). :vartype reviewStatus: str or None """ id: int name: Optional[str] = None slug: Optional[str] = None description: Optional[str] = None imageCount: Optional[int] = None reviewStatus: Optional[str] = None creatorId: Optional[int] = None source_tag: Optional[SourceTag] = None
[docs] class WaifuArtist(BackendRichModel): """A single artist record (also nested inside :class:`WaifuImage`). :ivar id: Numeric artist ID. :vartype id: int :ivar name: Artist display name. :vartype name: str or None :ivar patreon: Artist's Patreon URL. :vartype patreon: str or None :ivar pixiv: Artist's Pixiv URL. :vartype pixiv: str or None :ivar twitter: Artist's Twitter / X URL. :vartype twitter: str or None :ivar deviantArt: Artist's DeviantArt URL. :vartype deviantArt: str or None :ivar imageCount: Number of images by this artist in the catalogue. :vartype imageCount: int or None """ id: int name: Optional[str] = None patreon: Optional[str] = None pixiv: Optional[str] = None twitter: Optional[str] = None deviantArt: Optional[str] = None reviewStatus: Optional[str] = None creatorId: Optional[int] = None imageCount: Optional[int] = None source_tag: Optional[SourceTag] = None
[docs] class WaifuImageDimensions(BackendRichModel): """Width / height block on :class:`WaifuImage`. Note that Waifu.im flattens ``width`` / ``height`` directly onto the image record rather than nesting them, so this class is here for parity with other backends' typed access — Waifu.im itself has no nested dimensions object.""" width: Optional[int] = None height: Optional[int] = None
[docs] class WaifuImage(BackendRichModel): """A single image record from Waifu.im. Returned by ``/images`` and ``/images/{id}``. Carries explicit ``isNsfw`` / ``isAnimated`` flags + nested ``tags`` and ``artists`` lists. :ivar id: Numeric image ID. :vartype id: int :ivar url: Direct asset URL. :vartype url: str :ivar source: Upstream source URL (artist's pixiv, twitter, etc). :vartype source: str or None :ivar isNsfw: NSFW flag — the canonical signal for the ``isNsfw=`` query parameter on ``/images``. :vartype isNsfw: bool or None :ivar isAnimated: Whether the asset is an animated GIF / WebP. :vartype isAnimated: bool or None :ivar width: Image width in pixels. :vartype width: int or None :ivar height: Image height in pixels. :vartype height: int or None :ivar dominantColor: Hex colour summarising the image (used for placeholder backgrounds). :vartype dominantColor: str or None :ivar tags: Nested list of typed tag records. :vartype tags: list of WaifuTag :ivar artists: Nested list of typed artist records. :vartype artists: list of WaifuArtist """ id: int url: str source: Optional[str] = None isNsfw: Optional[bool] = None isAnimated: Optional[bool] = None width: Optional[int] = None height: Optional[int] = None perceptualHash: Optional[str] = None extension: Optional[str] = None dominantColor: Optional[str] = None uploaderId: Optional[int] = None uploadedAt: Optional[str] = None byteSize: Optional[int] = None favorites: Optional[int] = None likedAt: Optional[str] = None addedToAlbumAt: Optional[str] = None reviewStatus: Optional[str] = None tags: List[WaifuTag] = [] artists: List[WaifuArtist] = [] albums: Optional[List[dict]] = None source_tag: Optional[SourceTag] = None
[docs] def to_common(self) -> ArtPost: """Project this image onto the cross-source :class:`~animedex.models.art.ArtPost` shape. ``rating`` derives from ``isNsfw``: ``True`` → ``"e"`` (Danbooru's ``explicit``), ``False`` → ``"g"`` (general). ``tags`` come from the nested ``tags[].slug`` list. ``artist`` is the first nested artist's ``name``, when present. """ rating = "e" if self.isNsfw else ("g" if self.isNsfw is False else None) artist_name = None if self.artists: artist_name = self.artists[0].name tag_slugs = [t.slug for t in self.tags if t.slug] return ArtPost( id=f"waifu:{self.id}", url=self.url, rating=rating, tags=tag_slugs, score=self.favorites, artist=artist_name, source_url=self.source, width=self.width, height=self.height, source=self.source_tag or _default_src(), )
[docs] class WaifuUser(BackendRichModel): """The authenticated user record from ``/users/me``. :ivar id: Numeric Waifu.im user id. :vartype id: int :ivar name: Display name (typically the user's Discord username). :vartype name: str or None :ivar discordId: Discord snowflake. :vartype discordId: str or None :ivar avatarUrl: Discord-issued avatar URL. :vartype avatarUrl: str or None :ivar role: Role label (``"User"`` / ``"Moderator"`` / ``"Administrator"`` etc.). :vartype role: str or None """ id: int name: Optional[str] = None discordId: Optional[str] = None avatarUrl: Optional[str] = None role: Optional[str] = None isBlacklisted: Optional[bool] = None blacklistReason: Optional[str] = None requestCount: Optional[int] = None apiKeyRequestCount: Optional[int] = None jwtRequestCount: Optional[int] = None uploadedAt: Optional[str] = None source_tag: Optional[SourceTag] = None
[docs] class WaifuStats(BackendRichModel): """Public-statistics envelope from ``/stats/public``: counters summarising the upstream's content + traffic. :ivar totalRequests: Total request count served by the upstream (lifetime). :vartype totalRequests: int or None :ivar totalImages: Total image count in the catalogue. :vartype totalImages: int or None :ivar totalTags: Total tag count. :vartype totalTags: int or None :ivar totalArtists: Total artist count. :vartype totalArtists: int or None """ totalRequests: Optional[int] = None totalImages: Optional[int] = None totalTags: Optional[int] = None totalArtists: Optional[int] = None source_tag: Optional[SourceTag] = None
# ---------- helpers ---------- def _default_src() -> SourceTag: """Construct a fallback :class:`SourceTag` when one isn't already attached. Used by ``to_common()`` for direct-from-JSON construction paths that bypass the high-level fetch helper.""" from datetime import datetime, timezone return SourceTag(backend="waifu", fetched_at=datetime.now(timezone.utc))
[docs] def selftest() -> bool: """Smoke-test the Waifu.im rich models. Validates a synthetic :class:`WaifuImage` round-trips through ``model_dump_json`` / ``model_validate_json`` and projects to a well-formed :class:`~animedex.models.art.ArtPost`, including the ``isNsfw`` → rating mapping. :return: ``True`` on success; raises on schema drift. :rtype: bool """ from datetime import datetime, timezone src = SourceTag(backend="_selftest", fetched_at=datetime.now(timezone.utc)) img = WaifuImage.model_validate( { "id": 7115, "url": "https://cdn.waifu.im/7115.jpg", "isNsfw": False, "width": 2000, "height": 3000, "tags": [{"id": 12, "name": "Waifu", "slug": "waifu"}], "artists": [{"id": 6, "name": "An Artist"}], "source_tag": src.model_dump(), } ) WaifuImage.model_validate_json(img.model_dump_json()) common = img.to_common() assert common.id == "waifu:7115" assert common.rating == "g" assert common.tags == ["waifu"] assert common.artist == "An Artist" nsfw = WaifuImage.model_validate({"id": 1, "url": "x", "isNsfw": True}) assert nsfw.to_common().rating == "e" return True