use std::{env, fs::{self, File}, path::Path, fmt::format, os::fd::IntoRawFd}; use std::io::{Error, ErrorKind, BufReader, Read}; use lofty::{Probe, TaggedFileExt, LoftyError, TagExt, Tag, Picture, Accessor, PictureType}; use clap::Parser; use rayon::prelude::*; #[derive(Parser, Debug)] #[command(version, about, long_about = None)] struct Args { /// Path to your music folder #[arg(short, long)] path: Option, /// Print more logs while running TODO #[arg(short, long, default_value_t = false)] verbose: bool, } /* Title Artist - Title Artist - Album - Title Artist - Album - Nr - Title Artist - Album - Nr - Max Nr - Title */ struct InnerTag { title: String, artist: Option, album: Option, track: Option, } impl InnerTag { fn from_parts(title: String, artist: Option, album: Option, track: Option) -> Self { Self { title, artist, album, track } } pub fn from(name: &str) -> Result { // Replace '--' with a unique placeholder that is unlikely to be part of any filename. // Ensure this placeholder does not accidentally create new splitting points. let placeholder = "\u{FDD0}"; // Use a Unicode non-character as a placeholder. let sanitized_name = name.replace("--", placeholder); let parts: Vec<_> = sanitized_name.split('-') .map(|part| part.replace(placeholder, "-")) .collect(); let err = Err(format!("🔥 Invalid tag format in filename: {}", name)); match parts.as_slice() { [title] => Ok(Self::from_parts(title.to_string(), None, None, None)), [artist, title] => Ok(Self::from_parts(title.to_string(), Some(artist.to_string()), None, None)), [artist, album, title] => Ok(Self::from_parts(title.to_string(), Some(artist.to_string()), Some(album.to_string()), None)), [artist, album, track_str, title] => track_str.parse::().map_or(err, |track| Ok(Self::from_parts(title.to_string(), Some(artist.to_string()), Some(album.to_string()), Some(track)))), [artist, album, track_str, _, title] => track_str.parse::().map_or(err, |track| Ok(Self::from_parts(title.to_string(), Some(artist.to_string()), Some(album.to_string()), Some(track)))), _ => err, } } } fn tag_ogg_file(path: &Path) -> Result<(), Box> { let mut probed_file = Probe::open(path)?.read()?; if probed_file.primary_tag_mut().is_none() { let tag_type = probed_file.primary_tag_type(); eprintln!("👻 No tags found, creating a new tag of type `{tag_type:?}`"); probed_file.insert_tag(Tag::new(tag_type)); } let tag = probed_file.primary_tag_mut().unwrap(); let name = path.file_name().and_then(|n| n.to_str()).expect("Filename is not valid UTF-8"); let inner_tag = InnerTag::from(name)?; tag.clear(); tag.set_title(inner_tag.title); tag.set_artist(inner_tag.artist.unwrap_or("".into())); tag.set_album(inner_tag.album.unwrap_or("".into())); if let Some(track) = inner_tag.track { tag.set_track(track); } if let Some(album) = tag.album() { if album != "" { let image_path = path.parent().unwrap().join(format!(".cover/{}/{}.jpg", tag.artist().unwrap(), album)); if image_path.exists() { let image_file = &mut File::open(image_path.clone()).unwrap(); let mut picture = Picture::from_reader(image_file).unwrap(); picture.set_pic_type(PictureType::CoverFront); tag.set_picture(0, picture) } else { eprintln!("👻 Can't found image {}", image_path.to_str().unwrap()); } } } tag.save_to_path(path).expect("ERROR: Can't save ogg file"); println!("✅ {}", name); return Ok(()); } fn main() -> Result<(), Box> { let args = Args::parse(); let music_path = args.path.unwrap_or_else(|| { println!("💡 Trying to find the music folder automatically"); env::var("HOME").map(|home| format!("{}/Music", home)) .expect("🔥 Couldn't find a music folder automatically") }); let music_dir = fs::read_dir(&music_path) .map_err(|_| format!("🔥 Dir \"{}\" doesn't exist", music_path))?; println!("💡 Start scanning \"{}\" dir for ogg file", music_path.as_str()); let paths: Vec<_> = music_dir.filter_map(Result::ok) .map(|entry| entry.path()) .filter(|path| path.extension().and_then(std::ffi::OsStr::to_str) == Some("ogg")) .collect(); paths.par_iter().for_each(|path| { if path.is_file() { if let Err(e) = tag_ogg_file(path) { eprintln!("🔥 Error tagging file {:?}: {}", path, e); } } }); println!("💡 Ended successfully"); Ok(()) }