streams

package
v1.6.3 Latest Latest
Warning

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

Go to latest
Published: Jul 19, 2026 License: GPL-2.0, GPL-3.0 Imports: 5 Imported by: 0

Documentation

Overview

Package streams holds the pure (I/O-free) stream-selection core for plex-language-sync along with the Plex value types it operates on.

The types here mirror the JSON wire format returned by the Plex HTTP API; JSON struct tags are part of Plex's API contract (inviolate) and must not change during refactors.

Callers (the internal/plex HTTP client, composition root, and tests) import this package; it has no dependencies on other internal packages so there are no circular-import risks.

Index

Constants

View Source
const None = "none"

None is the sentinel Desc returns for a nil stream pointer. It lands in log attributes like `"audio"="none"` so downstream queries can distinguish "no selection" from an absent field.

Variables

This section is empty.

Functions

func ContainsDescriptive

func ContainsDescriptive(title string) bool

ContainsDescriptive reports whether the lowercased title string mentions any "commentary" / "descriptive" keyword. Used to filter atypical audio tracks out of the default matching path.

func Desc

func Desc(s *Stream) string

Desc returns a human-readable description of the stream for log output: the best title if any, otherwise "stream-<id>", otherwise None for a nil stream.

func FirstPartID

func FirstPartID(ep *Episode) int

FirstPartID returns the part ID of the first media part, or 0 when the episode has no media/parts.

func ID

func ID(s *Stream) int

ID returns s.ID or 0 when s is nil. Used to build stable dedup keys from a (possibly absent) current audio/subtitle selection.

func ScoreAudio

func ScoreAudio(ref, s *Stream) int

ScoreAudio ranks a candidate audio stream against a reference for the tie-break stage of MatchAudio. Higher is better. Codec match, channel layout match, and the same title fields each contribute.

func ScoreSubtitle

func ScoreSubtitle(ref, s *Stream) int

ScoreSubtitle ranks a candidate subtitle stream against a reference. Higher is better. Returns 0 when ref is nil.

func ShouldSkipSubtitleForCommentary

func ShouldSkipSubtitleForCommentary(refAudio *Stream, targetAudioStreams []*Stream) bool

ShouldSkipSubtitleForCommentary returns true if the reference audio is a commentary/descriptive track but the target episode has no audio track in the reference's language at all (MatchAudio matches by language; commentary is only a soft preference, so a same-language non-commentary track still counts as a match) — in which case subtitle changes should be skipped to avoid generalizing an atypical pairing.

func SubtitleCodecScore

func SubtitleCodecScore(codec string) int

SubtitleCodecScore ranks subtitle codecs by quality/reliability. Higher is better: styled text > image-based (source) > plain text (Bazarr). Scores are defined in the subtitleCodecScores table above.

func SubtitleCriteria

func SubtitleCriteria(ref, _ *Stream) (langCode string, forcedOnly, hiOnly bool)

SubtitleCriteria extracts the language/flags used to match a subtitle stream on the target episode. Policy: "no subtitle means no subtitle" — when the reference episode has no subtitle selected (ref == nil), we never search for forced subs based on the audio language. The user explicitly chose "no subtitle" and we respect that. The caller's disable-subtitles guard then fires unconditionally when the target has subtitles selected.

func TitleMatchScore

func TitleMatchScore(ref, s *Stream) int

TitleMatchScore rewards exact equality on any of the three title fields. Each match adds 5. Empty fields never contribute.

Types

type Episode

type Episode struct {
	RatingKey       string `json:"ratingKey"`
	ParentRatingKey string `json:"parentRatingKey"`
	GrandparentKey  string `json:"grandparentKey"`
	// The four title fields are Plex metadata sourced from the wild (agents,
	// filenames), tagged runesafe.Untrusted at this decode boundary: raw
	// bytes in (matching, e.g. the ignore policy, reads Raw()), sanitized
	// automatically at every slog/fmt emit.
	GrandparentTitle     runesafe.Untrusted `json:"grandparentTitle"`
	ParentTitle          runesafe.Untrusted `json:"parentTitle"`
	Title                runesafe.Untrusted `json:"title"`
	Type                 string             `json:"type"`
	LibraryTitle         runesafe.Untrusted `json:"librarySectionTitle"`
	GrandparentRatingKey string             `json:"grandparentRatingKey"`
	Label                []Label            `json:"Label"`
	Media                []Media            `json:"Media"`
	AddedAt              int64              `json:"addedAt"`
	Index                FlexInt            `json:"index"`
	ParentIndex          FlexInt            `json:"parentIndex"`
	LibrarySectionID     FlexInt            `json:"librarySectionID"`
}

Episode is a Plex metadata item of type="episode" (and, by extension, show or season metadata since /library/metadata/{key} is polymorphic).

func (*Episode) EpisodeNum

func (e *Episode) EpisodeNum() int

EpisodeNum returns the parsed episode index, or 0 when the Index field is absent. See SeasonNum for the FlexInt rationale.

func (*Episode) SeasonNum

func (e *Episode) SeasonNum() int

SeasonNum returns the parsed season index, or 0 when the ParentIndex field is absent. FlexInt decodes both `14` and `"14"` JSON shapes directly to int, so this is now a trivial conversion — no strconv fallback needed.

func (*Episode) ShortName

func (e *Episode) ShortName() string

ShortName returns a concise "'Show' (SxxEyy)" identifier useful for structured log lines.

type FlexInt

type FlexInt int

FlexInt unmarshals a Plex JSON field that may arrive as a number OR a quoted numeric string. Plex's JSON responses are famously inconsistent on numeric index fields (episode index, parent index, library-section ID, account ID): some endpoints return `"14"`, others return `14`. json.Number accommodates both but forces every reader to call .String() and re-parse, which every call site used to do.

FlexInt is the ergonomic replacement: it decodes both shapes into a plain int so callers can use the value directly (arithmetic, log formatting, comparisons) without a trailing strconv.Atoi step. Null and absent JSON fields decode to the zero value, matching the previous json.Number behaviour where an absent field produced the empty-string "" that strconv.Atoi rejected and callers treated as 0.

Exported (capital F) because non-streams packages (internal/plex for Show.LibrarySectionID, Season.Index, HistoryItem.AccountID and HistoryItem.LibrarySectionID) now embed FlexInt in their struct definitions. Keeping it unexported would force those packages to redeclare a mirror type or reach through a getter, both of which defeat the "one primitive, one place" design.

Wire-origin string fields (streams.Episode.RatingKey, plex.Section.Key, plex.HistoryItem.RatingKey, plex.Show.RatingKey, plex.Season.RatingKey) deliberately stay typed as string — the Plex JSON wire format for rating keys is a string and inviolate item 9 requires that representation be preserved on the wire. FlexInt only replaces json.Number fields whose semantic intent was always "an integer".

func (*FlexInt) UnmarshalJSON

func (f *FlexInt) UnmarshalJSON(data []byte) error

UnmarshalJSON accepts either a JSON number (`14`) or a quoted numeric string (`"14"`) and decodes it into the underlying int. Null and empty-string payloads decode to 0 without error to match the previous json.Number-backed behaviour where an absent or null field produced the zero-value int through strconv.Atoi("").

The decode is a thin shim over jsonx.ParseInt64 under the StrictAbsentZero policy, which was built to reproduce exactly this type's pinned behaviour: bare or quoted decimal integers anywhere in int64, null/absent/"" tolerated as 0, everything else — float forms, non-numeric strings, objects, arrays — a parse error. The library additionally hardens the string path: forms strconv would loosely accept elsewhere (hex floats, "Inf"/"NaN", underscore separators) stay rejected, and large integers never round-trip through float64.

Malformed inputs return a parse error under the "flexint:" prefix. The error phrasing deliberately does NOT reuse the "invalid rating key" prefix owned by plex.RatingKey.Validate — inviolate item 5 reserves that exact string for rating-key validation, and conflating the two would muddle Loki alerts keyed on rating-key failures.

type Intent added in v1.6.3

type Intent struct {
	// Subtitle is the observed subtitle selection; nil means the user
	// chose "no subtitles" for this audio, and the policy "no subtitle
	// means no subtitle" applies when the intent is re-applied.
	// (Field ordered first for fieldalignment; JSON schema is keyed by
	// name, not order.)
	Subtitle *IntentStream `json:"subtitle"`
	// Audio is the observed audio selection. Every intent has one: an
	// episode with no selected audio never records an intent.
	Audio IntentStream `json:"audio"`
	// ObservedAt is the unix timestamp of the observation (app clock at
	// the resolved play event).
	ObservedAt int64 `json:"observed_at"`
}

Intent is the app's own durable record of a user's last deliberately observed track selection for a show — captured on the event plane (a resolved play session), applied on the reconcile plane (scheduler history replay) and when seeding new/updated episodes.

The point of recording intents is that "the user's choice" is an event-time fact: Plex's metadata reads expose only an ambient current selection whose per-user attribution is not reliable after the fact (see the scheduler package doc). An intent captures the choice at the only moment it is attributable, so no later code path has to fabricate attribution by joining a historical identity with a current read.

JSON tags are part of the on-disk profiles.json schema (inviolate contract item 7) — do not change without a migration.

func NewIntent added in v1.6.3

func NewIntent(audio, subtitle *Stream, observedAt int64) *Intent

NewIntent projects an observed (audio, subtitle) selection into an Intent. audio must be non-nil (callers gate on a selected audio stream before recording); subtitle may be nil ("no subtitles").

func (*Intent) Clone added in v1.6.3

func (i *Intent) Clone() Intent

Clone returns a deep copy of the intent (the Subtitle pointer is the only indirection). Used by the cache to keep exclusive ownership of stored values.

func (*Intent) RefStreams added in v1.6.3

func (i *Intent) RefStreams() (audio, subtitle *Stream)

RefStreams reconstructs reference *Stream values for the matchers from the persisted projection. The audio return is always non-nil; the subtitle return is nil when the intent recorded "no subtitles".

type IntentStream added in v1.6.3

type IntentStream struct {
	LanguageCode         string `json:"languageCode"`
	Title                string `json:"title,omitempty"`
	DisplayTitle         string `json:"displayTitle,omitempty"`
	ExtendedDisplayTitle string `json:"extendedDisplayTitle,omitempty"`
	Codec                string `json:"codec,omitempty"`
	AudioChannelLayout   string `json:"audioChannelLayout,omitempty"`
	Channels             int    `json:"channels,omitempty"`
	Forced               bool   `json:"forced,omitempty"`
	HearingImpaired      bool   `json:"hearingImpaired,omitempty"`
	VisualImpaired       bool   `json:"visualImpaired,omitempty"`
}

IntentStream is the persisted projection of a Stream: exactly the fields the matchers and scorers consume when the stream is used as a REFERENCE (MatchAudio / MatchSubtitle / ScoreAudio / ScoreSubtitle / SubtitleCriteria / ShouldSkipSubtitleForCommentary / TitleForMatch). Per-episode identity fields (ID, Selected, StreamType) are deliberately absent — they are meaningless outside the episode the stream was observed on.

type Label

type Label struct {
	Tag string `json:"tag"`
}

Label represents a label tag on a Plex metadata item.

type Media

type Media struct {
	Part []Part `json:"Part"`
	ID   int    `json:"id"`
}

Media wraps a list of Parts for an Episode.

type Part

type Part struct {
	Stream []Stream `json:"Stream"`
	ID     int      `json:"id"`
}

Part wraps a list of Streams for a Media.

type Stream

type Stream struct {
	LanguageCode         string     `json:"languageCode"`
	LanguageTag          string     `json:"languageTag"`
	DisplayTitle         string     `json:"displayTitle"`
	ExtendedDisplayTitle string     `json:"extendedDisplayTitle"`
	Title                string     `json:"title"`
	Codec                string     `json:"codec"`
	AudioChannelLayout   string     `json:"audioChannelLayout"`
	ID                   int        `json:"id"`
	StreamType           StreamType `json:"streamType"`
	Channels             int        `json:"channels"`
	Selected             bool       `json:"selected"`
	Forced               bool       `json:"forced"`
	HearingImpaired      bool       `json:"hearingImpaired"`
	VisualImpaired       bool       `json:"visualImpaired"`
}

Stream is a single audio / subtitle / video stream on a Part.

func Audio

func Audio(ep *Episode) []*Stream

Audio returns all audio streams from the first part of the first media. Returns nil when the episode has no parts.

func BestByScore

func BestByScore(streams []*Stream, scoreFn func(*Stream) int) *Stream

BestByScore returns the stream with the highest scoreFn value. Ties go to the earlier entry. Returns nil for an empty list.

func FilterByBoolPref

func FilterByBoolPref(streams []*Stream, desired bool, fn func(*Stream) bool) []*Stream

FilterByBoolPref returns streams whose fn value matches desired. If no streams match, the original list is returned unchanged — callers treat the predicate as a preference, not a requirement.

func FilterByLanguage

func FilterByLanguage(streams []*Stream, langCode string) []*Stream

FilterByLanguage returns streams whose LanguageCode equals langCode.

func FindSubtitleByLanguage

func FindSubtitleByLanguage(streams []*Stream, langCode string) *Stream

FindSubtitleByLanguage returns the best subtitle stream matching the given language code, preferring higher-quality codecs (see SubtitleCodecScore). Returns nil if none match.

func MatchAudio

func MatchAudio(ref *Stream, candidates []*Stream) *Stream

MatchAudio finds the best matching audio stream from candidates against a reference stream. Matching logic inspired by Plex-Auto-Languages.

func MatchSubtitle

func MatchSubtitle(ref, refAudio *Stream, candidates []*Stream) *Stream

MatchSubtitle finds the best matching subtitle stream against the reference subtitle. The refAudio parameter is accepted for call-site symmetry with the audio path but is NOT consulted: SubtitleCriteria discards it, because the "no subtitle means no subtitle" policy means the audio language is never used to derive subtitle criteria. Returns nil when no match applies — either because the reference had no subtitle (respecting "no subtitle means no subtitle") or because no candidate meets the derived criteria.

func Selected

func Selected(ep *Episode) (audio, subtitle *Stream)

Selected returns the currently-selected audio and subtitle streams from the first part of the first media of an episode. Either return may be nil if no stream of that type is marked selected (or if the episode has no media/parts at all).

func Subtitle

func Subtitle(ep *Episode) []*Stream

Subtitle returns all subtitle streams from the first part of the first media. Returns nil when the episode has no parts.

func (*Stream) IsAudio

func (s *Stream) IsAudio() bool

IsAudio reports whether the stream is an audio track.

func (*Stream) IsSubtitle

func (s *Stream) IsSubtitle() bool

IsSubtitle reports whether the stream is a subtitle track.

func (*Stream) TitleForMatch

func (s *Stream) TitleForMatch() string

TitleForMatch returns the most-specific non-empty title field on the stream, preferring ExtendedDisplayTitle > DisplayTitle > Title. Used both for scoring (same title wins) and human-readable descriptions.

type StreamType

type StreamType int

StreamType identifies the kind of stream (video, audio, subtitle). The underlying int values match the Plex API wire format and unmarshal directly from JSON integers without a custom decoder.

const (
	StreamTypeVideo    StreamType = 1
	StreamTypeAudio    StreamType = 2
	StreamTypeSubtitle StreamType = 3
)

StreamTypeVideo, StreamTypeAudio, and StreamTypeSubtitle enumerate the stream-type integer values used in the Plex API wire format.

Jump to

Keyboard shortcuts

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