259 lines
7.0 KiB
Rust
259 lines
7.0 KiB
Rust
use shanty_db::entities::download_queue::DownloadStatus;
|
|
use shanty_db::{Database, queries};
|
|
use shanty_dl::backend::{
|
|
AudioFormat, BackendConfig, DownloadBackend, DownloadResult, DownloadTarget, SearchResult,
|
|
};
|
|
use shanty_dl::error::DlResult;
|
|
use shanty_dl::queue::{download_single, run_queue};
|
|
use tempfile::TempDir;
|
|
|
|
/// Mock backend for testing without yt-dlp.
|
|
struct MockBackend {
|
|
/// If true, downloads will fail.
|
|
should_fail: bool,
|
|
}
|
|
|
|
impl MockBackend {
|
|
fn new(should_fail: bool) -> Self {
|
|
Self { should_fail }
|
|
}
|
|
}
|
|
|
|
impl DownloadBackend for MockBackend {
|
|
async fn check_available(&self) -> DlResult<()> {
|
|
Ok(())
|
|
}
|
|
|
|
async fn search(&self, query: &str) -> DlResult<Vec<SearchResult>> {
|
|
Ok(vec![SearchResult {
|
|
url: format!("https://example.com/{}", query.replace(' ', "_")),
|
|
title: query.to_string(),
|
|
artist: Some("Test Artist".to_string()),
|
|
duration: Some(180.0),
|
|
source: "mock".to_string(),
|
|
}])
|
|
}
|
|
|
|
async fn download(
|
|
&self,
|
|
target: &DownloadTarget,
|
|
config: &BackendConfig,
|
|
) -> DlResult<DownloadResult> {
|
|
if self.should_fail {
|
|
return Err(shanty_dl::DlError::DownloadFailed("mock failure".into()));
|
|
}
|
|
|
|
let title = match target {
|
|
DownloadTarget::Url(u) => u.clone(),
|
|
DownloadTarget::Query(q) => q.clone(),
|
|
};
|
|
|
|
let file_name = format!("{}.{}", title.replace(' ', "_"), config.format);
|
|
let file_path = config.output_dir.join(&file_name);
|
|
std::fs::write(&file_path, b"fake audio data").unwrap();
|
|
|
|
Ok(DownloadResult {
|
|
file_path,
|
|
title,
|
|
artist: Some("Test Artist".into()),
|
|
duration: Some(180.0),
|
|
source_url: "https://example.com/test".into(),
|
|
})
|
|
}
|
|
}
|
|
|
|
async fn test_db() -> Database {
|
|
Database::new("sqlite::memory:")
|
|
.await
|
|
.expect("failed to create test database")
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_queue_process_success() {
|
|
let db = test_db().await;
|
|
let dir = TempDir::new().unwrap();
|
|
let backend = MockBackend::new(false);
|
|
|
|
let config = BackendConfig {
|
|
output_dir: dir.path().to_owned(),
|
|
format: AudioFormat::Opus,
|
|
cookies_path: None,
|
|
};
|
|
|
|
// Enqueue an item
|
|
queries::downloads::enqueue(db.conn(), "Test Song", None, "mock")
|
|
.await
|
|
.unwrap();
|
|
|
|
// Process queue
|
|
let stats = run_queue(db.conn(), &backend, &config, false)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(stats.downloads_attempted, 1);
|
|
assert_eq!(stats.downloads_completed, 1);
|
|
assert_eq!(stats.downloads_failed, 0);
|
|
|
|
// Verify status in DB
|
|
let items = queries::downloads::list(db.conn(), Some(DownloadStatus::Completed))
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(items.len(), 1);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_queue_process_failure() {
|
|
let db = test_db().await;
|
|
let dir = TempDir::new().unwrap();
|
|
let backend = MockBackend::new(true);
|
|
|
|
let config = BackendConfig {
|
|
output_dir: dir.path().to_owned(),
|
|
format: AudioFormat::Opus,
|
|
cookies_path: None,
|
|
};
|
|
|
|
queries::downloads::enqueue(db.conn(), "Failing Song", None, "mock")
|
|
.await
|
|
.unwrap();
|
|
|
|
let stats = run_queue(db.conn(), &backend, &config, false)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(stats.downloads_attempted, 1);
|
|
assert_eq!(stats.downloads_failed, 1);
|
|
|
|
// Check it's marked as failed
|
|
let items = queries::downloads::list(db.conn(), Some(DownloadStatus::Failed))
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(items.len(), 1);
|
|
assert!(items[0].error_message.is_some());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_queue_dry_run() {
|
|
let db = test_db().await;
|
|
let dir = TempDir::new().unwrap();
|
|
let backend = MockBackend::new(false);
|
|
|
|
let config = BackendConfig {
|
|
output_dir: dir.path().to_owned(),
|
|
format: AudioFormat::Opus,
|
|
cookies_path: None,
|
|
};
|
|
|
|
queries::downloads::enqueue(db.conn(), "Dry Run Song", None, "mock")
|
|
.await
|
|
.unwrap();
|
|
|
|
let stats = run_queue(db.conn(), &backend, &config, true).await.unwrap();
|
|
assert_eq!(stats.downloads_attempted, 1);
|
|
assert_eq!(stats.downloads_skipped, 1);
|
|
assert_eq!(stats.downloads_completed, 0);
|
|
|
|
// No files should exist
|
|
let entries: Vec<_> = std::fs::read_dir(dir.path()).unwrap().collect();
|
|
assert!(entries.is_empty());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_download_single_success() {
|
|
let dir = TempDir::new().unwrap();
|
|
let backend = MockBackend::new(false);
|
|
|
|
let config = BackendConfig {
|
|
output_dir: dir.path().to_owned(),
|
|
format: AudioFormat::Mp3,
|
|
cookies_path: None,
|
|
};
|
|
|
|
download_single(
|
|
&backend,
|
|
DownloadTarget::Query("Test Song".into()),
|
|
&config,
|
|
false,
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
// File should exist
|
|
let entries: Vec<_> = std::fs::read_dir(dir.path())
|
|
.unwrap()
|
|
.filter_map(|e| e.ok())
|
|
.collect();
|
|
assert_eq!(entries.len(), 1);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_queue_retry() {
|
|
let db = test_db().await;
|
|
|
|
// Enqueue and manually fail an item
|
|
let item = queries::downloads::enqueue(db.conn(), "Retry Song", None, "mock")
|
|
.await
|
|
.unwrap();
|
|
queries::downloads::update_status(db.conn(), item.id, DownloadStatus::Failed, Some("oops"))
|
|
.await
|
|
.unwrap();
|
|
|
|
// Retry it
|
|
queries::downloads::retry_failed(db.conn(), item.id)
|
|
.await
|
|
.unwrap();
|
|
|
|
// Should be pending again with retry_count = 1
|
|
let pending = queries::downloads::list(db.conn(), Some(DownloadStatus::Pending))
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(pending.len(), 1);
|
|
assert_eq!(pending[0].retry_count, 1);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_wanted_item_status_updated_on_download() {
|
|
use shanty_db::entities::wanted_item::{ItemType, WantedStatus};
|
|
|
|
let db = test_db().await;
|
|
let dir = TempDir::new().unwrap();
|
|
let backend = MockBackend::new(false);
|
|
|
|
let config = BackendConfig {
|
|
output_dir: dir.path().to_owned(),
|
|
format: AudioFormat::Opus,
|
|
cookies_path: None,
|
|
};
|
|
|
|
// Create a wanted item
|
|
let wanted = queries::wanted::add(
|
|
db.conn(),
|
|
queries::wanted::AddWantedItem {
|
|
item_type: ItemType::Track,
|
|
name: "Wanted Song",
|
|
musicbrainz_id: None,
|
|
artist_id: None,
|
|
album_id: None,
|
|
track_id: None,
|
|
user_id: None,
|
|
},
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(wanted.status, WantedStatus::Wanted);
|
|
|
|
// Enqueue download linked to the wanted item
|
|
queries::downloads::enqueue(db.conn(), "Wanted Song", Some(wanted.id), "mock")
|
|
.await
|
|
.unwrap();
|
|
|
|
// Process queue
|
|
run_queue(db.conn(), &backend, &config, false)
|
|
.await
|
|
.unwrap();
|
|
|
|
// Wanted item should now be Downloaded
|
|
let updated = queries::wanted::get_by_id(db.conn(), wanted.id)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(updated.status, WantedStatus::Downloaded);
|
|
}
|