Skip to content

Commit 4e3f633

Browse files
fix(watcher): return original tracks on filter_removed_tracks error
Change filter_removed_tracks to return the original Vec alongside the DbError so callers can fall back to unfiltered insertion. Previously the function consumed the vec via into_iter(), making it unavailable on error, causing the watcher to drop all new tracks instead of proceeding unfiltered. Refs: TASK-292, roborev job #29 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2b3fd62 commit 4e3f633

4 files changed

Lines changed: 44 additions & 12 deletions

File tree

backlog/tasks/task-292 - Fix-watcher-error-path-dropping-new-tracks-instead-of-proceeding-unfiltered.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
---
22
id: TASK-292
33
title: Fix watcher error path dropping new tracks instead of proceeding unfiltered
4-
status: To Do
4+
status: In Progress
55
assignee: []
66
created_date: '2026-02-26 00:10'
7+
updated_date: '2026-02-26 16:05'
78
labels:
89
- bug
910
- roborev

crates/mt-tauri/src/db/removed.rs

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
use rusqlite::{Connection, params};
77
use std::collections::HashSet;
88

9-
use crate::db::DbResult;
9+
use crate::db::{DbError, DbResult};
10+
11+
type TrackVec = Vec<(String, crate::db::TrackMetadata)>;
1012

1113
/// Record a track removal by filepath and optional content hash.
1214
/// Uses INSERT OR REPLACE so re-removing the same filepath just updates the timestamp.
@@ -93,12 +95,21 @@ pub(crate) fn clear_all_removals(conn: &Connection) -> DbResult<usize> {
9395
/// Filter a list of `(filepath, TrackMetadata)` pairs, removing any that match
9496
/// previously-removed filepaths or content hashes. Returns the filtered vec and
9597
/// the number of tracks that were skipped.
98+
///
99+
/// On DB errors, the original `tracks` vec is returned alongside the error so
100+
/// the caller can fall back to unfiltered insertion.
96101
pub(crate) fn filter_removed_tracks(
97102
conn: &Connection,
98-
tracks: Vec<(String, crate::db::TrackMetadata)>,
99-
) -> DbResult<(Vec<(String, crate::db::TrackMetadata)>, usize)> {
100-
let removed_paths = get_removed_filepaths(conn)?;
101-
let removed_hashes = get_removed_content_hashes(conn)?;
103+
tracks: TrackVec,
104+
) -> Result<(TrackVec, usize), (DbError, TrackVec)> {
105+
let removed_paths = match get_removed_filepaths(conn) {
106+
Ok(v) => v,
107+
Err(e) => return Err((e, tracks)),
108+
};
109+
let removed_hashes = match get_removed_content_hashes(conn) {
110+
Ok(v) => v,
111+
Err(e) => return Err((e, tracks)),
112+
};
102113

103114
if removed_paths.is_empty() && removed_hashes.is_empty() {
104115
return Ok((tracks, 0));
@@ -456,4 +467,26 @@ mod tests {
456467
let info = get_track_removal_info_bulk(&conn, &[]).unwrap();
457468
assert!(info.is_empty());
458469
}
470+
471+
#[test]
472+
fn test_filter_removed_tracks_returns_tracks_on_error() {
473+
use crate::db::TrackMetadata;
474+
475+
// Use a closed connection to force a DB error.
476+
let conn = setup_test_db();
477+
conn.execute_batch("DROP TABLE removed_tracks").unwrap();
478+
479+
let tracks = vec![
480+
("/music/a.mp3".to_string(), TrackMetadata::default()),
481+
("/music/b.mp3".to_string(), TrackMetadata::default()),
482+
];
483+
484+
let result = filter_removed_tracks(&conn, tracks);
485+
assert!(result.is_err());
486+
487+
let (_, returned_tracks) = result.unwrap_err();
488+
assert_eq!(returned_tracks.len(), 2);
489+
assert_eq!(returned_tracks[0].0, "/music/a.mp3");
490+
assert_eq!(returned_tracks[1].0, "/music/b.mp3");
491+
}
459492
}

crates/mt-tauri/src/scanner/commands.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -261,8 +261,8 @@ pub(crate) async fn scan_paths_to_library(
261261
// Filter out tracks that the user has previously removed from the library.
262262
let truly_new = {
263263
let conn = db.conn().map_err(|e| e.to_string())?;
264-
let (filtered, skipped) =
265-
removed::filter_removed_tracks(&conn, truly_new).map_err(|e| e.to_string())?;
264+
let (filtered, skipped) = removed::filter_removed_tracks(&conn, truly_new)
265+
.map_err(|(e, _tracks)| e.to_string())?;
266266
if skipped > 0 {
267267
info!(skipped, "Skipped previously removed tracks during scan");
268268
}

crates/mt-tauri/src/watcher.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -635,11 +635,9 @@ impl WatcherManager {
635635
}
636636
filtered
637637
}
638-
Err(e) => {
638+
Err((e, unfiltered)) => {
639639
error!(folder_id, error = %e, "Failed to filter removed tracks, proceeding unfiltered");
640-
// filter_removed_tracks consumed truly_new but failed before
641-
// filtering; tracks on disk will be picked up on next scan cycle.
642-
vec![]
640+
unfiltered
643641
}
644642
},
645643
Err(e) => {

0 commit comments

Comments
 (0)