Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "symlinkarr"
version = "1.1.0-rc.15"
version = "1.1.0-rc.16"
edition = "2021"
authors = ["Lenny", "Gemini CLI", "Antigravity"]
description = "Intelligent symlink manager for Real-Debrid-backed media libraries"
Expand Down
8 changes: 7 additions & 1 deletion docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@

## Release Target

- package version for this push: `1.1.0-rc.15`
- package version for this push: `1.1.0-rc.16`
- posture: `v1.1 release candidate with Arr backfill, fail-closed acquisition ranking, current dependencies, synchronized Docker release channels, and updated operator documentation`
- intended use: local-first host or Docker installs, with Windows 11 users running through WSL2 or a Linux container

## 2026-09-19 - v1.1.0-rc.16 Revert Cold-File Probe Retry

### Code Changes

- reverted the rc.14 cold-file probe retry. In production it cost 240 × 12.5 s per hourly scan (link step 367 s → 2,274 s) and rescued nothing: files that need more than 2.5 s for their first byte also need more than 10 s. Folders that fail the probe are skipped for the run exactly as before rc.14.

## 2026-09-18 - v1.1.0-rc.15 Mount Folder From Original Torrent Name

### Code Changes
Expand Down
97 changes: 18 additions & 79 deletions src/api/decypharr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,11 +246,6 @@ impl DecypharrClient {
Ok(url)
}

/// How much longer the second attempt may take when the first one timed out. A cold
/// debrid file needs a link generated and a CDN warm-up before its first byte (often
/// several seconds); once served it answers in well under a second.
const COLD_PROBE_RETRY_FACTOR: u32 = 4;

pub async fn probe_webdav_path(
&self,
relative_path: &Path,
Expand All @@ -260,42 +255,18 @@ impl DecypharrClient {
.build_webdav_url(relative_path)
.map_err(|err| WebDavProbeError::Unreadable(err.to_string()))?;

let send = |timeout: Duration| {
let mut req = self
.client
.get(url.clone())
.timeout(timeout)
.header(reqwest::header::RANGE, "bytes=0-0");
if let Some((key, val)) = self.auth_header() {
req = req.header(key, val);
}
req.send()
};
let mut req = self
.client
.get(url)
.timeout(timeout)
.header(reqwest::header::RANGE, "bytes=0-0");
if let Some((key, val)) = self.auth_header() {
req = req.header(key, val);
}

let resp = match send(timeout).await {
Ok(resp) => resp,
Err(err) if err.is_timeout() => {
let retry_timeout = timeout * Self::COLD_PROBE_RETRY_FACTOR;
debug!(
"WebDAV probe of {} timed out after {:?}; retrying once with {:?} (cold file?)",
relative_path.display(),
timeout,
retry_timeout
);
send(retry_timeout).await.map_err(|err| {
WebDavProbeError::Unreadable(format!(
"webdav probe transport error after cold retry ({:?}): {}",
retry_timeout, err
))
})?
}
Err(err) => {
return Err(WebDavProbeError::Unreadable(format!(
"webdav probe transport error: {}",
err
)))
}
};
let resp = req.send().await.map_err(|err| {
WebDavProbeError::Unreadable(format!("webdav probe transport error: {}", err))
})?;

match resp.status() {
StatusCode::OK | StatusCode::PARTIAL_CONTENT => Ok(()),
Expand Down Expand Up @@ -699,51 +670,19 @@ mod tests {
assert!(request.to_ascii_lowercase().contains("range: bytes=0-0"));
}

#[tokio::test]
async fn probe_webdav_path_retries_once_with_a_longer_timeout_for_cold_files() {
// First byte arrives after 120 ms: too slow for the 50 ms probe, fine for the
// 200 ms cold retry.
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap();
thread::spawn(move || {
for _ in 0..2 {
if let Ok((mut stream, _)) = listener.accept() {
let mut req_buf = [0u8; 1024];
let _ = stream.read(&mut req_buf);
thread::sleep(Duration::from_millis(120));
let _ = stream.write_all(
b"HTTP/1.1 206 Partial Content\r\nContent-Length: 1\r\nConnection: close\r\n\r\nx",
);
}
}
});

let client = DecypharrClient::new(&format!("http://{}", addr), None);
client
.probe_webdav_path(
Path::new("__all__/cold/file.mkv"),
Duration::from_millis(50),
)
.await
.unwrap();
}

#[tokio::test]
async fn probe_webdav_path_times_out_when_server_stalls() {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap();

thread::spawn(move || {
// Stall both the probe and its cold retry.
for _ in 0..2 {
if let Ok((mut stream, _)) = listener.accept() {
let mut req_buf = [0u8; 1024];
let _ = stream.read(&mut req_buf);
thread::sleep(Duration::from_millis(400));
let _ = stream.write_all(
b"HTTP/1.1 206 Partial Content\r\nContent-Length: 1\r\nConnection: close\r\n\r\nx",
);
}
if let Ok((mut stream, _)) = listener.accept() {
let mut req_buf = [0u8; 1024];
let _ = stream.read(&mut req_buf);
thread::sleep(Duration::from_millis(200));
let _ = stream.write_all(
b"HTTP/1.1 206 Partial Content\r\nContent-Length: 1\r\nConnection: close\r\n\r\nx",
);
}
});

Expand Down
Loading