App v0.2.26 and gateway 0.1.20
Client: seek controls, Bazarr subtitle download and cast panel in the player; MDBList ratings strip; episode and schedule detail pages; series pace estimate; what's new panel; install-permission onboarding step; synced per-profile preferences; Emby outage banner. Gateway: rebuilt admin console (one fragment per page), preference history and restore, merged Continue Watching, Emby health probe, subtitle selection and Bazarr download, structured request logging with per-request identity, and embedded build version. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
2675e6d82b
commit
4a4df7a73c
@@ -0,0 +1,167 @@
|
||||
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 ""
|
||||
}
|
||||
Reference in New Issue
Block a user