From 92da4cce7b9042ca98a939598846c4064d36d0dc Mon Sep 17 00:00:00 2001 From: Toasterkitten Date: Sun, 23 Aug 2026 10:45:14 -0400 Subject: [PATCH] importing music --- src/main.rs | 151 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 148 insertions(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index 329ff28..7d80de4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,9 +2,10 @@ use clap::{ Parser, Subcommand, builder::styling::{AnsiColor, Effects, Styles}, }; +use lofty::{file::TaggedFileExt, probe::Probe, tag::Accessor}; use sha2::{Digest, Sha256}; use std::{ - fs::{self, File, create_dir_all}, + fs::{self, File, create_dir_all, remove_dir}, io::{BufReader, Read}, path::{Path, PathBuf}, }; @@ -60,6 +61,8 @@ enum Commands { Scan, /// attempt to repair timbre's home, this won't restore lost tracks Repair, + /// import music files + Import, } fn is_audio(path: &Path) -> bool { @@ -94,7 +97,38 @@ fn scan(dir: &Path, out: &mut Vec) { } } } +enum SafeName { + Ok(String), + Empty, + Malicious, +} +fn sanitize(name: &str) -> SafeName { + let trimmed = name.trim(); + if trimmed == ".." || trimmed == "." { + return SafeName::Malicious; + } + + let cleaned = trimmed.replace(['/', '\\'], "-"); + let cleaned = cleaned.trim(); + + if cleaned.is_empty() { + SafeName::Empty + } else { + SafeName::Ok(cleaned.to_string()) + } +} +fn remove_empty_dirs(dir: &Path) { + if let Ok(entries) = fs::read_dir(dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + remove_empty_dirs(&path); + let _ = fs::remove_dir(&path); + } + } + } +} fn main() { let cat = include_bytes!("../cat_01.jpg"); // my kitty! @@ -119,7 +153,8 @@ fn main() { Commands::Init => { if !timbre_dir.exists() { println!("making a home..."); - create_dir_all(&timbre_dir).expect("wat"); + create_dir_all(&timbre_dir) + .expect("i couldn't create my house :( i don't know what to do :(\n"); fs::write(timbre_dir.join("cat_01.jpg"), cat) .expect("my cat! he can't come! what did you do!?!\n"); for dir in ["Albums", "Singles", "Unidentified"] { @@ -141,6 +176,115 @@ fn main() { println!("i found {} audio files :)", tracks.len()); } } + Commands::Import => { + let import_dir = timbre_dir.join("Import"); + if !import_dir.exists() { + create_dir_all(&import_dir) + .expect("i can't make the folder for you to put your music in :(\n"); + println!("i made an Import folder! put your music there and run this again."); + return; + } + + let mut to_import = Vec::new(); + scan(&import_dir, &mut to_import); + + if to_import.is_empty() { + println!("the Import folder is empty :( put some music in it first!"); + return; + } + + println!("found {} files to import!", to_import.len()); + + for file in to_import { + let tagged = Probe::open(&file) + .expect("i couldn't open this file :(\n") + .read() + .expect("i couldn't read this file's tags :(\n"); + match tagged.primary_tag() { + Some(tag) => { + let track = tag.track(); + let artist = match tag.artist() { + None => SafeName::Empty, + Some(a) => sanitize(&a), + }; + let album = match tag.album() { + None => None, + Some(a) => Some(sanitize(&a)), + }; + let title = match tag.title() { + None => SafeName::Empty, + Some(t) => sanitize(&t), + }; + + if matches!(artist, SafeName::Malicious) + || matches!(title, SafeName::Malicious) + || matches!(album, Some(SafeName::Malicious)) + { + println!( + "hey, help! i just found this file: {}\nit seems to be trying to do something bad and hurt your computer!\ni'm moving it to a Quarantine folder for you to check\nyou should probably check the source of it.\ntip: upload it to https://virustotal.com\n", + file.display() + ); + + create_dir_all(&timbre_dir.join("Quarantine")).expect("i can't create the Quarantine folder, could be something normal, potentially malware :(\nbe careful with the file! it's still in the Import folder.\ntip: upload it to https://virustotal.com\n"); + fs::rename(&file, &timbre_dir.join("Quarantine").join(file.file_name().expect("no filename??\n"))) + .expect("i couldn't move this file :(, still in the Import folder, be careful!\ntip: upload it to https://virustotal.com\n"); + continue; + } + + let (SafeName::Ok(artist), SafeName::Ok(title)) = (artist, title) else { + let unid = timbre_dir.join("Unidentified"); + fs::rename(&file, unid.join(file.file_name().unwrap())) + .expect("i couldn't move an Unidentified file to a folder :(\nis the folder missing? try timbre repair!\n"); + continue; + }; + let ext = file.extension().and_then(|e| e.to_str()).unwrap_or(""); + + let dest = match album { + Some(SafeName::Ok(album)) => { + let filename = match track { + Some(n) => format!("{:02} {}.{}", n, title, ext), + None => format!("{}.{}", title, ext), + }; + timbre_dir + .join("Albums") + .join(&artist) + .join(&album) + .join(filename) + } + _ => timbre_dir + .join("Singles") + .join(&artist) + .join(format!("{}.{}", title, ext)), + }; + + if !dest.starts_with(&timbre_dir) { + println!( + "hey, help! i just found this file: {}\nit seems to be trying to do something bad and hurt your computer!\ni'm moving it to a Quarantine folder for you to check\nyou should probably check the source of it.\ntip: upload it to https://virustotal.com\n", + file.display() + ); + + create_dir_all(&timbre_dir.join("Quarantine")).expect("i can't create the Quarantine folder, could be something normal, potentially malware :(\nbe careful with the file! it's still in the Import folder.\ntip: upload it to https://virustotal.com\n"); + fs::rename(&file, &timbre_dir.join("Quarantine").join(file.file_name().expect("no filename??\n"))) + .expect("i couldn't move this file :(, still in the Import folder, be careful!\ntip: upload it to https://virustotal.com\n"); + continue; + } + if dest.exists() { + println!("already there, skipping {} :(", file.display()); + continue; + } + create_dir_all(dest.parent().unwrap()) + .expect("can't create directory to put a file in :(\n"); + fs::rename(&file, &dest).expect("can't move file to directory :(\n"); + } + None => { + println!("no tags on {} :(", file.display()); + } + } + } + remove_empty_dirs(&import_dir); + let _ = remove_dir(&import_dir); + println!("all done! cleaned up the Import folder if it's empty :)"); + } Commands::Repair => { println!("repairing my home..."); let mut fixed_something = false; @@ -149,7 +293,8 @@ fn main() { println!( "it appears that my home is gone. your songs may have been deleted :(\nrebuilding my home..." ); - create_dir_all(&timbre_dir).expect("wat"); + create_dir_all(&timbre_dir) + .expect("i couldn't create my house :( i don't know what to do :(\n"); fs::write(timbre_dir.join("cat_01.jpg"), cat) .expect("my cat! he can't come! what did you do!?!\n"); for dir in ["Albums", "Singles", "Unidentified"] {