Compare commits

4 Commits

Author SHA1 Message Date
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
5 changed files with 92 additions and 10 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,30 @@ 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 it 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.
## ⌨️ Usage ## ⌨️ Usage
- `timbre init` - give Timbre a home (`~/Music/Timbre/`) - `timbre init` - give Timbre a home (`~/Music/Timbre/`)

View File

@@ -9,13 +9,12 @@ use std::{
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("http://catgpt-server:5000");
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,12 +78,12 @@ 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 flac_name = format!("track{track_num:02}.flac"); let wav_path = tmp_dir.join(format!("track{track_num:02}.wav"));
let flac_path = tmp_dir.join(&flac_name); 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(&flac_path) // 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");
@@ -94,6 +93,7 @@ pub fn adopt(timbre_dir: &Path) {
&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(&flac_path, timbre_dir); import::import_file(&flac_path, timbre_dir);

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,10 +1,12 @@
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 sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
@@ -67,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 {
@@ -257,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");
} }