From 3f2b002f52a9891d5667c2e26d6b338554b30290 Mon Sep 17 00:00:00 2001 From: Marc Plano-Lesay Date: Thu, 1 May 2025 12:51:07 +1000 Subject: [PATCH] Add a send to Transmission action --- Cargo.lock | 656 +++++++++++++++++- Cargo.toml | 4 + .../down.sql | 1 + .../up.sql | 9 + src/actions/mod.rs | 1 + src/actions/transmission.rs | 99 +++ src/db.rs | 182 +++-- src/main.rs | 52 +- src/models.rs | 19 +- src/schema.rs | 12 + 10 files changed, 952 insertions(+), 83 deletions(-) create mode 100644 migrations/2025-05-01-082736_create_transmission_processed/down.sql create mode 100644 migrations/2025-05-01-082736_create_transmission_processed/up.sql create mode 100644 src/actions/mod.rs create mode 100644 src/actions/transmission.rs diff --git a/Cargo.lock b/Cargo.lock index 0eeb047..11077bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -127,6 +127,12 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bitflags" version = "1.3.2" @@ -172,6 +178,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.41" @@ -197,6 +209,16 @@ dependencies = [ "clap_derive", ] +[[package]] +name = "clap-verbosity-flag" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2678fade3b77aa3a8ff3aae87e9c008d3fb00473a41c71fbf74e91c8c7b37e84" +dependencies = [ + "clap", + "log", +] + [[package]] name = "clap_builder" version = "4.5.37" @@ -426,6 +448,39 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "enum-iterator" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c280b9e6b3ae19e152d8e31cf47f18389781e119d4013a2a2bb0180e5facc635" +dependencies = [ + "enum-iterator-derive", +] + +[[package]] +name = "enum-iterator-derive" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ab991c1362ac86c61ab6f556cff143daa22e5a15e4e189df818b2fd19fe65b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -558,8 +613,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -569,9 +626,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasi 0.14.2+wasi-0.2.4", + "wasm-bindgen", ] [[package]] @@ -591,7 +650,7 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "http", + "http 0.2.12", "indexmap", "slab", "tokio", @@ -611,6 +670,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e" + [[package]] name = "http" version = "0.2.12" @@ -622,6 +687,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http-body" version = "0.4.6" @@ -629,7 +705,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", - "http", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.3.1", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http 1.3.1", + "http-body 1.0.1", "pin-project-lite", ] @@ -645,6 +744,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "humantime" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" + [[package]] name = "hyper" version = "0.14.32" @@ -656,8 +761,8 @@ dependencies = [ "futures-core", "futures-util", "h2", - "http", - "http-body", + "http 0.2.12", + "http-body 0.4.6", "httparse", "httpdate", "itoa", @@ -669,6 +774,43 @@ dependencies = [ "want", ] +[[package]] +name = "hyper" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +dependencies = [ + "futures-util", + "http 1.3.1", + "hyper 1.6.0", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + [[package]] name = "hyper-tls" version = "0.5.0" @@ -676,12 +818,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ "bytes", - "hyper", + "hyper 0.14.32", "native-tls", "tokio", "tokio-native-tls", ] +[[package]] +name = "hyper-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "hyper 1.6.0", + "libc", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + [[package]] name = "iana-time-zone" version = "0.1.63" @@ -879,6 +1041,17 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +[[package]] +name = "is-terminal" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -1180,6 +1353,25 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "pretty_env_logger" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "865724d4dbe39d9f3dd3b52b88d859d66bcb2d6a0acfd5ea68a65fb66d4bdc1c" +dependencies = [ + "env_logger", + "log", +] + [[package]] name = "proc-macro2" version = "1.0.95" @@ -1202,6 +1394,60 @@ dependencies = [ "yansi", ] +[[package]] +name = "quinn" +version = "0.11.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3bd15a6f2967aef83887dcb9fec0014580467e33720d073560cf015a5683012" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcbafbbdbb0f638fe3f35f3c56739f77a8a1d070cb25603226c83339b391472b" +dependencies = [ + "bytes", + "getrandom 0.3.2", + "rand", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4e529991f949c5e25755532370b8af5d114acae52326361d68d47af64aa842" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.59.0", +] + [[package]] name = "quote" version = "1.0.40" @@ -1217,12 +1463,42 @@ version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +[[package]] +name = "rand" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.2", +] + [[package]] name = "reddit-magnet" version = "0.1.0" dependencies = [ "chrono", "clap", + "clap-verbosity-flag", "color-eyre", "diesel", "diesel_migrations", @@ -1231,11 +1507,14 @@ dependencies = [ "figment_file_provider_adapter", "log", "multimap", + "pretty_env_logger", "regex", "roux", "serde", "tempfile", "tokio", + "transmission-rpc", + "url", ] [[package]] @@ -1284,15 +1563,15 @@ version = "0.11.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" dependencies = [ - "base64", + "base64 0.21.7", "bytes", "encoding_rs", "futures-core", "futures-util", "h2", - "http", - "http-body", - "hyper", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.32", "hyper-tls", "ipnet", "js-sys", @@ -1302,11 +1581,11 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls-pemfile", + "rustls-pemfile 1.0.4", "serde", "serde_json", "serde_urlencoded", - "sync_wrapper", + "sync_wrapper 0.1.2", "system-configuration", "tokio", "tokio-native-tls", @@ -1318,6 +1597,63 @@ dependencies = [ "winreg", ] +[[package]] +name = "reqwest" +version = "0.12.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "hyper 1.6.0", + "hyper-rustls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pemfile 2.2.0", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 1.0.2", + "tokio", + "tokio-rustls", + "tower", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", + "windows-registry", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "roux" version = "2.2.14" @@ -1325,7 +1661,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "315c0187bf1b2a01aefaf4dcde2efeafa6a3bfb4b27ea271d12e78dfd5867259" dependencies = [ "maybe-async", - "reqwest", + "reqwest 0.11.27", "serde", "serde_json", ] @@ -1336,6 +1672,12 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustix" version = "1.0.5" @@ -1349,13 +1691,56 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "rustls" +version = "0.23.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df51b5869f3a441595eac5e8ff14d486ff285f7b8c0df8770e49c3b56351f0f0" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + [[package]] name = "rustls-pemfile" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ - "base64", + "base64 0.21.7", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" +dependencies = [ + "web-time", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", ] [[package]] @@ -1434,6 +1819,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "serde_spanned" version = "0.6.8" @@ -1507,6 +1903,12 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.101" @@ -1524,6 +1926,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + [[package]] name = "synstructure" version = "0.13.1" @@ -1569,6 +1980,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "thiserror" version = "2.0.12" @@ -1640,6 +2060,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.44.2" @@ -1677,6 +2112,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.15" @@ -1731,6 +2176,27 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076" +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper 1.0.2", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + [[package]] name = "tower-service" version = "0.3.3" @@ -1778,6 +2244,22 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "transmission-rpc" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05204060bd751037cbbfe488e22d4a47991aa583052e85adbf4e96ab834a41bf" +dependencies = [ + "base64 0.22.1", + "chrono", + "enum-iterator", + "log", + "reqwest 0.12.15", + "serde", + "serde_json", + "serde_repr", +] + [[package]] name = "try-lock" version = "0.2.5" @@ -1799,6 +2281,12 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.4" @@ -1951,6 +2439,34 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.26.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37493cadf42a2a939ed404698ded7fb378bf301b5011f973361779a3a74f8c93" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "windows-core" version = "0.61.0" @@ -1961,7 +2477,7 @@ dependencies = [ "windows-interface", "windows-link", "windows-result", - "windows-strings", + "windows-strings 0.4.0", ] [[package]] @@ -1992,6 +2508,17 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +[[package]] +name = "windows-registry" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +dependencies = [ + "windows-result", + "windows-strings 0.3.1", + "windows-targets 0.53.0", +] + [[package]] name = "windows-result" version = "0.3.2" @@ -2001,6 +2528,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-strings" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-strings" version = "0.4.0" @@ -2061,13 +2597,29 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -2080,6 +2632,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -2092,6 +2650,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -2104,12 +2668,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -2122,6 +2698,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -2134,6 +2716,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -2146,6 +2734,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -2158,6 +2752,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + [[package]] name = "winnow" version = "0.7.7" @@ -2228,6 +2828,26 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zerocopy" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zerofrom" version = "0.1.6" @@ -2249,6 +2869,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + [[package]] name = "zerovec" version = "0.10.4" diff --git a/Cargo.toml b/Cargo.toml index 2a40620..1239caa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,3 +23,7 @@ multimap = "0.10.0" diesel = { version = "2.2.10", features = ["sqlite", "chrono"] } diesel_migrations = "2.2.0" tempfile = "3.19.1" +transmission-rpc = "0.5.0" +url = "2.5.4" +clap-verbosity-flag = "3.0.2" +pretty_env_logger = "0.5.0" diff --git a/migrations/2025-05-01-082736_create_transmission_processed/down.sql b/migrations/2025-05-01-082736_create_transmission_processed/down.sql new file mode 100644 index 0000000..37fc7dd --- /dev/null +++ b/migrations/2025-05-01-082736_create_transmission_processed/down.sql @@ -0,0 +1 @@ +DROP TABLE transmission_processed; \ No newline at end of file diff --git a/migrations/2025-05-01-082736_create_transmission_processed/up.sql b/migrations/2025-05-01-082736_create_transmission_processed/up.sql new file mode 100644 index 0000000..1f0a876 --- /dev/null +++ b/migrations/2025-05-01-082736_create_transmission_processed/up.sql @@ -0,0 +1,9 @@ +CREATE TABLE transmission_processed +( + id INTEGER PRIMARY KEY, + magnet_id INTEGER NOT NULL, + processed_at DATETIME NOT NULL, + FOREIGN KEY (magnet_id) REFERENCES magnets(id) +); + +CREATE INDEX transmission_processed_magnet_id ON transmission_processed(magnet_id); \ No newline at end of file diff --git a/src/actions/mod.rs b/src/actions/mod.rs new file mode 100644 index 0000000..f43bdff --- /dev/null +++ b/src/actions/mod.rs @@ -0,0 +1 @@ +pub mod transmission; diff --git a/src/actions/transmission.rs b/src/actions/transmission.rs new file mode 100644 index 0000000..b2fdb38 --- /dev/null +++ b/src/actions/transmission.rs @@ -0,0 +1,99 @@ +use crate::db::{Database, TransmissionProcessedTable}; +use color_eyre::eyre::{eyre, Result, WrapErr}; +use log::{debug, info, warn}; +use serde::{Deserialize, Serialize}; +use transmission_rpc::{ + types::{BasicAuth, TorrentAddArgs}, + TransClient, +}; +use url::Url; + +/// Configuration for the Transmission action +#[derive(Debug, Serialize, Deserialize)] +pub struct TransmissionConfig { + pub enable: bool, + pub host: String, + pub username: String, + pub password: String, + pub port: u16, + pub download_dir: String, +} + +/// Action for submitting magnet links to Transmission +pub struct TransmissionAction { + client: TransClient, + download_dir: String, + db: Database, +} + +impl TransmissionAction { + pub async fn new(config: &TransmissionConfig, db: Database) -> Result { + if !config.enable { + return Err(eyre!("Transmission action is disabled")); + } + + let url_str = format!("{}:{}/transmission/rpc", config.host, config.port); + let url = Url::parse(&url_str).wrap_err_with(|| format!("Invalid URL: {}", url_str))?; + + let auth = BasicAuth { + user: config.username.clone(), + password: config.password.clone(), + }; + + let client = TransClient::with_auth(url, auth); + + Ok(TransmissionAction { + client, + download_dir: config.download_dir.clone(), + db, + }) + } + + /// Process all unprocessed magnet links + pub async fn process_unprocessed_magnets(&mut self) -> Result { + let unprocessed_magnets = self + .db + .get_unprocessed_magnets_for_table::()?; + let mut processed_count = 0; + + for magnet in unprocessed_magnets { + if let Some(id) = magnet.id { + match self.submit_magnet(&magnet.link).await { + Ok(_) => { + info!( + "Successfully submitted magnet link to Transmission: {}", + magnet.title + ); + debug!("Magnet link: {}", magnet.link); + self.db + .mark_magnet_processed_for_table::(id)?; + processed_count += 1; + } + Err(e) => { + warn!("Failed to submit magnet link to Transmission: {}", e); + } + } + } else { + warn!("Skipping magnet with null ID: {}", magnet.link); + } + } + + Ok(processed_count) + } + + /// Submit a magnet link to Transmission + async fn submit_magnet(&mut self, magnet: &str) -> Result<()> { + let args = TorrentAddArgs { + filename: Some(magnet.to_string()), + download_dir: Some(self.download_dir.clone()), + ..Default::default() + }; + + self.client + .torrent_add(args) + .await + .map_err(|e| eyre!("Failed to add torrent to Transmission: {}", e))?; + + Ok(()) + } +} diff --git a/src/db.rs b/src/db.rs index 9a04bd8..854180a 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1,5 +1,5 @@ -use crate::models::{Magnet, NewMagnet}; -use crate::schema::magnets; +use crate::models::{Magnet, NewMagnet, NewTransmissionProcessed, TransmissionProcessed}; +use crate::schema::{magnets, transmission_processed}; use crate::PostInfo; use color_eyre::eyre::{eyre, Result, WrapErr}; use diesel::prelude::*; @@ -8,6 +8,40 @@ use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness}; use std::fs::create_dir_all; use std::path::Path; +pub trait ProcessedTable { + /// Get all processed magnet IDs from this table + fn get_processed_ids(conn: &mut SqliteConnection) -> Result>; + + /// Mark a magnet as processed in this table + fn mark_processed(conn: &mut SqliteConnection, magnet_id: i32) -> Result<()>; +} + +pub struct TransmissionProcessedTable; + +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 @@ -15,6 +49,90 @@ pub struct Database { conn: SqliteConnection, } +impl Database { + pub fn new>(path: P) -> Result { + let database_url = path + .as_ref() + .to_str() + .ok_or_else(|| eyre!("Database path is not valid UTF-8"))?; + + if let Some(parent) = path.as_ref().parent() { + create_dir_all(parent) + .wrap_err_with(|| format!("Failed to create directory: {:?}", parent))?; + } + + let mut conn = SqliteConnection::establish(database_url) + .wrap_err("Failed to open database connection")?; + + conn.run_pending_migrations(MIGRATIONS) + .expect("Failed to apply database migrations"); + + Ok(Database { conn }) + } + + pub fn get_all_magnets(&mut self) -> Result> { + let results = magnets::table + .select(Magnet::as_select()) + .load(&mut self.conn) + .wrap_err("Failed to load magnets from database")?; + + Ok(results) + } + + pub fn store_magnets(&mut self, post: &PostInfo) -> Result { + let published_at = post.timestamp.naive_utc(); + + // Filter out magnet links that already exist in the database + let existing_links: Vec = magnets::table + .select(magnets::link) + .filter(magnets::link.eq_any(&post.magnet_links)) + .load(&mut self.conn) + .wrap_err("Failed to query existing magnets")?; + + let links = post + .magnet_links + .iter() + .filter(|link| !existing_links.contains(link)) + .map(|m| NewMagnet { + title: post.title.as_str(), + submitter: post.submitter.as_str(), + subreddit: post.subreddit.as_str(), + link: m, + published_at: &published_at, + }) + .collect::>(); + + diesel::insert_into(magnets::table) + .values(&links) + .execute(&mut self.conn) + .wrap_err("Failed to save new magnet") + } + + /// Get all magnets that have not been processed by a specific table + pub fn get_unprocessed_magnets_for_table(&mut self) -> Result> { + // Get all magnet IDs that have been processed by the specified table + let processed_ids = T::get_processed_ids(&mut self.conn)?; + + // Get all magnets that are not in the processed list + let results = magnets::table + .select(Magnet::as_select()) + .filter(magnets::id.is_not_null()) + .filter(magnets::id.ne_all(processed_ids)) + .load(&mut self.conn) + .wrap_err("Failed to load unprocessed magnets from database")?; + + Ok(results) + } + + /// Mark a magnet as processed by a specific table + pub fn mark_magnet_processed_for_table( + &mut self, + magnet_id: i32, + ) -> Result<()> { + T::mark_processed(&mut self.conn, magnet_id) + } +} + #[cfg(test)] mod tests { use super::*; @@ -137,63 +255,3 @@ mod tests { assert_eq!(magnets.len(), 3); } } - -impl Database { - pub fn new>(path: P) -> Result { - let database_url = path - .as_ref() - .to_str() - .ok_or_else(|| eyre!("Database path is not valid UTF-8"))?; - - if let Some(parent) = path.as_ref().parent() { - create_dir_all(parent) - .wrap_err_with(|| format!("Failed to create directory: {:?}", parent))?; - } - - let mut conn = SqliteConnection::establish(database_url) - .wrap_err("Failed to open database connection")?; - - conn.run_pending_migrations(MIGRATIONS) - .expect("Failed to apply database migrations"); - - Ok(Database { conn }) - } - - pub fn get_all_magnets(&mut self) -> Result> { - let results = magnets::table - .select(Magnet::as_select()) - .load(&mut self.conn) - .wrap_err("Failed to load magnets from database")?; - - Ok(results) - } - - pub fn store_magnets(&mut self, post: &PostInfo) -> Result { - let published_at = post.timestamp.naive_utc(); - - // Filter out magnet links that already exist in the database - let existing_links: Vec = magnets::table - .select(magnets::link) - .filter(magnets::link.eq_any(&post.magnet_links)) - .load(&mut self.conn) - .wrap_err("Failed to query existing magnets")?; - - let links = post - .magnet_links - .iter() - .filter(|link| !existing_links.contains(link)) - .map(|m| NewMagnet { - title: post.title.as_str(), - submitter: post.submitter.as_str(), - subreddit: post.subreddit.as_str(), - link: m, - published_at: &published_at, - }) - .collect::>(); - - diesel::insert_into(magnets::table) - .values(&links) - .execute(&mut self.conn) - .wrap_err("Failed to save new magnet") - } -} diff --git a/src/main.rs b/src/main.rs index 334b74c..e79c4c2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,9 @@ +use crate::actions::transmission::{TransmissionAction, TransmissionConfig}; +use crate::db::Database; +use crate::magnet::{extract_magnet_links, Magnet}; use chrono::{DateTime, Utc}; use clap::Parser; +use clap_verbosity_flag::{InfoLevel, Verbosity}; use color_eyre::eyre::{eyre, Result, WrapErr}; use directories::ProjectDirs; use figment::providers::Env; @@ -8,18 +12,16 @@ use figment::{ Figment, }; use figment_file_provider_adapter::FileAdapter; -use log::{debug, warn}; +use log::{debug, info, warn}; use multimap::MultiMap; +use reddit_client::RedditClient; use regex::Regex; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; use std::fs::create_dir_all; use std::path::{Path, PathBuf}; -use crate::db::Database; -use crate::magnet::{extract_magnet_links, Magnet}; -use reddit_client::RedditClient; - +mod actions; mod db; mod magnet; mod models; @@ -34,6 +36,9 @@ struct SourceConfig { #[derive(Debug, Serialize, Deserialize)] struct Config { + #[serde(default)] + transmission: Option, + #[serde(default)] sources: HashMap, } @@ -57,6 +62,9 @@ struct Args { /// Path to the database file #[arg(short, long)] db: Option, + + #[command(flatten)] + verbose: Verbosity, } /// Filters posts based on a title filter pattern @@ -110,6 +118,10 @@ async fn main() -> Result<()> { let args = Args::parse(); + pretty_env_logger::formatted_timed_builder() + .filter_level(args.verbose.log_level_filter()) + .init(); + // Initialize database let db_path = match args.db { Some(path) => PathBuf::from(path), @@ -168,6 +180,7 @@ async fn main() -> Result<()> { user_posts.insert_many(username, submissions); } + // Process sources and store magnet links for (source_name, source_config) in conf.sources { println!("\nProcessing source [{}]", source_name); @@ -214,5 +227,34 @@ async fn main() -> Result<()> { } } + // Process magnet links with Transmission if enabled + if let Some(transmission_config) = conf.transmission { + if transmission_config.enable { + info!("Processing magnet links with Transmission"); + match TransmissionAction::new(&transmission_config, db).await { + Ok(mut transmission_action) => { + match transmission_action.process_unprocessed_magnets().await { + Ok(count) => { + info!( + "Successfully processed {} magnet links with Transmission", + count + ); + } + Err(e) => { + warn!("Failed to process magnet links with Transmission: {}", e); + } + } + } + Err(e) => { + warn!("Failed to initialize Transmission action: {}", e); + } + } + } else { + debug!("Transmission action is disabled"); + } + } else { + debug!("No Transmission configuration found"); + } + Ok(()) } diff --git a/src/models.rs b/src/models.rs index a546e2a..ace44d4 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,4 +1,4 @@ -use crate::schema::magnets; +use crate::schema::{magnets, transmission_processed}; use chrono::NaiveDateTime; use diesel::prelude::*; @@ -24,3 +24,20 @@ pub struct NewMagnet<'a> { pub link: &'a str, pub published_at: &'a NaiveDateTime, } + +#[derive(Queryable, Selectable)] +#[diesel(table_name = transmission_processed)] +#[diesel(check_for_backend(diesel::sqlite::Sqlite))] +pub struct TransmissionProcessed { + pub id: Option, + pub magnet_id: i32, + pub processed_at: NaiveDateTime, +} + +#[derive(Insertable)] +#[diesel(table_name = transmission_processed)] +#[diesel(check_for_backend(diesel::sqlite::Sqlite))] +pub struct NewTransmissionProcessed<'a> { + pub magnet_id: i32, + pub processed_at: &'a NaiveDateTime, +} diff --git a/src/schema.rs b/src/schema.rs index 8feab8f..e7dabdc 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -10,3 +10,15 @@ diesel::table! { published_at -> Timestamp, } } + +diesel::table! { + transmission_processed (id) { + id -> Nullable, + magnet_id -> Integer, + processed_at -> Timestamp, + } +} + +diesel::joinable!(transmission_processed -> magnets (magnet_id)); + +diesel::allow_tables_to_appear_in_same_query!(magnets, transmission_processed,);