use std::fs::{create_dir_all, rename};
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use audiotags::Tag;
use walkdir::WalkDir;
fn build_unique_path(path: &Path) -> String {
if !path.exists() {
return path.to_string_lossy().to_string();
}
println!(
"Duplicate file, building unique path: {}",
path.to_string_lossy().to_string()
);
let stem = path.file_stem().unwrap().to_string_lossy().to_string();
let ext = path
.extension()
.map(|e| e.to_string_lossy().to_string())
.unwrap_or_default();
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let parent = path.parent().unwrap().to_string_lossy().to_string();
format!("{}/{}_{}.{}", parent, stem, timestamp, ext)
}
fn build_missing_tag_filename(
path: &Path,
artist: &Option<String>,
album: &Option<String>,
date: &Option<i32>,
track_num: &Option<u16>,
track_title: &Option<String>,
) -> String {
let stem = path.file_stem().unwrap().to_string_lossy().to_string();
let ext = path
.extension()
.map(|e| e.to_string_lossy().to_string())
.unwrap_or_default();
let mut filename = stem;
if album.is_none() {
filename.push_str("_ALBUMNAME");
}
if date.is_none() {
filename.push_str("_DATE");
}
if artist.is_none() {
filename.push_str("_ARTIST");
}
if track_num.is_none() {
filename.push_str("_TRACKNUM");
}
if track_title.is_none() {
filename.push_str("_TRACKTITLE");
}
format!("{}.{}", filename, ext)
}
fn main() {
let root = "/home/merlin/MusicTest";
let processed = format!("{}/PROCESSED", root);
for entry in WalkDir::new(root).into_iter().filter_map(|e| e.ok()) {
// Skip hidden directories (starting with .)
if entry.file_type().is_dir() {
if let Some(name) = entry.path().file_name()
&& entry.path().parent().unwrap().to_str().unwrap() == root
{
if name.to_string_lossy().starts_with('.') {
let target_path_str = format!("{}/{}", processed, entry.file_name().display());
let target_path = Path::new(&target_path_str);
let _ = rename(entry.path(), build_unique_path(target_path));
continue;
}
}
}
if entry.file_type().is_file() {
let ext = entry.path().extension().and_then(|s| s.to_str());
if ext == Some("m3u") {
// playlist file, move it to playlist folder
let target_path_str =
format!("{}/Playlists/{}", processed, entry.file_name().display());
let target_path = Path::new(&target_path_str);
let _ = create_dir_all(target_path.parent().unwrap());
let _ = rename(entry.path(), build_unique_path(target_path));
continue;
} else if ext != Some("flac") && ext != Some("mp3") {
println!("File not in good format: {}", entry.path().display());
let target_path_str =
format!("{}/FORMAT/{}", processed, entry.file_name().display());
let target_path = Path::new(&target_path_str);
let _ = create_dir_all(target_path.parent().unwrap());
let _ = rename(entry.path(), build_unique_path(target_path));
continue;
}
let tag = match Tag::new().read_from_path(entry.path()) {
Ok(t) => t,
Err(_) => {
println!("Could not process file {}", entry.path().display());
let target_path_str =
format!("{}/ERROR/{}", processed, entry.file_name().display());
let target_path = Path::new(&target_path_str);
let _ = create_dir_all(target_path.parent().unwrap());
let _ = rename(entry.path(), build_unique_path(target_path));
continue;
}
};
let artist = tag.album_artist();
let album = tag.album_title();
let date = tag.date();
let track_num = tag.track_number();
let track_title = tag.title();
if artist.is_none()
|| album.is_none()
|| date.is_none()
|| track_num.is_none()
|| track_title.is_none()
{
println!("missing tag! {}", entry.path().display());
let filename_with_tags = build_missing_tag_filename(
entry.path(),
&artist.map(|s| s.to_string()),
&album.map(|s| s.to_string()),
&date.map(|t| t.year),
&track_num,
&track_title.map(|s| s.to_string()),
);
let target_path_str = format!("{}/TAG/{}", processed, filename_with_tags);
let target_path = Path::new(&target_path_str);
let _ = create_dir_all(target_path.parent().unwrap());
let _ = rename(entry.path(), build_unique_path(target_path));
continue;
}
let newpath = format!(
"{}/{}/{} ({})/{:02} {}.{}",
processed,
artist.unwrap(),
album.unwrap(),
date.unwrap().year,
track_num.unwrap(),
track_title.unwrap(),
entry.path().extension().unwrap().display()
);
let target_path = Path::new(&newpath);
let _ = create_dir_all(target_path.parent().unwrap());
let _ = rename(entry.path(), build_unique_path(target_path));
}
}
let processed_path = Path::new(&processed);
if processed_path.exists() {
// Move all files from PROCESSED to root, preserving organization
for entry in WalkDir::new(&processed).into_iter().filter_map(|e| e.ok()) {
if entry.file_type().is_file() {
let relative_path = entry
.path()
.strip_prefix(processed_path)
.expect("Failed to get relative path");
let target_path = Path::new(root).join(relative_path);
let _ = create_dir_all(target_path.parent().unwrap());
let _ = std::fs::rename(entry.path(), &target_path);
}
}
// Remove all empty directories in root
for entry in WalkDir::new(root).into_iter().filter_map(|e| e.ok()) {
if entry.file_type().is_dir() {
let _ = std::fs::remove_dir(entry.path());
}
}
let _ = std::fs::remove_dir_all(processed_path);
}
}