feat(arr): wire translation backends into daemon

Forward each translate-* feature from arr-daemon to arr-subs, add the
missing OpenAI model bootstrap key, and construct the compiled and
credentialed backends at startup so the translate endpoint stops
answering 503 unconditionally.
This commit is contained in:
Miguel Palhas
2026-08-25 02:01:55 +01:00
parent f1a58c0810
commit e49bc2736d
3 changed files with 206 additions and 34 deletions
+93 -4
View File
@@ -131,6 +131,7 @@ fn api_state(
config.opensubtitles_username.clone(),
config.opensubtitles_password.clone(),
))
.with_translation_backends(translation_backends(config))
.with_jellyfin(jellyfin)
.with_syncer(arr_subs::Syncer::new().with_binary(config.alass_path.clone())))
}
@@ -480,10 +481,6 @@ fn jellyfin_client(config: &Config) -> Result<arr_api::jellyfin::JellyfinClient,
/// search *runs* is the `providers_enabled` setting the API reads per
/// request. OpenSubtitles.com needs a registered API key to be called at all,
/// so without one it is not offered; Podnapisi is anonymous and always is.
///
/// Translation backends are not wired: none is implemented yet (#191-#193),
/// and `AppState` defaults to an empty registry, which makes a manual
/// translation answer "no such engine" rather than fail obscurely.
fn subtitle_providers(
opensubtitles_api_key: Option<String>,
username: Option<String>,
@@ -511,3 +508,95 @@ fn subtitle_providers(
providers
}
/// The translation backends this deployment can offer (DESIGN.md §15,
/// issue #216).
///
/// Which cargo features this binary was built with decides what could ever
/// be here (`compiled_engines`); credentials decide what actually is, same
/// split `subtitle_providers` makes for search. Which one of these a
/// translation *uses* is the `translation_engine` database setting, read per
/// request (#198) — this only decides which ids exist to be picked.
#[cfg_attr(
not(any(
feature = "translate-openai",
feature = "translate-deepl",
feature = "translate-google",
feature = "translate-command"
)),
allow(unused_variables, unused_mut)
)]
fn translation_backends(config: &Config) -> Vec<std::sync::Arc<dyn arr_subs::Backend>> {
let mut backends: Vec<std::sync::Arc<dyn arr_subs::Backend>> = Vec::new();
#[cfg(feature = "translate-openai")]
{
let openai_config = arr_subs::OpenAiConfig {
api_key: config.translate_openai_api_key.clone(),
};
let backend = match &config.translate_openai_base_url {
Some(base_url) => arr_subs::OpenAi::with_base_url(
config.translate_openai_model.clone(),
openai_config,
base_url,
),
None => arr_subs::OpenAi::new(config.translate_openai_model.clone(), openai_config),
};
match backend {
Ok(backend) => backends.push(std::sync::Arc::new(backend)),
Err(error) => tracing::warn!(%error, "OpenAI-compatible translator not available"),
}
}
#[cfg(feature = "translate-deepl")]
{
if let Some(auth_key) = config.translate_deepl_api_key.clone() {
let deepl_config = arr_subs::DeepLConfig { auth_key };
let backend = match &config.translate_deepl_base_url {
Some(base_url) => arr_subs::DeepL::with_base_url(deepl_config, base_url),
None => arr_subs::DeepL::new(deepl_config),
};
match backend {
Ok(backend) => backends.push(std::sync::Arc::new(backend)),
Err(error) => tracing::warn!(%error, "DeepL not available"),
}
} else {
tracing::info!("no DeepL auth key configured; that translator is off");
}
}
#[cfg(feature = "translate-google")]
{
if let Some(api_key) = config.translate_google_api_key.clone() {
let google_config = arr_subs::GoogleConfig { api_key };
let backend = match &config.translate_google_base_url {
Some(base_url) => arr_subs::Google::with_base_url(google_config, base_url),
None => arr_subs::Google::new(google_config),
};
match backend {
Ok(backend) => backends.push(std::sync::Arc::new(backend)),
Err(error) => tracing::warn!(%error, "Google Translate not available"),
}
} else {
tracing::info!("no Google Translate API key configured; that translator is off");
}
}
#[cfg(feature = "translate-command")]
{
if let Some(template) = config.translate_command_template.clone() {
let command_config = arr_subs::CommandConfig {
template,
timeout: arr_subs::COMMAND_DEFAULT_TIMEOUT,
};
match arr_subs::Command::new(command_config) {
Ok(backend) => backends.push(std::sync::Arc::new(backend)),
Err(error) => tracing::warn!(%error, "remote-command translator not available"),
}
} else {
tracing::info!("no remote-command template configured; that translator is off");
}
}
backends
}