feat(arr): offer subtitle providers to the API

Provider credentials are bootstrap config and never reach the database
(DESIGN.md §10), so which providers exist is settled once at startup;
which of them a search runs is the `providers_enabled` row the API reads
per request. OpenSubtitles.com cannot be called without a registered API
key, so without one it is not offered at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Miguel Palhas
2026-08-24 23:27:08 +01:00
parent 449750e426
commit faa7a0c056
3 changed files with 48 additions and 1 deletions
+46 -1
View File
@@ -144,7 +144,13 @@ async fn run() -> Result<(), Error> {
if let Some(tmdb_url) = config.tmdb_url {
upstreams = upstreams.with_tmdb_url(tmdb_url);
}
let state = AppState::new(upstreams)?.with_database(database.clone());
let state = AppState::new(upstreams)?
.with_database(database.clone())
.with_subtitle_providers(subtitle_providers(
config.opensubtitles_api_key,
config.opensubtitles_username,
config.opensubtitles_password,
));
let app = arr_api::router(state.clone())
.merge(arr_compat::router(compat))
@@ -443,3 +449,42 @@ async fn shutdown() {
tracing::info!("shutting down");
}
/// The subtitle providers this deployment can reach (DESIGN.md §15).
///
/// Credentials are bootstrap config and never reach the database (§10), so
/// which providers *exist* is decided here, once, at startup; which of them a
/// 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>,
password: Option<String>,
) -> Vec<std::sync::Arc<dyn arr_subs::Provider>> {
let mut providers: Vec<std::sync::Arc<dyn arr_subs::Provider>> = Vec::new();
match arr_subs::Podnapisi::new() {
Ok(podnapisi) => providers.push(std::sync::Arc::new(podnapisi)),
Err(error) => tracing::warn!(%error, "Podnapisi not available"),
}
let Some(api_key) = opensubtitles_api_key else {
tracing::info!("no OpenSubtitles.com API key configured; that provider is off");
return providers;
};
match arr_subs::OpenSubtitles::new(arr_subs::OpenSubtitlesConfig {
api_key,
username,
password,
}) {
Ok(opensubtitles) => providers.push(std::sync::Arc::new(opensubtitles)),
Err(error) => tracing::warn!(%error, "OpenSubtitles.com not available"),
}
providers
}