commit efbc80c6bd270370108e9f918e82b3fef8421ebf
parent 3cafe8063709b35562005390c1a35e90798a7ff0
Author: mtmn <miro@haravara.org>
Date: Thu, 7 May 2026 22:07:13 +0200
chore: remove hakunadata
Diffstat:
17 files changed, 21 insertions(+), 994 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -473,21 +473,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
-name = "foreign-types"
-version = "0.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
-dependencies = [
- "foreign-types-shared",
-]
-
-[[package]]
-name = "foreign-types-shared"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
-
-[[package]]
name = "form_urlencoded"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -605,9 +590,9 @@ dependencies = [
[[package]]
name = "h2"
-version = "0.4.13"
+version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54"
+checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733"
dependencies = [
"atomic-waker",
"bytes",
@@ -623,22 +608,6 @@ dependencies = [
]
[[package]]
-name = "hakunadata"
-version = "0.1.0"
-dependencies = [
- "anyhow",
- "clap",
- "lofty",
- "reqwest",
- "serde",
- "serde_json",
- "tempfile",
- "tokio",
- "url",
- "walkdir",
-]
-
-[[package]]
name = "hashbrown"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -735,22 +704,6 @@ dependencies = [
]
[[package]]
-name = "hyper-tls"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
-dependencies = [
- "bytes",
- "http-body-util",
- "hyper",
- "hyper-util",
- "native-tls",
- "tokio",
- "tokio-native-tls",
- "tower-service",
-]
-
-[[package]]
name = "hyper-util"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -903,16 +856,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
[[package]]
-name = "iri-string"
-version = "0.7.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20"
-dependencies = [
- "memchr",
- "serde",
-]
-
-[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -985,9 +928,9 @@ dependencies = [
[[package]]
name = "js-sys"
-version = "0.3.97"
+version = "0.3.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf"
+checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08"
dependencies = [
"cfg-if",
"futures-util",
@@ -1041,15 +984,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
[[package]]
-name = "lock_api"
-version = "0.4.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
-dependencies = [
- "scopeguard",
-]
-
-[[package]]
name = "lofty"
version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1136,23 +1070,6 @@ dependencies = [
]
[[package]]
-name = "native-tls"
-version = "0.2.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2"
-dependencies = [
- "libc",
- "log",
- "openssl",
- "openssl-probe",
- "openssl-sys",
- "schannel",
- "security-framework",
- "security-framework-sys",
- "tempfile",
-]
-
-[[package]]
name = "natord"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1180,73 +1097,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
-name = "openssl"
-version = "0.10.78"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f38c4372413cdaaf3cc79dd92d29d7d9f5ab09b51b10dded508fb90bb70b9222"
-dependencies = [
- "bitflags",
- "cfg-if",
- "foreign-types",
- "libc",
- "once_cell",
- "openssl-macros",
- "openssl-sys",
-]
-
-[[package]]
-name = "openssl-macros"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
-
-[[package]]
name = "openssl-probe"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
[[package]]
-name = "openssl-sys"
-version = "0.9.114"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "13ce1245cd07fcc4cfdb438f7507b0c7e4f3849a69fd84d52374c66d83741bb6"
-dependencies = [
- "cc",
- "libc",
- "pkg-config",
- "vcpkg",
-]
-
-[[package]]
-name = "parking_lot"
-version = "0.12.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
-dependencies = [
- "lock_api",
- "parking_lot_core",
-]
-
-[[package]]
-name = "parking_lot_core"
-version = "0.9.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
-dependencies = [
- "cfg-if",
- "libc",
- "redox_syscall",
- "smallvec",
- "windows-link",
-]
-
-[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1265,12 +1121,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
-name = "pkg-config"
-version = "0.3.33"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
-
-[[package]]
name = "potential_utf"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1434,15 +1284,6 @@ dependencies = [
]
[[package]]
-name = "redox_syscall"
-version = "0.5.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
-dependencies = [
- "bitflags",
-]
-
-[[package]]
name = "regex"
version = "1.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1489,12 +1330,10 @@ dependencies = [
"http-body-util",
"hyper",
"hyper-rustls",
- "hyper-tls",
"hyper-util",
"js-sys",
"log",
"mime",
- "native-tls",
"percent-encoding",
"pin-project-lite",
"quinn",
@@ -1503,10 +1342,8 @@ dependencies = [
"rustls-platform-verifier",
"serde",
"serde_json",
- "serde_urlencoded",
"sync_wrapper",
"tokio",
- "tokio-native-tls",
"tokio-rustls",
"tower",
"tower-http",
@@ -1665,12 +1502,6 @@ dependencies = [
]
[[package]]
-name = "scopeguard"
-version = "1.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
-
-[[package]]
name = "security-framework"
version = "3.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1743,34 +1574,12 @@ dependencies = [
]
[[package]]
-name = "serde_urlencoded"
-version = "0.7.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
-dependencies = [
- "form_urlencoded",
- "itoa",
- "ryu",
- "serde",
-]
-
-[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
-name = "signal-hook-registry"
-version = "1.4.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
-dependencies = [
- "errno",
- "libc",
-]
-
-[[package]]
name = "simd-adler32"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1990,43 +1799,19 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
-version = "1.52.1"
+version = "1.52.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6"
+checksum = "110a78583f19d5cdb2c5ccf321d1290344e71313c6c37d43520d386027d18386"
dependencies = [
"bytes",
"libc",
"mio",
- "parking_lot",
"pin-project-lite",
- "signal-hook-registry",
"socket2",
- "tokio-macros",
"windows-sys 0.61.2",
]
[[package]]
-name = "tokio-macros"
-version = "2.7.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
-
-[[package]]
-name = "tokio-native-tls"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
-dependencies = [
- "native-tls",
- "tokio",
-]
-
-[[package]]
name = "tokio-rustls"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2066,20 +1851,20 @@ dependencies = [
[[package]]
name = "tower-http"
-version = "0.6.8"
+version = "0.6.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
+checksum = "68d6fdd9f81c2819c9a8b0e0cd91660e7746a8e6ea2ba7c6b2b057985f6bcb51"
dependencies = [
"bitflags",
"bytes",
"futures-util",
"http",
"http-body",
- "iri-string",
"pin-project-lite",
"tower",
"tower-layer",
"tower-service",
+ "url",
]
[[package]]
@@ -2168,12 +1953,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
-name = "vcpkg"
-version = "0.2.15"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
-
-[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2224,9 +2003,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen"
-version = "0.2.120"
+version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1"
+checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790"
dependencies = [
"cfg-if",
"once_cell",
@@ -2237,9 +2016,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
-version = "0.4.70"
+version = "0.4.71"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084"
+checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -2247,9 +2026,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
-version = "0.2.120"
+version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103"
+checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -2257,9 +2036,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
-version = "0.2.120"
+version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41"
+checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2"
dependencies = [
"bumpalo",
"proc-macro2",
@@ -2270,9 +2049,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
-version = "0.2.120"
+version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea"
+checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441"
dependencies = [
"unicode-ident",
]
@@ -2313,9 +2092,9 @@ dependencies = [
[[package]]
name = "web-sys"
-version = "0.3.97"
+version = "0.3.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602"
+checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa"
dependencies = [
"js-sys",
"wasm-bindgen",
diff --git a/Cargo.toml b/Cargo.toml
@@ -2,7 +2,6 @@
resolver = "2"
members = [
"diffamer",
- "hakunadata",
"lastfm_rs",
"mack",
]
diff --git a/MODULE.bazel b/MODULE.bazel
@@ -32,7 +32,6 @@ crate.from_cargo(
cargo_lockfile = "//:Cargo.lock",
manifests = [
"//:diffamer/Cargo.toml",
- "//:hakunadata/Cargo.toml",
"//:mack/Cargo.toml",
"//:lastfm_rs/Cargo.toml",
],
diff --git a/hakunadata/BUILD b/hakunadata/BUILD
@@ -1,7 +0,0 @@
-load("@crates//:defs.bzl", "all_crate_deps")
-load("//bazel:rust.bzl", "rust_app")
-
-rust_app(
- name = "hakunadata",
- deps = all_crate_deps(),
-)
diff --git a/hakunadata/Cargo.toml b/hakunadata/Cargo.toml
@@ -1,16 +0,0 @@
-[package]
-name = "hakunadata"
-version = "0.1.0"
-edition = "2024"
-
-[dependencies]
-anyhow = "1.0"
-clap = { version = "4.4", features = ["derive"] }
-lofty = "0.24.0"
-reqwest = { version = "0.13", features = ["json", "native-tls", "query"] }
-serde = { version = "1.0", features = ["derive"] }
-serde_json = "1.0"
-tempfile = "3.8"
-tokio = { version = "1.0", features = ["full", "macros", "rt-multi-thread"] }
-url = "2.5"
-walkdir = "2"
diff --git a/hakunadata/README.md b/hakunadata/README.md
@@ -1,51 +0,0 @@
-# hakunadata
-
-`hakunadata` is an opinionated tool that fetches music metadata (genres and labels) from Discogs or MusicBrainz and writes them to audio files.
-
-## Usage
-
-### Fetching metadata for an artist and album (proposal only)
-
-```bash
-hakunadata --artist 'The Beatles' --album 'Abbey Road'
-```
-
-### Reading tags from a file
-
-This will fetch metadata based on the artist and album tags in the file and show you what tags would be written, without actually changing the file.
-
-```bash
-hakunadata --read file.flac
-```
-
-### Writing tags to a file
-
-This will fetch metadata and write it to the file's tags.
-
-```bash
-hakunadata --write file.flac
-```
-
-### Processing a directory recursively
-
-You can also provide a directory path to process all supported audio files (`.flac`, `.mp3`, `.ogg`, `.m4a`) within that directory and its subdirectories.
-
-```bash
-# Show proposed tags for all files in a directory
-hakunadata --read /path/to/music
-
-# Write tags for all files in a directory
-hakunadata --write /path/to/music
-```
-
-## API Credentials
-
-The tool can use either Discogs or MusicBrainz.
-
-- **Discogs (Default, recommended):** For better results, especially for genre and label information, a Discogs token is recommended. You can get one from your Discogs developer settings.
-
- ```bash
- export DISCOGS_TOKEN="your_discogs_token_here"
- ```
-
-- **MusicBrainz (Fallback):** If the `DISCOGS_TOKEN` environment variable is not set, the tool will fall back to using the MusicBrainz API, which does not require authentication. Results might be less detailed.
diff --git a/hakunadata/src/context.rs b/hakunadata/src/context.rs
@@ -1,7 +0,0 @@
-use crate::fetchers::discogs::DiscogsClient;
-use crate::fetchers::musicbrainz::MusicBrainzClient;
-
-pub struct AppContext {
- pub mb_client: Option<MusicBrainzClient>,
- pub discogs_client: Option<DiscogsClient>,
-}
diff --git a/hakunadata/src/fetchers/discogs.rs b/hakunadata/src/fetchers/discogs.rs
@@ -1,82 +0,0 @@
-use anyhow::{Context, Result};
-use reqwest::header;
-use serde::Deserialize;
-
-const USER_AGENT: &str = "hakunadata/0.1.0 ( miro@haravara.org )";
-
-pub struct DiscogsClient {
- client: reqwest::Client,
- token: Option<String>,
-}
-
-impl DiscogsClient {
- pub fn new() -> Result<Self> {
- let mut headers = header::HeaderMap::new();
- headers.insert(
- header::USER_AGENT,
- header::HeaderValue::from_static(USER_AGENT),
- );
-
- let client = reqwest::Client::builder()
- .default_headers(headers)
- .build()?;
-
- let token = std::env::var("DISCOGS_TOKEN").ok();
-
- Ok(Self { client, token })
- }
-
- pub async fn fetch_metadata(
- &self,
- artist: &str,
- release: &str,
- ) -> Result<Option<DiscogsResult>> {
- let url = "https://api.discogs.com/database/search";
- let mut query = vec![
- ("type", "release"),
- ("artist", artist),
- ("release_title", release),
- ];
-
- let token_string;
- if let Some(t) = &self.token {
- token_string = t.clone();
- query.push(("token", &token_string));
- }
-
- let response = self
- .client
- .get(url)
- .query(&query)
- .send()
- .await
- .context("Failed to send Discogs request")?;
-
- if !response.status().is_success() {
- if response.status() == reqwest::StatusCode::UNAUTHORIZED {
- use std::sync::atomic::{AtomicBool, Ordering};
- static WARNED: AtomicBool = AtomicBool::new(false);
- if !WARNED.swap(true, Ordering::Relaxed) {
- eprintln!("DISCOGS_TOKEN is required");
- }
- }
- return Ok(None);
- }
-
- let search_result: DiscogsSearchResponse = response.json().await?;
-
- Ok(search_result.results.into_iter().next())
- }
-}
-
-#[derive(Deserialize, Debug, Clone)]
-pub struct DiscogsSearchResponse {
- pub results: Vec<DiscogsResult>,
-}
-
-#[derive(Deserialize, Debug, Clone)]
-pub struct DiscogsResult {
- pub genre: Option<Vec<String>>,
- pub style: Option<Vec<String>>,
- pub label: Option<Vec<String>>,
-}
diff --git a/hakunadata/src/fetchers/mod.rs b/hakunadata/src/fetchers/mod.rs
@@ -1,2 +0,0 @@
-pub mod discogs;
-pub mod musicbrainz;
diff --git a/hakunadata/src/fetchers/musicbrainz.rs b/hakunadata/src/fetchers/musicbrainz.rs
@@ -1,134 +0,0 @@
-use anyhow::{Context, Result};
-use reqwest::header;
-use serde::Deserialize;
-
-const USER_AGENT: &str = "hakunadata/0.1.0 ( miro@haravara.org )";
-
-pub struct MusicBrainzClient {
- client: reqwest::Client,
-}
-
-impl MusicBrainzClient {
- pub fn new() -> Result<Self> {
- let mut headers = header::HeaderMap::new();
- headers.insert(
- header::USER_AGENT,
- header::HeaderValue::from_static(USER_AGENT),
- );
-
- let client = reqwest::Client::builder()
- .default_headers(headers)
- .build()?;
-
- Ok(Self { client })
- }
-
- pub async fn fetch_genres(&self, artist: &str, release: &str) -> Result<Vec<String>> {
- // First search for the release group to get a broader set of tags, or specific release.
- // Let's try searching for "release" first as it is more specific, but release-group often has the tags.
- // Actually, searching for release-group is usually better for genres as they adhere to the abstract album.
-
- let query = format!("artist:\"{artist}\" AND release:\"{release}\"");
- let url = "https://musicbrainz.org/ws/2/release";
-
- let response = self
- .client
- .get(url)
- .query(&[
- ("query", query.as_str()),
- ("fmt", "json"),
- ("limit", "1"), // Just take the best match
- ])
- .send()
- .await
- .context("Failed to send MusicBrainz request")?;
-
- if !response.status().is_success() {
- // It's okay if we don't find it, but we should log it?
- // For now just return empty.
- return Ok(vec![]);
- }
-
- let search_result: MbSearchResponse = response.json().await?;
-
- // If we found a release, let's fetch its group to get tags, OR if the release itself has tags (usually not in search result).
- // The search result usually contains some info. But to get tags we might need to look up.
- // Wait, the search result for 'release' DOES return a list of releases.
- // If we want tags, we often need to include 'tags' in an 'inc' parameter for a direct lookup, but for search we get what we get.
- // Actually, MB search API returns tags if present? Usually not.
- // Better flow:
- // 1. Search for release-group (or release).
- // 2. Get ID.
- // 3. Lookup release-group with inc=tags.
-
- if let Some(release_match) = search_result.releases.first() {
- // If we have a release-group ID, use that.
- if let Some(rg) = &release_match.release_group {
- return self.lookup_release_group_tags(&rg.id).await;
- }
- }
-
- Ok(vec![])
- }
-
- async fn lookup_release_group_tags(&self, id: &str) -> Result<Vec<String>> {
- let url = format!("https://musicbrainz.org/ws/2/release-group/{id}");
- let response = self
- .client
- .get(&url)
- .query(&[("fmt", "json"), ("inc", "tags+genres")])
- .send()
- .await?;
-
- if !response.status().is_success() {
- return Ok(vec![]);
- }
-
- let rg: MbReleaseGroup = response.json().await?;
- let mut genres = Vec::new();
-
- if let Some(tags) = rg.tags {
- for tag in tags {
- // Filter out garbage tags? maybe later.
- genres.push(tag.name);
- }
- }
- if let Some(genres_list) = rg.genres {
- for g in genres_list {
- genres.push(g.name);
- }
- }
-
- Ok(genres)
- }
-}
-
-// --- Serde Structs ---
-
-#[derive(Deserialize, Debug)]
-struct MbSearchResponse {
- #[serde(default)]
- releases: Vec<MbRelease>,
-}
-
-#[derive(Deserialize, Debug)]
-struct MbRelease {
- #[serde(rename = "release-group")]
- release_group: Option<MbReleaseGroupRef>,
-}
-
-#[derive(Deserialize, Debug)]
-struct MbReleaseGroupRef {
- id: String,
-}
-
-#[derive(Deserialize, Debug)]
-struct MbReleaseGroup {
- tags: Option<Vec<MbTag>>,
- genres: Option<Vec<MbTag>>,
-}
-
-#[derive(Deserialize, Debug)]
-struct MbTag {
- name: String,
-}
diff --git a/hakunadata/src/main.rs b/hakunadata/src/main.rs
@@ -1,121 +0,0 @@
-mod context;
-mod fetchers;
-mod metadata;
-mod tagging;
-
-use anyhow::{Context, Result};
-use clap::Parser;
-use context::AppContext;
-use fetchers::discogs::DiscogsClient;
-use fetchers::musicbrainz::MusicBrainzClient;
-use metadata::fetch::process_query;
-use std::path::Path;
-use tagging::{print_metadata, process_file};
-use walkdir::WalkDir;
-
-static EXAMPLES: &str = r"EXAMPLES:
- Fetch metadata for an artist and album:
- hakunadata --artist 'Djrum' --album 'Under Tangled Silence'
-
- Preview tags for a single file:
- hakunadata --read file.mp3
-
- Write tags to a single file:
- hakunadata --write file.mp3
-
- Show proposed tags for all files in a directory:
- hakunadata --read /path/to/music
-
- Write tags to all files in a directory:
- hakunadata --write /path/to/music";
-
-#[derive(Parser, Debug)]
-#[command(
- author,
- version,
- about,
- long_about = None,
- after_help = EXAMPLES
-)]
-struct Args {
- /// Artist name
- #[arg(long)]
- artist: Option<String>,
-
- /// Album name
- #[arg(long)]
- album: Option<String>,
-
- /// File(s) to process
- #[arg(required_unless_present_any = ["artist", "album"])]
- files: Vec<String>,
-
- /// Write tags to file
- #[arg(short, long, default_value_t = false)]
- write: bool,
-
- /// Read tags from file and show what would be written
- #[arg(short, long, default_value_t = true)]
- read: bool,
-}
-
-#[tokio::main]
-async fn main() -> Result<()> {
- let args = Args::parse();
-
- let (discogs_client, mb_client) = if std::env::var("DISCOGS_TOKEN").is_ok() {
- (
- Some(DiscogsClient::new().context("Failed to init Discogs client")?),
- None,
- )
- } else {
- (
- None,
- Some(MusicBrainzClient::new().context("Failed to init MusicBrainz client")?),
- )
- };
-
- let ctx = AppContext {
- mb_client,
- discogs_client,
- };
-
- if !args.files.is_empty() {
- for path_str in args.files {
- let path = Path::new(&path_str);
- if !path.exists() {
- eprintln!("Path does not exist: {}", path.display());
- continue;
- }
-
- let mut files_to_process = Vec::new();
- if path.is_dir() {
- // Fix redundant_closure: |e| e.ok() -> Result::ok
- for entry in WalkDir::new(path).into_iter().filter_map(Result::ok) {
- if entry.file_type().is_file()
- && entry
- .path()
- .extension()
- .and_then(|s| s.to_str())
- .is_some_and(|ext| matches!(ext, "flac" | "mp3" | "ogg" | "m4a"))
- {
- files_to_process.push(entry.path().to_path_buf());
- }
- }
- } else {
- files_to_process.push(path.to_path_buf());
- }
-
- for file_path in files_to_process {
- if let Err(e) = process_file(&ctx, &file_path, args.read, args.write).await {
- eprintln!("Failed to process file {}: {e:?}", file_path.display());
- }
- }
- }
- } else if let (Some(artist), Some(album)) = (args.artist, args.album) {
- let result = process_query(&ctx, &artist, &album).await?;
- print_metadata(&result);
- }
-
- Ok(())
-}
diff --git a/hakunadata/src/metadata/fetch.rs b/hakunadata/src/metadata/fetch.rs
@@ -1,74 +0,0 @@
-use crate::context::AppContext;
-use crate::metadata::{genres, labels, subgenres};
-use anyhow::Result;
-
-pub struct FetchedMetadata {
- pub genres: Vec<String>,
- pub subgenres: Vec<String>,
- pub labels: Vec<String>,
-}
-
-pub async fn process_query(ctx: &AppContext, artist: &str, album: &str) -> Result<FetchedMetadata> {
- // Run fetchers concurrently (or rather, run whichever is enabled)
- let discogs_future = async {
- if let Some(client) = &ctx.discogs_client {
- client.fetch_metadata(artist, album).await
- } else {
- Ok(None)
- }
- };
-
- let mb_future = async {
- if let Some(client) = &ctx.mb_client {
- client.fetch_genres(artist, album).await
- } else {
- Ok(vec![])
- }
- };
-
- let (discogs_res, mb_res) = tokio::join!(discogs_future, mb_future);
-
- let mut genres = std::collections::HashSet::new();
- let mut subgenres = std::collections::HashSet::new();
- let mut labels = std::collections::HashSet::new();
-
- // Process Discogs
- if let Ok(Some(data)) = discogs_res {
- let g = genres::process(&data);
- for item in g {
- genres.insert(item);
- }
-
- let s = subgenres::process(&data);
- for item in s {
- subgenres.insert(item);
- }
-
- let l = labels::process(&data);
- for item in l {
- labels.insert(item);
- }
- }
-
- // Process MusicBrainz
- if let Ok(mb_genres) = mb_res {
- for g in mb_genres {
- genres.insert(g);
- }
- }
-
- let mut sorted_genres: Vec<_> = genres.into_iter().collect();
- sorted_genres.sort();
-
- let mut sorted_subgenres: Vec<_> = subgenres.into_iter().collect();
- sorted_subgenres.sort();
-
- let mut sorted_labels: Vec<_> = labels.into_iter().collect();
- sorted_labels.sort();
-
- Ok(FetchedMetadata {
- genres: sorted_genres,
- subgenres: sorted_subgenres,
- labels: sorted_labels,
- })
-}
diff --git a/hakunadata/src/metadata/genres.rs b/hakunadata/src/metadata/genres.rs
@@ -1,16 +0,0 @@
-use crate::fetchers::discogs::DiscogsResult;
-use std::collections::HashSet;
-
-pub fn process(result: &DiscogsResult) -> Vec<String> {
- let mut genres = HashSet::<String>::new();
-
- if let Some(g) = &result.genre {
- for g_item in g {
- genres.insert(g_item.clone());
- }
- }
-
- let mut sorted_genres: Vec<_> = genres.into_iter().collect();
- sorted_genres.sort();
- sorted_genres
-}
diff --git a/hakunadata/src/metadata/labels.rs b/hakunadata/src/metadata/labels.rs
@@ -1,16 +0,0 @@
-use crate::fetchers::discogs::DiscogsResult;
-use std::collections::HashSet;
-
-pub fn process(result: &DiscogsResult) -> Vec<String> {
- let mut labels = HashSet::<String>::new();
-
- if let Some(l) = &result.label {
- for label in l {
- labels.insert(label.clone());
- }
- }
-
- let mut sorted_labels: Vec<_> = labels.into_iter().collect();
- sorted_labels.sort();
- sorted_labels
-}
diff --git a/hakunadata/src/metadata/mod.rs b/hakunadata/src/metadata/mod.rs
@@ -1,4 +0,0 @@
-pub mod fetch;
-pub mod genres;
-pub mod labels;
-pub mod subgenres;
diff --git a/hakunadata/src/metadata/subgenres.rs b/hakunadata/src/metadata/subgenres.rs
@@ -1,16 +0,0 @@
-use crate::fetchers::discogs::DiscogsResult;
-use std::collections::HashSet;
-
-pub fn process(result: &DiscogsResult) -> Vec<String> {
- let mut subgenres = HashSet::<String>::new();
-
- if let Some(s) = &result.style {
- for style in s {
- subgenres.insert(style.clone());
- }
- }
-
- let mut sorted_subgenres: Vec<_> = subgenres.into_iter().collect();
- sorted_subgenres.sort();
- sorted_subgenres
-}
diff --git a/hakunadata/src/tagging.rs b/hakunadata/src/tagging.rs
@@ -1,204 +0,0 @@
-use crate::context::AppContext;
-use crate::metadata::fetch::{process_query, FetchedMetadata};
-use anyhow::{Context, Result};
-use lofty::config::WriteOptions;
-use lofty::prelude::*;
-use lofty::probe::Probe;
-use lofty::tag::{ItemKey, Tag};
-use std::fs::File;
-use std::path::{Component, Path};
-
-/// Process a music file to read or write metadata tags
-pub async fn process_file(ctx: &AppContext, path: &Path, read: bool, write: bool) -> Result<()> {
- if !read && !write {
- return Ok(());
- }
-
- // Normalize the path to remove relative components like './' and '../'
- let normalized_path = normalize_path(path);
- let abs_path = std::env::current_dir().map_or_else(
- |_| normalized_path.clone(),
- |cwd| cwd.join(&normalized_path),
- );
-
- let path_display = abs_path.display();
-
- // Check if file exists
- if !abs_path.exists() {
- return Err(anyhow::anyhow!("File does not exist: {path_display}"));
- }
-
- // Check if file is empty
- let metadata = abs_path
- .metadata()
- .with_context(|| format!("Failed to read metadata for: {path_display}"))?;
-
- if metadata.len() == 0 {
- eprintln!("Warning: File is empty, skipping: {path_display}");
- return Ok(());
- }
-
- // Read the file and extract current metadata
- let (mut tagged_file, artist, album) = {
- // Use the Probe API which can read from readers
- let file = File::open(&abs_path)
- .with_context(|| format!("Failed to open file: {path_display}"))?;
- let mut probe = Probe::new(file);
-
- // Hint the file type based on extension if possible
- if let Some(file_type) = path
- .extension()
- .and_then(|e| e.to_str())
- .and_then(lofty::file::FileType::from_ext)
- {
- probe = probe.set_file_type(file_type);
- }
-
- let mut tagged_file = probe
- .read()
- .with_context(|| format!("Failed to read tags from {path_display}"))?;
-
- let tag = tagged_file
- .primary_tag_mut()
- .context("No primary tag found")?;
-
- let artist = tag.artist().context("Artist not found")?.to_string();
- let album = tag.album().context("Album not found")?.to_string();
-
- (tagged_file, artist, album)
- }; // file handle goes out of scope here
-
- println!("Processing: {artist} - {album}");
-
- let result = process_query(ctx, &artist, &album).await?;
-
- if read {
- print_proposed_tags(&result);
- }
-
- if write {
- // Get mutable reference to tag for writing
- let tag = tagged_file
- .primary_tag_mut()
- .context("No primary tag found")?;
- write_tags(tag, &result);
-
- // Create a backup of the original file before modifying it to prevent data loss on failure
- let backup_path = abs_path.with_extension(format!(
- "backup.{}",
- abs_path
- .extension()
- .and_then(|s| s.to_str())
- .unwrap_or("bak")
- ));
-
- // Copy the original file to backup
- std::fs::copy(&abs_path, &backup_path)
- .with_context(|| format!("Failed to create backup file: {}", backup_path.display()))?;
-
- // Attempt to save the modified file to the original location
- let save_result = tagged_file.save_to_path(&abs_path, WriteOptions::default());
-
- match save_result {
- Ok(()) => {
- // Success: remove the backup file
- let _ = std::fs::remove_file(&backup_path); // Ignore errors when removing backup
- }
- Err(e) => {
- // Failure: restore from backup before returning the error
- if std::path::Path::exists(&backup_path) {
- if let Err(restore_err) = std::fs::copy(&backup_path, &abs_path) {
- eprintln!(
- "ERROR: Failed to restore from backup after write failure: {restore_err}",
- );
- eprintln!(
- "WARNING: Original file may be corrupted. Backup preserved at: {}",
- backup_path.display()
- );
- return Err(anyhow::anyhow!(
- "Also failed to restore from backup: {restore_err}"
- )
- .context(e));
- }
- eprintln!("Restored file from backup after write failure");
- let _ = std::fs::remove_file(&backup_path); // Clean up backup after successful restore
- }
- return Err(anyhow::anyhow!("Failed to write tags to file").context(e));
- }
- }
-
- println!("Tags written to {path_display}");
- }
-
- Ok(())
-}
-
-/// Helper function to normalize a path by removing relative components
-fn normalize_path(path: &Path) -> std::path::PathBuf {
- let mut result = std::path::PathBuf::new();
-
- for component in path.components() {
- match component {
- Component::Prefix(prefix_component) => result.push(prefix_component.as_os_str()),
- Component::RootDir => result.push("/"),
- Component::CurDir => {} // Skip './'
- Component::ParentDir => {
- result.pop(); // Handle '../' by popping the last component
- }
- Component::Normal(os_string) => result.push(os_string),
- }
- }
-
- result
-}
-
-/// Print the proposed tags to stdout
-fn print_proposed_tags(metadata: &FetchedMetadata) {
- println!("Proposed tags:");
- let genre_str = metadata.genres.join("/");
- println!(" Genre: {genre_str}");
-
- if let Some(label) = metadata.labels.first() {
- println!(" Label: {label}");
- }
-}
-
-/// Write metadata to the tag
-fn write_tags(tag: &mut Tag, metadata: &FetchedMetadata) {
- let genre_str = metadata.genres.join("/");
- tag.insert_text(ItemKey::Genre, genre_str);
-
- if let Some(label) = metadata.labels.first() {
- tag.insert_text(ItemKey::Label, label.clone());
- }
-}
-
-/// Print metadata to stdout
-pub fn print_metadata(result: &FetchedMetadata) {
- if result.genres.is_empty() {
- println!("Genres: (none)");
- } else {
- println!("Genres:");
- for genre in &result.genres {
- println!(" {genre}");
- }
- }
-
- if result.subgenres.is_empty() {
- println!("Subgenres: (none)");
- } else {
- println!("Subgenres:");
- for subgenre in &result.subgenres {
- println!(" {subgenre}");
- }
- }
-
- if result.labels.is_empty() {
- println!("Label: (none)");
- } else {
- println!("Label:");
- for label in &result.labels {
- println!(" {label}");
- }
- }
-}