recognition: глобальный переключатель языка вывода [general].language
- новое поле [general].language (ru|en, дефолт en) — единый источник языка локализованного title детектора и локали запросов к метабазам; original_title всегда на языке оригинала - локаль TMDB (поиск + credits) выводится из него, [metadata.tmdb].language убран - промпт LLM явно задаёт язык title с fallback на оригинал
This commit is contained in:
@@ -38,6 +38,12 @@ type General struct {
|
||||
// Пусто → UTC. База зон встроена (time/tzdata), поэтому имя валидируется
|
||||
// одинаково на любом хосте (см. DisplayLocation).
|
||||
Timezone string `toml:"timezone"`
|
||||
// Language — язык локализованного вывода: `title` от LLM-детектора и
|
||||
// режиссёр/локаль запросов к метабазам. Допустимо: "ru" | "en", пусто → "en"
|
||||
// (см. ContentLanguage). `original_title` от настройки не зависит — всегда на
|
||||
// языке оригинала. Диалект локали провайдера (напр. TMDB ru-RU/en-US) выводит
|
||||
// сам провайдер из этого кода.
|
||||
Language string `toml:"language"`
|
||||
}
|
||||
|
||||
// QBittorrent — доступ к qBittorrent WebUI и раскладка путей загрузок.
|
||||
@@ -86,14 +92,14 @@ type Metadata struct {
|
||||
}
|
||||
|
||||
// MetadataProvider — настройки одного провайдера метаданных. У keyless-баз
|
||||
// (TVMaze) поле api_key не используется; language учитывает только TMDB
|
||||
// (локаль возвращаемых названий, дефолт ru-RU).
|
||||
// (TVMaze) поле api_key не используется. Язык названий задаёт глобальный
|
||||
// [general].language (локаль провайдера выводится из него), отдельной настройки
|
||||
// у провайдера нет.
|
||||
type MetadataProvider struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
APIKey string `toml:"api_key"`
|
||||
Proxy string `toml:"proxy"`
|
||||
Timeout Duration `toml:"timeout"`
|
||||
Language string `toml:"language"`
|
||||
Enabled bool `toml:"enabled"`
|
||||
APIKey string `toml:"api_key"`
|
||||
Proxy string `toml:"proxy"`
|
||||
Timeout Duration `toml:"timeout"`
|
||||
}
|
||||
|
||||
// Jellyfin — пересканирование медиатеки после раскладки (опц.). Включается
|
||||
@@ -181,11 +187,22 @@ func (c *Config) DisplayLocation() (*time.Location, error) {
|
||||
return loc, nil
|
||||
}
|
||||
|
||||
// ContentLanguage возвращает язык локализованного вывода абстрактным кодом
|
||||
// ("ru" | "en"); пусто → "en". Диалект локали конкретного провайдера (ru-RU,
|
||||
// en-US, …) выводит сам провайдер из этого кода — здесь его не знаем. Значение
|
||||
// провалидировано на старте (validate): либо пусто, либо один из кодов.
|
||||
func (c *Config) ContentLanguage() string {
|
||||
if c.General.Language == "" {
|
||||
return "en"
|
||||
}
|
||||
return c.General.Language
|
||||
}
|
||||
|
||||
// Default возвращает конфиг с разумными умолчаниями; значения из файла
|
||||
// перекрывают их при загрузке.
|
||||
func Default() *Config {
|
||||
return &Config{
|
||||
General: General{Timezone: "UTC"},
|
||||
General: General{Timezone: "UTC", Language: "en"},
|
||||
QBittorrent: QBittorrent{
|
||||
URL: "http://qbit:8989",
|
||||
Username: "admin",
|
||||
@@ -204,7 +221,7 @@ func Default() *Config {
|
||||
MaxRetries: 3,
|
||||
},
|
||||
Metadata: Metadata{
|
||||
TMDB: MetadataProvider{Timeout: Duration(10 * time.Second), Language: "ru-RU"},
|
||||
TMDB: MetadataProvider{Timeout: Duration(10 * time.Second)},
|
||||
TVDB: MetadataProvider{Timeout: Duration(10 * time.Second)},
|
||||
},
|
||||
Jellyfin: Jellyfin{Timeout: Duration(10 * time.Second)},
|
||||
@@ -264,6 +281,16 @@ func (c *Config) validate() error {
|
||||
if _, err := c.DisplayLocation(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
// Язык локализованного вывода: пусто (→ en) или один из кодов. Fail-fast,
|
||||
// как llm.type: мусорное значение не должно молча дефолтить. Множество
|
||||
// {ru, en} — канон; при добавлении кода синхронно расширь мапперы
|
||||
// metadata.tmdbLocale и recognize.languageDirective, иначе новый язык молча
|
||||
// даст английский вывод.
|
||||
switch c.General.Language {
|
||||
case "", "ru", "en":
|
||||
default:
|
||||
errs = append(errs, fmt.Errorf("unsupported general.language %q (supported: ru, en)", c.General.Language))
|
||||
}
|
||||
|
||||
// Медиа-пути песочницы: абсолютные, без traversal, существующие каталоги.
|
||||
for _, p := range []struct{ name, path string }{
|
||||
|
||||
@@ -74,6 +74,30 @@ func TestDisplayLocation(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestContentLanguage — язык вывода: дефолт en (в т.ч. из Default()), пусто → en,
|
||||
// коды ru/en возвращаются как есть.
|
||||
func TestContentLanguage(t *testing.T) {
|
||||
if got := Default().ContentLanguage(); got != "en" {
|
||||
t.Fatalf("Default().ContentLanguage() = %q, want en", got)
|
||||
}
|
||||
cases := map[string]string{"": "en", "ru": "ru", "en": "en"}
|
||||
for in, want := range cases {
|
||||
c := &Config{General: General{Language: in}}
|
||||
if got := c.ContentLanguage(); got != want {
|
||||
t.Errorf("ContentLanguage(%q) = %q, want %q", in, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidate_EmptyLanguageOK — пустой язык валиден (нормализуется в en аксессором).
|
||||
func TestValidate_EmptyLanguageOK(t *testing.T) {
|
||||
c := validCfg(t)
|
||||
c.General.Language = ""
|
||||
if err := c.validate(); err != nil {
|
||||
t.Fatalf("пустой general.language должен быть валиден, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidate_Errors(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
@@ -94,6 +118,7 @@ func TestValidate_Errors(t *testing.T) {
|
||||
{"jellyfin enabled no key", func(c *Config) { c.Jellyfin.Enabled = true; c.Jellyfin.URL = "http://j"; c.Jellyfin.APIKey = "" }, "jellyfin.api_key"},
|
||||
{"telegram enabled no token", func(c *Config) { c.Telegram.Enabled = true }, "telegram.token"},
|
||||
{"bad timezone", func(c *Config) { c.General.Timezone = "Mars/Phobos" }, "general.timezone"},
|
||||
{"bad language", func(c *Config) { c.General.Language = "de" }, "general.language"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user