Compare commits

10 Commits

Author SHA1 Message Date
029e192f6a bugfix 2026-09-05 18:24:58 -04:00
064b385687 update 2026-08-24 18:05:21 -04:00
81b4bed8eb update readme 2026-08-24 18:04:43 -04:00
f0a35c1aa2 emergency fix 2026-08-24 17:54:30 -04:00
cabcc5fd34 update readme 2026-08-24 17:47:25 -04:00
dcd1a233e8 update readme 2026-08-24 17:36:57 -04:00
6cf0820a1f disc adoption 2026-08-24 17:35:33 -04:00
6b45477a9d cover art 2026-08-24 16:46:42 -04:00
e6769164d6 more cleanup 2026-08-24 16:10:25 -04:00
81b8e72137 cleanup 2026-08-24 16:10:15 -04:00
5 changed files with 101 additions and 18 deletions

View File

@@ -3,8 +3,19 @@
**a music manager to finally solve this huge mess of music management.** **a music manager to finally solve this huge mess of music management.**
> ⚠️ **Timbre is in extreme alpha.** Right now it can create a library, import > ⚠️ **Timbre is in extreme alpha.** Right now it can create a library, import
> and organize your music, scan it, and repair itself. Everything else, CD > and organize your music, scan it, repair itself, and adopt (rip) CDs to
> ripping, automatic tagging, and more, is the planned future of the project. > tagged, lossless FLAC with cover art. More is planned. It works, but expect
> rough edges and breaking changes, so use it on music you have backed up
> elsewhere.
## 📑 Contents
- [Info](#-info)
- [Installation](#-installation)
- [Usage](#️-usage)
- [Building](#️-building)
- [The Cat](#-the-cat)
- [License](#-license)
## 📋 Info ## 📋 Info
@@ -16,6 +27,32 @@ features in one app. Rip your CDs (or bring your own media files!), auto-organiz
them, and play them back in your favorite music player, all with one person's them, and play them back in your favorite music player, all with one person's
opinion instead of tons, seamlessly working, in a fast Rust app. opinion instead of tons, seamlessly working, in a fast Rust app.
## 📦 Installation
You will need the following commands on your system for adopting a CD:
- `cdparanoia`
- `flac`
Here is some ways to get them depending on your system:
Arch-based systems
```
sudo pacman -S cdparanoia flac
```
Debian-based systems
```
sudo apt install cdparanoia flac
```
Fedora-based systems
```
sudo dnf install cdparanoia flac
```
Most are the same, you should be able to figure it out.
Afterwards, download the binary from the latest release and put it somewhere on path, usually `~/.local/bin/`.
Then run timbre init and get started.
Timbre is also in the AUR as the package [`timbre-bin`](https://aur.archlinux.org/packages/timbre-bin).
## ⌨️ Usage ## ⌨️ Usage
- `timbre init` - give Timbre a home (`~/Music/Timbre/`) - `timbre init` - give Timbre a home (`~/Music/Timbre/`)

View File

@@ -2,20 +2,19 @@
// adopte::adopt which is odd so i didn't // adopte::adopt which is odd so i didn't
use std::{ use std::{
fs::{create_dir_all, write}, fs::{create_dir_all, remove_dir_all, write},
io::{Read, Write}, io::{Read, Write},
path::Path, path::Path,
process::Command, process::Command,
time::{SystemTime, UNIX_EPOCH}, time::{SystemTime, UNIX_EPOCH},
}; };
use crate::{import, musicbrainz::get_disc_info, tagging}; use crate::{coverartarchive::fetch_cover_art, import, musicbrainz::get_disc_info, tagging};
// adopt = rip a disc but ripping sounds like destroying so i'm not using it // adopt = rip a disc but ripping sounds like destroying so i'm not using it
pub fn adopt(timbre_dir: &Path) { pub fn adopt(timbre_dir: &Path) {
let disc = get_disc_info("http://catgpt-server:5000"); let disc = get_disc_info("https://musicbrainz.org");
let release = &disc.releases[0]; let release = &disc.releases[0];
println!("{}", release.id);
let tmp_dir = timbre_dir.join(format!( let tmp_dir = timbre_dir.join(format!(
".tmp/{}", ".tmp/{}",
SystemTime::now() SystemTime::now()
@@ -79,25 +78,26 @@ pub fn adopt(timbre_dir: &Path) {
} }
child.wait().expect("cdparanoia didn't finish cleanly :(\n"); child.wait().expect("cdparanoia didn't finish cleanly :(\n");
let wav_path = tmp_dir.join(format!("track{track_num:02}.wav"));
let flac_path = tmp_dir.join(format!("track{track_num:02}.flac"));
Command::new("flac") Command::new("flac")
.arg("--best") // max compression, lossless .arg("--best") // max compression, lossless
.arg(&tmp_dir.join(&file_name)) // input WAV .arg(&wav_path) // input WAV
.output() .output()
.expect("flac wouldn't encode :( is it installed?\n"); .expect("flac wouldn't encode :( is it installed?\n");
tagging::write_tags( tagging::write_tags(
Path::new(&tmp_dir.join(format!("track{track_num:02}.flac"))), &flac_path,
&current_track.title, &current_track.title,
&current_track.artist_credit[0].name, &current_track.artist_credit[0].name,
&release.title, &release.title,
track_num as u32, track_num as u32,
fetch_cover_art(&release.id).as_deref(),
); );
import::import_file( import::import_file(&flac_path, timbre_dir);
Path::new(&tmp_dir.join(format!("track{track_num:02}.flac"))),
&timbre_dir,
);
} }
remove_dir_all(&tmp_dir).expect("couldn't clean up my mess :(\n");
println!("done!"); println!("done!");
} }

19
src/coverartarchive.rs Normal file
View File

@@ -0,0 +1,19 @@
use std::io::Read;
pub fn fetch_cover_art(release_id: &str) -> Option<Vec<u8>> {
let url = format!("https://coverartarchive.org/release/{}/front", release_id);
let response = ureq::get(&url)
.header("User-Agent", "Timbre/0.1 ( ...your repo... )")
.call();
match response {
Ok(resp) => {
let mut bytes = Vec::new();
resp.into_body()
.into_reader()
.read_to_end(&mut bytes)
.expect("i couldn't read the cover art :(\n");
Some(bytes)
}
Err(_) => None,
}
}

View File

@@ -1,13 +1,14 @@
mod adopt; mod adopt;
mod coverartarchive;
mod disc_id; mod disc_id;
mod import; mod import;
mod musicbrainz; mod musicbrainz;
mod tagging; mod tagging;
use clap::{ use clap::{
Parser, Subcommand, Command, Parser, Subcommand,
builder::styling::{AnsiColor, Effects, Styles}, builder::styling::{AnsiColor, Effects, Styles},
}; };
use lofty::{file::TaggedFileExt, probe::Probe, tag::Accessor};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use std::{ use std::{
fs::{self, File, create_dir_all, remove_dir}, fs::{self, File, create_dir_all, remove_dir},
@@ -68,6 +69,8 @@ enum Commands {
Repair, Repair,
/// import music files /// import music files
Import, Import,
/// Adopt a CD, import it into your library, also getting metadata
Adopt,
} }
fn is_audio(path: &Path) -> bool { fn is_audio(path: &Path) -> bool {
@@ -142,7 +145,7 @@ fn main() {
let timbre_dir = Path::new(&std::env::var("HOME").expect("NERD!!!")) let timbre_dir = Path::new(&std::env::var("HOME").expect("NERD!!!"))
.join("Music") .join("Music")
.join("Timbre"); .join("Timbre");
adopt::adopt(&timbre_dir);
if cli.command != Commands::Init && cli.command != Commands::Repair { if cli.command != Commands::Init && cli.command != Commands::Repair {
if timbre_dir.exists() { if timbre_dir.exists() {
hashcat(&timbre_dir.join("cat_01.jpg")); // meow hashcat(&timbre_dir.join("cat_01.jpg")); // meow
@@ -258,5 +261,8 @@ fn main() {
); );
} }
} }
Commands::Adopt => {
adopt::adopt(&timbre_dir);
}
} }
} }

View File

@@ -1,7 +1,20 @@
use lofty::{config::WriteOptions, prelude::*, probe::Probe, tag::Tag}; use lofty::{
config::WriteOptions,
picture::{MimeType, Picture, PictureType},
prelude::*,
probe::Probe,
tag::Tag,
};
use std::path::Path; use std::path::Path;
pub fn write_tags(path: &Path, title: &str, artist: &str, album: &str, track_num: u32) { pub fn write_tags(
path: &Path,
title: &str,
artist: &str,
album: &str,
track_num: u32,
cover: Option<&[u8]>,
) {
let mut tagged_file = Probe::open(path) let mut tagged_file = Probe::open(path)
.expect("couldn't open the flac :(\n") .expect("couldn't open the flac :(\n")
.read() .read()
@@ -24,6 +37,14 @@ pub fn write_tags(path: &Path, title: &str, artist: &str, album: &str, track_num
tag.set_album(album.to_string()); tag.set_album(album.to_string());
tag.set_track(track_num); tag.set_track(track_num);
if let Some(image_bytes) = cover {
let picture = Picture::unchecked(image_bytes.to_vec())
.pic_type(PictureType::CoverFront)
.mime_type(MimeType::Jpeg)
.build();
tag.set_picture(0, picture);
}
tag.save_to_path(path, WriteOptions::default()) tag.save_to_path(path, WriteOptions::default())
.expect("couldn't save the tags :(\n"); .expect("couldn't save the tags :(\n");
} }