From 7c82b582ce36215c3ef80273f135b45bca904f7b Mon Sep 17 00:00:00 2001 From: Toasterkitten Date: Mon, 24 Aug 2026 16:07:14 -0400 Subject: [PATCH] ripping+tagging --- Cargo.lock | 45 ++++++++++++++++++++ Cargo.toml | 1 + src/adopt.rs | 103 +++++++++++++++++++++++++++++++++++++++++++++ src/import.rs | 94 +++++++++++++++++++++++++++++++++++++++++ src/main.rs | 92 +++------------------------------------- src/musicbrainz.rs | 41 ++++++++++-------- src/tagging.rs | 29 +++++++++++++ 7 files changed, 300 insertions(+), 105 deletions(-) create mode 100644 src/adopt.rs create mode 100644 src/import.rs create mode 100644 src/tagging.rs diff --git a/Cargo.lock b/Cargo.lock index a1792bc..c663a1e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -64,6 +64,12 @@ version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + [[package]] name = "block-buffer" version = "0.12.1" @@ -258,6 +264,16 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "find-msvc-tools" version = "0.1.11" @@ -469,6 +485,12 @@ version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "litemap" version = "0.8.3" @@ -592,6 +614,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pty-process" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71cec9e2670207c5ebb9e477763c74436af3b9091dd550b9fb3c1bec7f3ea266" +dependencies = [ + "rustix", +] + [[package]] name = "quote" version = "1.0.47" @@ -615,6 +646,19 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + [[package]] name = "rustls" version = "0.23.43" @@ -791,6 +835,7 @@ dependencies = [ "clap", "hex", "lofty", + "pty-process", "serde", "serde_json", "sha1", diff --git a/Cargo.toml b/Cargo.toml index 183d95e..7009372 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ authors = ["Owen Feldman"] clap = { version = "4.6.6", features = ["derive"] } hex = "0.4.3" lofty = "0.25.1" +pty-process = "0.5.3" serde = { version = "1.0.229", features = ["derive"] } serde_json = "1.0.151" sha1 = "0.11.0" diff --git a/src/adopt.rs b/src/adopt.rs new file mode 100644 index 0000000..fb02bf2 --- /dev/null +++ b/src/adopt.rs @@ -0,0 +1,103 @@ +// i was gonna do a "filename hack" and name this file "adopte.rs" as in "adopters" but i would need to type +// adopte::adopt which is odd so i didn't + +use std::{ + fs::{create_dir_all, write}, + io::{Read, Write}, + path::Path, + process::Command, + time::{SystemTime, UNIX_EPOCH}, +}; + +use crate::{import, musicbrainz::get_disc_info, tagging}; + +// adopt = rip a disc but ripping sounds like destroying so i'm not using it +pub fn adopt(timbre_dir: &Path) { + let disc = get_disc_info("http://catgpt-server:5000"); + let release = &disc.releases[0]; + println!("{}", release.id); + let tmp_dir = timbre_dir.join(format!( + ".tmp/{}", + SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("time went backwards?? ¯\\_(ツ)_/¯\nquit trying to make me timber!\n") + .as_secs() + )); + create_dir_all(&tmp_dir).expect("i couldn't make my workspace :(\n"); + write(tmp_dir.join("README.txt"), "I'M DOING IT CHILLAX BRO\n") + .expect("couldn't even write my chillax note :( bro fix ur pc\n"); + // as of writing this line here i just donated $2.75 + transaction fees to Wikipedia! + + println!("adopting..."); + + let tracks = &release.media[0].tracks; + let tracks_len = tracks.len(); + + for track_num in 3..=tracks_len { + let file_name = format!("track{track_num:02}.wav"); + let current_track = &tracks[track_num - 1]; + + let (mut pty, pts) = pty_process::blocking::open().expect("couldn't open a pty :(\n"); + pty.resize(pty_process::Size::new(24, 80)) + .expect("couldn't size the pty :(\n"); + + let cmd = pty_process::blocking::Command::new("cdparanoia") + .arg("-e") + .arg(track_num.to_string()) + .arg(&tmp_dir.join(&file_name)); + + let mut child = cmd.spawn(pts).expect("cdparanoia wouldn't start :(\n"); + + let mut buf = [0u8; 512]; + let mut acc = String::new(); + loop { + let n = pty.read(&mut buf).unwrap_or(0); + if n == 0 { + // done + println!(); + break; + } + acc.push_str(&String::from_utf8_lossy(&buf[..n])); + + while let Some(pos) = acc.find('\r') { + let chunk: String = acc.drain(..=pos).collect(); + if chunk.contains("PROGRESS") { + let bar = chunk + .split_once("== PROGRESS == [") + .unwrap() + .1 + .split_once("|") + .unwrap() + .0; + print!( + "\r[{}] {track_num}/{} {}", + bar, tracks_len, current_track.title + ); + std::io::stdout().flush().unwrap(); + } + } + } + child.wait().expect("cdparanoia didn't finish cleanly :(\n"); + + Command::new("flac") + .arg("--best") // max compression, lossless + .arg(&tmp_dir.join(&file_name)) // input WAV + .output() + .expect("flac wouldn't encode :( is it installed?\n"); + + tagging::write_tags( + Path::new(&tmp_dir.join(format!("track{track_num:02}.flac"))), + ¤t_track.title, + ¤t_track.artist_credit[0].name, + &release.title, + track_num as u32, + ); + + import::import_file( + Path::new(&tmp_dir.join(format!("track{track_num:02}.flac"))), + &timbre_dir, + ); + } + + println!("done!"); +} diff --git a/src/import.rs b/src/import.rs new file mode 100644 index 0000000..9f2183a --- /dev/null +++ b/src/import.rs @@ -0,0 +1,94 @@ +use lofty::{prelude::*, probe::Probe}; +use std::{ + fs::{self, create_dir_all}, + path::Path, +}; + +use crate::{SafeName, sanitize}; + +pub fn import_file(file: &Path, timbre_dir: &Path) { + 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"); + return; + } + + 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"); + return; + }; + 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"); + return; + } + if dest.exists() { + println!("already there, skipping {} :(", file.display()); + return; + } + 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()); + } + } +} diff --git a/src/main.rs b/src/main.rs index ded2cc9..6410612 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,8 @@ +mod adopt; mod disc_id; +mod import; mod musicbrainz; - +mod tagging; use clap::{ Parser, Subcommand, builder::styling::{AnsiColor, Effects, Styles}, @@ -133,7 +135,6 @@ fn remove_empty_dirs(dir: &Path) { } } fn main() { - musicbrainz::get_disc_info("http://catgpt-server:5000"); let cat = include_bytes!("../cat_01.jpg"); // my kitty! let cli = Cli::parse(); @@ -141,7 +142,7 @@ fn main() { let timbre_dir = Path::new(&std::env::var("HOME").expect("NERD!!!")) .join("Music") .join("Timbre"); - + adopt::adopt(&timbre_dir); if cli.command != Commands::Init && cli.command != Commands::Repair { if timbre_dir.exists() { hashcat(&timbre_dir.join("cat_01.jpg")); // meow @@ -200,90 +201,7 @@ fn main() { 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()); - } - } + import::import_file(&file, &timbre_dir); } remove_empty_dirs(&import_dir); let _ = remove_dir(&import_dir); diff --git a/src/musicbrainz.rs b/src/musicbrainz.rs index 4aee1af..d747faf 100644 --- a/src/musicbrainz.rs +++ b/src/musicbrainz.rs @@ -1,32 +1,35 @@ +// if there's musicbrainz why no musicorganz + use crate::disc_id; use serde::Deserialize; #[derive(Deserialize, Debug)] -struct DiscResponse { - releases: Vec, +pub struct DiscResponse { + pub releases: Vec, } #[derive(Deserialize, Debug)] -struct Release { - title: String, - media: Vec, +pub struct Release { + pub title: String, + pub media: Vec, + pub id: String, } #[derive(Deserialize, Debug)] -struct Medium { - tracks: Vec, +pub struct Medium { + pub tracks: Vec, } #[derive(Deserialize, Debug)] -struct Track { - title: String, - position: u32, +pub struct Track { + pub title: String, + pub position: u32, #[serde(rename = "artist-credit")] - artist_credit: Vec, + pub artist_credit: Vec, } #[derive(Deserialize, Debug)] -struct ArtistCredit { - name: String, +pub struct ArtistCredit { + pub name: String, } -pub fn get_disc_info(base_url: &str) { +pub fn get_disc_info(base_url: &str) -> DiscResponse { let disc_id = disc_id::get_disc_id(); let url = format!("{base_url}/ws/2/discid/{disc_id}"); @@ -38,10 +41,12 @@ pub fn get_disc_info(base_url: &str) { "Timbre/0.1 ( https://gitea.owendeed.com/Toasterkitten/timbre )", ) .call() - .expect("idk\n") + .expect("i couldn't connect to the musicbrainz server\n¯\\_(ツ)_/¯\ncheck your internet or something?\n") .body_mut() .read_to_string() - .expect("?"); - let parsed: DiscResponse = serde_json::from_str(&body).unwrap(); - println!("{parsed:?}"); + .expect("musicbrainz isn't happy with you. ¯\\_(ツ)_/¯\n"); + let parsed: DiscResponse = serde_json::from_str(&body).expect( + "musicbrainz said something i don't understand\n¯\\_(ツ)_/¯\nmaybe ur disc is unique\n", + ); + parsed } diff --git a/src/tagging.rs b/src/tagging.rs new file mode 100644 index 0000000..fb2f031 --- /dev/null +++ b/src/tagging.rs @@ -0,0 +1,29 @@ +use lofty::{config::WriteOptions, prelude::*, probe::Probe, tag::Tag}; +use std::path::Path; + +pub fn write_tags(path: &Path, title: &str, artist: &str, album: &str, track_num: u32) { + let mut tagged_file = Probe::open(path) + .expect("couldn't open the flac :(\n") + .read() + .expect("couldn't read the flac :(\n"); + let tag = match tagged_file.primary_tag_mut() { + Some(t) => t, + None => { + if let Some(first_tag) = tagged_file.first_tag_mut() { + first_tag + } else { + let tag_type = tagged_file.primary_tag_type(); + tagged_file.insert_tag(Tag::new(tag_type)); + tagged_file.primary_tag_mut().unwrap() + } + } + }; + + tag.set_title(title.to_string()); + tag.set_artist(artist.to_string()); + tag.set_album(album.to_string()); + tag.set_track(track_num); + + tag.save_to_path(path, WriteOptions::default()) + .expect("couldn't save the tags :(\n"); +}