package api import "strings" // Which subtitle track a viewer gets, decided here rather than on the television. // // The gateway is already the thing that asks Emby what tracks exist — it calls // PlaybackInfo and enumerates the streams — so it is also the thing that should say which // of them to turn on. The television used to guess (forced, then default, then whatever was // first), which meant the answer depended on which set somebody happened to be watching on, // and turning subtitles off in the bedroom taught the living room nothing. // // The preference itself lives in the synced settings document (`subtitlesEnabled`, // `subtitleLanguage`), so it follows the person rather than the box, and an operator can // read and push it from the accounts page like any other setting. // subtitleLanguage is one entry in the vocabulary offered for `subtitleLanguage`. // // Value is ISO 639-1 because that is what a media3 track reports after normalisation; // aliases are the ISO 639-2 forms Emby actually puts in a stream's Language field, which // for several languages is two different three-letter codes (bibliographic and // terminological) for the same thing. type subtitleLanguage struct { Value string Label string Aliases []string } var subtitleLanguages = []subtitleLanguage{ {Value: "en", Label: "English", Aliases: []string{"eng"}}, {Value: "it", Label: "Italian", Aliases: []string{"ita"}}, {Value: "es", Label: "Spanish", Aliases: []string{"spa", "esp"}}, {Value: "fr", Label: "French", Aliases: []string{"fre", "fra"}}, {Value: "de", Label: "German", Aliases: []string{"ger", "deu"}}, {Value: "pt", Label: "Portuguese", Aliases: []string{"por"}}, {Value: "nl", Label: "Dutch", Aliases: []string{"dut", "nld"}}, {Value: "sv", Label: "Swedish", Aliases: []string{"swe"}}, {Value: "da", Label: "Danish", Aliases: []string{"dan"}}, {Value: "no", Label: "Norwegian", Aliases: []string{"nor", "nob", "nno"}}, {Value: "fi", Label: "Finnish", Aliases: []string{"fin"}}, {Value: "pl", Label: "Polish", Aliases: []string{"pol"}}, {Value: "cs", Label: "Czech", Aliases: []string{"cze", "ces"}}, {Value: "hu", Label: "Hungarian", Aliases: []string{"hun"}}, {Value: "ro", Label: "Romanian", Aliases: []string{"rum", "ron"}}, {Value: "el", Label: "Greek", Aliases: []string{"gre", "ell"}}, {Value: "ru", Label: "Russian", Aliases: []string{"rus"}}, {Value: "uk", Label: "Ukrainian", Aliases: []string{"ukr"}}, {Value: "tr", Label: "Turkish", Aliases: []string{"tur"}}, {Value: "ar", Label: "Arabic", Aliases: []string{"ara"}}, {Value: "he", Label: "Hebrew", Aliases: []string{"heb", "iw"}}, {Value: "hi", Label: "Hindi", Aliases: []string{"hin"}}, {Value: "ja", Label: "Japanese", Aliases: []string{"jpn"}}, {Value: "ko", Label: "Korean", Aliases: []string{"kor"}}, {Value: "zh", Label: "Chinese", Aliases: []string{"chi", "zho", "cmn", "yue"}}, {Value: "th", Label: "Thai", Aliases: []string{"tha"}}, {Value: "vi", Label: "Vietnamese", Aliases: []string{"vie"}}, } // subtitleLanguageAuto means "no preference": keep the old flag-driven behaviour, which is // what somebody who has never opened the subtitle menu should still get. const subtitleLanguageAuto = "auto" func subtitleLanguageOptions() []preferenceOption { options := make([]preferenceOption, 0, len(subtitleLanguages)+1) options = append(options, option(subtitleLanguageAuto, "Automatic")) for _, language := range subtitleLanguages { options = append(options, option(language.Value, language.Label)) } return options } // normalizeSubtitleLanguage folds a stream's language onto the catalogue's vocabulary. // Anything unrecognised comes back as the lowercased original rather than empty, so an // exact string match on a language Memby has no entry for still works. func normalizeSubtitleLanguage(raw string) string { value := strings.ToLower(strings.TrimSpace(raw)) if value == "" { return "" } // Emby writes plain codes, but a region tag ("pt-BR") or a script suffix is possible. if cut := strings.IndexAny(value, "-_"); cut > 0 { value = value[:cut] } for _, language := range subtitleLanguages { if language.Value == value { return language.Value } for _, alias := range language.Aliases { if alias == value { return language.Value } } } return value } // selectSubtitle returns the id of the track to turn on, or "" for none. // // The rules, in order, and each one is there for a reason worth keeping: // // - Subtitles off means off. Nothing else is consulted. // - A chosen language wins, and among tracks in it a plain full track beats a forced or // hearing-impaired one — somebody who picked Italian wants the dialogue, not the // signs-only track that happens to sort first. // - A chosen language that the title does not have falls back to a *forced* track only. // Forced subtitles carry the parts of a film that are foreign to its own audio, so they // are wanted regardless; falling through to English instead would put a language the // viewer did not ask for on screen. // - With no language chosen, the old behaviour stands: forced, then default, then the // first track. That is what an install that has never touched the setting keeps. func selectSubtitle(subtitles []playableSubtitle, enabled bool, language string) string { if !enabled || len(subtitles) == 0 { return "" } preferred := normalizeSubtitleLanguage(language) if preferred != "" && preferred != subtitleLanguageAuto { if match := bestSubtitleInLanguage(subtitles, preferred); match != "" { return match } return firstSubtitleWhere(subtitles, func(s playableSubtitle) bool { return s.IsForced }) } if forced := firstSubtitleWhere( subtitles, func(s playableSubtitle) bool { return s.IsForced }, ); forced != "" { return forced } if fallback := firstSubtitleWhere( subtitles, func(s playableSubtitle) bool { return s.IsDefault }, ); fallback != "" { return fallback } return subtitles[0].ID } // bestSubtitleInLanguage ranks the tracks in one language: a plain track, then a default // one, then hearing-impaired, then forced. Forced sorts last here precisely because it is // not a substitute for a full track in the language somebody asked for. func bestSubtitleInLanguage(subtitles []playableSubtitle, language string) string { best, bestRank := "", 0 for _, subtitle := range subtitles { if normalizeSubtitleLanguage(subtitle.Language) != language { continue } rank := 4 switch { case subtitle.IsForced: rank = 1 case subtitle.IsHearingImpaired: rank = 2 case subtitle.IsDefault: rank = 5 } if rank > bestRank { best, bestRank = subtitle.ID, rank } } return best } func firstSubtitleWhere(subtitles []playableSubtitle, match func(playableSubtitle) bool) string { for _, subtitle := range subtitles { if match(subtitle) { return subtitle.ID } } return "" }