Initial commit

This commit is contained in:
Connor Johnstone
2026-03-17 18:22:20 -04:00
commit 3159ee51ad
11 changed files with 1110 additions and 0 deletions

244
tests/integration.rs Normal file
View File

@@ -0,0 +1,244 @@
use std::fs;
use std::io::Write;
use chrono::Utc;
use sea_orm::ActiveValue::Set;
use shanty_db::{Database, queries};
use shanty_org::{DEFAULT_FORMAT, OrgConfig, organize_from_db, organize_from_directory};
use tempfile::TempDir;
/// Create a minimal valid MP3 file with ID3v2 tags.
fn create_test_mp3(path: &std::path::Path, title: &str, artist: &str, album: &str, track: u32) {
use lofty::config::WriteOptions;
use lofty::tag::{Accessor, ItemKey, ItemValue, Tag, TagExt, TagItem, TagType};
// Write minimal valid MPEG frames
let frame_header: [u8; 4] = [0xFF, 0xFB, 0x90, 0x00];
let frame_size = 417;
let mut frame_data = vec![0u8; frame_size];
frame_data[..4].copy_from_slice(&frame_header);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).unwrap();
}
let mut file = fs::File::create(path).unwrap();
for _ in 0..10 {
file.write_all(&frame_data).unwrap();
}
drop(file);
let mut tag = Tag::new(TagType::Id3v2);
tag.set_title(title.to_string());
tag.set_artist(artist.to_string());
tag.set_album(album.to_string());
tag.set_track(track);
tag.insert(TagItem::new(
ItemKey::AlbumArtist,
ItemValue::Text(artist.to_string()),
));
tag.save_to_path(path, WriteOptions::default()).unwrap();
}
async fn test_db() -> Database {
Database::new("sqlite::memory:")
.await
.expect("failed to create test database")
}
#[tokio::test]
async fn test_organize_from_directory() {
let source = TempDir::new().unwrap();
let target = TempDir::new().unwrap();
create_test_mp3(
&source.path().join("song1.mp3"),
"Time",
"Pink Floyd",
"The Dark Side of the Moon",
3,
);
create_test_mp3(
&source.path().join("song2.mp3"),
"Money",
"Pink Floyd",
"The Dark Side of the Moon",
6,
);
let config = OrgConfig {
target_dir: target.path().to_owned(),
format: DEFAULT_FORMAT.to_string(),
dry_run: false,
copy: false,
};
let stats = organize_from_directory(source.path(), &config)
.await
.unwrap();
assert_eq!(stats.files_found, 2);
assert_eq!(stats.files_organized, 2);
assert_eq!(stats.files_errored, 0);
// Verify the organized structure
let time_path = target
.path()
.join("Pink Floyd/The Dark Side of the Moon/03 - Time.mp3");
let money_path = target
.path()
.join("Pink Floyd/The Dark Side of the Moon/06 - Money.mp3");
assert!(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)
assert!(!source.path().join("song1.mp3").exists());
assert!(!source.path().join("song2.mp3").exists());
}
#[tokio::test]
async fn test_organize_copy_mode() {
let source = TempDir::new().unwrap();
let target = TempDir::new().unwrap();
create_test_mp3(
&source.path().join("song.mp3"),
"Time",
"Pink Floyd",
"DSOTM",
1,
);
let config = OrgConfig {
target_dir: target.path().to_owned(),
format: DEFAULT_FORMAT.to_string(),
dry_run: false,
copy: true,
};
let stats = organize_from_directory(source.path(), &config)
.await
.unwrap();
assert_eq!(stats.files_organized, 1);
// Source should still exist (copy mode)
assert!(source.path().join("song.mp3").exists());
// Target should exist too
assert!(target.path().join("Pink Floyd/DSOTM/01 - Time.mp3").exists());
}
#[tokio::test]
async fn test_organize_dry_run() {
let source = TempDir::new().unwrap();
let target = TempDir::new().unwrap();
create_test_mp3(
&source.path().join("song.mp3"),
"Time",
"Pink Floyd",
"DSOTM",
1,
);
let config = OrgConfig {
target_dir: target.path().to_owned(),
format: DEFAULT_FORMAT.to_string(),
dry_run: true,
copy: false,
};
let stats = organize_from_directory(source.path(), &config)
.await
.unwrap();
assert_eq!(stats.files_organized, 1);
// Nothing should have actually moved
assert!(source.path().join("song.mp3").exists());
// Target dir should be empty
let entries: Vec<_> = fs::read_dir(target.path()).unwrap().collect();
assert!(entries.is_empty());
}
#[tokio::test]
async fn test_organize_from_db_updates_path() {
let db = test_db().await;
let source = TempDir::new().unwrap();
let target = TempDir::new().unwrap();
let source_path = source.path().join("song.mp3");
create_test_mp3(&source_path, "Time", "Pink Floyd", "DSOTM", 3);
// Insert track into DB
let now = Utc::now().naive_utc();
let active = shanty_db::entities::track::ActiveModel {
file_path: Set(source_path.to_string_lossy().to_string()),
title: Set(Some("Time".into())),
artist: Set(Some("Pink Floyd".into())),
album: Set(Some("DSOTM".into())),
album_artist: Set(Some("Pink Floyd".into())),
track_number: Set(Some(3)),
file_size: Set(source_path.metadata().unwrap().len() as i64),
added_at: Set(now),
updated_at: Set(now),
..Default::default()
};
let track = queries::tracks::upsert(db.conn(), active).await.unwrap();
let config = OrgConfig {
target_dir: target.path().to_owned(),
format: DEFAULT_FORMAT.to_string(),
dry_run: false,
copy: false,
};
let stats = organize_from_db(db.conn(), &config).await.unwrap();
assert_eq!(stats.files_organized, 1);
// Verify DB path was updated
let updated = queries::tracks::get_by_id(db.conn(), track.id)
.await
.unwrap();
let expected = target
.path()
.join("Pink Floyd/DSOTM/03 - Time.mp3")
.to_string_lossy()
.to_string();
assert_eq!(updated.file_path, expected);
}
#[tokio::test]
async fn test_organize_missing_metadata() {
let source = TempDir::new().unwrap();
let target = TempDir::new().unwrap();
// Create an MP3 with no tags (just valid audio frames)
let path = source.path().join("untagged.mp3");
let frame_header: [u8; 4] = [0xFF, 0xFB, 0x90, 0x00];
let mut frame_data = vec![0u8; 417];
frame_data[..4].copy_from_slice(&frame_header);
let mut file = fs::File::create(&path).unwrap();
for _ in 0..10 {
file.write_all(&frame_data).unwrap();
}
drop(file);
let config = OrgConfig {
target_dir: target.path().to_owned(),
format: DEFAULT_FORMAT.to_string(),
dry_run: false,
copy: false,
};
let stats = organize_from_directory(source.path(), &config)
.await
.unwrap();
assert_eq!(stats.files_organized, 1);
// Should use "Unknown" fallbacks
assert!(target
.path()
.join("Unknown Artist/Unknown Album/00 - Unknown Title.mp3")
.exists());
}