feat(arr): add an env-only Podnapisi URL seam
ARR_PODNAPISI_URL, same shape as the existing tmdb_url seam: a test harness can point the provider at a wiremock fake without touching DESIGN.md §10's config surface.
This commit is contained in:
@@ -48,6 +48,11 @@ pub const ENV_TRANSLATE_GOOGLE_BASE_URL: &str = "ARR_TRANSLATE_GOOGLE_BASE_URL";
|
||||
pub const ENV_TRANSLATE_COMMAND_TEMPLATE: &str = "ARR_TRANSLATE_COMMAND_TEMPLATE";
|
||||
pub const ENV_ALASS_PATH: &str = "ARR_ALASS_PATH";
|
||||
pub const ENV_FFMPEG_PATH: &str = "ARR_FFMPEG_PATH";
|
||||
/// E2E seam only, env-only, same shape as [`ENV_TMDB_URL`]: `None` means
|
||||
/// Podnapisi's real address. Kept out of the config file for the same
|
||||
/// reason `tmdb_url` is — DESIGN.md §10 has no business exposing a seam that
|
||||
/// only a test harness uses.
|
||||
pub const ENV_PODNAPISI_URL: &str = "ARR_PODNAPISI_URL";
|
||||
|
||||
pub const DEFAULT_BIND_ADDR: &str = "0.0.0.0:7878";
|
||||
pub const DEFAULT_DATABASE_PATH: &str = "arr.db";
|
||||
@@ -167,6 +172,7 @@ pub struct EnvOverrides {
|
||||
pub tmdb_url: Option<String>,
|
||||
pub jellyfin_url: Option<String>,
|
||||
pub jellyfin_api_key: Option<String>,
|
||||
pub podnapisi_url: Option<String>,
|
||||
pub ntfy_url: Option<String>,
|
||||
pub ntfy_operator_topic: Option<String>,
|
||||
pub opensubtitles_api_key: Option<String>,
|
||||
@@ -203,6 +209,7 @@ impl EnvOverrides {
|
||||
tmdb_url: std::env::var(ENV_TMDB_URL).ok(),
|
||||
jellyfin_url: std::env::var(ENV_JELLYFIN_URL).ok(),
|
||||
jellyfin_api_key: std::env::var(ENV_JELLYFIN_API_KEY).ok(),
|
||||
podnapisi_url: std::env::var(ENV_PODNAPISI_URL).ok(),
|
||||
ntfy_url: std::env::var(ENV_NTFY_URL).ok(),
|
||||
ntfy_operator_topic: std::env::var(ENV_NTFY_OPERATOR_TOPIC).ok(),
|
||||
opensubtitles_api_key: std::env::var(ENV_OPENSUBTITLES_API_KEY).ok(),
|
||||
@@ -245,6 +252,9 @@ pub struct Config {
|
||||
pub tmdb_url: Option<String>,
|
||||
pub jellyfin_url: String,
|
||||
pub jellyfin_api_key: Option<String>,
|
||||
/// E2E seam only, env-only. `None` means Podnapisi's built-in default
|
||||
/// address; see [`ENV_PODNAPISI_URL`].
|
||||
pub podnapisi_url: Option<String>,
|
||||
pub ntfy_url: String,
|
||||
/// The operator's ntfy topic (DESIGN.md §9.5) for *needs a decision* and
|
||||
/// *broken*. `None` means those two notifications are skipped — there is
|
||||
@@ -424,6 +434,7 @@ impl Config {
|
||||
.or(file.jellyfin_url)
|
||||
.unwrap_or_else(|| DEFAULT_JELLYFIN_URL.to_string()),
|
||||
jellyfin_api_key: env.jellyfin_api_key,
|
||||
podnapisi_url: env.podnapisi_url,
|
||||
ntfy_url: env
|
||||
.ntfy_url
|
||||
.or(file.ntfy_url)
|
||||
@@ -483,6 +494,7 @@ mod tests {
|
||||
DEFAULT_SEED_IDLE_LIMIT_MINUTES
|
||||
);
|
||||
assert_eq!(config.jellyfin_url, DEFAULT_JELLYFIN_URL);
|
||||
assert_eq!(config.podnapisi_url, None);
|
||||
assert!(config.tracker_seeding.is_empty());
|
||||
assert_eq!(config.ntfy_url, DEFAULT_NTFY_URL);
|
||||
assert_eq!(config.ntfy_operator_topic, None);
|
||||
@@ -675,6 +687,37 @@ alass_path = "/usr/local/bin/alass"
|
||||
));
|
||||
}
|
||||
|
||||
/// Same env-only seam as `tmdb_url`, for the same reason: `arr-e2e` needs
|
||||
/// to point Podnapisi at a `wiremock` fake without a live-tracker risk
|
||||
/// creeping into the config file (DESIGN.md §15, §10).
|
||||
#[test]
|
||||
fn podnapisi_url_is_an_env_only_seam() {
|
||||
let config = Config::resolve(EnvOverrides::default()).unwrap();
|
||||
assert_eq!(config.podnapisi_url, None);
|
||||
|
||||
let env = EnvOverrides {
|
||||
podnapisi_url: Some("http://127.0.0.1:9/subtitles".into()),
|
||||
..EnvOverrides::default()
|
||||
};
|
||||
let config = Config::resolve(env).unwrap();
|
||||
assert_eq!(
|
||||
config.podnapisi_url.as_deref(),
|
||||
Some("http://127.0.0.1:9/subtitles")
|
||||
);
|
||||
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("arr.toml");
|
||||
std::fs::write(&path, "podnapisi_url = \"http://127.0.0.1:9/subtitles\"\n").unwrap();
|
||||
let env = EnvOverrides {
|
||||
config_file: Some(path.to_string_lossy().into_owned()),
|
||||
..EnvOverrides::default()
|
||||
};
|
||||
assert!(matches!(
|
||||
Config::resolve(env),
|
||||
Err(ConfigError::TomlDecode(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_secret_in_the_config_file_is_a_parse_error() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
||||
@@ -133,6 +133,7 @@ fn api_state(
|
||||
config.opensubtitles_api_key.clone(),
|
||||
config.opensubtitles_username.clone(),
|
||||
config.opensubtitles_password.clone(),
|
||||
config.podnapisi_url.clone(),
|
||||
))
|
||||
.with_translation_backends(translators.backends.clone())
|
||||
.with_jellyfin(jellyfin)
|
||||
@@ -541,6 +542,7 @@ fn subtitle_action(
|
||||
config.opensubtitles_api_key.clone(),
|
||||
config.opensubtitles_username.clone(),
|
||||
config.opensubtitles_password.clone(),
|
||||
config.podnapisi_url.clone(),
|
||||
),
|
||||
translators.backends.clone(),
|
||||
arr_subs::Syncer::new().with_binary(config.alass_path.clone()),
|
||||
@@ -564,10 +566,17 @@ fn subtitle_providers(
|
||||
opensubtitles_api_key: Option<String>,
|
||||
username: Option<String>,
|
||||
password: Option<String>,
|
||||
podnapisi_url: 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() {
|
||||
// `podnapisi_url` is the e2e-only seam (`ARR_PODNAPISI_URL`); absent, this
|
||||
// is the real Podnapisi.net.
|
||||
let mut builder = arr_subs::Podnapisi::builder();
|
||||
if let Some(url) = podnapisi_url {
|
||||
builder = builder.base_url(url);
|
||||
}
|
||||
match builder.build() {
|
||||
Ok(podnapisi) => providers.push(std::sync::Arc::new(podnapisi)),
|
||||
Err(error) => tracing::warn!(%error, "Podnapisi not available"),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user