use serde::Serialize; use crate::indexer::SearchResult; use super::{IndexerService, MetadataService, TorrentService}; #[derive(Debug, Serialize)] pub struct AlbumDownloadResult { pub album_id: String, pub album_title: String, pub artist_name: String, pub status: DownloadStatus, pub torrent_hash: Option, pub indexer: Option, pub error: Option, } #[derive(Debug, Serialize)] #[serde(rename_all = "lowercase")] pub enum DownloadStatus { Added, NoResults, Failed, Skipped, } #[derive(Debug, Serialize)] pub struct ArtistSyncResult { pub artist_id: String, pub artist_name: String, pub total_albums: usize, pub albums_added: usize, pub albums_failed: usize, pub albums_no_results: usize, pub results: Vec, } pub struct DownloadService; impl DownloadService { pub async fn sync_artist( artist_name: &str, metadata: &MetadataService, indexers: &IndexerService, torrent: &TorrentService, ) -> Result { let search_result = metadata .search_artists(artist_name, Some(1), None) .await .map_err(|e| format!("metadata search failed: {}", e))?; let artist = search_result .artists .first() .ok_or_else(|| format!("artist '{}' not found", artist_name))?; let albums_response = metadata .get_artist_albums(&artist.id, Some(100), None) .await .map_err(|e| format!("failed to get albums: {}", e))?; let mut results = Vec::new(); let mut albums_added = 0; let mut albums_failed = 0; let mut albums_no_results = 0; for album in &albums_response.albums { let result = Self::download_album( &artist.name, &album.id, &album.title, album .release_date .split('-') .next() .and_then(|y| y.parse().ok()), indexers, torrent, ) .await; match result.status { DownloadStatus::Added => albums_added += 1, DownloadStatus::NoResults => albums_no_results += 1, DownloadStatus::Failed | DownloadStatus::Skipped => albums_failed += 1, } results.push(result); } Ok(ArtistSyncResult { artist_id: artist.id.clone(), artist_name: artist.name.clone(), total_albums: albums_response.albums.len(), albums_added, albums_failed, albums_no_results, results, }) } async fn download_album( artist_name: &str, album_id: &str, album_title: &str, year: Option, indexers: &IndexerService, torrent: &TorrentService, ) -> AlbumDownloadResult { let criteria = crate::indexer::MusicSearchCriteria { artist: artist_name.to_string(), album: Some(album_title.to_string()), year, limit: 20, offset: 0, }; let search_results = match indexers.search(&criteria, None).await { Ok(r) => r, Err(e) => { return AlbumDownloadResult { album_id: album_id.to_string(), album_title: album_title.to_string(), artist_name: artist_name.to_string(), status: DownloadStatus::Failed, torrent_hash: None, indexer: None, error: Some(format!("indexer search failed: {}", e)), }; } }; if search_results.is_empty() { return AlbumDownloadResult { album_id: album_id.to_string(), album_title: album_title.to_string(), artist_name: artist_name.to_string(), status: DownloadStatus::NoResults, torrent_hash: None, indexer: None, error: None, }; } let best = Self::select_best_result(&search_results); match torrent.add_torrent_url(&best.download_url, None).await { Ok(()) => AlbumDownloadResult { album_id: album_id.to_string(), album_title: album_title.to_string(), artist_name: artist_name.to_string(), status: DownloadStatus::Added, torrent_hash: best.infohash.clone(), indexer: Some(best.indexer.clone()), error: None, }, Err(e) => AlbumDownloadResult { album_id: album_id.to_string(), album_title: album_title.to_string(), artist_name: artist_name.to_string(), status: DownloadStatus::Failed, torrent_hash: None, indexer: Some(best.indexer.clone()), error: Some(format!("failed to add torrent: {}", e)), }, } } fn select_best_result(results: &[SearchResult]) -> &SearchResult { results .iter() .max_by_key(|r| { let seeders = r.seeders.unwrap_or(0); let is_flac = r.title.to_lowercase().contains("flac"); let score = seeders as i64 + if is_flac { 1000 } else { 0 }; score }) .unwrap() } }