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 Intent
- type IntentStream
- 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"`
// 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 ¶
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("").
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
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
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
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 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.