From 0ba1caaa23ad4db1733b076c750a6630540ec88f Mon Sep 17 00:00:00 2001 From: AB-UK Date: Thu, 19 Mar 2026 01:09:49 +0000 Subject: [PATCH] Added merge --- furumi-agent/src/db.rs | 16 +++++++---- furumi-agent/src/merge.rs | 58 +++++++++++++++++++++++++++------------ 2 files changed, 51 insertions(+), 23 deletions(-) diff --git a/furumi-agent/src/db.rs b/furumi-agent/src/db.rs index fd5bf6e..e7686ec 100644 --- a/furumi-agent/src/db.rs +++ b/furumi-agent/src/db.rs @@ -714,11 +714,6 @@ pub async fn set_album_artist(pool: &PgPool, album_id: i64, artist_id: i64) -> R Ok(()) } -pub async fn move_albums_to_artist(pool: &PgPool, from_artist_id: i64, to_artist_id: i64) -> Result<(), sqlx::Error> { - sqlx::query("UPDATE albums SET artist_id = $2 WHERE artist_id = $1") - .bind(from_artist_id).bind(to_artist_id).execute(pool).await?; - Ok(()) -} pub async fn move_track_artists(pool: &PgPool, from_artist_id: i64, to_artist_id: i64) -> Result<(), sqlx::Error> { // Update, but avoid duplicate (track_id, artist_id, role) - delete first any conflicting rows @@ -772,3 +767,14 @@ pub async fn update_track_storage_path(pool: &PgPool, track_id: i64, new_path: & .bind(track_id).bind(new_path).execute(pool).await?; Ok(()) } + +pub async fn get_albums_for_artist(pool: &PgPool, artist_id: i64) -> Result, sqlx::Error> { + sqlx::query_as::<_, Album>("SELECT * FROM albums WHERE artist_id = $1 ORDER BY year NULLS LAST, name") + .bind(artist_id).fetch_all(pool).await +} + +pub async fn find_album_by_artist_id_and_name(pool: &PgPool, artist_id: i64, name: &str) -> Result, sqlx::Error> { + let row: Option<(i64,)> = sqlx::query_as("SELECT id FROM albums WHERE artist_id = $1 AND name = $2") + .bind(artist_id).bind(name).fetch_optional(pool).await?; + Ok(row.map(|r| r.0)) +} diff --git a/furumi-agent/src/merge.rs b/furumi-agent/src/merge.rs index bcc7f2a..bd164b1 100644 --- a/furumi-agent/src/merge.rs +++ b/furumi-agent/src/merge.rs @@ -102,30 +102,35 @@ pub async fn execute_merge(state: &Arc, merge_id: Uuid) -> anyhow::Res // 2. Process album mappings for mapping in &proposal.album_mappings { if let Some(target_id) = mapping.merge_into_album_id { - // Remove duplicate tracks (same file_hash in both albums) - let dup_ids = db::get_duplicate_track_ids_in_albums(&state.pool, mapping.source_album_id, target_id).await?; - for dup_id in dup_ids { - if let Ok(Some(path)) = db::get_track_storage_path(&state.pool, dup_id).await { - let p = std::path::Path::new(&path); - if p.exists() { - let _ = tokio::fs::remove_file(p).await; - } - } - db::delete_track(&state.pool, dup_id).await?; - } - // Move remaining tracks to target album - db::move_tracks_to_album(&state.pool, mapping.source_album_id, target_id).await?; - db::delete_album(&state.pool, mapping.source_album_id).await?; + merge_albums_into(&state.pool, mapping.source_album_id, target_id).await?; } else { - // Rename album and move to winner artist + // Rename album, then move to winner — merging if winner already has same name db::rename_album(&state.pool, mapping.source_album_id, &mapping.canonical_name).await?; - db::set_album_artist(&state.pool, mapping.source_album_id, proposal.winner_artist_id).await?; + if let Some(existing_id) = db::find_album_by_artist_id_and_name( + &state.pool, proposal.winner_artist_id, &mapping.canonical_name, + ).await? { + if existing_id != mapping.source_album_id { + merge_albums_into(&state.pool, mapping.source_album_id, existing_id).await?; + } + // else: source_album already belongs to winner with that name — nothing to do + } else { + db::set_album_artist(&state.pool, mapping.source_album_id, proposal.winner_artist_id).await?; + } } } - // 3. Move remaining albums from losers to winner + // 3. Move remaining albums from losers to winner, merging name conflicts for &loser_id in &loser_ids { - db::move_albums_to_artist(&state.pool, loser_id, proposal.winner_artist_id).await?; + let albums = db::get_albums_for_artist(&state.pool, loser_id).await?; + for album in albums { + if let Some(existing_id) = db::find_album_by_artist_id_and_name( + &state.pool, proposal.winner_artist_id, &album.name, + ).await? { + merge_albums_into(&state.pool, album.id, existing_id).await?; + } else { + db::set_album_artist(&state.pool, album.id, proposal.winner_artist_id).await?; + } + } } // 4. Move track_artists from losers to winner @@ -174,6 +179,23 @@ pub async fn execute_merge(state: &Arc, merge_id: Uuid) -> anyhow::Res Ok(()) } +/// Merge source album into target: deduplicate tracks, move the rest, delete source. +async fn merge_albums_into(pool: &sqlx::PgPool, source_id: i64, target_id: i64) -> anyhow::Result<()> { + let dup_ids = db::get_duplicate_track_ids_in_albums(pool, source_id, target_id).await?; + for dup_id in dup_ids { + if let Ok(Some(path)) = db::get_track_storage_path(pool, dup_id).await { + let p = std::path::Path::new(&path); + if p.exists() { + let _ = tokio::fs::remove_file(p).await; + } + } + db::delete_track(pool, dup_id).await?; + } + db::move_tracks_to_album(pool, source_id, target_id).await?; + db::delete_album(pool, source_id).await?; + Ok(()) +} + fn sanitize(name: &str) -> String { name.chars() .map(|c| match c {