diff --git a/src/actions/transmission.rs b/src/actions/transmission.rs deleted file mode 100644 index b2fdb38..0000000 --- a/src/actions/transmission.rs +++ /dev/null @@ -1,99 +0,0 @@ -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/actions/transmission/action.rs b/src/actions/transmission/action.rs new file mode 100644 index 0000000..8093d83 --- /dev/null +++ b/src/actions/transmission/action.rs @@ -0,0 +1,51 @@ +use crate::actions::transmission::client::TransmissionClient; +use crate::actions::transmission::config::TransmissionConfig; +use crate::db::{Database, TransmissionProcessedTable}; +use color_eyre::eyre::Result; +use log::{debug, info, warn}; + +/// Action for submitting magnet links to Transmission +pub struct TransmissionAction { + client: TransmissionClient, + db: Database, +} + +impl TransmissionAction { + pub async fn new(config: &TransmissionConfig, db: Database) -> Result { + let client = TransmissionClient::new(config)?; + + Ok(TransmissionAction { client, 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.client.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) + } +} diff --git a/src/actions/transmission/client.rs b/src/actions/transmission/client.rs new file mode 100644 index 0000000..5f1f9a5 --- /dev/null +++ b/src/actions/transmission/client.rs @@ -0,0 +1,53 @@ +use crate::actions::transmission::config::TransmissionConfig; +use color_eyre::eyre::{eyre, Result, WrapErr}; +use transmission_rpc::{ + types::{BasicAuth, TorrentAddArgs}, + TransClient, +}; +use url::Url; + +/// High-level Transmission client +pub struct TransmissionClient { + client: TransClient, + download_dir: String, +} + +impl TransmissionClient { + /// Create a new Transmission client from configuration + pub fn new(config: &TransmissionConfig) -> 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(TransmissionClient { + client, + download_dir: config.download_dir.clone(), + }) + } + + /// Submit a magnet link to Transmission + pub 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/actions/transmission/config.rs b/src/actions/transmission/config.rs new file mode 100644 index 0000000..ffd7f2b --- /dev/null +++ b/src/actions/transmission/config.rs @@ -0,0 +1,12 @@ +use serde::{Deserialize, Serialize}; + +/// 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, +} diff --git a/src/actions/transmission/mod.rs b/src/actions/transmission/mod.rs new file mode 100644 index 0000000..106ed1f --- /dev/null +++ b/src/actions/transmission/mod.rs @@ -0,0 +1,6 @@ +pub mod action; +pub mod client; +pub mod config; + +pub use action::TransmissionAction; +pub use config::TransmissionConfig; diff --git a/src/args.rs b/src/args.rs new file mode 100644 index 0000000..41f503d --- /dev/null +++ b/src/args.rs @@ -0,0 +1,17 @@ +use clap::Parser; +use clap_verbosity_flag::{InfoLevel, Verbosity}; + +#[derive(Parser, Debug)] +#[command(author, version, about = "Display recent posts from a Reddit user")] +pub struct Args { + /// Path to the configuration file + #[arg(short, long)] + pub config: Option, + + /// Path to the database file + #[arg(short, long)] + pub db: Option, + + #[command(flatten)] + pub verbose: Verbosity, +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..c2ed482 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,75 @@ +use crate::actions::transmission::TransmissionConfig; +use crate::args::Args; +use color_eyre::eyre::{eyre, Result, WrapErr}; +use directories::ProjectDirs; +use figment::providers::Env; +use figment::{ + providers::{Format, Toml}, + Figment, +}; +use figment_file_provider_adapter::FileAdapter; +use log::debug; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; + +/// Configuration for a Reddit source +#[derive(Debug, Serialize, Deserialize)] +pub struct SourceConfig { + pub username: String, + pub title_filter: Option, +} + +/// Main application configuration +#[derive(Debug, Serialize, Deserialize)] +pub struct Config { + #[serde(default)] + pub transmission: Option, + + #[serde(default)] + pub sources: HashMap, +} + +/// Loads the configuration from the specified file or default location +pub fn load_config(args: &Args) -> Result { + let mut conf_extractor = Figment::new(); + let config_file_path: Option = match &args.config { + Some(path) => Some(Path::new(path).to_path_buf()), + None => ProjectDirs::from("fr", "enoent", "reddit-magnet") + .map(|p| p.config_dir().join("config.toml")), + }; + match config_file_path { + Some(path) => { + if path.exists() { + debug!("Reading configuration from {:?}", path); + conf_extractor = conf_extractor.merge(FileAdapter::wrap(Toml::file_exact(path))); + } else { + debug!("Configuration file doesn't exist at {:?}", path); + } + } + None => { + debug!("No configuration file specified, using default configuration"); + } + } + + let conf: Config = conf_extractor + .merge(FileAdapter::wrap(Env::prefixed("REDDIT_MAGNET_"))) + .extract() + .wrap_err_with(|| "Invalid configuration or insufficient command line arguments")?; + + if conf.sources.is_empty() { + return Err(eyre!("No sources found in configuration. Please add at least one source to your configuration file.").into()); + } + + Ok(conf) +} + +/// Gets the database path from the command line arguments or default location +pub fn get_db_path(args: &Args) -> Result { + match &args.db { + Some(path) => Ok(PathBuf::from(path)), + None => ProjectDirs::from("fr", "enoent", "reddit-magnet") + .map(|p| p.data_dir().join("reddit-magnet.db")) + .ok_or_else(|| eyre!("Could not determine data directory")), + } +} diff --git a/src/db.rs b/src/db.rs index 854180a..9cd52b4 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1,4 +1,4 @@ -use crate::models::{Magnet, NewMagnet, NewTransmissionProcessed, TransmissionProcessed}; +use crate::models::{Magnet, NewMagnet, NewTransmissionProcessed}; use crate::schema::{magnets, transmission_processed}; use crate::PostInfo; use color_eyre::eyre::{eyre, Result, WrapErr}; diff --git a/src/main.rs b/src/main.rs index e79c4c2..69b1c0d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,48 +1,27 @@ -use crate::actions::transmission::{TransmissionAction, TransmissionConfig}; +use crate::actions::transmission::TransmissionAction; +use crate::args::Args; +use crate::config::{get_db_path, load_config}; 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; -use figment::{ - providers::{Format, Toml}, - Figment, -}; -use figment_file_provider_adapter::FileAdapter; 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::collections::HashSet; use std::fs::create_dir_all; -use std::path::{Path, PathBuf}; mod actions; +mod args; +mod config; mod db; mod magnet; mod models; mod reddit_client; mod schema; -#[derive(Debug, Serialize, Deserialize)] -struct SourceConfig { - username: String, - title_filter: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -struct Config { - #[serde(default)] - transmission: Option, - - #[serde(default)] - sources: HashMap, -} - #[derive(Debug)] struct PostInfo { title: String, @@ -52,21 +31,6 @@ struct PostInfo { timestamp: DateTime, } -#[derive(Parser, Debug)] -#[command(author, version, about = "Display recent posts from a Reddit user")] -struct Args { - /// Path to the configuration file - #[arg(short, long)] - config: Option, - - /// Path to the database file - #[arg(short, long)] - db: Option, - - #[command(flatten)] - verbose: Verbosity, -} - /// Filters posts based on a title filter pattern fn filter_posts(title: &str, title_filter: Option) -> bool { match title_filter { @@ -123,12 +87,7 @@ async fn main() -> Result<()> { .init(); // Initialize database - let db_path = match args.db { - Some(path) => PathBuf::from(path), - None => ProjectDirs::from("fr", "enoent", "reddit-magnet") - .map(|p| p.data_dir().join("reddit-magnet.db")) - .ok_or_else(|| eyre!("Could not determine data directory"))?, - }; + let db_path = get_db_path(&args)?; // Create parent directory if it doesn't exist if let Some(parent) = db_path.parent() { @@ -139,30 +98,7 @@ async fn main() -> Result<()> { let mut db = Database::new(&db_path) .wrap_err_with(|| format!("Failed to initialize database at {:?}", db_path))?; - let mut conf_extractor = Figment::new(); - let config_file_path: Option = match args.config { - Some(path) => Some(Path::new(&path).to_path_buf()), - None => ProjectDirs::from("fr", "enoent", "reddit-magnet") - .map(|p| p.config_dir().join("config.toml")), - }; - match config_file_path { - Some(path) => { - if path.exists() { - debug!("Reading configuration from {:?}", path); - conf_extractor = conf_extractor.merge(FileAdapter::wrap(Toml::file_exact(path))); - } else { - debug!("Configuration file doesn't exist at {:?}", path); - } - } - None => { - debug!("No configuration file specified, using default configuration"); - } - } - - let conf: Config = conf_extractor - .merge(FileAdapter::wrap(Env::prefixed("REDDIT_MAGNET_"))) - .extract() - .wrap_err_with(|| "Invalid configuration or insufficient command line arguments")?; + let conf = load_config(&args)?; if conf.sources.is_empty() { return Err(eyre!("No sources found in configuration. Please add at least one source to your configuration file.").into());