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
Generated
+1
View File
@@ -94,6 +94,7 @@ dependencies = [
"arr-meta", "arr-meta",
"arr-parse", "arr-parse",
"arr-probe", "arr-probe",
"arr-subs",
"axum", "axum",
"base64", "base64",
"chrono", "chrono",
+1
View File
@@ -20,6 +20,7 @@ arr-indexer = { workspace = true }
arr-meta = { workspace = true } arr-meta = { workspace = true }
arr-parse = { workspace = true } arr-parse = { workspace = true }
arr-probe = { workspace = true } arr-probe = { workspace = true }
arr-subs = { workspace = true }
axum = { workspace = true } axum = { workspace = true }
chrono = { workspace = true } chrono = { workspace = true }
include_dir = { workspace = true } include_dir = { workspace = true }
+46 -1
View File
@@ -144,7 +144,13 @@ async fn run() -> Result<(), Error> {
if let Some(tmdb_url) = config.tmdb_url { if let Some(tmdb_url) = config.tmdb_url {
upstreams = upstreams.with_tmdb_url(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()) let app = arr_api::router(state.clone())
.merge(arr_compat::router(compat)) .merge(arr_compat::router(compat))
@@ -443,3 +449,42 @@ async fn shutdown() {
tracing::info!("shutting down"); 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
}