From cc3ef04cbe33c2d038ec43000265d9f6c6e61146 Mon Sep 17 00:00:00 2001 From: AB-UK Date: Thu, 19 Mar 2026 23:37:33 +0000 Subject: [PATCH] Fix phantom duplicate tracks created on Merged file ingestion When the mover returns MoveOutcome::Merged (destination already exists, source deleted), approve_and_finalize was checking only file_hash to detect duplicates. Since the second ingestion had a different hash (different quality/mastering), it bypassed the check and created a phantom track record pointing to an existing storage_path with the wrong hash (of the now-deleted source file). Added a second dedup check by storage_path: if a non-hidden track already exists at that path, mark pending as 'merged' instead of inserting a new track row. This prevents phantom entries for any subsequent ingestion of a different-quality version of an already stored file. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- furumi-agent/src/db.rs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/furumi-agent/src/db.rs b/furumi-agent/src/db.rs index 6182e99..92cccd5 100644 --- a/furumi-agent/src/db.rs +++ b/furumi-agent/src/db.rs @@ -334,18 +334,31 @@ pub async fn approve_and_finalize( .fetch_one(pool) .await?; - // Check if track already exists (e.g. previously approved but pending not cleaned up) + // Check if track already exists by file_hash (re-approval of same file) let existing: Option<(i64,)> = sqlx::query_as("SELECT id FROM tracks WHERE file_hash = $1") .bind(&pt.file_hash) .fetch_optional(pool) .await?; if let Some((track_id,)) = existing { - // Already finalized — just mark pending as approved update_pending_status(pool, pending_id, "approved", None).await?; return Ok(track_id); } + // Check if track already exists by storage_path (Merged: different quality file landed + // at the same destination, source was deleted — don't create a phantom duplicate) + let existing_path: Option<(i64,)> = sqlx::query_as( + "SELECT id FROM tracks WHERE storage_path = $1 AND NOT hidden" + ) + .bind(storage_path) + .fetch_optional(pool) + .await?; + + if let Some((track_id,)) = existing_path { + update_pending_status(pool, pending_id, "merged", None).await?; + return Ok(track_id); + } + let artist_name = pt.norm_artist.as_deref().unwrap_or("Unknown Artist"); let artist_id = upsert_artist(pool, artist_name).await?;