diff --git a/Cargo.lock b/Cargo.lock index d78f27f..3158e6c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -312,6 +312,35 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "cookie_store" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eac901828f88a5241ee0600950ab981148a18f2f756900ffba1b125ca6a3ef9" +dependencies = [ + "cookie", + "document-features", + "idna", + "log", + "publicsuffix", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -449,6 +478,15 @@ dependencies = [ "syn", ] +[[package]] +name = "document-features" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +dependencies = [ + "litrs", +] + [[package]] name = "dsl_auto_type" version = "0.1.3" @@ -463,6 +501,18 @@ dependencies = [ "syn", ] +[[package]] +name = "educe" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4bd92664bf78c4d3dba9b7cdafce6fa15b13ed3ed16175218196942e99168a8" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "either" version = "1.15.0" @@ -504,6 +554,26 @@ dependencies = [ "syn", ] +[[package]] +name = "enum-ordinalize" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea0dcfa4e54eeb516fe454635a95753ddd39acda650ce703031c6973e315dd5" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "env_logger" version = "0.10.2" @@ -530,7 +600,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -603,6 +673,21 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.31" @@ -610,6 +695,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -618,6 +704,34 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "futures-sink" version = "0.3.31" @@ -636,10 +750,16 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ + "futures-channel", "futures-core", + "futures-io", + "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", + "slab", ] [[package]] @@ -859,10 +979,10 @@ dependencies = [ "http 1.3.1", "hyper 1.6.0", "hyper-util", - "rustls", + "rustls 0.23.26", "rustls-pki-types", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.2", "tower-service", "webpki-roots", ] @@ -1121,7 +1241,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1190,6 +1310,12 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" +[[package]] +name = "litrs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" + [[package]] name = "log" version = "0.4.27" @@ -1424,6 +1550,26 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -1489,6 +1635,22 @@ dependencies = [ "yansi", ] +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + +[[package]] +name = "publicsuffix" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf" +dependencies = [ + "idna", + "psl-types", +] + [[package]] name = "quinn" version = "0.11.7" @@ -1501,7 +1663,7 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls", + "rustls 0.23.26", "socket2", "thiserror 2.0.12", "tokio", @@ -1520,7 +1682,7 @@ dependencies = [ "rand", "ring", "rustc-hash", - "rustls", + "rustls 0.23.26", "rustls-pki-types", "slab", "thiserror 2.0.12", @@ -1540,7 +1702,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1600,8 +1762,10 @@ dependencies = [ "diesel", "diesel_migrations", "directories", + "env_logger", "figment", "figment_file_provider_adapter", + "futures", "log", "magnet-url", "multimap", @@ -1610,10 +1774,15 @@ dependencies = [ "regex", "reqwest 0.12.15", "roux", + "rustls 0.22.4", + "rustls-native-certs", "serde", "serde_json", "tempfile", "tokio", + "tokio-rustls 0.25.0", + "tokio-serde", + "tokio-util", "transmission-rpc", "url", "urlencoding", @@ -1707,6 +1876,8 @@ checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" dependencies = [ "base64 0.22.1", "bytes", + "cookie", + "cookie_store", "encoding_rs", "futures-core", "futures-util", @@ -1727,7 +1898,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls", + "rustls 0.23.26", "rustls-pemfile 2.2.0", "rustls-pki-types", "serde", @@ -1737,7 +1908,7 @@ dependencies = [ "system-configuration 0.6.1", "tokio", "tokio-native-tls", - "tokio-rustls", + "tokio-rustls 0.26.2", "tokio-socks", "tower", "tower-service", @@ -1797,7 +1968,21 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" +dependencies = [ + "log", + "ring", + "rustls-pki-types", + "rustls-webpki 0.102.8", + "subtle", + "zeroize", ] [[package]] @@ -1809,11 +1994,24 @@ dependencies = [ "once_cell", "ring", "rustls-pki-types", - "rustls-webpki", + "rustls-webpki 0.103.1", "subtle", "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" +dependencies = [ + "openssl-probe", + "rustls-pemfile 2.2.0", + "rustls-pki-types", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pemfile" version = "1.0.4" @@ -1841,6 +2039,17 @@ dependencies = [ "web-time", ] +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustls-webpki" version = "0.103.1" @@ -2107,7 +2316,7 @@ dependencies = [ "getrandom 0.3.2", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2262,16 +2471,42 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" +dependencies = [ + "rustls 0.22.4", + "rustls-pki-types", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ - "rustls", + "rustls 0.23.26", "tokio", ] +[[package]] +name = "tokio-serde" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf600e7036b17782571dd44fa0a5cea3c82f60db5137f774a325a76a0d6852b" +dependencies = [ + "bytes", + "educe", + "futures-core", + "futures-sink", + "pin-project", + "serde", + "serde_json", +] + [[package]] name = "tokio-socks" version = "0.5.2" @@ -2639,7 +2874,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 454bc95..a327e65 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,12 +13,15 @@ roux = "2.2.14" figment = { version = "0.10", features = ["toml", "json", "env"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -tokio = { version = "1.44.2", features = ["rt", "rt-multi-thread", "macros"] } +tokio = { version = "1.44.2", features = ["rt", "rt-multi-thread", "macros", "io-util", "net"] } regex = "1.11.1" figment_file_provider_adapter = "0.1.1" directories = "6.0.0" log = "0.4.27" color-eyre = "0.6.3" +tokio-rustls = "0.25.0" +rustls = "0.22.2" +rustls-native-certs = "0.7.0" chrono = { version = "0.4", features = ["serde"] } multimap = "0.10.0" diesel = { version = "2.2.10", features = ["sqlite", "chrono"] } @@ -30,7 +33,11 @@ clap-verbosity-flag = "3.0.2" pretty_env_logger = "0.5.0" async-trait = "0.1.88" console = "0.15.11" -reqwest = "0.12.15" +reqwest = { version = "0.12.15", features = ["cookies"] } magnet-url = "2.0.0" urlencoding = "2.1.3" ntfy = "0.7.0" +tokio-util = "0.7.15" +env_logger = "0.10.2" +tokio-serde = { version = "0.9.0", features = ["json"] } +futures = "0.3.31" diff --git a/migrations/2025-05-08-113100_create_deluge_processed/down.sql b/migrations/2025-05-08-113100_create_deluge_processed/down.sql new file mode 100644 index 0000000..98daf4a --- /dev/null +++ b/migrations/2025-05-08-113100_create_deluge_processed/down.sql @@ -0,0 +1 @@ +DROP TABLE deluge_processed; diff --git a/migrations/2025-05-08-113100_create_deluge_processed/up.sql b/migrations/2025-05-08-113100_create_deluge_processed/up.sql new file mode 100644 index 0000000..392d2c9 --- /dev/null +++ b/migrations/2025-05-08-113100_create_deluge_processed/up.sql @@ -0,0 +1,9 @@ +CREATE TABLE deluge_processed +( + id INTEGER PRIMARY KEY NOT NULL, + magnet_id INTEGER NOT NULL, + processed_at DATETIME NOT NULL, + FOREIGN KEY (magnet_id) REFERENCES magnets (id) +); + +CREATE INDEX deluge_processed_magnet_id ON deluge_processed (magnet_id); diff --git a/src/actions/bitmagnet/action.rs b/src/actions/bitmagnet/action.rs index b121b2d..6e37092 100644 --- a/src/actions/bitmagnet/action.rs +++ b/src/actions/bitmagnet/action.rs @@ -1,7 +1,8 @@ use crate::actions::action::{Action, ProcessedMagnets}; use crate::actions::bitmagnet::client::BitmagnetClient; use crate::actions::bitmagnet::config::BitmagnetConfig; -use crate::db::{BitmagnetProcessedTable, Database}; +use crate::actions::bitmagnet::db::BitmagnetProcessedTable; +use crate::db::Database; use color_eyre::eyre::Result; use log::{debug, warn}; diff --git a/src/actions/bitmagnet/db.rs b/src/actions/bitmagnet/db.rs new file mode 100644 index 0000000..342d7da --- /dev/null +++ b/src/actions/bitmagnet/db.rs @@ -0,0 +1,31 @@ +use crate::db::ProcessedTable; +use crate::models::NewBitmagnetProcessed; +use crate::schema::bitmagnet_processed; +use color_eyre::eyre::Context; +use diesel::{QueryDsl, RunQueryDsl, SqliteConnection}; + +pub struct BitmagnetProcessedTable; + +impl ProcessedTable for BitmagnetProcessedTable { + fn get_processed_ids(conn: &mut SqliteConnection) -> color_eyre::Result> { + bitmagnet_processed::table + .select(bitmagnet_processed::magnet_id) + .load(conn) + .wrap_err("Failed to load processed magnet IDs for Bitmagnet") + } + + fn mark_processed(conn: &mut SqliteConnection, magnet_id: i32) -> color_eyre::Result<()> { + let now = chrono::Utc::now().naive_utc(); + let new_processed = NewBitmagnetProcessed { + magnet_id, + processed_at: &now, + }; + + diesel::insert_into(bitmagnet_processed::table) + .values(&new_processed) + .execute(conn) + .wrap_err("Failed to mark magnet as processed by Bitmagnet")?; + + Ok(()) + } +} diff --git a/src/actions/bitmagnet/mod.rs b/src/actions/bitmagnet/mod.rs index f586b91..a3fb7b7 100644 --- a/src/actions/bitmagnet/mod.rs +++ b/src/actions/bitmagnet/mod.rs @@ -1,6 +1,7 @@ mod action; mod client; mod config; +mod db; pub use action::BitmagnetAction; pub use config::BitmagnetConfig; diff --git a/src/actions/deluge/action.rs b/src/actions/deluge/action.rs new file mode 100644 index 0000000..fcf5e03 --- /dev/null +++ b/src/actions/deluge/action.rs @@ -0,0 +1,65 @@ +use crate::actions::action::{Action, ProcessedMagnets}; +use crate::actions::deluge::client::DelugeClient; +use crate::actions::deluge::config::DelugeConfig; +use crate::actions::deluge::db::DelugeProcessedTable; +use crate::db::Database; +use color_eyre::eyre::Result; +use log::{debug, warn}; + +/// Action for submitting magnet links to Deluge +pub struct DelugeAction { + client: DelugeClient, +} + +impl DelugeAction { + pub fn new(config: &DelugeConfig) -> Result { + let client = DelugeClient::new(config)?; + + Ok(DelugeAction { client }) + } +} + +#[async_trait::async_trait] +impl Action for DelugeAction { + /// Return the name of the action + fn name() -> &'static str { + "Deluge" + } + + fn get_name(&self) -> &'static str { + Self::name() + } + + /// Process all unprocessed magnet links and return the list of processed magnets + async fn process_unprocessed_magnets(&mut self, db: &mut Database) -> Result { + let unprocessed_magnets = db.get_unprocessed_magnets_for_table::()?; + let mut processed_magnets = Vec::new(); + let mut failed_magnets = Vec::new(); + + self.client.login().await?; + + for magnet in unprocessed_magnets { + match self.client.submit_magnet(&magnet.link).await { + Ok(_) => { + debug!( + "Successfully submitted magnet link to {}: {}", + Self::name(), + magnet.title + ); + debug!("Magnet link: {}", magnet.link); + db.mark_magnet_processed_for_table::(magnet.id)?; + processed_magnets.push(magnet); + } + Err(e) => { + warn!("Failed to submit magnet link to {}: {}", Self::name(), e); + failed_magnets.push(magnet); + } + } + } + + Ok(ProcessedMagnets { + success: processed_magnets, + failed: failed_magnets, + }) + } +} diff --git a/src/actions/deluge/client.rs b/src/actions/deluge/client.rs new file mode 100644 index 0000000..90e8698 --- /dev/null +++ b/src/actions/deluge/client.rs @@ -0,0 +1,159 @@ +use std::net::ToSocketAddrs; + +use color_eyre::eyre::{eyre, Result, WrapErr}; +use futures::SinkExt; +use futures::StreamExt; +use log::{debug, info}; +use serde_json::{json, Value}; +use tokio::net::TcpStream; +use tokio_util::codec::{Framed, LengthDelimitedCodec}; + +use crate::actions::deluge::config::DelugeConfig; + +#[derive(Debug)] +pub enum DelugeError { + AuthenticationFailed, + Rpc(String), +} + +impl std::error::Error for DelugeError {} + +impl std::fmt::Display for DelugeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + DelugeError::AuthenticationFailed => write!(f, "Authentication failed"), + DelugeError::Rpc(s) => write!(f, "RPC error: {}", s), + } + } +} + +pub struct DelugeClient { + config: DelugeConfig, + stream: Option>, + request_id: u64, +} + +impl DelugeClient { + pub fn new(config: &DelugeConfig) -> Result { + let config = config.clone(); + + Ok(DelugeClient { + config, + stream: None, + request_id: 0, + }) + } + + async fn connect(&mut self) -> Result<()> { + if self.stream.is_none() { + let addr = format!("{}:{}", self.config.host, self.config.port) + .to_socket_addrs() + .wrap_err_with(|| format!("Failed to resolve host: {}", self.config.host))? + .next() + .ok_or_else(|| eyre!("Failed to get socket address"))?; + let tcp_stream = TcpStream::connect(addr).await.wrap_err_with(|| { + format!( + "Failed to connect to Deluge daemon at {}:{}", + self.config.host, self.config.port + ) + })?; + + let length_delimited = LengthDelimitedCodec::new(); + let framed = Framed::new(tcp_stream, length_delimited); + + info!( + "Connected to Deluge daemon at {}:{}", + self.config.host, self.config.port + ); + + self.stream = Some(framed); + } + Ok(()) + } + + async fn send_request(&mut self, method: &str, params: Vec) -> Result { + self.connect().await?; + + self.request_id += 1; + let request = json!({ + "method": method, + "params": params, + "id": self.request_id, + }); + debug!( + "Sending request: {}", + serde_json::to_string(&request).unwrap() + ); + + if let Some(stream) = &mut self.stream { + let bytes = serde_json::to_vec(&request)?; + stream.send(bytes.into()).await?; + Ok(self.request_id) + } else { + Err(eyre!("Stream is not initialized")) + } + } + + async fn receive_response(&mut self, expected_id: u64) -> Result { + if let Some(stream) = &mut self.stream { + while let Some(response) = stream.next().await { + let bytes = response?; + let response: Value = serde_json::from_slice(&bytes)?; + debug!( + "Received response: {}", + serde_json::to_string(&response).unwrap() + ); + if response.get("id") == Some(&json!(expected_id)) { + if let Some(error) = response.get("error") { + if !error.is_null() { + let error_string = serde_json::to_string(error) + .unwrap_or_else(|_| "Unknown error".to_string()); + return Err(DelugeError::Rpc(error_string).into()); + } + } + if let Some(result) = response.get("result") { + return Ok(result.clone()); + } + } + } + Err(eyre!("Connection closed prematurely")) + } else { + Err(eyre!("Stream is not initialized")) + } + } + + pub async fn login(&mut self) -> Result<()> { + let auth_id = self + .send_request("auth.login", vec![json!(self.config.password)]) + .await?; + let response = self.receive_response(auth_id).await?; + if response.as_bool().unwrap_or(false) { + info!("Successfully authenticated with Deluge daemon."); + Ok(()) + } else { + Err(DelugeError::AuthenticationFailed.into()) + } + } + + pub async fn submit_magnet(&mut self, magnet_link: &str) -> Result { + let add_torrent_id = self + .send_request( + "core.add_torrent_magnet", + vec![ + json!(magnet_link), + json!({ + "download_location": self.config.download_dir, + "paused": false, + }), + ], + ) + .await?; + let response = self.receive_response(add_torrent_id).await?; + if let Some(torrent_hash) = response.as_str() { + info!("Successfully added torrent with hash: {}", torrent_hash); + Ok(torrent_hash.to_string()) + } else { + Err(DelugeError::Rpc("Failed to add torrent".to_string()).into()) + } + } +} diff --git a/src/actions/deluge/config.rs b/src/actions/deluge/config.rs new file mode 100644 index 0000000..b0cfb15 --- /dev/null +++ b/src/actions/deluge/config.rs @@ -0,0 +1,19 @@ +use crate::app::Enableable; +use serde::{Deserialize, Serialize}; + +/// Configuration for the Deluge action +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DelugeConfig { + pub enable: bool, + pub host: String, + pub login: String, + pub password: String, + pub port: u16, + pub download_dir: String, +} + +impl Enableable for DelugeConfig { + fn is_enabled(&self) -> bool { + self.enable + } +} diff --git a/src/actions/deluge/db.rs b/src/actions/deluge/db.rs new file mode 100644 index 0000000..be73d71 --- /dev/null +++ b/src/actions/deluge/db.rs @@ -0,0 +1,31 @@ +use crate::db::ProcessedTable; +use crate::models::NewDelugeProcessed; +use crate::schema::deluge_processed; +use color_eyre::eyre::Context; +use diesel::{QueryDsl, RunQueryDsl, SqliteConnection}; + +pub struct DelugeProcessedTable; + +impl ProcessedTable for DelugeProcessedTable { + fn get_processed_ids(conn: &mut SqliteConnection) -> color_eyre::Result> { + deluge_processed::table + .select(deluge_processed::magnet_id) + .load(conn) + .wrap_err("Failed to load processed magnet IDs for Bitmagnet") + } + + fn mark_processed(conn: &mut SqliteConnection, magnet_id: i32) -> color_eyre::Result<()> { + let now = chrono::Utc::now().naive_utc(); + let new_processed = NewDelugeProcessed { + magnet_id, + processed_at: &now, + }; + + diesel::insert_into(deluge_processed::table) + .values(&new_processed) + .execute(conn) + .wrap_err("Failed to mark magnet as processed by Bitmagnet")?; + + Ok(()) + } +} diff --git a/src/actions/deluge/mod.rs b/src/actions/deluge/mod.rs new file mode 100644 index 0000000..2ea1aca --- /dev/null +++ b/src/actions/deluge/mod.rs @@ -0,0 +1,7 @@ +pub mod action; +pub mod client; +pub mod config; +pub mod db; + +pub use action::DelugeAction; +pub use config::DelugeConfig; diff --git a/src/actions/mod.rs b/src/actions/mod.rs index ef69f6b..aff8fce 100644 --- a/src/actions/mod.rs +++ b/src/actions/mod.rs @@ -1,4 +1,5 @@ pub mod action; pub mod bitmagnet; +pub mod deluge; pub mod factory; pub mod transmission; diff --git a/src/actions/transmission/action.rs b/src/actions/transmission/action.rs index 554dc2b..3e46128 100644 --- a/src/actions/transmission/action.rs +++ b/src/actions/transmission/action.rs @@ -1,7 +1,8 @@ use crate::actions::action::{Action, ProcessedMagnets}; use crate::actions::transmission::client::TransmissionClient; use crate::actions::transmission::config::TransmissionConfig; -use crate::db::{Database, TransmissionProcessedTable}; +use crate::actions::transmission::db::TransmissionProcessedTable; +use crate::db::Database; use color_eyre::eyre::Result; use log::{debug, warn}; diff --git a/src/actions/transmission/db.rs b/src/actions/transmission/db.rs new file mode 100644 index 0000000..1ea8575 --- /dev/null +++ b/src/actions/transmission/db.rs @@ -0,0 +1,31 @@ +use crate::db::ProcessedTable; +use crate::models::NewTransmissionProcessed; +use crate::schema::transmission_processed; +use color_eyre::eyre::Context; +use diesel::{QueryDsl, RunQueryDsl, SqliteConnection}; + +pub struct TransmissionProcessedTable; + +impl ProcessedTable for TransmissionProcessedTable { + fn get_processed_ids(conn: &mut SqliteConnection) -> color_eyre::Result> { + transmission_processed::table + .select(transmission_processed::magnet_id) + .load(conn) + .wrap_err("Failed to load processed magnet IDs for Transmission") + } + + fn mark_processed(conn: &mut SqliteConnection, magnet_id: i32) -> color_eyre::Result<()> { + let now = chrono::Utc::now().naive_utc(); + let new_processed = NewTransmissionProcessed { + magnet_id, + processed_at: &now, + }; + + diesel::insert_into(transmission_processed::table) + .values(&new_processed) + .execute(conn) + .wrap_err("Failed to mark magnet as processed by Transmission")?; + + Ok(()) + } +} diff --git a/src/actions/transmission/mod.rs b/src/actions/transmission/mod.rs index 106ed1f..8eebdc6 100644 --- a/src/actions/transmission/mod.rs +++ b/src/actions/transmission/mod.rs @@ -1,6 +1,7 @@ pub mod action; pub mod client; pub mod config; +pub mod db; pub use action::TransmissionAction; pub use config::TransmissionConfig; diff --git a/src/app.rs b/src/app.rs index b4e4509..e9a9d5f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,5 +1,6 @@ use crate::actions::action::{Action, ProcessedMagnets}; use crate::actions::bitmagnet::BitmagnetAction; +use crate::actions::deluge::DelugeAction; use crate::actions::factory::init_action; use crate::actions::transmission::TransmissionAction; use crate::config::Config; @@ -47,6 +48,9 @@ impl App { if let Some(action) = init_action(&self.config.bitmagnet, BitmagnetAction::new)? { self.actions.push(action); } + if let Some(action) = init_action(&self.config.deluge, DelugeAction::new)? { + self.actions.push(action); + } if let Some(action) = init_action(&self.config.transmission, TransmissionAction::new)? { self.actions.push(action); } diff --git a/src/config.rs b/src/config.rs index fdcd861..68f166a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,4 +1,5 @@ use crate::actions::bitmagnet::BitmagnetConfig; +use crate::actions::deluge::DelugeConfig; use crate::actions::transmission::TransmissionConfig; use crate::args::Args; use crate::notifications::ntfy::NtfyConfig; @@ -31,6 +32,9 @@ pub struct Config { #[serde(default)] pub bitmagnet: Option, + #[serde(default)] + pub deluge: Option, + #[serde(default)] pub transmission: Option, diff --git a/src/db.rs b/src/db.rs index f21c800..29ee87d 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1,5 +1,5 @@ -use crate::models::{Magnet, NewBitmagnetProcessed, NewMagnet, NewTag, NewTransmissionProcessed}; -use crate::schema::{bitmagnet_processed, magnet_tags, magnets, tags, transmission_processed}; +use crate::models::{Magnet, NewMagnet, NewTag}; +use crate::schema::{magnet_tags, magnets, tags}; use crate::PostInfo; use color_eyre::eyre::{eyre, Result, WrapErr}; use diesel::prelude::*; @@ -16,57 +16,6 @@ pub trait ProcessedTable { fn mark_processed(conn: &mut SqliteConnection, magnet_id: i32) -> Result<()>; } -pub struct BitmagnetProcessedTable; -pub struct TransmissionProcessedTable; - -impl ProcessedTable for BitmagnetProcessedTable { - fn get_processed_ids(conn: &mut SqliteConnection) -> Result> { - bitmagnet_processed::table - .select(bitmagnet_processed::magnet_id) - .load(conn) - .wrap_err("Failed to load processed magnet IDs for Bitmagnet") - } - - fn mark_processed(conn: &mut SqliteConnection, magnet_id: i32) -> Result<()> { - let now = chrono::Utc::now().naive_utc(); - let new_processed = NewBitmagnetProcessed { - magnet_id, - processed_at: &now, - }; - - diesel::insert_into(bitmagnet_processed::table) - .values(&new_processed) - .execute(conn) - .wrap_err("Failed to mark magnet as processed by Bitmagnet")?; - - Ok(()) - } -} - -impl ProcessedTable for TransmissionProcessedTable { - fn get_processed_ids(conn: &mut SqliteConnection) -> Result> { - transmission_processed::table - .select(transmission_processed::magnet_id) - .load(conn) - .wrap_err("Failed to load processed magnet IDs for Transmission") - } - - fn mark_processed(conn: &mut SqliteConnection, magnet_id: i32) -> Result<()> { - let now = chrono::Utc::now().naive_utc(); - let new_processed = NewTransmissionProcessed { - magnet_id, - processed_at: &now, - }; - - diesel::insert_into(transmission_processed::table) - .values(&new_processed) - .execute(conn) - .wrap_err("Failed to mark magnet as processed by Transmission")?; - - Ok(()) - } -} - pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations"); /// Database for storing magnet links and associated information diff --git a/src/models.rs b/src/models.rs index b255cb6..c67065d 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,4 +1,6 @@ -use crate::schema::{bitmagnet_processed, magnet_tags, magnets, tags, transmission_processed}; +use crate::schema::{ + bitmagnet_processed, deluge_processed, magnet_tags, magnets, tags, transmission_processed, +}; use chrono::NaiveDateTime; use diesel::prelude::*; @@ -73,6 +75,24 @@ pub struct NewBitmagnetProcessed<'a> { pub processed_at: &'a NaiveDateTime, } +#[allow(dead_code)] +#[derive(Queryable, Selectable)] +#[diesel(table_name = deluge_processed)] +#[diesel(check_for_backend(diesel::sqlite::Sqlite))] +pub struct DelugeProcessed { + pub id: i32, + pub magnet_id: i32, + pub processed_at: NaiveDateTime, +} + +#[derive(Insertable)] +#[diesel(table_name = deluge_processed)] +#[diesel(check_for_backend(diesel::sqlite::Sqlite))] +pub struct NewDelugeProcessed<'a> { + pub magnet_id: i32, + pub processed_at: &'a NaiveDateTime, +} + #[allow(dead_code)] #[derive(Queryable, Selectable)] #[diesel(table_name = transmission_processed)] diff --git a/src/schema.rs b/src/schema.rs index d1014fd..160eadd 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -8,6 +8,14 @@ diesel::table! { } } +diesel::table! { + deluge_processed (id) { + id -> Integer, + magnet_id -> Integer, + processed_at -> Timestamp, + } +} + diesel::table! { magnet_tags (magnet_id, tag_id) { magnet_id -> Integer, @@ -43,12 +51,14 @@ diesel::table! { } diesel::joinable!(bitmagnet_processed -> magnets (magnet_id)); +diesel::joinable!(deluge_processed -> magnets (magnet_id)); diesel::joinable!(magnet_tags -> magnets (magnet_id)); diesel::joinable!(magnet_tags -> tags (tag_id)); diesel::joinable!(transmission_processed -> magnets (magnet_id)); diesel::allow_tables_to_appear_in_same_query!( bitmagnet_processed, + deluge_processed, magnet_tags, magnets, tags,