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_TRANSLATE_COMMAND_TEMPLATE: &str = "ARR_TRANSLATE_COMMAND_TEMPLATE";
|
||||||
pub const ENV_ALASS_PATH: &str = "ARR_ALASS_PATH";
|
pub const ENV_ALASS_PATH: &str = "ARR_ALASS_PATH";
|
||||||
pub const ENV_FFMPEG_PATH: &str = "ARR_FFMPEG_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_BIND_ADDR: &str = "0.0.0.0:7878";
|
||||||
pub const DEFAULT_DATABASE_PATH: &str = "arr.db";
|
pub const DEFAULT_DATABASE_PATH: &str = "arr.db";
|
||||||
@@ -167,6 +172,7 @@ pub struct EnvOverrides {
|
|||||||
pub tmdb_url: Option<String>,
|
pub tmdb_url: Option<String>,
|
||||||
pub jellyfin_url: Option<String>,
|
pub jellyfin_url: Option<String>,
|
||||||
pub jellyfin_api_key: Option<String>,
|
pub jellyfin_api_key: Option<String>,
|
||||||
|
pub podnapisi_url: Option<String>,
|
||||||
pub ntfy_url: Option<String>,
|
pub ntfy_url: Option<String>,
|
||||||
pub ntfy_operator_topic: Option<String>,
|
pub ntfy_operator_topic: Option<String>,
|
||||||
pub opensubtitles_api_key: Option<String>,
|
pub opensubtitles_api_key: Option<String>,
|
||||||
@@ -203,6 +209,7 @@ impl EnvOverrides {
|
|||||||
tmdb_url: std::env::var(ENV_TMDB_URL).ok(),
|
tmdb_url: std::env::var(ENV_TMDB_URL).ok(),
|
||||||
jellyfin_url: std::env::var(ENV_JELLYFIN_URL).ok(),
|
jellyfin_url: std::env::var(ENV_JELLYFIN_URL).ok(),
|
||||||
jellyfin_api_key: std::env::var(ENV_JELLYFIN_API_KEY).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_url: std::env::var(ENV_NTFY_URL).ok(),
|
||||||
ntfy_operator_topic: std::env::var(ENV_NTFY_OPERATOR_TOPIC).ok(),
|
ntfy_operator_topic: std::env::var(ENV_NTFY_OPERATOR_TOPIC).ok(),
|
||||||
opensubtitles_api_key: std::env::var(ENV_OPENSUBTITLES_API_KEY).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 tmdb_url: Option<String>,
|
||||||
pub jellyfin_url: String,
|
pub jellyfin_url: String,
|
||||||
pub jellyfin_api_key: Option<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,
|
pub ntfy_url: String,
|
||||||
/// The operator's ntfy topic (DESIGN.md §9.5) for *needs a decision* and
|
/// The operator's ntfy topic (DESIGN.md §9.5) for *needs a decision* and
|
||||||
/// *broken*. `None` means those two notifications are skipped — there is
|
/// *broken*. `None` means those two notifications are skipped — there is
|
||||||
@@ -424,6 +434,7 @@ impl Config {
|
|||||||
.or(file.jellyfin_url)
|
.or(file.jellyfin_url)
|
||||||
.unwrap_or_else(|| DEFAULT_JELLYFIN_URL.to_string()),
|
.unwrap_or_else(|| DEFAULT_JELLYFIN_URL.to_string()),
|
||||||
jellyfin_api_key: env.jellyfin_api_key,
|
jellyfin_api_key: env.jellyfin_api_key,
|
||||||
|
podnapisi_url: env.podnapisi_url,
|
||||||
ntfy_url: env
|
ntfy_url: env
|
||||||
.ntfy_url
|
.ntfy_url
|
||||||
.or(file.ntfy_url)
|
.or(file.ntfy_url)
|
||||||
@@ -483,6 +494,7 @@ mod tests {
|
|||||||
DEFAULT_SEED_IDLE_LIMIT_MINUTES
|
DEFAULT_SEED_IDLE_LIMIT_MINUTES
|
||||||
);
|
);
|
||||||
assert_eq!(config.jellyfin_url, DEFAULT_JELLYFIN_URL);
|
assert_eq!(config.jellyfin_url, DEFAULT_JELLYFIN_URL);
|
||||||
|
assert_eq!(config.podnapisi_url, None);
|
||||||
assert!(config.tracker_seeding.is_empty());
|
assert!(config.tracker_seeding.is_empty());
|
||||||
assert_eq!(config.ntfy_url, DEFAULT_NTFY_URL);
|
assert_eq!(config.ntfy_url, DEFAULT_NTFY_URL);
|
||||||
assert_eq!(config.ntfy_operator_topic, None);
|
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]
|
#[test]
|
||||||
fn a_secret_in_the_config_file_is_a_parse_error() {
|
fn a_secret_in_the_config_file_is_a_parse_error() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ fn api_state(
|
|||||||
config.opensubtitles_api_key.clone(),
|
config.opensubtitles_api_key.clone(),
|
||||||
config.opensubtitles_username.clone(),
|
config.opensubtitles_username.clone(),
|
||||||
config.opensubtitles_password.clone(),
|
config.opensubtitles_password.clone(),
|
||||||
|
config.podnapisi_url.clone(),
|
||||||
))
|
))
|
||||||
.with_translation_backends(translators.backends.clone())
|
.with_translation_backends(translators.backends.clone())
|
||||||
.with_jellyfin(jellyfin)
|
.with_jellyfin(jellyfin)
|
||||||
@@ -541,6 +542,7 @@ fn subtitle_action(
|
|||||||
config.opensubtitles_api_key.clone(),
|
config.opensubtitles_api_key.clone(),
|
||||||
config.opensubtitles_username.clone(),
|
config.opensubtitles_username.clone(),
|
||||||
config.opensubtitles_password.clone(),
|
config.opensubtitles_password.clone(),
|
||||||
|
config.podnapisi_url.clone(),
|
||||||
),
|
),
|
||||||
translators.backends.clone(),
|
translators.backends.clone(),
|
||||||
arr_subs::Syncer::new().with_binary(config.alass_path.clone()),
|
arr_subs::Syncer::new().with_binary(config.alass_path.clone()),
|
||||||
@@ -564,10 +566,17 @@ fn subtitle_providers(
|
|||||||
opensubtitles_api_key: Option<String>,
|
opensubtitles_api_key: Option<String>,
|
||||||
username: Option<String>,
|
username: Option<String>,
|
||||||
password: Option<String>,
|
password: Option<String>,
|
||||||
|
podnapisi_url: Option<String>,
|
||||||
) -> Vec<std::sync::Arc<dyn arr_subs::Provider>> {
|
) -> Vec<std::sync::Arc<dyn arr_subs::Provider>> {
|
||||||
let mut providers: Vec<std::sync::Arc<dyn arr_subs::Provider>> = Vec::new();
|
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)),
|
Ok(podnapisi) => providers.push(std::sync::Arc::new(podnapisi)),
|
||||||
Err(error) => tracing::warn!(%error, "Podnapisi not available"),
|
Err(error) => tracing::warn!(%error, "Podnapisi not available"),
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user