prerolls

package
v0.3.3 Latest Latest
Warning

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

Go to latest
Published: Nov 4, 2026 License: GPL-3.0 Imports: 23 Imported by: 0

Documentation

Overview

Package prerolls stores owner pre-roll videos, converts them for direct play and chooses at most one to play before a movie or episode (GitHub #298). Plex prepends the chosen pre-roll to the play queue next to Cinema Trailers; Jellyfin and Emby list it as an intro.

Selection never scans the library: per-play work is one cached list of pre-rolls, one bounded query of the viewer's recent pre-roll plays and checks of the single feature title.

Index

Constants

View Source
const (
	StateNone       = ""
	StateProcessing = "processing"
	StateReady      = "ready"
	StateFailed     = "failed"

	ProblemUnreadable    = "unreadable"
	ProblemTooLong       = "too_long"
	ProblemConvertFailed = "convert_failed"

	SourceUpload = "upload"
	SourcePath   = "path"
)

Video states and problems (owner API contract).

View Source
const (
	SelectPriority = "priority"
	SelectShuffle  = "shuffle"
)

Selection modes.

View Source
const (
	FrequencyAlways = "always"
	FrequencyDaily  = "daily"
	FrequencyHours  = "hours"
	FrequencyTitle  = "title"
)

Frequency modes.

View Source
const (
	// MaxDuration is the longest clip accepted.
	MaxDuration = 3 * time.Minute
	// MaxRetention bounds the play log.
	MaxRetention = 90 * 24 * time.Hour
)

Limits.

View Source
const (
	ReasonDisabled       = "disabled"
	ReasonNotReady       = "not_ready"
	ReasonUnsupportedApp = "unsupported_app"
	ReasonResumed        = "resumed"
	ReasonKind           = "kind"
	ReasonFirstEpisode   = "first_episode"
	ReasonBingeGap       = "binge_gap"
	ReasonGenre          = "genre"
	ReasonExcludedGenre  = "excluded_genre"
	ReasonCollection     = "collection"
	ReasonRating         = "rating"
	ReasonDateWindow     = "date_window"
	ReasonAudience       = "audience"
	ReasonNetwork        = "network"
	ReasonDevice         = "device"
	ReasonFrequency      = "frequency"
	ReasonLowerPriority  = "lower_priority"
	ReasonShuffledOut    = "shuffled_out"
)

Reason codes (owner API contract), in evaluation order.

View Source
const (
	DefaultMaxFileBytes  int64 = 500 << 20
	DefaultMaxTotalBytes int64 = 5 << 30
)

Default storage caps.

Variables

View Source
var ErrNotFound = core.ErrNotFound

ErrNotFound is returned for an unknown pre-roll.

View Source
var ErrStorageFull = errors.New("prerolls: storage full")

ErrStorageFull is returned when the pre-roll folder is at its total cap.

View Source
var ErrTooLarge = errors.New("prerolls: file too large")

ErrTooLarge is returned when an upload exceeds the per-file cap.

Functions

This section is empty.

Types

type Activity

type Activity interface {
	History(ctx context.Context, userID int64, since time.Time, limit int) ([]core.HistoryEntry, error)
	InProgress(ctx context.Context, userID int64, limit int) ([]core.WatchStatus, error)
}

Activity is the part of watch state the binge rule reads: the viewer's most recent finished play and most recent stopped (resumable) play. Both are single indexed rows.

type Audience

type Audience struct {
	Mode    string  `json:"mode"` // everyone | only | except
	Viewers []int64 `json:"viewers"`
}

Audience limits the viewers a pre-roll plays for.

type Candidate

type Candidate struct {
	ID      string   `json:"id"`
	Name    string   `json:"name"`
	Plays   bool     `json:"plays"`
	Reasons []Reason `json:"reasons"`
}

Candidate is one pre-roll's verdict for a play.

type Converter

type Converter interface {
	// Probe reads the source's streams and duration.
	Probe(ctx context.Context, path string) (*core.MediaInfo, error)
	// Convert writes an H.264 High / AAC stereo MP4 (at most 1080p,
	// faststart) to out.
	Convert(ctx context.Context, in, out string, info *core.MediaInfo) error
	// Thumbnail writes one JPEG frame taken at offset to out.
	Thumbnail(ctx context.Context, in, out string, at time.Duration) error
}

Converter probes a source clip and converts it for direct play.

type Decision

type Decision struct {
	Winner     *Winner     `json:"winner"`
	Candidates []Candidate `json:"candidates"`
	// contains filtered or unexported fields
}

Decision is the outcome of one selection.

type FFmpeg

type FFmpeg struct {
	// Path returns the ffmpeg executable; "" means unavailable.
	Path func() string
	// ProbeFunc reads media info (the transcoder's ffprobe).
	ProbeFunc func(ctx context.Context, input string) (*core.MediaInfo, error)
}

FFmpeg converts with the bundled FFmpeg (internal/transcode discovery).

func (FFmpeg) Convert

func (f FFmpeg) Convert(ctx context.Context, in, out string, info *core.MediaInfo) error

Convert implements Converter.

func (FFmpeg) Probe

func (f FFmpeg) Probe(ctx context.Context, path string) (*core.MediaInfo, error)

Probe implements Converter.

func (FFmpeg) Thumbnail

func (f FFmpeg) Thumbnail(ctx context.Context, in, out string, at time.Duration) error

Thumbnail implements Converter.

type Frequency

type Frequency struct {
	Mode  string `json:"mode"`
	Hours int    `json:"hours"`
}

Frequency limits how often one viewer sees a pre-roll.

type Input

type Input struct {
	Name      string    `json:"name"`
	Rules     Rules     `json:"rules"`
	Frequency Frequency `json:"frequency"`
	Priority  int       `json:"priority"`
	Order     string    `json:"order"`
	Enabled   *bool     `json:"enabled"`
}

Input is the create/update body.

type Options

type Options struct {
	DB *sql.DB
	// Dir holds converted clips and thumbnails (<data>/prerolls).
	Dir string
	// MaxFileBytes caps one source file; MaxTotalBytes caps the folder.
	MaxFileBytes, MaxTotalBytes int64
	// Converter probes and converts clips; nil leaves new clips processing
	// until FFmpeg is available.
	Converter Converter
	// Activity feeds the binge rule; nil disables it.
	Activity Activity
	// Title loads one title (an episode's show for its genres, rating and
	// collections; a simulated feature). Nil skips the show lookup.
	Title func(ctx context.Context, key core.RatingKey) (*core.Title, error)
	// InCollection reports whether key is a member of owner collection id.
	// Nil makes collection rules never match.
	InCollection func(ctx context.Context, id string, key core.RatingKey) bool
	// Viewer loads a viewer for the simulator.
	Viewer func(ctx context.Context, id int64) (*core.User, error)
	Logger *slog.Logger
	Now    func() time.Time
	// Rand returns a number in [0,n); nil uses math/rand.
	Rand func(n int) int
}

Options are the service dependencies.

type PlayRow

type PlayRow struct {
	ViewerID   int64     `json:"viewerId"`
	ViewerName string    `json:"viewerName"`
	Title      string    `json:"title"`
	RatingKey  string    `json:"ratingKey"`
	At         time.Time `json:"at"`
	Client     string    `json:"client"`
}

PlayRow is one entry of a pre-roll's play log.

type Preroll

type Preroll struct {
	ID        string    `json:"id"`
	Name      string    `json:"name"`
	Enabled   bool      `json:"enabled"`
	Video     Video     `json:"video"`
	Rules     Rules     `json:"rules"`
	Frequency Frequency `json:"frequency"`
	Priority  int       `json:"priority"`
	Order     string    `json:"order"`
	Plays30d  int       `json:"plays30d"`
	CreatedAt time.Time `json:"createdAt"`
	UpdatedAt time.Time `json:"updatedAt"`
	// contains filtered or unexported fields
}

Preroll is the owner API representation.

func (Preroll) Key

func (p Preroll) Key() core.RatingKey

Key is the pre-roll's rating key.

type Reason

type Reason struct {
	Code string `json:"code"`
	OK   bool   `json:"ok"`
}

Reason is one evaluated condition.

type Rules

type Rules struct {
	Kinds            []string `json:"kinds"`
	FirstEpisodeOnly bool     `json:"firstEpisodeOnly"`
	BingeGapMinutes  int      `json:"bingeGapMinutes"`
	Genres           []string `json:"genres"`
	ExcludeGenres    []string `json:"excludeGenres"`
	Collections      []string `json:"collections"`
	MaxContentRating string   `json:"maxContentRating"`
	DateFrom         string   `json:"dateFrom"` // MM-DD, inclusive
	DateTo           string   `json:"dateTo"`   // MM-DD, inclusive; may wrap the new year
	Audience         Audience `json:"audience"`
	Network          string   `json:"network"`
	Devices          []string `json:"devices"`
}

Rules decide which plays a pre-roll may precede. Empty lists match all.

type Service

type Service struct {
	// contains filtered or unexported fields
}

Service implements the owner API and core.Prerolls.

func New

func New(o Options) (*Service, error)

New returns the service. It creates Dir.

func (*Service) Active

func (s *Service) Active(ctx context.Context) bool

Active implements core.Prerolls: whether any pre-roll is enabled and ready, read from the cached list.

func (*Service) AttachPath

func (s *Service) AttachPath(ctx context.Context, id, path string) (Preroll, error)

AttachPath uses a file on the server as the clip source. The file is only read; Ether converts it into its own folder.

func (*Service) Clip

func (s *Service) Clip(ctx context.Context, key core.RatingKey) (core.PrerollClip, bool)

Clip implements core.Prerolls. A disabled pre-roll already queued for a play still plays; a deleted or reconverting one does not.

func (*Service) Create

func (s *Service) Create(ctx context.Context, in Input) (Preroll, error)

Create stores a new pre-roll (without a video yet).

func (*Service) Delete

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

Delete removes a pre-roll, its files and its plays.

func (*Service) Files

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

Files returns a pre-roll's converted video and thumbnail paths ("" when absent) for the owner preview.

func (*Service) Get

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

Get returns one pre-roll.

func (*Service) ImportPMS

func (s *Service) ImportPMS(ctx context.Context, value string) ([]Preroll, error)

ImportPMS creates pre-rolls from a Plex Media Server CinemaTrailersPrerollID value. PMS plays semicolon-separated paths in order and picks one of comma-separated paths at random. Ether plays one pre-roll per play, so each semicolon group gets a lower priority than the one before it (the first group wins) and the paths inside a group share a priority (ties are chosen at random). Every path is validated before anything is created.

func (*Service) List

func (s *Service) List(ctx context.Context) ([]Preroll, string, error)

List returns every pre-roll with its 30-day play count and the selection mode.

func (*Service) Pick

Pick implements core.Prerolls: it chooses at most one pre-roll for a play. Nothing is recorded until Started.

func (*Service) Plays

func (s *Service) Plays(ctx context.Context, id string, limit int) ([]PlayRow, error)

Plays returns a pre-roll's most recent plays (limit 1..500).

func (*Service) Prune

func (s *Service) Prune(ctx context.Context) error

Prune deletes plays older than the retention window.

func (*Service) Run

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

Run converts queued clips and prunes the play log until ctx ends.

func (*Service) Selection

func (s *Service) Selection(ctx context.Context) string

Selection is "priority" (default) or "shuffle".

func (*Service) SetSelection

func (s *Service) SetSelection(ctx context.Context, mode string) error

SetSelection stores the selection mode.

func (*Service) Simulate

func (s *Service) Simulate(ctx context.Context, in SimulateInput) (Decision, error)

Simulate explains which pre-roll a play would get, through the same rules as playback, without recording anything.

func (*Service) Started

func (s *Service) Started(ctx context.Context, play core.PrerollPlay)

Started implements core.Prerolls: it records one play per viewer, pre-roll and feature, however many requests the client makes.

func (*Service) Thumb

func (s *Service) Thumb(ctx context.Context, key core.RatingKey) (string, bool)

Thumb implements core.Prerolls.

func (*Service) Update

func (s *Service) Update(ctx context.Context, id string, in Input) (Preroll, error)

Update replaces a pre-roll's settings. An omitted enabled keeps the current value.

func (*Service) Upload

func (s *Service) Upload(ctx context.Context, id string, r io.Reader) (Preroll, error)

Upload streams a clip from r into the pre-roll folder and queues its conversion. It stops reading at the per-file cap.

type SimulateInput

type SimulateInput struct {
	ViewerID  int64  `json:"viewerId"`
	RatingKey string `json:"ratingKey"`
	Device    string `json:"device"`
	Away      bool   `json:"away"`
	Resume    bool   `json:"resume"`
	At        string `json:"at"`
	// Client optionally names the app, to explain unsupported apps.
	Client string `json:"client,omitempty"`
}

SimulateInput is the simulator request.

type ValidationError

type ValidationError struct{ Field, Message string }

ValidationError is an owner input problem (HTTP 400).

func (*ValidationError) Error

func (e *ValidationError) Error() string

type Video

type Video struct {
	State      string `json:"state"`
	DurationMs int64  `json:"durationMs"`
	Width      int    `json:"width"`
	Height     int    `json:"height"`
	ThumbURL   string `json:"thumbUrl"`
	PreviewURL string `json:"previewUrl"`
	Problem    string `json:"problem"`
	Source     string `json:"source"`
}

Video describes the converted clip.

type Winner

type Winner struct {
	ID   string `json:"id"`
	Name string `json:"name"`
}

Winner names the chosen pre-roll.

Jump to

Keyboard shortcuts

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