3aaeade4d3
- Add DownloadService to orchestrate metadata → indexer → torrent flow - Add POST /api/sync/artist endpoint for syncing artist albums - Add StubTorrentClient for testing (logs requests to file) - Refactor TorrentConfig to tagged enum (client_type: qbittorrent|stub|none) - Add POST /api/reload endpoint for hot config reload - Add chrono dependency for timestamps
181 lines
5.5 KiB
Rust
181 lines
5.5 KiB
Rust
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<String>,
|
|
pub indexer: Option<String>,
|
|
pub error: Option<String>,
|
|
}
|
|
|
|
#[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<AlbumDownloadResult>,
|
|
}
|
|
|
|
pub struct DownloadService;
|
|
|
|
impl DownloadService {
|
|
pub async fn sync_artist(
|
|
artist_name: &str,
|
|
metadata: &MetadataService,
|
|
indexers: &IndexerService,
|
|
torrent: &TorrentService,
|
|
) -> Result<ArtistSyncResult, String> {
|
|
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<u32>,
|
|
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()
|
|
}
|
|
}
|