Compare commits

..

1 Commits

Author SHA1 Message Date
Connor Johnstone
a2152cbf8d Formatting 2026-03-18 15:36:42 -04:00
6 changed files with 55 additions and 34 deletions

View File

@@ -7,7 +7,10 @@ use shanty_db::Database;
use shanty_org::{DEFAULT_FORMAT, OrgConfig, organize_from_db, organize_from_directory}; use shanty_org::{DEFAULT_FORMAT, OrgConfig, organize_from_db, organize_from_directory};
#[derive(Parser)] #[derive(Parser)]
#[command(name = "shanty-org", about = "Organize music files into a clean directory structure")] #[command(
name = "shanty-org",
about = "Organize music files into a clean directory structure"
)]
struct Cli { struct Cli {
/// Source directory of music files (standalone mode, reads tags from files). /// Source directory of music files (standalone mode, reads tags from files).
#[arg(long)] #[arg(long)]

View File

@@ -51,9 +51,7 @@ impl TrackMetadata {
.unwrap_or("") .unwrap_or("")
.to_string(); .to_string();
let tagged_file = Probe::open(path)? let tagged_file = Probe::open(path)?.options(ParseOptions::default()).read()?;
.options(ParseOptions::default())
.read()?;
let tag = tagged_file let tag = tagged_file
.primary_tag() .primary_tag()

View File

@@ -56,10 +56,7 @@ fn resolve_target(target: &Path) -> PathBuf {
.file_stem() .file_stem()
.and_then(|s| s.to_str()) .and_then(|s| s.to_str())
.unwrap_or("file"); .unwrap_or("file");
let ext = target let ext = target.extension().and_then(|s| s.to_str()).unwrap_or("");
.extension()
.and_then(|s| s.to_str())
.unwrap_or("");
let parent = target.parent().unwrap_or(Path::new(".")); let parent = target.parent().unwrap_or(Path::new("."));
for i in 2..=999 { for i in 2..=999 {
@@ -235,10 +232,7 @@ pub async fn organize_from_db(
} }
/// Organize music files from a source directory (standalone, no database). /// Organize music files from a source directory (standalone, no database).
pub async fn organize_from_directory( pub async fn organize_from_directory(source_dir: &Path, config: &OrgConfig) -> OrgResult<OrgStats> {
source_dir: &Path,
config: &OrgConfig,
) -> OrgResult<OrgStats> {
let mut stats = OrgStats::default(); let mut stats = OrgStats::default();
let source_root = source_dir let source_root = source_dir
.canonicalize() .canonicalize()
@@ -344,12 +338,11 @@ fn cleanup_empty_dirs_recursive(root: &Path) {
if dir == root { if dir == root {
continue; continue;
} }
if let Ok(mut entries) = std::fs::read_dir(&dir) { if let Ok(mut entries) = std::fs::read_dir(&dir)
if entries.next().is_none() { && entries.next().is_none()
if std::fs::remove_dir(&dir).is_ok() { && std::fs::remove_dir(&dir).is_ok()
{
tracing::debug!(path = %dir.display(), "removed empty directory"); tracing::debug!(path = %dir.display(), "removed empty directory");
} }
} }
} }
}
}

View File

@@ -9,7 +9,9 @@ pub fn sanitize_component(s: &str) -> String {
.collect(); .collect();
// Trim leading/trailing dots and spaces (problematic on Windows and some Linux tools) // Trim leading/trailing dots and spaces (problematic on Windows and some Linux tools)
result = result.trim_matches(|c: char| c == '.' || c == ' ').to_string(); result = result
.trim_matches(|c: char| c == '.' || c == ' ')
.to_string();
// Collapse consecutive underscores // Collapse consecutive underscores
while result.contains("__") { while result.contains("__") {

View File

@@ -49,7 +49,7 @@ pub fn render(template: &str, meta: &TrackMetadata) -> String {
// Split template by "/" so we can sanitize each path component individually. // Split template by "/" so we can sanitize each path component individually.
// The extension is special — it's part of the filename, not sanitized separately. // The extension is special — it's part of the filename, not sanitized separately.
let result = template template
.replace("{artist}", &sanitize_component(artist)) .replace("{artist}", &sanitize_component(artist))
.replace("{album_artist}", &sanitize_component(album_artist)) .replace("{album_artist}", &sanitize_component(album_artist))
.replace("{album}", &sanitize_component(album)) .replace("{album}", &sanitize_component(album))
@@ -58,9 +58,7 @@ pub fn render(template: &str, meta: &TrackMetadata) -> String {
.replace("{disc_number}", &disc_number) .replace("{disc_number}", &disc_number)
.replace("{year}", &year) .replace("{year}", &year)
.replace("{genre}", &sanitize_component(genre)) .replace("{genre}", &sanitize_component(genre))
.replace("{ext}", ext); .replace("{ext}", ext)
result
} }
#[cfg(test)] #[cfg(test)]
@@ -85,14 +83,23 @@ mod tests {
fn test_render_default_format() { fn test_render_default_format() {
let meta = test_meta(); let meta = test_meta();
let result = render(DEFAULT_FORMAT, &meta); let result = render(DEFAULT_FORMAT, &meta);
assert_eq!(result, "Pink Floyd/The Dark Side of the Moon/03 - Time.flac"); assert_eq!(
result,
"Pink Floyd/The Dark Side of the Moon/03 - Time.flac"
);
} }
#[test] #[test]
fn test_render_custom_format() { fn test_render_custom_format() {
let meta = test_meta(); let meta = test_meta();
let result = render("{artist} - {album}/{disc_number}-{track_number} {title}.{ext}", &meta); let result = render(
assert_eq!(result, "Pink Floyd - The Dark Side of the Moon/1-03 Time.flac"); "{artist} - {album}/{disc_number}-{track_number} {title}.{ext}",
&meta,
);
assert_eq!(
result,
"Pink Floyd - The Dark Side of the Moon/1-03 Time.flac"
);
} }
#[test] #[test]
@@ -102,7 +109,10 @@ mod tests {
..Default::default() ..Default::default()
}; };
let result = render(DEFAULT_FORMAT, &meta); let result = render(DEFAULT_FORMAT, &meta);
assert_eq!(result, "Unknown Artist/Unknown Album/00 - Unknown Title.mp3"); assert_eq!(
result,
"Unknown Artist/Unknown Album/00 - Unknown Title.mp3"
);
} }
#[test] #[test]

View File

@@ -87,8 +87,16 @@ async fn test_organize_from_directory() {
let money_path = target let money_path = target
.path() .path()
.join("Pink Floyd/The Dark Side of the Moon/06 - Money.mp3"); .join("Pink Floyd/The Dark Side of the Moon/06 - Money.mp3");
assert!(time_path.exists(), "Time should exist at {}", time_path.display()); assert!(
assert!(money_path.exists(), "Money should exist at {}", money_path.display()); time_path.exists(),
"Time should exist at {}",
time_path.display()
);
assert!(
money_path.exists(),
"Money should exist at {}",
money_path.display()
);
// Source files should be gone (moved, not copied) // Source files should be gone (moved, not copied)
assert!(!source.path().join("song1.mp3").exists()); assert!(!source.path().join("song1.mp3").exists());
@@ -124,7 +132,12 @@ async fn test_organize_copy_mode() {
// Source should still exist (copy mode) // Source should still exist (copy mode)
assert!(source.path().join("song.mp3").exists()); assert!(source.path().join("song.mp3").exists());
// Target should exist too // Target should exist too
assert!(target.path().join("Pink Floyd/DSOTM/01 - Time.mp3").exists()); assert!(
target
.path()
.join("Pink Floyd/DSOTM/01 - Time.mp3")
.exists()
);
} }
#[tokio::test] #[tokio::test]
@@ -237,8 +250,10 @@ async fn test_organize_missing_metadata() {
assert_eq!(stats.files_organized, 1); assert_eq!(stats.files_organized, 1);
// Should use "Unknown" fallbacks // Should use "Unknown" fallbacks
assert!(target assert!(
target
.path() .path()
.join("Unknown Artist/Unknown Album/00 - Unknown Title.mp3") .join("Unknown Artist/Unknown Album/00 - Unknown Title.mp3")
.exists()); .exists()
);
} }