Skip to content

Commit 1089eab

Browse files
committed
feat: Include notifications via NTFY
1 parent 3e47a7b commit 1089eab

5 files changed

Lines changed: 96 additions & 12 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ rustdoc-args = ["--document-private-items"]
2929
utoipa = { version = "5.5.0", features = ["actix_extras"] }
3030
utoipa-swagger-ui = { version = "9.0.2", features = ["actix-web"] }
3131
actix-web = "4.13.0"
32-
reqwest = { version = "0.13.3", features = ["blocking", "cookies", "json", "form"] }
32+
reqwest = { version = "0.13.3", features = ["cookies", "json", "form"] }
3333
serde = { version = "1.0.228", features = ["derive"] }
3434
serde_json = "1.0.149"
3535
actix-rt = "2.11.0"

src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#![allow(rustdoc::bare_urls)]
22
#![doc = include_str!("../README.md")]
33

4-
use actix_web::{web, App, HttpResponse, HttpServer};
4+
use actix_web::{web, App, HttpServer};
55
use std::{collections::HashMap, sync::Arc};
66
use tokio::sync::RwLock;
77

@@ -12,6 +12,7 @@ mod rsync;
1212
mod settings;
1313
mod squire;
1414
mod swagger;
15+
mod ntfy;
1516

1617
/// Contains entrypoint and initializer settings to trigger the asynchronous `HTTPServer`
1718
///

src/ntfy.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
use reqwest::Client;
2+
use crate::settings;
3+
4+
pub async fn send(config: &settings::Config, title: &str, data: &str) -> bool {
5+
let client = Client::builder().build().unwrap();
6+
let url = format!("{}/{}", config.ntfy_url, config.ntfy_topic);
7+
log::info!("Client url: {}", &url);
8+
9+
let mut request = client
10+
.post(&url)
11+
.header("X-Title", title)
12+
.header("Content-Type", "application/x-www-form-urlencoded")
13+
.body(data.to_string());
14+
15+
let username = config.ntfy_username.to_string();
16+
let password = config.ntfy_password.to_string();
17+
if !username.is_empty() && !password.is_empty() {
18+
request = request.basic_auth(username, Some(password));
19+
}
20+
21+
match request.send().await {
22+
Ok(resp) => match resp.error_for_status() {
23+
Ok(resp) => {
24+
match resp.text().await {
25+
Ok(body) => log::debug!("ntfy response: {}", body),
26+
Err(e) => log::error!("Failed to read response body: {}", e),
27+
}
28+
true
29+
}
30+
Err(e) => {
31+
log::error!("HTTP error: {}", e);
32+
false
33+
}
34+
},
35+
Err(e) => {
36+
log::error!("Request failed: {}", e);
37+
false
38+
}
39+
}
40+
}

src/settings.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,19 @@ pub struct Config {
2020
pub port: u16,
2121
pub apikey: String,
2222
pub workers: usize,
23+
2324
pub qbit_api: String,
25+
2426
pub username: String,
2527
pub password: String,
28+
2629
pub utc_logger: bool,
2730
pub log_level: log::LevelFilter,
31+
32+
pub ntfy_url: String,
33+
pub ntfy_topic: String,
34+
pub ntfy_username: String,
35+
pub ntfy_password: String,
2836
}
2937

3038
fn startup_error(msg: &str) {
@@ -76,7 +84,7 @@ impl Config {
7684
let mut qbit_api = squire::get_env_var("qbit_api", Some("http://localhost:8080/"));
7785
let username = squire::get_env_var("username", None);
7886
let password = squire::get_env_var("password", None);
79-
qbit_api = qbit_api.strip_suffix("/").unwrap_or_default().to_string();
87+
qbit_api = qbit_api.strip_suffix("/").unwrap_or(&qbit_api).to_string();
8088

8189
let utc_logger = squire::get_env_var("utc_logger", Some("true")) == "true";
8290
let default_log_level = squire::get_env_var("log_level", Some("info"));
@@ -92,6 +100,14 @@ impl Config {
92100
}
93101
};
94102

103+
let mut ntfy_url = squire::get_env_var("ntfy_url", None);
104+
let mut ntfy_topic = squire::get_env_var("ntfy_topic", None);
105+
let ntfy_username = squire::get_env_var("ntfy_username", None);
106+
let ntfy_password = squire::get_env_var("ntfy_password", None);
107+
108+
ntfy_url = ntfy_url.strip_suffix("/").unwrap_or(&ntfy_url).to_string();
109+
ntfy_topic = ntfy_topic.strip_prefix("/").unwrap_or(&ntfy_topic).to_string();
110+
95111
Self {
96112
host,
97113
port,
@@ -102,6 +118,10 @@ impl Config {
102118
password,
103119
utc_logger,
104120
log_level,
121+
ntfy_url,
122+
ntfy_topic,
123+
ntfy_username,
124+
ntfy_password,
105125
}
106126
}
107127
}

src/squire.rs

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::{qb, rsync, settings};
1+
use crate::{ntfy, qb, rsync, settings};
22
use regex::Regex;
33
use reqwest::Client;
44
use serde_json::Value;
@@ -181,21 +181,42 @@ pub fn spawn_worker(
181181
};
182182

183183
match entry.status {
184-
settings::Status::Copying(_) | settings::Status::Completed => continue,
184+
settings::Status::Copying(_) => continue,
185+
186+
settings::Status::Completed => {
187+
let config_cloned = config.clone();
188+
let name_clone = entry.name.clone();
189+
let entry_clone = entry.rsync.clone();
190+
tokio::spawn(async move {
191+
let _ = ntfy::send(
192+
&config_cloned, "RuTorrent: Transfer Complete",
193+
format!("{} has been transferred to {}", name_clone, entry_clone.unwrap().host).as_str()
194+
).await;
195+
});
196+
db.remove(&hash);
197+
}
185198

186199
settings::Status::Downloading(_) => {
187200
entry.status = settings::Status::Downloading(progress);
188201

189202
if progress >= 1.0 {
190203
if let Some(target) = entry.rsync.clone() {
204+
let config_cloned = config.clone();
205+
let name_clone = entry.name.clone();
206+
tokio::spawn(async move {
207+
let _ = ntfy::send(
208+
&config_cloned, "RuTorrent: Download Complete",
209+
format!("{} has been downloaded", name_clone).as_str()
210+
).await;
211+
});
212+
191213
log::info!("Download complete → rsync: {}", entry.name);
192214

193215
entry.status = settings::Status::Copying(0.0);
194216

195217
let state_clone = state.clone();
196218
let hash_clone = hash.clone();
197219
let name_clone = entry.name.clone();
198-
199220
tokio::spawn(async move {
200221
rsync::run(
201222
state_clone,
@@ -230,16 +251,18 @@ pub fn spawn_worker(
230251
///
231252
/// Returns the resolved env var or a default string.
232253
pub fn get_env_var(key: &str, default: Option<&str>) -> String {
233-
let lower = std::env::var(key.to_lowercase());
234-
let upper = std::env::var(key.to_uppercase());
235-
let default = default.unwrap_or_default();
236-
lower
237-
.unwrap_or(upper.unwrap_or(default.to_string()))
238-
.to_string()
254+
if let Ok(upper) = std::env::var(key.to_uppercase()) {
255+
return upper;
256+
}
257+
if let Ok(lower) = std::env::var(key.to_lowercase()) {
258+
return lower;
259+
}
260+
default.unwrap_or_default().to_string()
239261
}
240262

241263
/// Load dotenv file using the env var `env_file` or `ENV_FILE`
242264
pub fn load_env_file() {
265+
// TODO: dotenv does not load env vars with $ sign
243266
let env_file = get_env_var("env_file", Some(".env"));
244267
let env_file_path = std::env::current_dir().unwrap_or_default().join(env_file);
245268
let _ = dotenv::from_path(env_file_path.as_path());

0 commit comments

Comments
 (0)