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
- func ContainsDescriptive(title string) bool
- func Desc(s *Stream) string
- func FirstPartID(ep *Episode) int
- func ID(s *Stream) int
- func ScoreAudio(ref, s *Stream) int
- func ScoreSubtitle(ref, s *Stream) int
- func ShouldSkipSubtitleForCommentary(refAudio *Stream, targetAudioStreams []*Stream) bool
- func SubtitleCodecScore(codec string) int
- func SubtitleCriteria(ref, _ *Stream) (langCode string, forcedOnly, hiOnly bool)
- func TitleMatchScore(ref, s *Stream) int
- type Episode
- type FlexInt
- type Label
- type Media
- type Part
- type Stream
- func Audio(ep *Episode) []*Stream
- func BestByScore(streams []*Stream, scoreFn func(*Stream) int) *Stream
- func FilterByBoolPref(streams []*Stream, desired bool, fn func(*Stream) bool) []*Stream
- func FilterByLanguage(streams []*Stream, langCode string) []*Stream
- func FindSubtitleByLanguage(streams []*Stream, langCode string) *Stream
- func MatchAudio(ref *Stream, candidates []*Stream) *Stream
- func MatchSubtitle(ref, refAudio *Stream, candidates []*Stream) *Stream
- func Selected(ep *Episode) (audio, subtitle *Stream)
- func Subtitle(ep *Episode) []*Stream
- type StreamType
Constants ¶
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 ¶
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 ¶
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 ¶
FirstPartID returns the part ID of the first media part, or 0 when the episode has no media/parts.
func ID ¶
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 ¶
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 ¶
ScoreSubtitle ranks a candidate subtitle stream against a reference. Higher is better. Returns 0 when ref is nil.
func ShouldSkipSubtitleForCommentary ¶
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 ¶
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 ¶
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 ¶
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 ¶
EpisodeNum returns the parsed episode index, or 0 when the Index field is absent. See SeasonNum for the FlexInt rationale.
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 ¶
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 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 ¶
Audio returns all audio streams from the first part of the first media. Returns nil when the episode has no parts.
func BestByScore ¶
BestByScore returns the stream with the highest scoreFn value. Ties go to the earlier entry. Returns nil for an empty list.
func FilterByBoolPref ¶
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 ¶
FilterByLanguage returns streams whose LanguageCode equals langCode.
func FindSubtitleByLanguage ¶
FindSubtitleByLanguage returns the best subtitle stream matching the given language code, preferring higher-quality codecs (see SubtitleCodecScore). Returns nil if none match.
func MatchAudio ¶
MatchAudio finds the best matching audio stream from candidates against a reference stream. Matching logic inspired by Plex-Auto-Languages.
func MatchSubtitle ¶
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 ¶
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 ¶
Subtitle returns all subtitle streams from the first part of the first media. Returns nil when the episode has no parts.
func (*Stream) IsSubtitle ¶
IsSubtitle reports whether the stream is a subtitle track.
func (*Stream) TitleForMatch ¶
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.