feat: ntfy notifications for imported, needs-decision, broken (#96)
ci / web (push) Successful in 1m0s
e2e / e2e (push) Successful in 3m41s
ci / rust (push) Successful in 5m42s

This commit was merged in pull request #96.
This commit is contained in:
2026-08-23 02:04:36 +01:00
parent 9f812fac11
commit ad0eb0d9d3
11 changed files with 1006 additions and 62 deletions
+46 -2
View File
@@ -1,10 +1,13 @@
//! arr — reconcile loop and process entry point. See DESIGN.md §8.
mod attention;
mod broken;
mod config;
mod grab;
mod import;
mod indexers;
mod jellyfin;
mod notify;
mod reaper;
pub mod reconcile;
mod rss;
@@ -18,9 +21,12 @@ use arr_api::{AppState, Upstreams};
use arr_compat::CompatState;
use arr_db::Db;
use arr_meta::TmdbClient;
use attention::AttentionAction;
use broken::BrokenAction;
use config::Config;
use grab::{GrabAction, SeedingLimits, SeedingRules};
use import::ImportAction;
use notify::Notifier;
use reaper::ReaperAction;
use reconcile::{ReconcileLoop, Tick};
use rss::RssAction;
@@ -85,6 +91,8 @@ enum Error {
Transmission(#[from] arr_dl::Error),
#[error("jellyfin client: {0}")]
Jellyfin(#[from] jellyfin::Error),
#[error("ntfy client: {0}")]
Notify(#[from] notify::NotifyError),
#[error("bind {addr}: {source}")]
Bind {
addr: std::net::SocketAddr,
@@ -111,7 +119,8 @@ async fn run() -> Result<(), Error> {
} else {
None
};
let reconcile = reconcile_loop(&database, &config, &transmission, tmdb.as_ref())?;
let notifier = Notifier::new(config.ntfy_url.clone())?;
let reconcile = reconcile_loop(&database, &config, &transmission, tmdb.as_ref(), &notifier)?;
// Jellyseerr's Radarr shim (DESIGN.md §9.4) reads the same database and
// needs its own TMDB client for `movie/lookup`.
@@ -176,6 +185,7 @@ fn reconcile_loop(
config: &Config,
transmission: &arr_dl::TransmissionClient,
tmdb: Option<&Arc<TmdbClient>>,
notifier: &Notifier,
) -> Result<ReconcileLoop, Error> {
let mut reconcile = ReconcileLoop::new(database.clone());
let seeding = SeedingRules::new(
@@ -252,8 +262,42 @@ fn reconcile_loop(
// imported on this tick.
reconcile = reconcile.register(
Tick::Reconcile,
ImportAction::new(transmission.clone(), arr_probe::Prober::new(), jellyfin),
ImportAction::new(
transmission.clone(),
arr_probe::Prober::new(),
jellyfin,
notifier.clone(),
config.ntfy_operator_topic.clone(),
),
);
// §9.5 *needs a decision* and *broken* both go to the operator alone;
// without a topic configured there is nowhere to send them.
if let Some(operator_topic) = &config.ntfy_operator_topic {
reconcile = reconcile.register(
Tick::Reconcile,
AttentionAction::new(notifier.clone(), operator_topic.clone()),
);
let broken_upstreams = broken::Upstreams {
prowlarr_url: config.prowlarr_url.clone(),
prowlarr_api_key: config.prowlarr_api_key.clone(),
transmission_url: config.transmission_url.clone(),
tmdb_url: config
.tmdb_url
.clone()
.unwrap_or_else(|| arr_api::DEFAULT_TMDB_URL.to_string()),
tmdb_api_key: config.tmdb_api_key.clone(),
};
reconcile = reconcile.register(
Tick::Reconcile,
BrokenAction::new(broken_upstreams, notifier.clone(), operator_topic.clone()),
);
} else {
tracing::warn!(
"ARR_NTFY_OPERATOR_TOPIC is not configured: needs-a-decision and broken notifications are disabled"
);
}
Ok(reconcile.register(Tick::Reaper, ReaperAction::new(transmission.clone())))
}