show

package
v0.0.0-...-d771ed5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 29, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Overview

Package show manages TV series records in the Pilot library.

Index

Constants

This section is empty.

Variables

View Source
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

type ListResult struct {
	Series  []Series
	Total   int64
	Page    int
	PerPage int
}

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

type RenamePreview struct {
	FileID  string
	OldPath string
	NewPath string
}

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

func (s *Service) Add(ctx context.Context, req AddRequest) (Series, error)

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

func (s *Service) BackfillAnimeForAllSeries(ctx context.Context)

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

func (s *Service) Delete(ctx context.Context, id string) error

Delete removes a series by ID. Cascade deletes handle seasons and episodes.

func (*Service) DeleteFile

func (s *Service) DeleteFile(ctx context.Context, fileID string, deleteFromDisk bool) error

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

func (s *Service) Get(ctx context.Context, id string) (Series, error)

Get returns a single series by ID, with episode counts populated.

func (*Service) GetCours

func (s *Service) GetCours(ctx context.Context, seriesID string) ([]Cour, error)

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

func (s *Service) GetEpisode(ctx context.Context, episodeID string) (Episode, error)

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

func (s *Service) GetEpisodes(ctx context.Context, seasonID string) ([]Episode, error)

GetEpisodes returns all episodes for the given season ID.

func (*Service) GetSeasons

func (s *Service) GetSeasons(ctx context.Context, seriesID string) ([]Season, error)

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

func (s *Service) ListAllTMDBIDs(ctx context.Context) ([]int64, error)

ListAllTMDBIDs returns all TMDB IDs of series in the library. Used for "already added" detection in the Discover UI.

func (*Service) ListFiles

func (s *Service) ListFiles(ctx context.Context, seriesID string) ([]db.EpisodeFile, error)

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

func (s *Service) RefreshEpisodeMetadata(ctx context.Context, seriesID string, tmdbID int) error

RefreshEpisodeMetadata re-fetches episode details from TMDB for the given series and updates still_path and runtime_minutes on each episode.

func (*Service) RefreshMetadata

func (s *Service) RefreshMetadata(ctx context.Context, seriesID string) (Series, error)

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

func (s *Service) TVDBSeasonToAbsolute(tmdbID, tvdbSeason, tvdbEpisode int) (int, bool)

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.

func (*Service) Update

func (s *Service) Update(ctx context.Context, id string, req UpdateRequest) (Series, error)

Update modifies the mutable fields of a series.

func (*Service) UpdateEpisodeMonitored

func (s *Service) UpdateEpisodeMonitored(ctx context.Context, episodeID string, monitored bool) error

UpdateEpisodeMonitored sets the monitored flag on a single episode.

func (*Service) UpdateSeasonMonitored

func (s *Service) UpdateSeasonMonitored(ctx context.Context, seasonID string, monitored bool) error

UpdateSeasonMonitored sets the monitored flag on a season and cascades the same value to all episodes in that season.

type UpdateRequest

type UpdateRequest struct {
	Title            string
	Monitored        bool
	LibraryID        string
	QualityProfileID string
	SeriesType       string
	Path             string
}

UpdateRequest carries the mutable fields for updating a series.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL