diff --git a/README.md b/README.md index 0319d93..7aaccaa 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,11 @@ orchestration_workers = 10 # Optional number of download workers, default 4. This controls how many downloads we run in parallel. download_workers = 4 +# Optional. When false (default), locally downloaded files are deleted after +# the *arr imports them. Set to true to keep them in download_directory instead +# (put.io transfers/files are still cleaned up). +keep_downloads = false + [putio] # Required. Putio API key. You can generate one using `putioarr get-token` api_key = "MYPUTIOKEY" @@ -180,9 +185,6 @@ To prevent Sonarr/Radarr/Whisparr from seeing each other's downloads, you can co This feature ensures each *arr application only sees and processes its own downloads. ## TODO: -- Better Error handling and retry behavior -- The session ID provided is hard coded. Not sure if it matters. -- (Add option to not delete downloads) - Figure out a better way to map a transfer to a completed import. Since a transfer can contain multiple files (e.g. a whole season) we currently check if all video files have been imported. Most of the time this is fine, except when there are sample videos. sonarr/radarr/whisparr will not import samples, but will make no mention of the fact that the sample was skipped. Right now we check against the `skip_directories` list, which works, but might be tedious. - Automatically pick the right putio proxy based on speed diff --git a/src/download_system/orchestration.rs b/src/download_system/orchestration.rs index 1968976..63d3181 100644 --- a/src/download_system/orchestration.rs +++ b/src/download_system/orchestration.rs @@ -11,10 +11,7 @@ use anyhow::Result; use async_channel::{Receiver, Sender}; use colored::*; use log::{info, warn}; -use std::{ - fs, - time::{Duration, Instant}, -}; +use std::time::{Duration, Instant}; use tokio::{fs::metadata, time::sleep}; use super::transfer::TransferMessage; @@ -157,19 +154,29 @@ async fn watch_for_import( info!("{}: imported", transfer); let top_level_target = transfer.get_top_level(); - match metadata(&top_level_target.to).await { - Ok(m) if m.is_dir() => { - fs::remove_dir_all(&top_level_target.to).unwrap(); - info!("{}: deleted", &top_level_target); - } - Ok(m) if m.is_file() => { - fs::remove_file(&top_level_target.to).unwrap(); - info!("{}: deleted", &top_level_target); - } - Ok(_) | Err(_) => { - panic!("{}: no idea how to handle", &top_level_target) + // Remove the local copy now that it's imported, unless the user + // opted to keep downloads (e.g. the *arr copies imports and they + // want to keep the original). + if app_data.config.keep_downloads { + info!("{}: keeping local download (keep_downloads)", &top_level_target); + } else { + // Use async fs and log failures instead of unwrap/panic: a failed + // delete (permissions, a concurrent removal, the path already gone) + // shouldn't take the process down, and blocking fs here could stall + // the runtime under load. + let result = match metadata(&top_level_target.to).await { + Ok(m) if m.is_dir() => { + tokio::fs::remove_dir_all(&top_level_target.to).await + } + Ok(m) if m.is_file() => tokio::fs::remove_file(&top_level_target.to).await, + // Neither a file nor a dir (e.g. already removed): nothing to do. + Ok(_) | Err(_) => Ok(()), + }; + match result { + Ok(_) => info!("{}: deleted", &top_level_target), + Err(e) => warn!("{}: failed to delete local copy: {}", &top_level_target, e), } - }; + } // An orphan has no put.io transfer to remove or seed, so finish it // here directly instead of routing an Imported message through a // worker (which may be busy downloading and never pick it up), diff --git a/src/main.rs b/src/main.rs index 362cf46..db7c2ae 100644 --- a/src/main.rs +++ b/src/main.rs @@ -82,6 +82,14 @@ pub struct Config { /// lets the put.io account be shared with manual downloads. #[serde(default)] download_unmanaged: bool, + /// When true, keep the locally downloaded files in `download_directory` + /// after the *arr imports them, instead of deleting them. Off by default, + /// which preserves the normal behaviour of removing a download once its + /// import is confirmed. Useful when the *arr copies (rather than + /// hardlinks/moves) imports and you want to keep the original. Only affects + /// the local files; put.io transfers/files are still cleaned up as usual. + #[serde(default)] + keep_downloads: bool, /// put.io folder ids to additionally scan for *orphaned* completed files: /// files that were downloaded but whose transfer record no longer exists /// (e.g. put.io's "clear completed transfers" removes the transfer while @@ -184,6 +192,7 @@ async fn main() -> Result<()> { .join(Serialized::default("port", 9091)) .join(Serialized::default("uid", 1000)) .join(Serialized::default("download_unmanaged", false)) + .join(Serialized::default("keep_downloads", false)) .join(Serialized::default( "skip_directories", vec!["sample", "extras"], diff --git a/src/utils.rs b/src/utils.rs index 243a9da..1bf473a 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -44,6 +44,11 @@ skip_directories = ["sample", "extras"] # transfers. Set to true to download every transfer on the account. download_unmanaged = false +# Optional. When false (default), locally downloaded files are deleted after the +# *arr imports them. Set to true to keep them in download_directory instead (put.io +# transfers/files are still cleaned up as usual). +keep_downloads = false + # Optional. put.io folder ids to scan for *orphaned* completed files: files that # were downloaded but whose transfer record no longer exists (e.g. put.io's # "clear completed transfers" removes the transfer but leaves the file). Such