diff --git a/apps/api/src/torrent_import.rs b/apps/api/src/torrent_import.rs index 1d478cb..e55b446 100644 --- a/apps/api/src/torrent_import.rs +++ b/apps/api/src/torrent_import.rs @@ -410,8 +410,8 @@ async fn is_torrent_import_enabled(pool: &PgPool) -> bool { async fn process_torrent_import(pool: PgPool, torrent_id: Uuid) -> anyhow::Result<()> { let row = sqlx::query( - "SELECT library_id, series_name, expected_volumes, content_path, qb_hash, replace_existing \ - FROM torrent_downloads WHERE id = $1", + "SELECT td.library_id, td.series_name, td.expected_volumes, td.content_path, td.qb_hash, td.replace_existing, l.name AS library_name \ + FROM torrent_downloads td LEFT JOIN libraries l ON l.id = td.library_id WHERE td.id = $1", ) .bind(torrent_id) .fetch_one(&pool) @@ -419,6 +419,7 @@ async fn process_torrent_import(pool: PgPool, torrent_id: Uuid) -> anyhow::Resul let library_id: Uuid = row.get("library_id"); let series_name: String = row.get("series_name"); + let library_name: Option = row.get("library_name"); let expected_volumes: Vec = row.get("expected_volumes"); let content_path: Option = row.get("content_path"); let qb_hash: Option = row.get("qb_hash"); @@ -569,6 +570,15 @@ async fn process_torrent_import(pool: PgPool, torrent_id: Uuid) -> anyhow::Resul } } + notifications::notify( + pool.clone(), + notifications::NotificationEvent::TorrentImportCompleted { + library_name: library_name.clone(), + series_name: series_name.clone(), + imported_count: imported.len(), + }, + ); + info!( "Torrent import {} done: {} files imported, scan job {} queued", torrent_id, @@ -586,6 +596,15 @@ async fn process_torrent_import(pool: PgPool, torrent_id: Uuid) -> anyhow::Resul .bind(torrent_id) .execute(&pool) .await?; + + notifications::notify( + pool.clone(), + notifications::NotificationEvent::TorrentImportFailed { + library_name: library_name.clone(), + series_name: series_name.clone(), + error: msg, + }, + ); } } diff --git a/crates/notifications/src/lib.rs b/crates/notifications/src/lib.rs index 72fcecf..47b70d8 100644 --- a/crates/notifications/src/lib.rs +++ b/crates/notifications/src/lib.rs @@ -55,6 +55,10 @@ pub struct EventToggles { pub download_detection_completed: bool, #[serde(default = "default_true")] pub download_detection_failed: bool, + #[serde(default = "default_true")] + pub torrent_import_completed: bool, + #[serde(default = "default_true")] + pub torrent_import_failed: bool, } fn default_true() -> bool { @@ -81,6 +85,8 @@ fn default_events() -> EventToggles { reading_status_push_failed: true, download_detection_completed: true, download_detection_failed: true, + torrent_import_completed: true, + torrent_import_failed: true, } } @@ -296,6 +302,17 @@ pub enum NotificationEvent { library_name: Option, error: String, }, + // Torrent import (qBittorrent download completed → files imported into library) + TorrentImportCompleted { + library_name: Option, + series_name: String, + imported_count: usize, + }, + TorrentImportFailed { + library_name: Option, + series_name: String, + error: String, + }, } /// Classify an indexer job_type string into the right event constructor category. @@ -604,6 +621,38 @@ fn format_event(event: &NotificationEvent) -> String { ] .join("\n") } + NotificationEvent::TorrentImportCompleted { + library_name, + series_name, + imported_count, + } => { + let lib = library_name.as_deref().unwrap_or("Unknown"); + [ + format!("✅ Torrent import completed"), + String::new(), + format!("📂 Library: {lib}"), + format!("📚 Series: {series_name}"), + format!("📥 Files imported: {imported_count}"), + ] + .join("\n") + } + NotificationEvent::TorrentImportFailed { + library_name, + series_name, + error, + } => { + let lib = library_name.as_deref().unwrap_or("Unknown"); + let err = truncate(error, 200); + [ + format!("🚨 Torrent import failed"), + String::new(), + format!("📂 Library: {lib}"), + format!("📚 Series: {series_name}"), + String::new(), + format!("💬 {err}"), + ] + .join("\n") + } } } @@ -650,6 +699,8 @@ fn is_event_enabled(config: &TelegramConfig, event: &NotificationEvent) -> bool NotificationEvent::ReadingStatusPushFailed { .. } => config.events.reading_status_push_failed, NotificationEvent::DownloadDetectionCompleted { .. } => config.events.download_detection_completed, NotificationEvent::DownloadDetectionFailed { .. } => config.events.download_detection_failed, + NotificationEvent::TorrentImportCompleted { .. } => config.events.torrent_import_completed, + NotificationEvent::TorrentImportFailed { .. } => config.events.torrent_import_failed, } }