From b29511c23e0c1c43af306e84d6fd455a8f95cbd0 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Fri, 7 Mar 2025 23:49:08 +0200 Subject: [PATCH] read id3 tags --- Cargo.lock | 87 ++------------------------ Cargo.toml | 6 +- shell.nix | 35 +++-------- src/main.rs | 121 ++----------------------------------- src/model.rs | 168 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/view.rs | 106 +++++++++++++++++++------------- 6 files changed, 251 insertions(+), 272 deletions(-) create mode 100644 src/model.rs diff --git a/Cargo.lock b/Cargo.lock index e617395..0f1f71b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -109,12 +109,6 @@ dependencies = [ "windows-targets", ] -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - [[package]] name = "better-panic" version = "0.3.0" @@ -131,15 +125,6 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - [[package]] name = "bumpalo" version = "3.17.0" @@ -253,15 +238,6 @@ dependencies = [ "unicode-segmentation", ] -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - [[package]] name = "crc32fast" version = "1.4.2" @@ -302,16 +278,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - [[package]] name = "darling" version = "0.20.10" @@ -347,16 +313,6 @@ dependencies = [ "syn", ] -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - [[package]] name = "either" version = "1.14.0" @@ -422,16 +378,6 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - [[package]] name = "getrandom" version = "0.2.15" @@ -947,17 +893,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sha2" -version = "0.10.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - [[package]] name = "signal-hook" version = "0.3.17" @@ -1049,14 +984,12 @@ dependencies = [ name = "taggart" version = "0.1.0" dependencies = [ - "base64", "clap", "file_type", "hex", "id3", "moku", "pad", - "sha2", "tek_tui", "walkdir", "xxhash-rust", @@ -1065,7 +998,7 @@ dependencies = [ [[package]] name = "tek_edn" version = "0.1.0" -source = "git+https://codeberg.org/unspeaker/tengri#5352a9d5484198e35760d62a2cd6ef202990fb2c" +source = "git+https://codeberg.org/unspeaker/tengri?rev=6cd85ef#6cd85efe503135cc5a20da9366ff81a0bbe5f56c" dependencies = [ "itertools 0.14.0", "konst", @@ -1075,7 +1008,7 @@ dependencies = [ [[package]] name = "tek_input" version = "0.2.0" -source = "git+https://codeberg.org/unspeaker/tengri#5352a9d5484198e35760d62a2cd6ef202990fb2c" +source = "git+https://codeberg.org/unspeaker/tengri?rev=6cd85ef#6cd85efe503135cc5a20da9366ff81a0bbe5f56c" dependencies = [ "tek_edn", ] @@ -1083,7 +1016,7 @@ dependencies = [ [[package]] name = "tek_output" version = "0.2.0" -source = "git+https://codeberg.org/unspeaker/tengri#5352a9d5484198e35760d62a2cd6ef202990fb2c" +source = "git+https://codeberg.org/unspeaker/tengri?rev=6cd85ef#6cd85efe503135cc5a20da9366ff81a0bbe5f56c" dependencies = [ "tek_edn", ] @@ -1091,7 +1024,7 @@ dependencies = [ [[package]] name = "tek_tui" version = "0.2.0" -source = "git+https://codeberg.org/unspeaker/tengri#5352a9d5484198e35760d62a2cd6ef202990fb2c" +source = "git+https://codeberg.org/unspeaker/tengri?rev=6cd85ef#6cd85efe503135cc5a20da9366ff81a0bbe5f56c" dependencies = [ "atomic_float", "better-panic", @@ -1126,12 +1059,6 @@ dependencies = [ "syn", ] -[[package]] -name = "typenum" -version = "1.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" - [[package]] name = "typewit" version = "1.11.0" @@ -1188,12 +1115,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - [[package]] name = "walkdir" version = "2.5.0" diff --git a/Cargo.toml b/Cargo.toml index 7592725..5c9782a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] -tek_tui = { git = "https://codeberg.org/unspeaker/tengri", ref = "5352a9d" } +tek_tui = { git = "https://codeberg.org/unspeaker/tengri", rev = "6cd85ef" } clap = { version = "4.5.4", features = [ "cargo" ] } walkdir = "2" @@ -12,7 +12,7 @@ id3 = "1.16" moku = "0.2" file_type = "0.7" pad = "0.1" -sha2 = "0.10" +#sha2 = "0.10" hex = "0.4" xxhash-rust = { version = "0.8.5", features = ["xxh3"] } -base64 = "0.22" +#base64 = "0.22" diff --git a/shell.nix b/shell.nix index 7f22e60..445533d 100755 --- a/shell.nix +++ b/shell.nix @@ -1,34 +1,13 @@ #!/usr/bin/env nix-shell {pkgs?import{}}:let - stdenv = pkgs.clang19Stdenv; - name = "tek"; - nativeBuildInputs = with pkgs; [ pkg-config freetype libclang ]; - buildInputs = with pkgs; let - #suil = pkgs.enableDebugging (pkgs.suil.overrideAttrs (a: b: { - #dontStrip = true; separateDebugInfo = true; - #})); - in [ jack2 lilv serd libclang /*suil*/ glib gtk3 ]; - VST3_SDK_DIR = "/home/user/Lab/Music/tek/vst3sdk/"; - LIBCLANG_PATH = "${pkgs.libclang.lib.outPath}/lib"; - LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath (with pkgs; [ - pipewire.jack - # for ChowKick.lv2: - freetype - libgcc.lib - # for Panagement - xorg.libX11 - xorg.libXcursor - xorg.libXi - libxkbcommon - #suil - # for Helm: - alsa-lib - curl - libglvnd - #xorg_sys_opengl - ]); + stdenv = pkgs.clang19Stdenv; + name = "taggart"; + nativeBuildInputs = with pkgs; [ pkg-config libclang ]; + buildInputs = with pkgs; [ libclang ]; + LIBCLANG_PATH = "${pkgs.libclang.lib.outPath}/lib"; + LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath (with pkgs; []); in pkgs.mkShell.override { inherit stdenv; } { - inherit name nativeBuildInputs buildInputs VST3_SDK_DIR LIBCLANG_PATH LD_LIBRARY_PATH; + inherit name nativeBuildInputs buildInputs LIBCLANG_PATH LD_LIBRARY_PATH; } diff --git a/src/main.rs b/src/main.rs index 986b905..c6ec4ad 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,15 +10,14 @@ use tek_tui::tek_output::*; use tek_tui::tek_input::*; use crate::crossterm::event::{Event, KeyEvent, KeyCode, KeyModifiers, KeyEventState, KeyEventKind}; -use clap::{arg, command, value_parser, ArgAction, Command}; +use clap::{arg, command, value_parser}; use walkdir::WalkDir; -use sha2::{Sha256, Digest}; use xxhash_rust::xxh3::xxh3_64; -use base64::prelude::*; use file_type::FileType; mod keys; mod view; +mod model; pub(crate) use self::model::*; pub(crate) type Usually = std::result::Result>; pub(crate) type Perhaps = Usually>; @@ -31,49 +30,6 @@ fn cli () -> clap::Command { command!() .arg(arg!([path] "Path to root directory").value_parser(value_parser!(PathBuf))) } -struct Taggart { - root: PathBuf, - paths: Vec, - cursor: usize, - offset: usize, - column: usize, - size: Measure, - editing: Option<(usize, usize)>, - show_hash: bool, -} -#[derive(Default, Ord, Eq, PartialEq, PartialOrd)] -struct Entry { - path: PathBuf, - is_dir: Option, - is_mus: Option, - is_img: Option, - depth: usize, - hash: Option, - file_type: Option<&'static FileType>, -} -#[derive(Default, Ord, Eq, PartialEq, PartialOrd)] -struct DirInfo { - hash_file: Option<()>, - catalog_file: Option<()>, - artist_file: Option<()>, - release_file: Option<()>, -} -#[derive(Default, Ord, Eq, PartialEq, PartialOrd)] -struct MusInfo { - artist: Option, - release: Option, - track: Option, - title: Option, - date: Option, - year: Option, - people: Option>, - publisher: Option, - key: Option, -} -#[derive(Default, Ord, Eq, PartialEq, PartialOrd)] -struct ImgInfo { - author: Option, -} fn main () -> Usually<()> { let args = cli().get_matches(); @@ -109,78 +65,11 @@ impl Taggart { if entry.depth() == 0 { continue } - let depth = entry.depth(); - let path = entry.into_path(); - let short_path: PathBuf = path.strip_prefix(&root)?.into(); - let (is_dir, is_mus, is_img, hash, file_type) = if path.is_dir() { - (Some(Default::default()), None, None, None, None) - } else { - let bytes = read(&path)?; - let hash = hex::encode(xxh3_64(&bytes).to_be_bytes()); - let file_type = FileType::try_from_reader(&*bytes)?; - let mime_type = file_type.media_types().get(0); - let is_mus = match mime_type { - Some(&"audio/mpeg3") => Some(Default::default()), - _ => None, - }; - let is_img = match mime_type { - Some(&"image/png") => Some(Default::default()), - _ => None, - }; - println!("{hash} {:>10}b {}", bytes.len(), short_path.display()); - (None, is_mus, is_img, Some(hash), Some(file_type)) - }; - paths.push(Entry { - path: short_path, - is_dir, - is_mus, - is_img, - depth, - hash, - file_type - }); + if let Some(entry) = Entry::new(root, &entry)? { + paths.push(entry); + } } paths.sort(); Ok(paths) } } - -//pub enum Entry { - //Dir { - //path: PathBuf, - //name: OsString, - //entries: Vec>, - //}, - //File { - //path: PathBuf, - //name: OsString, - //} -//} - -//impl Entry { - //fn new (path: &impl AsRef) -> Usually { - //let mut paths = vec![]; - //for entry in WalkDir::new(&root) - //.into_iter() - //.filter_entry(|e|!e - //.file_name() - //.to_str() - //.map(|s|s.starts_with(".")) - //.unwrap_or(false)) - //{ - //let path = entry?.into_path().strip_prefix(&root)?.into(); - //paths.push(path); - //} - //paths.sort(); - //} -//} - -//struct FileTree { - //path: PathBuf, - //name: OsString, - //entries: Vec>, -//} - -//impl FileTree { - -//} diff --git a/src/model.rs b/src/model.rs new file mode 100644 index 0000000..42e490e --- /dev/null +++ b/src/model.rs @@ -0,0 +1,168 @@ +use crate::*; +use walkdir::DirEntry; +use id3::{Tag, TagLike}; +use std::io::Read; + +pub struct Taggart { + pub root: PathBuf, + pub paths: Vec, + pub cursor: usize, + pub offset: usize, + pub column: usize, + pub size: Measure, + pub editing: Option<(usize, usize)>, + pub show_hash: bool, +} + +#[derive(Ord, Eq, PartialEq, PartialOrd)] +pub struct Entry { + pub path: PathBuf, + pub depth: usize, + pub info: EntryInfo, +} + +#[derive(Ord, Eq, PartialEq, PartialOrd)] +pub enum EntryInfo { + Directory { + hash_file: Option<()>, + catalog_file: Option<()>, + artist_file: Option<()>, + release_file: Option<()>, + }, + Music { + hash: Arc, + file_type: &'static FileType, + artist: Option>, + album: Option>, + track: Option, + title: Option>, + date: Option>, + year: Option, + people: Option>>, + publisher: Option>, + key: Option>, + bpm: Option>, + invalid: bool, + }, + Image { + hash: Arc, + file_type: &'static FileType, + title: Option, + author: Option, + invalid: bool, + }, +} + +impl Entry { + + pub fn new (root: &impl AsRef, entry: &DirEntry) -> Perhaps { + if entry.path().is_dir() { + Self::new_dir(root, entry) + } else if entry.path().is_file() { + Self::new_file(root, entry) + } else { + Ok(None) + } + } + + fn new_dir (root: &impl AsRef, entry: &DirEntry) -> Perhaps { + Ok(Some(Self { + depth: entry.depth(), + path: entry.path().into(), + info: EntryInfo::Directory { + hash_file: None, + catalog_file: None, + artist_file: None, + release_file: None, + }, + })) + } + + fn new_file (root: &impl AsRef, entry: &DirEntry) -> Perhaps { + let bytes = read(entry.path())?; + let hash = hex::encode(xxh3_64(&bytes).to_be_bytes()); + let file_type = FileType::try_from_reader(&*bytes)?; + let mime_type = file_type.media_types().get(0); + return Ok(Some(Self { + depth: entry.depth(), + path: entry.path().into(), + info: match mime_type { + Some(&"audio/mpeg3") => { + let id3 = Tag::read_from_path(entry.path())?; + EntryInfo::Music { + file_type, + hash: hash.into(), + artist: id3.artist().map(|x|x.into()), + album: id3.album().map(|x|x.into()), + track: id3.track().map(|x|x.into()), + title: id3.title().map(|x|x.into()), + date: None, + year: id3.year().map(|x|x.into()), + people: None, + publisher: None, + key: None, + bpm: None, + invalid: false, + } + }, + Some(&"image/png") => EntryInfo::Image { + file_type, + hash: hash.into(), + title: None, + author: None, + invalid: false, + }, + Some(&"image/jpeg") => EntryInfo::Image { + file_type, + hash: hash.into(), + title: None, + author: None, + invalid: false, + }, + _ => return Ok(None) + }, + })) + } + + pub fn short_path (&self, root: &impl AsRef) -> Usually<&Path> { + Ok(self.path.strip_prefix(root.as_ref())?) + } + + pub fn is_dir (&self) -> bool { + matches!(self.info, EntryInfo::Directory { .. }) + } + + pub fn is_mus (&self) -> bool { + matches!(self.info, EntryInfo::Music { .. }) + } + + pub fn is_img (&self) -> bool { + matches!(self.info, EntryInfo::Image { .. }) + } + + pub fn hash (&self) -> Option> { + match self.info { + EntryInfo::Image { ref hash, .. } => Some(hash.clone()), + EntryInfo::Music { ref hash, .. } => Some(hash.clone()), + _ => None + } + } + pub fn artist (&self) -> Option> { + match self.info { + EntryInfo::Music { ref artist, .. } => artist.clone(), + _ => None + } + } + pub fn album (&self) -> Option> { + match self.info { + EntryInfo::Music { ref album, .. } => album.clone(), + _ => None + } + } + pub fn title (&self) -> Option> { + match self.info { + EntryInfo::Music { ref title, .. } => title.clone(), + _ => None + } + } +} diff --git a/src/view.rs b/src/view.rs index c2f3e4a..60dcc44 100644 --- a/src/view.rs +++ b/src/view.rs @@ -1,22 +1,7 @@ use crate::*; use tek_tui::ratatui::{style::{Color, Style}, prelude::Stylize}; use pad::PadStr; - -fn table_row ( - hash: Option<&str>, label: &str, artist: &str, album: &str, track: &str, title: &str -) -> String { - let hash = hash.unwrap_or("").pad_to_width(COLUMN_WIDTHS[0] as usize); - let label = label.pad_to_width(COLUMN_WIDTHS[1] as usize); - let artist = artist.pad_to_width(COLUMN_WIDTHS[2] as usize); - let album = album.pad_to_width(COLUMN_WIDTHS[3] as usize); - let track = track.pad_to_width(COLUMN_WIDTHS[4] as usize); - let title = title.pad_to_width(COLUMN_WIDTHS[5] as usize); - format!("{hash}│{label}│{artist}╎{album}╎{track}╎{title}") -} - -fn status_bar (content: impl Content) -> impl Content { - Fixed::y(1, Fill::x(Tui::bold(true, Tui::fg_bg(Color::Rgb(0,0,0), Color::Rgb(255,255,255), content)))) -} +use std::fmt::Display; impl Content for Taggart { fn content (&self) -> impl Render { @@ -24,7 +9,12 @@ impl Content for Taggart { let size = format!("{}x{}", self.size.w(), self.size.h()); let size_bar = status_bar(Align::e(size)); let titlebar = status_bar(Align::w(table_row( - Some("HASH"), "FILE", "ARTIST", "RELEASE", "TRACK", "TITLE" + Some("HASH".into()), + "FILE", + Some("ARTIST".into()), + Some("RELEASE".into()), + "TRACK", + Some("TITLE".into()) ))); let table = Fill::xy(TreeTable(self)); Bsp::n(size_bar, Bsp::s(titlebar, Bsp::b(sizer, table))) @@ -38,7 +28,7 @@ impl<'a> Content for TreeTable<'a> { let area = to.area(); let Taggart { offset, paths, cursor, column, .. } = self.0; let mut x = 0; - for (index, width) in COLUMN_WIDTHS.iter().enumerate() { + for (index, _width) in COLUMN_WIDTHS.iter().enumerate() { let w = COLUMN_WIDTHS[index] + 1; if index == *column { to.fill_bg([area.x() + x, area.y(), w, area.h()], Color::Rgb(0, 0, 0)); @@ -54,30 +44,9 @@ impl<'a> Content for TreeTable<'a> { for (index, fragment) in entry.path.iter().enumerate() { if index == entry.depth - 1 { let cursor = if selected { ">" } else { " " }; - let icon = if entry.is_dir.is_some() { - "" //"+" - } else if entry.is_img.is_some() { - "" - } else if entry.is_mus.is_some() { - "" - } else { - " " - }; - let style = if entry.is_dir.is_some() { - None - } else { - Some(Style::default().bold()) - }; - let name = fragment.display(); - let indent = "".pad_to_width((entry.depth - 1) * 2); - let label = table_row( - entry.hash.as_deref(), - &format!("{indent}{icon} {name}"), - "", - "", - "", - "" - ); + let icon = entry.icon(); + let style = entry.style(); + let label = entry.label(icon, &fragment.display()); to.blit(&label, area.x(), y, style); if selected { let fill = [area.x(), y, area.w(), 1]; @@ -92,3 +61,56 @@ impl<'a> Content for TreeTable<'a> { } } } + +impl Entry { + fn icon (&self) -> &'static str { + if self.is_dir() { + "" //"+" + } else if self.is_img() { + "" + } else if self.is_mus() { + "" + } else { + " " + } + } + fn style (&self) -> Option