"""
Trace.moe screenshot-search models.
:class:`TraceHit` is the typed shape returned by ``animedex trace
search``: each hit carries the AniList id (so the result can be
chained into the AniList backend for richer metadata), the episode
and timecode where the frame appears, a similarity score, and
optional preview media URLs.
:class:`TraceQuota` is the typed shape of ``animedex trace quota``
(``GET /me``). The upstream returns the caller's IP in the ``id``
field; the mapper unconditionally drops it before constructing the
record (of #4 covered the same vector for fixtures).
"""
from __future__ import annotations
from typing import Optional
from pydantic import Field
from animedex.models.anime import AnimeTitle
from animedex.models.common import AnimedexModel, SourceTag
[docs]
class TraceHit(AnimedexModel):
"""A single Trace.moe match.
:ivar anilist_id: AniList identifier of the matched series. Use
this to chain into the AniList backend for
full metadata.
:vartype anilist_id: int
:ivar anilist_title: AniList title block, populated when the
``/search`` call was made with
``anilistInfo=true``. Saves a follow-up
AniList round-trip.
:vartype anilist_title: AnimeTitle or None
:ivar similarity: Match confidence in the closed interval [0, 1].
Trace.moe documentation considers values below
~0.87 unreliable.
:vartype similarity: float
:ivar episode: Episode identifier (string-typed because some
matches return non-integer values like ``"1.5"``
for OVAs).
:vartype episode: str or None
:ivar start_at_seconds: Scene start timestamp inside the episode,
in seconds.
:vartype start_at_seconds: float
:ivar frame_at_seconds: Exact matched-frame timestamp, in seconds.
Lies between ``start_at_seconds`` and
``end_at_seconds``.
:vartype frame_at_seconds: float
:ivar end_at_seconds: Scene end timestamp, in seconds.
:vartype end_at_seconds: float
:ivar episode_filename: Source video filename. Useful for human
verification of the match (e.g.
``"[Group][Show][05][1080p].mkv"``).
:vartype episode_filename: str or None
:ivar episode_duration_seconds: Total length of the matched
episode, in seconds.
:vartype episode_duration_seconds: float or None
:ivar preview_video_url: Short MP4 preview of the matched scene.
:vartype preview_video_url: str or None
:ivar preview_image_url: Single-frame JPEG preview.
:vartype preview_image_url: str or None
:ivar source: Provenance tag.
:vartype source: SourceTag
"""
anilist_id: int
anilist_title: Optional[AnimeTitle] = None
similarity: float = Field(ge=0.0, le=1.0)
episode: Optional[str] = None
start_at_seconds: float
frame_at_seconds: float
end_at_seconds: float
episode_filename: Optional[str] = None
episode_duration_seconds: Optional[float] = None
preview_video_url: Optional[str] = None
preview_image_url: Optional[str] = None
source: SourceTag
[docs]
class TraceQuota(AnimedexModel):
"""Trace.moe quota state for the calling client.
Returned by ``GET /me``. The upstream payload also carries an
``id`` field that holds the caller's egress IP — the mapper
drops it unconditionally so the value never appears in cache
rows or rendered output.
:ivar priority: API priority class (0 for anonymous, higher for
sponsor / patron tiers).
:vartype priority: int
:ivar concurrency: Max simultaneous searches the tier allows.
Anonymous tier: 1.
:vartype concurrency: int
:ivar quota: Monthly search budget.
:vartype quota: int
:ivar quota_used: Searches consumed this month. Upstream returns
this as a JSON string; the mapper coerces to
``int``.
:vartype quota_used: int
:ivar source: Provenance tag.
:vartype source: SourceTag
"""
priority: int
concurrency: int
quota: int
quota_used: int
source: SourceTag
[docs]
def selftest() -> bool:
"""Smoke-test the trace models.
:return: ``True`` on success; raises on schema errors.
:rtype: bool
"""
from datetime import datetime, timezone
src = SourceTag(backend="_selftest", fetched_at=datetime.now(timezone.utc))
hit = TraceHit(
anilist_id=1,
anilist_title=AnimeTitle(romaji="x"),
similarity=0.9,
episode="1",
start_at_seconds=0.0,
frame_at_seconds=0.5,
end_at_seconds=1.0,
episode_filename="x.mkv",
episode_duration_seconds=1500.0,
preview_video_url="https://x.invalid/p.mp4",
preview_image_url="https://x.invalid/p.jpg",
source=src,
)
TraceHit.model_validate_json(hit.model_dump_json())
quota = TraceQuota(priority=0, concurrency=1, quota=100, quota_used=18, source=src)
TraceQuota.model_validate_json(quota.model_dump_json())
return True