Documentation
¶
Overview ¶
Package show manages TV series records in the Pilot library.
Index ¶
- Variables
- type AddRequest
- type AnimeLookup
- type Cour
- type CourBound
- type Episode
- type ListRequest
- type ListResult
- type LookupRequest
- type MetadataProvider
- type RenamePreview
- type RenameSettings
- type Season
- type Series
- type Service
- func (s *Service) Add(ctx context.Context, req AddRequest) (Series, error)
- func (s *Service) BackfillAnimeForAllSeries(ctx context.Context)
- func (s *Service) BackfillAnimeIfNeeded(ctx context.Context, seriesID string, tmdbID int, currentSeriesType string) bool
- func (s *Service) Delete(ctx context.Context, id string) error
- func (s *Service) DeleteFile(ctx context.Context, fileID string, deleteFromDisk bool) error
- func (s *Service) Get(ctx context.Context, id string) (Series, error)
- func (s *Service) GetCours(ctx context.Context, seriesID string) ([]Cour, error)
- func (s *Service) GetEpisode(ctx context.Context, episodeID string) (Episode, error)
- func (s *Service) GetEpisodeAbsoluteNumber(ctx context.Context, seriesID string, season, episode int) (int, error)
- func (s *Service) GetEpisodes(ctx context.Context, seasonID string) ([]Episode, error)
- func (s *Service) GetSeasons(ctx context.Context, seriesID string) ([]Season, error)
- func (s *Service) List(ctx context.Context, req ListRequest) (ListResult, error)
- func (s *Service) ListAllTMDBIDs(ctx context.Context) ([]int64, error)
- func (s *Service) ListFiles(ctx context.Context, seriesID string) ([]db.EpisodeFile, error)
- func (s *Service) Lookup(ctx context.Context, req LookupRequest) ([]tmdbtv.SearchResult, error)
- func (s *Service) RefreshEpisodeMetadata(ctx context.Context, seriesID string, tmdbID int) error
- func (s *Service) RefreshMetadata(ctx context.Context, seriesID string) (Series, error)
- func (s *Service) RenameFiles(ctx context.Context, seriesID string, settings RenameSettings, dryRun bool) ([]RenamePreview, error)
- func (s *Service) SetCourMonitored(ctx context.Context, seriesID string, tvdbSeason int, monitored bool) error
- func (s *Service) TVDBSeasonToAbsolute(tmdbID, tvdbSeason, tvdbEpisode int) (int, bool)
- func (s *Service) Update(ctx context.Context, id string, req UpdateRequest) (Series, error)
- func (s *Service) UpdateEpisodeMonitored(ctx context.Context, episodeID string, monitored bool) error
- func (s *Service) UpdateSeasonMonitored(ctx context.Context, seasonID string, monitored bool) error
- type UpdateRequest
Constants ¶
This section is empty.
Variables ¶
var ( ErrNotFound = errors.New("series not found") ErrAlreadyExists = errors.New("series already in library") ErrMetadataNotConfigured = errors.New("metadata provider not configured") ErrFileNotFound = errors.New("episode file not found") )
Sentinel errors returned by Service methods.
Functions ¶
This section is empty.
Types ¶
type AddRequest ¶
type AddRequest struct {
TMDBID int
LibraryID string
QualityProfileID string
Monitored bool
MonitorType string // "all", "future", "missing", "none", "pilot", "first_season", "last_season", "existing"
SeriesType string // "standard", "anime", "daily"
}
AddRequest carries the fields needed to add a series to the library.
type AnimeLookup ¶
type AnimeLookup interface {
IsAnime(tmdbID int) bool
TVDBSeasonToAbsolute(tmdbID, tvdbSeason, tvdbEpisode int) (int, bool)
CourBounds(tmdbID int) []CourBound
}
AnimeLookup answers "is this TMDB tv id an anime entry?" — used to auto-flag SeriesType=anime and to drive absolute-episode-number population during series add/refresh. The interface decouples the show service from the animelist package's concrete Service so unit tests can stub in a fake without touching the network.
TVDBSeasonToAbsolute translates a TVDB-tagged (season, episode) to the show's absolute episode number using the Anime-Lists XML mapping data. Used by the search filter to accept fansub releases tagged "Show S03E01" as the user's TMDB-relative S01E48. Returns (0, false) when conversion isn't possible (non-anime tmdb id, unmapped season, etc).
CourBounds returns every cour the show is split into per the Anime-Lists XML, sorted ascending by tvdb_season. The result drives the cour-shaped Series Detail view for anime — TMDB serves multi-cour shows like Jujutsu Kaisen as a single "Season 1" with 59 episodes, but the user expects three seasons. Returns an empty slice when the series has no Anime-Lists mapping; callers fall back to the regular TMDB-shape view.
type Cour ¶
type Cour struct {
// TVDBSeason is the cour identifier (1, 2, 3 …) sourced from
// Anime-Lists' defaulttvdbseason attribute. The frontend renders
// this as the season number ("Season 3").
TVDBSeason int
// TMDBSeason is the underlying TMDB season this cour pulls episodes
// from. Almost always 1 for multi-cour anime; 0 for the specials
// bucket. The UI uses this to fetch the correct season's episode
// list before filtering down to the cour's window.
TMDBSeason int
// EpisodeOffset is the count of TMDB episodes that sit before this
// cour within the same TMDBSeason. The UI subtracts this from each
// TMDB-relative episode number to display cour-relative numbers
// ("3x01" instead of "3x48"). Always 0 for specials and for cour 1.
EpisodeOffset int
// Name is the human-readable cour title from AniDB ("Jujutsu Kaisen
// Shimetsu Kaiyuu - Zenpen") when available; falls back to a
// generic "Season N" string in the API layer.
Name string
// Monitored reflects either the user's explicit cour-monitor
// override (anime_cour_monitored row) or the parent TMDB season's
// monitored bit when no override exists.
Monitored bool
// EpisodeCount is the number of episodes in this cour.
EpisodeCount int64
// EpisodeFileCount is the number of episodes in this cour with a
// linked file on disk.
EpisodeFileCount int64
// TotalSizeBytes is the sum of episode-file sizes for this cour.
TotalSizeBytes int64
// EpisodeIDs lists the show.Episode IDs that belong to this cour,
// in TMDB-relative episode order. Useful for the Episodes endpoint
// to filter to a single cour without having to recompute bounds.
EpisodeIDs []string
}
Cour is a presentation-layer view of a multi-cour anime "season" — what the user expects to see as Season 1/2/3 of Jujutsu Kaisen even though TMDB serves all 59 episodes as a single Season 1. The underlying episodes table is not reshaped: this is computed at read time from the Anime-Lists XML mapping plus the existing episodes rows.
type CourBound ¶
type CourBound struct {
TVDBSeason int // cour identifier (1, 2, 3, …) — matches Anime-Lists' defaulttvdbseason
TMDBSeason int // which TMDB season this cour falls inside (almost always 1)
TMDBOffset int // count of TMDB episodes in earlier cours of the same TMDB season
Name string // AniDB-supplied cour name, e.g. "Jujutsu Kaisen Season 2"
}
CourBound describes a single cour's slot within the show's TMDB layout. (tmdb_season, tmdb_offset+1) is the first TMDB-relative episode of the cour; the cour ends at the start of the next cour in tvdb_season order, or at the end of the season for the last cour.
type Episode ¶
type Episode struct {
ID string
SeriesID string
SeasonID string
SeasonNumber int
EpisodeNumber int
AbsoluteNumber *int
AirDate string
Title string
Overview string
Monitored bool
HasFile bool
StillPath string
RuntimeMinutes int
}
Episode is the domain representation of an episode record.
type ListRequest ¶
type ListRequest struct {
LibraryID string // empty = all libraries
Page int // 1-indexed; defaults to 1
PerPage int // defaults to 50
}
ListRequest carries filter and pagination options for listing series.
type ListResult ¶
ListResult is the paginated response from List.
type LookupRequest ¶
type LookupRequest struct {
Query string
TMDBID int // if set, fetch exact series; Query is ignored
Year int // optional year filter for query search
}
LookupRequest carries parameters for searching the metadata provider without adding a series to the library.
type MetadataProvider ¶
type MetadataProvider interface {
SearchSeries(ctx context.Context, query string, year int) ([]tmdbtv.SearchResult, error)
GetSeries(ctx context.Context, tmdbID int) (*tmdbtv.SeriesDetail, error)
GetSeasonEpisodes(ctx context.Context, tmdbID int, seasonNum int) ([]tmdbtv.EpisodeDetail, error)
GetAlternativeTitles(ctx context.Context, tmdbID int) ([]string, error)
}
MetadataProvider fetches TV series metadata from an external source.
type RenamePreview ¶
RenamePreview describes a single file rename operation (or proposed rename when dry_run=true).
type RenameSettings ¶
type RenameSettings struct {
EpisodeFormat string
SeriesFolderFormat string
SeasonFolderFormat string
ColonReplacement renamer.ColonReplacement
}
RenameSettings holds the format configuration for renaming files.
type Season ¶
type Season struct {
ID string
SeriesID string
SeasonNumber int
Monitored bool
EpisodeCount int64
EpisodeFileCount int64
TotalSizeBytes int64
}
Season is the domain representation of a season record.
type Series ¶
type Series struct {
ID string
TMDBID int
IMDBID string
Title string
SortTitle string
Year int
Overview string
RuntimeMinutes int
Genres []string
PosterURL string
FanartURL string
Status string
SeriesType string
MonitorType string
Network string
AirTime string
Certification string
Monitored bool
LibraryID string
QualityProfileID string
Path string
AddedAt time.Time
UpdatedAt time.Time
MetadataRefreshedAt *time.Time
EpisodeCount int64
EpisodeFileCount int64
// AlternateTitles are alternate marketing/translated names for the
// series (e.g. "Star Wars: Andor" for "Andor"), fetched from TMDB.
// Used by parser.TitleMatchesAny so indexer responses with non-
// canonical names aren't dropped by the strict series-title gate.
AlternateTitles []string
}
Series is the domain representation of a TV series record.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service manages TV series, seasons, and episodes.
func NewService ¶
func NewService(q db.Querier, meta MetadataProvider, anime AnimeLookup, bus *events.Bus, logger *slog.Logger) *Service
NewService creates a new Service. meta may be nil; methods that require it will return ErrMetadataNotConfigured. anime may be nil; when nil, no anime auto-detection happens (series add/refresh leaves SeriesType as the caller specified and absolute_number unset).
func (*Service) Add ¶
Add fetches metadata for the given TMDB ID, creates the series row, creates all season and episode rows, applies monitor_type logic, and publishes a TypeShowAdded event.
func (*Service) BackfillAnimeForAllSeries ¶
BackfillAnimeForAllSeries iterates every standard-typed series in the library and runs BackfillAnimeIfNeeded against each. Intended as a one-shot startup task: catches series added before anime detection existed without requiring the user to manually refresh each one.
Designed to be safe to call concurrently with other Service methods — each row is processed independently. Errors on individual rows are logged and skipped, never returned (one bad row shouldn't abort the whole sweep).
func (*Service) BackfillAnimeIfNeeded ¶
func (s *Service) BackfillAnimeIfNeeded(ctx context.Context, seriesID string, tmdbID int, currentSeriesType string) bool
BackfillAnimeIfNeeded is the standalone anime-detection + backfill helper, factored out of RefreshMetadata so it can run independently of the (sometimes-unreachable) TMDB metadata refresh path. Returns true when the series was upgraded to anime — caller can use this to update its in-memory representation; the DB write has already happened.
Idempotent: safe to call repeatedly. Skips when:
- anime lookup is disabled (s.anime == nil)
- the series is already non-standard (anime/daily — caller's explicit choice wins)
- the TMDB id isn't in the Anime-Lists XML
Logs all decisions at info level so the startup scan produces an auditable trail.
func (*Service) Delete ¶
Delete removes a series by ID. Cascade deletes handle seasons and episodes.
func (*Service) DeleteFile ¶
DeleteFile removes an episode_file record. When deleteFromDisk is true it also removes the underlying file from the filesystem and marks the episode as no longer having a file.
func (*Service) GetCours ¶
GetCours returns a cour-shaped projection of the series, suitable for display when series_type is anime. Returns (nil, nil) for non-anime series and for anime series with no Anime-Lists mapping — callers should fall back to GetSeasons in those cases.
Specials (TMDB Season 0) are always returned as their own bucket with TVDBSeason=0, regardless of cour structure, because they don't participate in the cour layout.
Implementation: bucket episodes by (tmdb_season, episode_number) against the cour bounds derived from the Anime-Lists XML. The bound for cour N within a TMDB season is `[TMDBOffset[N]+1, TMDBOffset[N+1]]`; the last cour's upper bound is the season's episode count.
func (*Service) GetEpisode ¶
GetEpisode returns a single episode by its UUID. Returns ErrNotFound when no row exists. Used by the episode-detail page route.
func (*Service) GetEpisodeAbsoluteNumber ¶
func (s *Service) GetEpisodeAbsoluteNumber(ctx context.Context, seriesID string, season, episode int) (int, error)
GetEpisodeAbsoluteNumber returns the absolute episode number for a given (seriesID, season, episode) tuple. Returns (0, nil) when the episode exists but has no absolute number set (the common case for non-anime series), or when the episode isn't found at all. Used by the search-query builder to emit anime-style queries ("Show - 48", "Show 48") in addition to "S01E48".
func (*Service) GetEpisodes ¶
GetEpisodes returns all episodes for the given season ID.
func (*Service) GetSeasons ¶
GetSeasons returns all seasons for the given series ID, annotated with per-season episode counts (total and with-file).
func (*Service) List ¶
func (s *Service) List(ctx context.Context, req ListRequest) (ListResult, error)
List returns a paginated list of series, optionally filtered by library.
func (*Service) ListAllTMDBIDs ¶
ListAllTMDBIDs returns all TMDB IDs of series in the library. Used for "already added" detection in the Discover UI.
func (*Service) ListFiles ¶
ListFiles returns all episode files associated with the given series ID.
func (*Service) Lookup ¶
func (s *Service) Lookup(ctx context.Context, req LookupRequest) ([]tmdbtv.SearchResult, error)
Lookup searches the metadata provider without adding anything to the library.
func (*Service) RefreshEpisodeMetadata ¶
RefreshEpisodeMetadata re-fetches episode details from TMDB for the given series and updates still_path and runtime_minutes on each episode.
func (*Service) RefreshMetadata ¶
RefreshMetadata re-fetches series-level metadata from TMDB (including alternate titles) and updates the row. Episode metadata is NOT refreshed here — call RefreshEpisodeMetadata for that.
Used to backfill alternate_titles for series that were added before the alternates feature shipped (the column starts as []), and to pick up alternate-title additions on TMDB after a series has aged in the library.
func (*Service) RenameFiles ¶
func (s *Service) RenameFiles(ctx context.Context, seriesID string, settings RenameSettings, dryRun bool) ([]RenamePreview, error)
RenameFiles computes (and optionally applies) renames for all episode files belonging to a series based on the naming format settings.
func (*Service) SetCourMonitored ¶
func (s *Service) SetCourMonitored(ctx context.Context, seriesID string, tvdbSeason int, monitored bool) error
SetCourMonitored upserts the user's explicit cour-monitor override. The DB row exists from now on, so subsequent GetCours calls return `monitored` regardless of what the parent season is set to.
func (*Service) TVDBSeasonToAbsolute ¶
TVDBSeasonToAbsolute is a thin pass-through to the configured anime lookup. Returns (0, false) when no anime lookup is configured. Used by the search filter to accept TVDB-tagged fansub releases against a TMDB-relative episode request — see filterByEpisode.