Compare commits

..

1 Commits

Author SHA1 Message Date
Connor Johnstone
ea1e3c6ac5 Updates for the "full flow" 2026-03-17 21:38:27 -04:00
5 changed files with 129 additions and 18 deletions

View File

@@ -11,5 +11,5 @@ pub mod ytdlp;
pub use backend::{AudioFormat, BackendConfig, DownloadBackend, DownloadResult, DownloadTarget, SearchResult}; pub use backend::{AudioFormat, BackendConfig, DownloadBackend, DownloadResult, DownloadTarget, SearchResult};
pub use error::{DlError, DlResult}; pub use error::{DlError, DlResult};
pub use queue::{DlStats, download_single, run_queue}; pub use queue::{DlStats, SyncStats, download_single, run_queue, sync_wanted_to_queue};
pub use ytdlp::{SearchSource, YtDlpBackend}; pub use ytdlp::{SearchSource, YtDlpBackend};

View File

@@ -105,6 +105,12 @@ enum QueueAction {
}, },
/// Retry all failed downloads. /// Retry all failed downloads.
Retry, Retry,
/// Sync wanted items from the watchlist to the download queue.
Sync {
/// Preview what would be enqueued without doing it.
#[arg(long)]
dry_run: bool,
},
} }
fn default_database_url() -> String { fn default_database_url() -> String {
@@ -276,6 +282,13 @@ async fn main() -> anyhow::Result<()> {
println!("Requeued {} failed downloads.", failed.len()); println!("Requeued {} failed downloads.", failed.len());
} }
} }
QueueAction::Sync { dry_run } => {
if dry_run {
println!("DRY RUN — no items will be enqueued");
}
let stats = shanty_dl::sync_wanted_to_queue(db.conn(), dry_run).await?;
println!("\nSync complete: {stats}");
}
} }
} }
} }

View File

@@ -1,7 +1,7 @@
use std::fmt; use std::fmt;
use std::time::Duration; use std::time::Duration;
use sea_orm::DatabaseConnection; use sea_orm::{ActiveValue::Set, DatabaseConnection};
use shanty_db::entities::download_queue::DownloadStatus; use shanty_db::entities::download_queue::DownloadStatus;
use shanty_db::entities::wanted_item::WantedStatus; use shanty_db::entities::wanted_item::WantedStatus;
@@ -106,20 +106,45 @@ pub async fn run_queue(
) )
.await?; .await?;
// Update wanted item status if linked // Update wanted item status and create track record with MBID
if let Some(wanted_id) = item.wanted_item_id { if let Some(wanted_id) = item.wanted_item_id {
if let Err(e) = queries::wanted::update_status( if let Ok(wanted) = queries::wanted::get_by_id(conn, wanted_id).await {
conn, // Create a track record with the MBID so the tagger
wanted_id, // can skip searching and go straight to the right recording
WantedStatus::Downloaded, let now = chrono::Utc::now().naive_utc();
) let file_path = result.file_path.to_string_lossy().to_string();
.await let file_size = std::fs::metadata(&result.file_path)
{ .map(|m| m.len() as i64)
tracing::warn!( .unwrap_or(0);
wanted_id = wanted_id,
error = %e, let track_active = shanty_db::entities::track::ActiveModel {
"failed to update wanted item status" file_path: Set(file_path),
); title: Set(Some(result.title.clone())),
artist: Set(result.artist.clone()),
file_size: Set(file_size),
musicbrainz_id: Set(wanted.musicbrainz_id.clone()),
artist_id: Set(wanted.artist_id),
added_at: Set(now),
updated_at: Set(now),
..Default::default()
};
if let Err(e) = queries::tracks::upsert(conn, track_active).await {
tracing::warn!(error = %e, "failed to create track record after download");
}
if let Err(e) = queries::wanted::update_status(
conn,
wanted_id,
WantedStatus::Downloaded,
)
.await
{
tracing::warn!(
wanted_id = wanted_id,
error = %e,
"failed to update wanted item status"
);
}
} }
} }
@@ -170,6 +195,79 @@ pub async fn run_queue(
Ok(stats) Ok(stats)
} }
/// Sync stats from a queue sync operation.
#[derive(Debug, Default)]
pub struct SyncStats {
pub found: u64,
pub enqueued: u64,
pub skipped: u64,
}
impl fmt::Display for SyncStats {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"found: {}, enqueued: {}, skipped (already queued): {}",
self.found, self.enqueued, self.skipped,
)
}
}
/// Sync wanted items to the download queue.
/// Finds all Track-type Wanted items and enqueues them for download,
/// skipping any that already have a queue entry.
pub async fn sync_wanted_to_queue(
conn: &DatabaseConnection,
dry_run: bool,
) -> DlResult<SyncStats> {
let wanted = queries::wanted::list(conn, Some(WantedStatus::Wanted)).await?;
let mut stats = SyncStats::default();
for item in &wanted {
stats.found += 1;
// Build search query from the name + artist
let artist_name = if let Some(id) = item.artist_id {
queries::artists::get_by_id(conn, id)
.await
.map(|a| a.name)
.ok()
} else {
None
};
let query = match artist_name {
Some(ref artist) if !item.name.is_empty() => format!("{} {}", artist, item.name),
_ if !item.name.is_empty() => item.name.clone(),
Some(ref artist) => artist.clone(),
None => {
tracing::warn!(id = item.id, "cannot build query — no name or artist");
continue;
}
};
// Check if already queued
if let Some(_existing) = queries::downloads::find_by_wanted_item_id(conn, item.id).await? {
tracing::debug!(id = item.id, name = %item.name, "already in queue, skipping");
stats.skipped += 1;
continue;
}
if dry_run {
println!("Would enqueue: {query}");
stats.enqueued += 1;
continue;
}
queries::downloads::enqueue(conn, &query, Some(item.id), "ytdlp").await?;
tracing::info!(id = item.id, query = %query, "enqueued for download");
stats.enqueued += 1;
}
tracing::info!(%stats, "sync complete");
Ok(stats)
}
/// Download a single item directly (not from queue). /// Download a single item directly (not from queue).
pub async fn download_single( pub async fn download_single(
backend: &impl DownloadBackend, backend: &impl DownloadBackend,

View File

@@ -280,10 +280,10 @@ impl DownloadBackend for YtDlpBackend {
} }
}; };
// Build output template // Build output template — include artist if available for filename-based metadata fallback
let output_template = config let output_template = config
.output_dir .output_dir
.join("%(title)s.%(ext)s") .join("%(artist,uploader,channel)s - %(title)s.%(ext)s")
.to_string_lossy() .to_string_lossy()
.to_string(); .to_string();

View File

@@ -218,7 +218,7 @@ async fn test_wanted_item_status_updated_on_download() {
}; };
// Create a wanted item // Create a wanted item
let wanted = queries::wanted::add(db.conn(), ItemType::Track, None, None, None) let wanted = queries::wanted::add(db.conn(), ItemType::Track, "Wanted Song", None, None, None, None)
.await .await
.unwrap(); .unwrap();
assert_eq!(wanted.status, WantedStatus::Wanted); assert_eq!(wanted.status, WantedStatus::Wanted);