Skip to content

Commit 2c8372c

Browse files
committed
feat: implement deduplication using metadata and fallback to filenames
1 parent 26c8063 commit 2c8372c

File tree

1 file changed

+93
-23
lines changed

1 file changed

+93
-23
lines changed

src/organize.rs

Lines changed: 93 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@ use std::{
1010
time::Instant,
1111
};
1212

13+
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
14+
struct MetadataKey {
15+
artist: String,
16+
album: String,
17+
title: String,
18+
track: Option<u16>,
19+
}
20+
1321
#[derive(Debug)]
1422
pub struct OrganizeResult {
1523
pub moved: usize,
@@ -32,13 +40,6 @@ pub fn organize_music_files(
3240
});
3341
}
3442

35-
let thread_count = rayon::current_num_threads();
36-
println!(
37-
" Organizing {} files using {} threads",
38-
music_files.len(),
39-
thread_count
40-
);
41-
4243
let start_time = Instant::now();
4344

4445
let pb = Arc::new(ProgressBar::new(music_files.len() as u64));
@@ -53,14 +54,24 @@ pub fn organize_music_files(
5354
let failed = Arc::new(Mutex::new(0));
5455
let duplicates = Arc::new(Mutex::new(0));
5556

56-
let used_paths: Arc<Mutex<HashMap<PathBuf, PathBuf>>> = Arc::new(Mutex::new(HashMap::new()));
57+
let used_metadata: Arc<Mutex<HashMap<MetadataKey, PathBuf>>> =
58+
Arc::new(Mutex::new(HashMap::new()));
59+
60+
if let Ok(existing_files) = crate::scan::scan_for_music(output_dir) {
61+
let mut metadata_map = used_metadata.lock().unwrap();
62+
for (path, metadata) in existing_files {
63+
if let Some(metadata_key) = create_metadata_key(&metadata) {
64+
metadata_map.insert(metadata_key, path);
65+
}
66+
}
67+
}
5768

5869
music_files.par_iter().for_each(|(source_path, metadata)| {
5970
if let Some(filename) = source_path.file_name() {
6071
pb.set_message(filename.to_string_lossy().to_string());
6172
}
6273

63-
match organize_single_file(source_path, metadata, output_dir, config, &used_paths) {
74+
match organize_single_file(source_path, metadata, output_dir, config, &used_metadata) {
6475
Ok(result) => match result {
6576
FileResult::Moved => {
6677
*moved.lock().unwrap() += 1;
@@ -120,7 +131,7 @@ fn organize_single_file(
120131
metadata: &AudioMetadata,
121132
output_dir: &PathBuf,
122133
config: &Config,
123-
used_paths: &Arc<Mutex<HashMap<PathBuf, PathBuf>>>,
134+
used_metadata: &Arc<Mutex<HashMap<MetadataKey, PathBuf>>>,
124135
) -> Result<FileResult, Box<dyn std::error::Error>> {
125136
let relative_path = match generate_target_path(source_path, metadata, config) {
126137
Some(path) => path,
@@ -136,19 +147,55 @@ fn organize_single_file(
136147
let target_path = output_dir.join(&relative_path);
137148

138149
let final_target_path = {
139-
let mut paths_map = used_paths.lock().unwrap();
140-
if paths_map.contains_key(&target_path) {
141-
match config.rules.handle_duplicates.as_str() {
142-
"skip" => {
143-
return Ok(FileResult::Duplicate);
150+
let metadata_key = create_metadata_key(metadata);
151+
let mut metadata_map = used_metadata.lock().unwrap();
152+
153+
if let Some(metadata_key) = metadata_key {
154+
if metadata_map.contains_key(&metadata_key) {
155+
match config.rules.handle_duplicates.as_str() {
156+
"skip" => {
157+
return Ok(FileResult::Duplicate);
158+
}
159+
"rename" => {
160+
handle_duplicate_rename(&target_path, &metadata_key, &mut metadata_map)
161+
}
162+
"overwrite" => {
163+
if let Some(old_path) = metadata_map.get(&metadata_key) {
164+
let _ = fs::remove_file(old_path);
165+
}
166+
metadata_map.insert(metadata_key, source_path.clone());
167+
target_path
168+
}
169+
_ => target_path,
144170
}
145-
"rename" => handle_duplicate_rename(&target_path, &mut paths_map),
146-
"overwrite" => target_path,
147-
_ => target_path,
171+
} else {
172+
metadata_map.insert(metadata_key, source_path.clone());
173+
target_path
148174
}
149175
} else {
150-
paths_map.insert(target_path.clone(), source_path.clone());
151-
target_path
176+
let fallback_key = MetadataKey {
177+
artist: "Unknown".to_string(),
178+
album: "Unknown".to_string(),
179+
title: source_path
180+
.file_stem()
181+
.unwrap_or_default()
182+
.to_string_lossy()
183+
.to_string(),
184+
track: None,
185+
};
186+
187+
if metadata_map.contains_key(&fallback_key) {
188+
match config.rules.handle_duplicates.as_str() {
189+
"skip" => return Ok(FileResult::Duplicate),
190+
"rename" => {
191+
handle_duplicate_rename(&target_path, &fallback_key, &mut metadata_map)
192+
}
193+
_ => target_path,
194+
}
195+
} else {
196+
metadata_map.insert(fallback_key, source_path.clone());
197+
target_path
198+
}
152199
}
153200
};
154201

@@ -324,7 +371,8 @@ fn sanitize_path(path: &str, config: &Config) -> String {
324371

325372
fn handle_duplicate_rename(
326373
target_path: &PathBuf,
327-
used_paths: &mut HashMap<PathBuf, PathBuf>,
374+
metadata_key: &MetadataKey,
375+
used_metadata: &mut HashMap<MetadataKey, PathBuf>,
328376
) -> PathBuf {
329377
let mut counter = 1;
330378
let stem = target_path
@@ -341,14 +389,36 @@ fn handle_duplicate_rename(
341389
let new_filename = format!("{} ({}){}", stem, counter, extension);
342390
let new_path = parent.join(new_filename);
343391

344-
if !used_paths.contains_key(&new_path) {
345-
used_paths.insert(new_path.clone(), target_path.clone());
392+
let mut new_metadata_key = metadata_key.clone();
393+
new_metadata_key.title = format!("{} ({})", metadata_key.title, counter);
394+
395+
if !used_metadata.contains_key(&new_metadata_key) {
396+
used_metadata.insert(new_metadata_key, new_path.clone());
346397
return new_path;
347398
}
348399
counter += 1;
349400
}
350401
}
351402

403+
fn create_metadata_key(metadata: &AudioMetadata) -> Option<MetadataKey> {
404+
let artist = if is_compilation(metadata) {
405+
metadata.artist.as_ref()
406+
} else {
407+
metadata.album_artist.as_ref().or(metadata.artist.as_ref())
408+
};
409+
410+
let artist = artist?;
411+
let album = metadata.album.as_ref()?;
412+
let title = metadata.title.as_ref()?;
413+
414+
Some(MetadataKey {
415+
artist: artist.clone(),
416+
album: album.clone(),
417+
title: title.clone(),
418+
track: metadata.track,
419+
})
420+
}
421+
352422
fn is_compilation(metadata: &AudioMetadata) -> bool {
353423
if let Some(album_artist) = &metadata.album_artist {
354424
album_artist.to_lowercase() == "various artists"

0 commit comments

Comments
 (0)