ripping+tagging

This commit is contained in:
2026-08-24 16:07:14 -04:00
parent 99379a1faa
commit 7c82b582ce
7 changed files with 300 additions and 105 deletions

45
Cargo.lock generated
View File

@@ -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",

View File

@@ -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"

103
src/adopt.rs Normal file
View File

@@ -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"))),
&current_track.title,
&current_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!");
}

94
src/import.rs Normal file
View File

@@ -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());
}
}
}

View File

@@ -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);

View File

@@ -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<Release>,
pub struct DiscResponse {
pub releases: Vec<Release>,
}
#[derive(Deserialize, Debug)]
struct Release {
title: String,
media: Vec<Medium>,
pub struct Release {
pub title: String,
pub media: Vec<Medium>,
pub id: String,
}
#[derive(Deserialize, Debug)]
struct Medium {
tracks: Vec<Track>,
pub struct Medium {
pub tracks: Vec<Track>,
}
#[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<ArtistCredit>,
pub artist_credit: Vec<ArtistCredit>,
}
#[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
}

29
src/tagging.rs Normal file
View File

@@ -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");
}