audio tagging utilities

initial commit

Stella f2103cc8 b1c7442c

Changed files
+712
src
+462
Cargo.lock
··· 1 + # This file is automatically @generated by Cargo. 2 + # It is not intended for manual editing. 3 + version = 4 4 + 5 + [[package]] 6 + name = "adler2" 7 + version = "2.0.1" 8 + source = "registry+https://github.com/rust-lang/crates.io-index" 9 + checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" 10 + 11 + [[package]] 12 + name = "anstream" 13 + version = "0.6.20" 14 + source = "registry+https://github.com/rust-lang/crates.io-index" 15 + checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" 16 + dependencies = [ 17 + "anstyle", 18 + "anstyle-parse", 19 + "anstyle-query", 20 + "anstyle-wincon", 21 + "colorchoice", 22 + "is_terminal_polyfill", 23 + "utf8parse", 24 + ] 25 + 26 + [[package]] 27 + name = "anstyle" 28 + version = "1.0.11" 29 + source = "registry+https://github.com/rust-lang/crates.io-index" 30 + checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" 31 + 32 + [[package]] 33 + name = "anstyle-parse" 34 + version = "0.2.7" 35 + source = "registry+https://github.com/rust-lang/crates.io-index" 36 + checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" 37 + dependencies = [ 38 + "utf8parse", 39 + ] 40 + 41 + [[package]] 42 + name = "anstyle-query" 43 + version = "1.1.4" 44 + source = "registry+https://github.com/rust-lang/crates.io-index" 45 + checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" 46 + dependencies = [ 47 + "windows-sys", 48 + ] 49 + 50 + [[package]] 51 + name = "anstyle-wincon" 52 + version = "3.0.10" 53 + source = "registry+https://github.com/rust-lang/crates.io-index" 54 + checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" 55 + dependencies = [ 56 + "anstyle", 57 + "once_cell_polyfill", 58 + "windows-sys", 59 + ] 60 + 61 + [[package]] 62 + name = "audiotags" 63 + version = "0.5.0" 64 + source = "registry+https://github.com/rust-lang/crates.io-index" 65 + checksum = "44e797ce0164cf599c71f2c3849b56301d96a3dc033544588e875686b050ed39" 66 + dependencies = [ 67 + "audiotags-macro", 68 + "id3", 69 + "metaflac", 70 + "mp4ameta", 71 + "readme-rustdocifier", 72 + "thiserror", 73 + ] 74 + 75 + [[package]] 76 + name = "audiotags-macro" 77 + version = "0.2.0" 78 + source = "registry+https://github.com/rust-lang/crates.io-index" 79 + checksum = "8eaa9b2312fc01f7291f3b7b0f52ed08b1c0177c96a2e696ab55695cc4d06889" 80 + 81 + [[package]] 82 + name = "bitflags" 83 + version = "2.9.4" 84 + source = "registry+https://github.com/rust-lang/crates.io-index" 85 + checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" 86 + 87 + [[package]] 88 + name = "byteorder" 89 + version = "1.5.0" 90 + source = "registry+https://github.com/rust-lang/crates.io-index" 91 + checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 92 + 93 + [[package]] 94 + name = "cfg-if" 95 + version = "1.0.3" 96 + source = "registry+https://github.com/rust-lang/crates.io-index" 97 + checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" 98 + 99 + [[package]] 100 + name = "clap" 101 + version = "4.5.48" 102 + source = "registry+https://github.com/rust-lang/crates.io-index" 103 + checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" 104 + dependencies = [ 105 + "clap_builder", 106 + "clap_derive", 107 + ] 108 + 109 + [[package]] 110 + name = "clap_builder" 111 + version = "4.5.48" 112 + source = "registry+https://github.com/rust-lang/crates.io-index" 113 + checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" 114 + dependencies = [ 115 + "anstream", 116 + "anstyle", 117 + "clap_lex", 118 + "strsim", 119 + ] 120 + 121 + [[package]] 122 + name = "clap_derive" 123 + version = "4.5.47" 124 + source = "registry+https://github.com/rust-lang/crates.io-index" 125 + checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" 126 + dependencies = [ 127 + "heck", 128 + "proc-macro2", 129 + "quote", 130 + "syn", 131 + ] 132 + 133 + [[package]] 134 + name = "clap_lex" 135 + version = "0.7.5" 136 + source = "registry+https://github.com/rust-lang/crates.io-index" 137 + checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" 138 + 139 + [[package]] 140 + name = "colorchoice" 141 + version = "1.0.4" 142 + source = "registry+https://github.com/rust-lang/crates.io-index" 143 + checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" 144 + 145 + [[package]] 146 + name = "crc32fast" 147 + version = "1.5.0" 148 + source = "registry+https://github.com/rust-lang/crates.io-index" 149 + checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" 150 + dependencies = [ 151 + "cfg-if", 152 + ] 153 + 154 + [[package]] 155 + name = "flate2" 156 + version = "1.1.2" 157 + source = "registry+https://github.com/rust-lang/crates.io-index" 158 + checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" 159 + dependencies = [ 160 + "crc32fast", 161 + "miniz_oxide", 162 + ] 163 + 164 + [[package]] 165 + name = "heck" 166 + version = "0.5.0" 167 + source = "registry+https://github.com/rust-lang/crates.io-index" 168 + checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 169 + 170 + [[package]] 171 + name = "hex" 172 + version = "0.4.3" 173 + source = "registry+https://github.com/rust-lang/crates.io-index" 174 + checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 175 + 176 + [[package]] 177 + name = "id3" 178 + version = "1.16.3" 179 + source = "registry+https://github.com/rust-lang/crates.io-index" 180 + checksum = "aadb14a5ba1a0d58ecd4a29bfc9b8f1d119eee24aa01a62c1ec93eb9630a1d86" 181 + dependencies = [ 182 + "bitflags", 183 + "byteorder", 184 + "flate2", 185 + ] 186 + 187 + [[package]] 188 + name = "is_terminal_polyfill" 189 + version = "1.70.1" 190 + source = "registry+https://github.com/rust-lang/crates.io-index" 191 + checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 192 + 193 + [[package]] 194 + name = "itoa" 195 + version = "1.0.15" 196 + source = "registry+https://github.com/rust-lang/crates.io-index" 197 + checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 198 + 199 + [[package]] 200 + name = "lazy_static" 201 + version = "1.5.0" 202 + source = "registry+https://github.com/rust-lang/crates.io-index" 203 + checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 204 + 205 + [[package]] 206 + name = "memchr" 207 + version = "2.7.5" 208 + source = "registry+https://github.com/rust-lang/crates.io-index" 209 + checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" 210 + 211 + [[package]] 212 + name = "metaflac" 213 + version = "0.2.8" 214 + source = "registry+https://github.com/rust-lang/crates.io-index" 215 + checksum = "fdf25a3451319c52a4a56d956475fbbb763bfb8420e2187d802485cb0fd8d965" 216 + dependencies = [ 217 + "byteorder", 218 + "hex", 219 + ] 220 + 221 + [[package]] 222 + name = "miniz_oxide" 223 + version = "0.8.9" 224 + source = "registry+https://github.com/rust-lang/crates.io-index" 225 + checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" 226 + dependencies = [ 227 + "adler2", 228 + ] 229 + 230 + [[package]] 231 + name = "mp4ameta" 232 + version = "0.11.0" 233 + source = "registry+https://github.com/rust-lang/crates.io-index" 234 + checksum = "eb23d62e8eb5299a3f79657c70ea9269eac8f6239a76952689bcd06a74057e81" 235 + dependencies = [ 236 + "lazy_static", 237 + "mp4ameta_proc", 238 + ] 239 + 240 + [[package]] 241 + name = "mp4ameta_proc" 242 + version = "0.6.0" 243 + source = "registry+https://github.com/rust-lang/crates.io-index" 244 + checksum = "07dcca13d1740c0a665f77104803360da0bdb3323ecce2e93fa2c959a6d52806" 245 + 246 + [[package]] 247 + name = "once_cell_polyfill" 248 + version = "1.70.1" 249 + source = "registry+https://github.com/rust-lang/crates.io-index" 250 + checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" 251 + 252 + [[package]] 253 + name = "proc-macro2" 254 + version = "1.0.101" 255 + source = "registry+https://github.com/rust-lang/crates.io-index" 256 + checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" 257 + dependencies = [ 258 + "unicode-ident", 259 + ] 260 + 261 + [[package]] 262 + name = "quote" 263 + version = "1.0.40" 264 + source = "registry+https://github.com/rust-lang/crates.io-index" 265 + checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 266 + dependencies = [ 267 + "proc-macro2", 268 + ] 269 + 270 + [[package]] 271 + name = "readme-rustdocifier" 272 + version = "0.1.1" 273 + source = "registry+https://github.com/rust-lang/crates.io-index" 274 + checksum = "08ad765b21a08b1a8e5cdce052719188a23772bcbefb3c439f0baaf62c56ceac" 275 + 276 + [[package]] 277 + name = "ryu" 278 + version = "1.0.20" 279 + source = "registry+https://github.com/rust-lang/crates.io-index" 280 + checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 281 + 282 + [[package]] 283 + name = "seekertools" 284 + version = "0.1.0" 285 + dependencies = [ 286 + "audiotags", 287 + "clap", 288 + "serde", 289 + "serde_json", 290 + ] 291 + 292 + [[package]] 293 + name = "serde" 294 + version = "1.0.226" 295 + source = "registry+https://github.com/rust-lang/crates.io-index" 296 + checksum = "0dca6411025b24b60bfa7ec1fe1f8e710ac09782dca409ee8237ba74b51295fd" 297 + dependencies = [ 298 + "serde_core", 299 + "serde_derive", 300 + ] 301 + 302 + [[package]] 303 + name = "serde_core" 304 + version = "1.0.226" 305 + source = "registry+https://github.com/rust-lang/crates.io-index" 306 + checksum = "ba2ba63999edb9dac981fb34b3e5c0d111a69b0924e253ed29d83f7c99e966a4" 307 + dependencies = [ 308 + "serde_derive", 309 + ] 310 + 311 + [[package]] 312 + name = "serde_derive" 313 + version = "1.0.226" 314 + source = "registry+https://github.com/rust-lang/crates.io-index" 315 + checksum = "8db53ae22f34573731bafa1db20f04027b2d25e02d8205921b569171699cdb33" 316 + dependencies = [ 317 + "proc-macro2", 318 + "quote", 319 + "syn", 320 + ] 321 + 322 + [[package]] 323 + name = "serde_json" 324 + version = "1.0.145" 325 + source = "registry+https://github.com/rust-lang/crates.io-index" 326 + checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" 327 + dependencies = [ 328 + "itoa", 329 + "memchr", 330 + "ryu", 331 + "serde", 332 + "serde_core", 333 + ] 334 + 335 + [[package]] 336 + name = "strsim" 337 + version = "0.11.1" 338 + source = "registry+https://github.com/rust-lang/crates.io-index" 339 + checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 340 + 341 + [[package]] 342 + name = "syn" 343 + version = "2.0.106" 344 + source = "registry+https://github.com/rust-lang/crates.io-index" 345 + checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" 346 + dependencies = [ 347 + "proc-macro2", 348 + "quote", 349 + "unicode-ident", 350 + ] 351 + 352 + [[package]] 353 + name = "thiserror" 354 + version = "1.0.69" 355 + source = "registry+https://github.com/rust-lang/crates.io-index" 356 + checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 357 + dependencies = [ 358 + "thiserror-impl", 359 + ] 360 + 361 + [[package]] 362 + name = "thiserror-impl" 363 + version = "1.0.69" 364 + source = "registry+https://github.com/rust-lang/crates.io-index" 365 + checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 366 + dependencies = [ 367 + "proc-macro2", 368 + "quote", 369 + "syn", 370 + ] 371 + 372 + [[package]] 373 + name = "unicode-ident" 374 + version = "1.0.19" 375 + source = "registry+https://github.com/rust-lang/crates.io-index" 376 + checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" 377 + 378 + [[package]] 379 + name = "utf8parse" 380 + version = "0.2.2" 381 + source = "registry+https://github.com/rust-lang/crates.io-index" 382 + checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 383 + 384 + [[package]] 385 + name = "windows-link" 386 + version = "0.1.3" 387 + source = "registry+https://github.com/rust-lang/crates.io-index" 388 + checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" 389 + 390 + [[package]] 391 + name = "windows-sys" 392 + version = "0.60.2" 393 + source = "registry+https://github.com/rust-lang/crates.io-index" 394 + checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" 395 + dependencies = [ 396 + "windows-targets", 397 + ] 398 + 399 + [[package]] 400 + name = "windows-targets" 401 + version = "0.53.3" 402 + source = "registry+https://github.com/rust-lang/crates.io-index" 403 + checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" 404 + dependencies = [ 405 + "windows-link", 406 + "windows_aarch64_gnullvm", 407 + "windows_aarch64_msvc", 408 + "windows_i686_gnu", 409 + "windows_i686_gnullvm", 410 + "windows_i686_msvc", 411 + "windows_x86_64_gnu", 412 + "windows_x86_64_gnullvm", 413 + "windows_x86_64_msvc", 414 + ] 415 + 416 + [[package]] 417 + name = "windows_aarch64_gnullvm" 418 + version = "0.53.0" 419 + source = "registry+https://github.com/rust-lang/crates.io-index" 420 + checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" 421 + 422 + [[package]] 423 + name = "windows_aarch64_msvc" 424 + version = "0.53.0" 425 + source = "registry+https://github.com/rust-lang/crates.io-index" 426 + checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" 427 + 428 + [[package]] 429 + name = "windows_i686_gnu" 430 + version = "0.53.0" 431 + source = "registry+https://github.com/rust-lang/crates.io-index" 432 + checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" 433 + 434 + [[package]] 435 + name = "windows_i686_gnullvm" 436 + version = "0.53.0" 437 + source = "registry+https://github.com/rust-lang/crates.io-index" 438 + checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" 439 + 440 + [[package]] 441 + name = "windows_i686_msvc" 442 + version = "0.53.0" 443 + source = "registry+https://github.com/rust-lang/crates.io-index" 444 + checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" 445 + 446 + [[package]] 447 + name = "windows_x86_64_gnu" 448 + version = "0.53.0" 449 + source = "registry+https://github.com/rust-lang/crates.io-index" 450 + checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" 451 + 452 + [[package]] 453 + name = "windows_x86_64_gnullvm" 454 + version = "0.53.0" 455 + source = "registry+https://github.com/rust-lang/crates.io-index" 456 + checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" 457 + 458 + [[package]] 459 + name = "windows_x86_64_msvc" 460 + version = "0.53.0" 461 + source = "registry+https://github.com/rust-lang/crates.io-index" 462 + checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
+10
Cargo.toml
··· 1 + [package] 2 + name = "seekertools" 3 + version = "0.1.0" 4 + edition = "2024" 5 + 6 + [dependencies] 7 + audiotags = "0.5.0" 8 + clap = { version = "4", features = ["derive"] } 9 + serde = { version = "1.0", features = ["derive"] } 10 + serde_json = "1.0"
+240
src/main.rs
··· 1 + use std::{fs, path::{Path, PathBuf}}; 2 + 3 + use audiotags::{AudioTag, Tag}; 4 + use clap::Parser; 5 + 6 + /// Simple audio tag reader 7 + #[derive(Parser, Debug)] 8 + #[command(author, version, about)] 9 + struct Args { 10 + /// Read files recursively 11 + #[arg(short, long)] 12 + recursive: bool, 13 + 14 + /// Output format: one of json, print, debug 15 + /// Output format: one of json, pjson, print, debug 16 + #[arg(short = 'o', long, default_value_t = Output::Json)] 17 + output: Output, 18 + 19 + /// Pretty-print output. With `--output json` this prints pretty JSON. With `--output debug` this prints human-friendly text. 20 + #[arg(long)] 21 + pretty: bool, 22 + /// Directory to scan 23 + directory: String, 24 + } 25 + 26 + #[derive(clap::ValueEnum, Clone, Debug)] 27 + enum Output { 28 + Json, 29 + Debug, 30 + } 31 + 32 + impl std::fmt::Display for Output { 33 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 34 + match self { 35 + Output::Json => write!(f, "json"), 36 + Output::Debug => write!(f, "debug"), 37 + } 38 + } 39 + } 40 + 41 + #[derive(serde::Serialize)] 42 + struct AudioFileSummary { 43 + path: PathBuf, 44 + title: String, 45 + artist: String, 46 + album: Option<String>, 47 + cover_present: bool, 48 + } 49 + 50 + impl AudioFileEntry { 51 + fn to_summary(&self) -> AudioFileSummary { 52 + let title = self.tags.title().unwrap_or("Unknown").to_string(); 53 + let artist = self.tags.artist().unwrap_or("Unknown").to_string(); 54 + 55 + let (album, cover_present) = match self.tags.album() { 56 + Some(a) => (Some(a.title.to_string()), a.cover.is_some()), 57 + None => (None, false), 58 + }; 59 + 60 + AudioFileSummary { 61 + path: self.path.clone(), 62 + title, 63 + artist, 64 + album, 65 + cover_present, 66 + } 67 + } 68 + } 69 + 70 + pub struct AudioFileEntry { 71 + pub path: PathBuf, 72 + pub tags: Box<dyn AudioTag>, 73 + } 74 + 75 + impl std::fmt::Debug for AudioFileEntry { 76 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 77 + let cover_present = match self.tags.album() { 78 + Some(album) => album.cover.is_some(), 79 + None => false, 80 + }; 81 + 82 + let album_info = self.tags.album().as_ref().map(|a| format!("{} by {}", a.title, a.artist.as_deref().unwrap_or("Unknown Artist"))); 83 + 84 + f.debug_struct("AudioFileEntry") 85 + .field("path", &self.path) 86 + .field("title", &self.tags.title().unwrap_or("Unknown")) 87 + .field("artist", &self.tags.artist().unwrap_or("Unknown")) 88 + .field("album", &album_info) 89 + .field("cover_present", &cover_present) 90 + .finish() 91 + } 92 + } 93 + 94 + impl std::fmt::Display for AudioFileEntry { 95 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 96 + let title = self.tags.title().unwrap_or("Unknown"); 97 + let artist = self.tags.artist().unwrap_or("Unknown"); 98 + let (album_info, cover_present) = match self.tags.album() { 99 + Some(album) => { 100 + let artist = album.artist.as_ref().map(|s| &**s).unwrap_or("Unknown Artist"); 101 + let info = format!("{} by {}", album.title, artist); 102 + (info, album.cover.is_some()) 103 + } 104 + None => ("No album".to_string(), false), 105 + }; 106 + 107 + write!(f, "{} | {} | {} | {} | cover_present: {}", self.path.display(), title, artist, album_info, cover_present) 108 + } 109 + } 110 + 111 + type ResultType = Result<Vec<AudioFileEntry>, Box<dyn std::error::Error>>; 112 + 113 + fn is_audio_file(path: &Path) -> bool { 114 + if let Some(extension) = path.extension().and_then(|s| s.to_str()) { 115 + matches!( 116 + extension.to_lowercase().as_str(), 117 + "mp3" | "flac" | "ogg" | "wav" | "m4a" | "aac" 118 + ) 119 + } else { 120 + false 121 + } 122 + } 123 + 124 + fn read_folder<P: AsRef<Path>>(path: P, recursive: bool) -> ResultType { 125 + let dir_path = path.as_ref(); 126 + if !dir_path.is_dir() { 127 + return Err(format!("Path is not a directory: {}", dir_path.display()).into()); 128 + } 129 + 130 + let mut audio_files = Vec::new(); 131 + 132 + // Use a stack for iterative traversal so we can do recursive or single-level scanning 133 + let mut dirs = vec![dir_path.to_path_buf()]; 134 + 135 + while let Some(current_dir) = dirs.pop() { 136 + let entries = match fs::read_dir(&current_dir) { 137 + Ok(e) => e, 138 + Err(err) => { 139 + eprintln!("Failed to read directory {}: {}", current_dir.display(), err); 140 + continue; 141 + } 142 + }; 143 + 144 + for entry_res in entries { 145 + let entry = match entry_res { 146 + Ok(e) => e, 147 + Err(e) => { 148 + eprintln!("Failed to read dir entry in {}: {}", current_dir.display(), e); 149 + continue; 150 + } 151 + }; 152 + 153 + let file_path = entry.path(); 154 + 155 + if file_path.is_dir() { 156 + if recursive { 157 + dirs.push(file_path); 158 + } 159 + continue; 160 + } 161 + 162 + if !(file_path.is_file() && is_audio_file(&file_path)) { 163 + continue; 164 + } 165 + 166 + match Tag::new().read_from_path(&file_path) { 167 + Ok(tags) => { 168 + // store the path relative to the provided root directory 169 + let rel_path = if let Ok(rel) = file_path.strip_prefix(dir_path) { 170 + rel.to_path_buf() 171 + } else if let Some(name) = file_path.file_name() { 172 + PathBuf::from(name) 173 + } else { 174 + // fallback to the full path if all else fails 175 + file_path.clone() 176 + }; 177 + 178 + audio_files.push(AudioFileEntry { 179 + path: rel_path, 180 + tags, 181 + }); 182 + } 183 + Err(e) => { 184 + eprintln!("Failed to read tags from {}: {:?}", file_path.display(), e); 185 + // Continue processing other files instead of failing entirely 186 + } 187 + } 188 + } 189 + } 190 + 191 + Ok(audio_files) 192 + } 193 + 194 + 195 + fn main() { 196 + let args = Args::parse(); 197 + 198 + match read_folder(&args.directory, args.recursive) { 199 + Ok(audio_files) => { 200 + println!("Found {} audio files in '{}':", audio_files.len(), args.directory); 201 + // Decide behavior based on `--pretty` and `--output` 202 + if args.pretty { 203 + match args.output { 204 + Output::Json => { 205 + // pretty JSON array 206 + let summaries: Vec<_> = audio_files.into_iter().map(|e| e.to_summary()).collect(); 207 + match serde_json::to_string_pretty(&summaries) { 208 + Ok(s) => println!("{}", s), 209 + Err(e) => eprintln!("Failed to serialize summaries to pretty JSON: {}", e), 210 + } 211 + } 212 + Output::Debug => { 213 + // pretty + debug => human-friendly printed lines 214 + for entry in audio_files { 215 + println!("{}", entry); 216 + } 217 + } 218 + } 219 + } else { 220 + // not pretty: per-entry outputs 221 + for entry in audio_files { 222 + match args.output { 223 + Output::Debug => println!("{:?}", entry), 224 + Output::Json => { 225 + let summary = entry.to_summary(); 226 + match serde_json::to_string(&summary) { 227 + Ok(s) => println!("{}", s), 228 + Err(e) => eprintln!("Failed to serialize JSON for {}: {}", entry.path.display(), e), 229 + } 230 + } 231 + } 232 + } 233 + } 234 + } 235 + Err(e) => { 236 + eprintln!("Error reading folder '{}': {}", args.directory, e); 237 + std::process::exit(1); 238 + } 239 + } 240 + }