feat: Jellyfin library refresh on import (#93)
ci / web (push) Successful in 28s
e2e / e2e (push) Successful in 55s
ci / rust (push) Successful in 2m12s

This commit was merged in pull request #93.
This commit is contained in:
2026-08-23 01:14:46 +01:00
parent 225be33b55
commit 35391f02fc
3 changed files with 144 additions and 4 deletions
+38 -3
View File
@@ -25,6 +25,7 @@ use arr_db::Db;
use arr_dl::TransmissionClient;
use arr_probe::Prober;
use crate::jellyfin::JellyfinClient;
use crate::reconcile::{Action, ActionFuture, Outcome};
/// A failure during one import tick.
@@ -73,6 +74,7 @@ enum ProbeOutcome {
pub struct ImportAction {
transmission: TransmissionClient,
prober: Prober,
jellyfin: JellyfinClient,
/// Probe results by path, kept across ticks. The reconcile lane cancels
/// the whole action after its 25 s budget while one probe alone may take
/// up to 60 s, so without this a large multi-file torrent would restart
@@ -99,10 +101,11 @@ struct PendingImport {
impl ImportAction {
#[must_use]
pub fn new(transmission: TransmissionClient, prober: Prober) -> Self {
pub fn new(transmission: TransmissionClient, prober: Prober, jellyfin: JellyfinClient) -> Self {
Self {
transmission,
prober,
jellyfin,
probed: std::sync::Arc::new(tokio::sync::Mutex::new(HashMap::new())),
}
}
@@ -288,6 +291,7 @@ impl ImportAction {
record_import(database, pending, &feature, waiver.as_ref(), &destination).await?;
self.forget_probes(&paths).await;
self.refresh_jellyfin().await;
let path_text = destination.to_string_lossy().into_owned();
tracing::info!(
grab_id = pending.grab_id,
@@ -304,6 +308,15 @@ impl ImportAction {
)))
}
/// §7.5: the filesystem watcher misses the just-hardlinked file. A
/// failure to reach Jellyfin must not fail the import, which has already
/// succeeded.
async fn refresh_jellyfin(&self) {
if let Err(error) = self.jellyfin.refresh().await {
tracing::warn!(%error, "jellyfin refresh failed");
}
}
/// §5.7 hard fail: blacklist the release, fail the grab, reopen the gap.
/// The torrent is deliberately untouched (§7.3).
async fn hard_fail(
@@ -545,7 +558,7 @@ mod tests {
use std::path::PathBuf;
use serde_json::json;
use wiremock::matchers::method;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
use super::*;
@@ -583,6 +596,7 @@ mod tests {
library: PathBuf,
action: ImportAction,
_server: MockServer,
jellyfin_server: MockServer,
}
/// An `ffprobe` stand-in: canned JSON for media, a `tty` document for the
@@ -667,8 +681,20 @@ mod tests {
.mount(&server)
.await;
let jellyfin_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/Library/Refresh"))
.respond_with(ResponseTemplate::new(204))
.mount(&jellyfin_server)
.await;
let prober = Prober::new().with_binary(fake_ffprobe(dir.path(), media_json));
let action = ImportAction::new(TransmissionClient::new(&server.uri()).unwrap(), prober);
let jellyfin = JellyfinClient::new(jellyfin_server.uri(), None).unwrap();
let action = ImportAction::new(
TransmissionClient::new(&server.uri()).unwrap(),
prober,
jellyfin,
);
Harness {
_dir: dir,
@@ -677,6 +703,7 @@ mod tests {
library,
action,
_server: server,
jellyfin_server,
}
}
@@ -725,6 +752,12 @@ mod tests {
.await
.unwrap();
assert_eq!(movie_state, "available");
assert_eq!(
h.jellyfin_server.received_requests().await.unwrap().len(),
1,
"§7.5: one refresh call at the end of a successful import"
);
}
/// §5.3 through §5.7: Profile 5 is a hard fail — blacklisted, grab
@@ -948,6 +981,7 @@ mod tests {
let action = ImportAction::new(
TransmissionClient::new("http://127.0.0.1:1").unwrap(),
Prober::new().with_binary(&script),
JellyfinClient::new("http://127.0.0.1:1", None).unwrap(),
);
let paths = vec![media];
@@ -992,6 +1026,7 @@ mod tests {
let action = ImportAction::new(
TransmissionClient::new("http://127.0.0.1:1").unwrap(),
Prober::new().with_binary(&script),
JellyfinClient::new("http://127.0.0.1:1", None).unwrap(),
);
let paths = vec![media];