use crate::actions::action::{Action, ProcessedMagnets}; use crate::actions::bitmagnet::BitmagnetAction; use crate::actions::factory::init_action; use crate::actions::transmission::TransmissionAction; use crate::config::Config; use crate::db::Database; use crate::notifications::factory::init_notification; use crate::notifications::notification::Notification; use crate::notifications::ntfy::NtfyNotification; use crate::reddit_client::{RedditClient, RedditPost}; use crate::report; use color_eyre::eyre::{Result, WrapErr}; use log::{debug, info, warn}; use multimap::MultiMap; use regex::Regex; use std::collections::{HashMap, HashSet}; /// Trait for configurations that can be enabled or disabled pub trait Enableable { /// Returns whether the configuration is enabled fn is_enabled(&self) -> bool; } /// Application state and behavior pub struct App { db: Database, config: Config, actions: Vec>, notifications: Vec>, reddit_client: RedditClient, } impl App { /// Create a new App instance pub fn new(db: Database, config: Config) -> Self { Self { db, config, actions: Vec::new(), notifications: Vec::new(), reddit_client: RedditClient::new(), } } /// Initialize actions based on configuration pub fn init_actions(&mut self) -> Result<()> { if let Some(action) = init_action(&mut &self.config.bitmagnet, BitmagnetAction::new)? { self.actions.push(action); } if let Some(action) = init_action(&mut &self.config.transmission, TransmissionAction::new)? { self.actions.push(action); } Ok(()) } /// Initialize notifications based on configuration pub fn init_notifications(&mut self) -> Result<()> { if let Some(notification) = init_notification(&self.config.ntfy, NtfyNotification::new)? { self.notifications.push(notification); } Ok(()) } /// Fetch posts from Reddit pub async fn fetch_posts(&self, post_count: u32) -> Result> { let mut unique_usernames = HashSet::new(); for (_, source_config) in &self.config.sources { unique_usernames.insert(source_config.username.clone()); } let mut user_posts = MultiMap::new(); for username in unique_usernames { info!("Fetching posts from user [{}]", username); let submissions = self .reddit_client .fetch_user_submissions(&username, post_count) .await?; user_posts.insert_many(username, submissions); } Ok(user_posts) } /// Process sources and extract magnet links pub async fn process_sources( &mut self, user_posts: &MultiMap, ) -> Result { let mut total_new_links = 0; for (source_name, source_config) in &self.config.sources { info!("Processing source [{}]", source_name); let username = source_config.username.clone(); let title_filter = match &source_config.title_filter { Some(filter) => Some( Regex::new(filter.as_str()) .context(format!("Invalid regex pattern: {}", filter))?, ), None => None, }; if let Some(submissions) = user_posts.get_vec(&username) { for post in submissions .iter() .filter(|s| filter_post(&s.title, title_filter.clone())) { let title = &post.title; let body = &post.body; let subreddit = &post.subreddit; let magnet_links = crate::magnet::extract_magnet_links(body); if !magnet_links.is_empty() { let post_info = crate::PostInfo { title: title.to_string(), submitter: username.clone(), subreddit: subreddit.to_string(), magnet_links, timestamp: post.created, imdb_id: source_config.imdb_id.clone(), }; // Store the post info in the database match self.db.store_magnets(&post_info) { Ok(count) => { total_new_links += count; } Err(e) => { warn!("Failed to store post info in database: {}", e); } } } } } } Ok(total_new_links) } /// Process magnet links with actions pub async fn process_actions(&mut self) -> Result> { let mut action_results = HashMap::new(); for action in &mut self.actions { debug!("Processing magnet links with {}", action.get_name()); match action.process_unprocessed_magnets(&mut self.db).await { Ok(processed_magnets) => { let count = processed_magnets.success.len(); debug!( "Successfully processed {} magnet links with {}", count, action.get_name() ); action_results.insert(action.get_name().to_string(), processed_magnets); } Err(e) => { warn!( "Failed to process magnet links with {}: {}", action.get_name(), e ); } } } Ok(action_results) } /// Send notifications pub async fn send_notifications( &self, action_results: &HashMap, total_new_links: usize, ) -> Result<()> { for notification in &self.notifications { match notification .send_notification(action_results, total_new_links) .await { Ok(_) => { debug!( "Successfully sent notification to {}", notification.get_name() ); } Err(e) => { warn!( "Failed to send notification to {}: {}", notification.get_name(), e ); } } } Ok(()) } /// Generate and display report pub fn generate_report( &self, action_results: &HashMap, total_new_links: usize, ) { for line in report::generate_report(action_results, total_new_links, true).lines() { info!("{}", line); } } pub async fn run(&mut self, post_count: u32) -> Result<()> { self.init_actions()?; self.init_notifications()?; // Fetch posts from Reddit let user_posts = self.fetch_posts(post_count).await?; // Process sources and extract magnet links let total_new_links = self.process_sources(&user_posts).await?; // Process magnet links with actions let action_results = self.process_actions().await?; // Send notifications self.send_notifications(&action_results, total_new_links) .await?; // Generate and display report self.generate_report(&action_results, total_new_links); Ok(()) } } /// Filters posts based on a title filter pattern fn filter_post(title: &str, title_filter: Option) -> bool { match title_filter { Some(pattern) => pattern.is_match(title), None => true, } }