streams

package
v1.5.8 Latest Latest
Warning

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

Go to latest
Published: Jul 14, 2026 License: GPL-2.0, GPL-3.0 Imports: 6 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"`
	GrandparentTitle     string  `json:"grandparentTitle"`
	ParentTitle          string  `json:"parentTitle"`
	Title                string  `json:"title"`
	Type                 string  `json:"type"`
	LibraryTitle         string  `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("").

Malformed inputs (non-numeric strings, floating-point numbers, objects, arrays) return a parse error. 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 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