Documentation
¶
Overview ¶
Package animelist consumes the Anime-Lists/anime-lists community XML and exposes per-TMDB-id lookups for downstream consumers (the show refresh path and the search query builder).
Why this exists: TMDB's anime metadata is structurally inconsistent — many shows are served as one giant Season 1 (Jujutsu Kaisen has 47+ episodes under TMDB S1 even though TVDB and the wider anime community treat it as 3 distinct seasons). Indexers and fansub release groups tag anime episodes with absolute numbering ("Show - 48"), not the TMDB-style S01E48 we naively emit. Without this mapping, episode search for anime returns zero results.
The Anime-Lists XML at https://github.com/Anime-Lists/anime-lists is the canonical community-maintained mapping among AniDB / TVDB / TMDB ids and per-episode offsets. We fetch it on a daily schedule, persist it to disk for fallback, and serve point lookups by TMDB id.
Direct TVDB or AniDB integration was considered and rejected — see memory/project_beacon_tvdb_proxy.md for the reasoning. tl;dr both require credentials end users won't have.
Index ¶
- type Mapping
- type Service
- func (s *Service) AbsoluteToTMDBEpisode(tmdbID, abs int) (season, episode int, ok bool)
- func (s *Service) IsAnime(tmdbID int) bool
- func (s *Service) LastLoaded() time.Time
- func (s *Service) Lookup(tmdbID int) (*Mapping, bool)
- func (s *Service) Mappings(tmdbID int) []*Mapping
- func (s *Service) Start(ctx context.Context)
- func (s *Service) TVDBSeasonToAbsolute(tmdbID, tvdbSeason, tvdbEpisode int) (int, bool)
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Mapping ¶
type Mapping struct {
// AniDBID is the AniDB anime ID (the canonical anime metadata id).
AniDBID int
// TVDBID is the TheTVDB series ID, or 0 when no TVDB mapping exists.
TVDBID int
// TMDBTV is the TMDB TV id; 0 when no mapping. This is our primary
// lookup key — Pilot stores TMDB tv ids on every series row.
TMDBTV int
// DefaultTVDBSeason is the TVDB season number this anime maps to.
// Special value -1 indicates "absolute" — the show's episodes are
// tracked by absolute number on TVDB, not season-relative. For our
// purposes this just confirms the show is anime; we use the
// absolute number for indexer queries either way.
DefaultTVDBSeason int
// EpisodeOffset shifts incoming episode numbers by this much when
// translating between AniDB and TVDB layouts. 0 for most shows.
EpisodeOffset int
// TMDBSeason is the TMDB season number this AniDB entry maps to.
// Used to disambiguate when a single TMDB tv id covers multiple
// AniDB cours (rare but happens, e.g. Jujutsu Kaisen 95479 has
// entries for both tmdbseason=1 and tmdbseason=2).
TMDBSeason int
// TMDBOffset is added to the cour-relative episode number to get
// the TMDB-relative episode number. For the JJK incident: cour 3
// (tvdbseason=3) maps to TMDB season=1 with tmdboffset=47, so
// TVDB S03E01 → TMDB S01E48 → absolute 48.
TMDBOffset int
// Name is the AniDB name for this anime. Surfaced in logs only.
Name string
}
Mapping is one entry from anime-list-master.xml — a single AniDB "anime" pointing at its TVDB and TMDB equivalents. Multiple entries can share a TMDB id (TMDB lumps cours into one tmdbtv id, but each cour has its own anidb id). Callers that look up by TMDB id receive the FIRST matching entry — usually the original season, which is what the search-query builder needs to flag the show as anime.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service is the runtime handle: fetch the XML, hold an indexed map, serve lookups. Safe for concurrent reads after Start returns.
func New ¶
New constructs a Service. cachePath is where we persist the most recently fetched XML on disk so we have a fallback when GitHub is unreachable; pass "" to disable disk caching (tests).
func (*Service) AbsoluteToTMDBEpisode ¶
AbsoluteToTMDBEpisode is the inverse of TVDBSeasonToAbsolute: given an absolute episode number from an anime fansub release ("Show - 48"), return the TMDB (season, episode) tuple Pilot stores in its database. Used by the importer to attach anime files to the right row when SxxExx isn't present in the filename.
Currently only supports the same "TMDB collapses cours into season 1" case as TVDBSeasonToAbsolute — i.e. JJK 95479 maps abs N → S01EN regardless of cour. Shows where TMDB genuinely splits into multiple seasons (rare in anime) need cumulative episode-count math that the XML mapping doesn't carry.
Returns (0, 0, false) when:
- tmdbID is 0 or abs <= 0
- the show has no anime mapping (not in animelist.xml)
- the show's mappings don't use the TMDBSeason=1 layout
func (*Service) IsAnime ¶
IsAnime is a convenience wrapper for callers that only care about the boolean — anime detection during series add/refresh.
func (*Service) LastLoaded ¶
LastLoaded reports the wall-clock time of the most recent successful load (from disk OR upstream). Zero when never loaded. Useful for system-status UI.
func (*Service) Lookup ¶
Lookup returns the AniDB mapping for a given TMDB tv id. Returns nil, false when the show isn't in the anime list (the common case for non-anime series). Returns the FIRST matching mapping — for shows where the same tmdbtv id covers multiple AniDB cours, we surface the one with the lowest tmdbseason number (typically the original cour).
func (*Service) Mappings ¶
Mappings returns every AniDB cour entry sharing the given TMDB tv id, sorted ascending by tvdb_season. Multi-cour anime have one entry per cour (Jujutsu Kaisen 95479: tvdbseason=1/2/3, with offsets 0/24/47); the cour-shaped Series Detail view buckets episodes by these (start, end, offset) tuples. Returns an empty slice when the TMDB id is unknown — callers fall back to a single-cour view.
The returned slice is a defensive copy; callers may sort or filter without mutating the service's index.
func (*Service) Start ¶
Start kicks off the lifecycle: load from disk cache (if any) so lookups work immediately, then fetch fresh from upstream in the background, then keep refreshing on the schedule until ctx cancels.
Returns nil immediately — failures are logged, not returned. Callers don't block on network I/O.
func (*Service) TVDBSeasonToAbsolute ¶
TVDBSeasonToAbsolute converts a TVDB-style (season, episode) to the show's absolute episode number using the Anime-Lists mapping data. Returns (absolute, true) when conversion succeeded; (0, false) when the tmdb id isn't anime, or when no entry covers the requested tvdbSeason, or when the entry lacks a tmdboffset (no cumulative data to compute from).
This is what unblocks fansub releases tagged "Show S03E01" for an anime where TMDB collapses everything into a single S01: the Anime-Lists XML has an entry like
<anime tmdbtv="95479" defaulttvdbseason="3" tmdbseason="1" tmdboffset="47">
telling us TVDB S03E01 corresponds to TMDB S01E48 (i.e. absolute 48). Without this lookup, filterByEpisode would drop the release as wrong-season because parsed_season=3 != requested_season=1.
Limitation (intentional): only handles entries where tmdboffset is non-zero AND the corresponding tmdbseason is 1. The few anime with genuine multi-TMDB-season layouts (rare) need separate logic that inspects the full mapping-list XML elements — out of scope for now.