Fixed agent UI
All checks were successful
Publish Metadata Agent Image / build-and-push-image (push) Successful in 1m7s
Publish Web Player Image / build-and-push-image (push) Successful in 1m9s
Publish Server Image / build-and-push-image (push) Successful in 2m10s

This commit is contained in:
2026-03-18 04:05:47 +00:00
parent 6e2155d8bd
commit a4010e1173
5 changed files with 639 additions and 403 deletions

View File

@@ -19,6 +19,12 @@ pub async fn run(state: Arc<AppState>) {
Ok(count) => tracing::info!(count, "processed new files"),
Err(e) => tracing::error!(?e, "inbox scan failed"),
}
// Re-process pending tracks (e.g. retried from admin UI)
match reprocess_pending(&state).await {
Ok(0) => {}
Ok(count) => tracing::info!(count, "re-processed pending tracks"),
Err(e) => tracing::error!(?e, "pending re-processing failed"),
}
tokio::time::sleep(interval).await;
}
}
@@ -61,6 +67,137 @@ async fn scan_inbox(state: &Arc<AppState>) -> anyhow::Result<usize> {
Ok(count)
}
/// Re-process pending tracks from DB (e.g. tracks retried via admin UI).
/// These already have raw metadata and path hints stored — just need RAG + LLM.
async fn reprocess_pending(state: &Arc<AppState>) -> anyhow::Result<usize> {
let pending = db::list_pending_for_processing(&state.pool, 10).await?;
if pending.is_empty() {
return Ok(0);
}
let mut count = 0;
for pt in &pending {
tracing::info!(id = %pt.id, title = pt.raw_title.as_deref().unwrap_or("?"), "Re-processing pending track");
db::update_pending_status(&state.pool, pt.id, "processing", None).await?;
// Build raw metadata and hints from stored DB fields
let raw_meta = metadata::RawMetadata {
title: pt.raw_title.clone(),
artist: pt.raw_artist.clone(),
album: pt.raw_album.clone(),
track_number: pt.raw_track_number.map(|n| n as u32),
year: pt.raw_year.map(|n| n as u32),
genre: pt.raw_genre.clone(),
duration_secs: pt.duration_secs,
};
let hints = db::PathHints {
title: pt.path_title.clone(),
artist: pt.path_artist.clone(),
album: pt.path_album.clone(),
year: pt.path_year,
track_number: pt.path_track_number,
};
// RAG lookup
let artist_query = raw_meta.artist.as_deref()
.or(hints.artist.as_deref())
.unwrap_or("");
let album_query = raw_meta.album.as_deref()
.or(hints.album.as_deref())
.unwrap_or("");
let similar_artists = if !artist_query.is_empty() {
db::find_similar_artists(&state.pool, artist_query, 5).await.unwrap_or_default()
} else {
Vec::new()
};
let similar_albums = if !album_query.is_empty() {
db::find_similar_albums(&state.pool, album_query, 5).await.unwrap_or_default()
} else {
Vec::new()
};
// LLM normalization
match normalize::normalize(state, &raw_meta, &hints, &similar_artists, &similar_albums).await {
Ok(normalized) => {
let confidence = normalized.confidence.unwrap_or(0.0);
let status = if confidence >= state.config.confidence_threshold {
"approved"
} else {
"review"
};
tracing::info!(
id = %pt.id,
norm_artist = normalized.artist.as_deref().unwrap_or("-"),
norm_title = normalized.title.as_deref().unwrap_or("-"),
confidence,
status,
"Re-processing complete"
);
db::update_pending_normalized(&state.pool, pt.id, status, &normalized, None).await?;
if status == "approved" {
let artist = normalized.artist.as_deref().unwrap_or("Unknown Artist");
let album = normalized.album.as_deref().unwrap_or("Unknown Album");
let title = normalized.title.as_deref().unwrap_or("Unknown Title");
let source = std::path::Path::new(&pt.inbox_path);
let ext = source.extension().and_then(|e| e.to_str()).unwrap_or("flac");
let track_num = normalized.track_number.unwrap_or(0);
let dest_filename = if track_num > 0 {
format!("{:02} - {}.{}", track_num, sanitize_filename(title), ext)
} else {
format!("{}.{}", sanitize_filename(title), ext)
};
// Check if already moved
let dest = state.config.storage_dir
.join(sanitize_filename(artist))
.join(sanitize_filename(album))
.join(&dest_filename);
let storage_path = if dest.exists() && !source.exists() {
dest.to_string_lossy().to_string()
} else if source.exists() {
match mover::move_to_storage(
&state.config.storage_dir, artist, album, &dest_filename, source,
).await {
Ok(p) => p.to_string_lossy().to_string(),
Err(e) => {
tracing::error!(id = %pt.id, ?e, "Failed to move file");
db::update_pending_status(&state.pool, pt.id, "error", Some(&e.to_string())).await?;
continue;
}
}
} else {
tracing::error!(id = %pt.id, "Source file missing: {:?}", source);
db::update_pending_status(&state.pool, pt.id, "error", Some("Source file missing")).await?;
continue;
};
match db::approve_and_finalize(&state.pool, pt.id, &storage_path).await {
Ok(track_id) => tracing::info!(id = %pt.id, track_id, "Track finalized"),
Err(e) => tracing::error!(id = %pt.id, ?e, "Failed to finalize"),
}
}
count += 1;
}
Err(e) => {
tracing::error!(id = %pt.id, ?e, "LLM normalization failed");
db::update_pending_status(&state.pool, pt.id, "error", Some(&e.to_string())).await?;
}
}
}
Ok(count)
}
/// Recursively remove empty directories inside the inbox.
/// Does not remove the inbox root itself.
async fn cleanup_empty_dirs(dir: &std::path::Path) -> bool {