use crate::*; use std::fs::File; use std::io::{BufReader, Read}; use std::cmp::{Eq, PartialEq, Ord, PartialOrd, Ordering}; use byte_unit::{Byte, Unit::MB}; use lofty::{ probe::Probe, file::TaggedFileExt, config::{ParseOptions, ParsingMode}, tag::{Accessor, Tag, TagType} }; pub struct Entry { /// How many levels deep is this from the working directory pub depth: usize, /// Full path to this entry pub path: Arc, /// Type-specific metadata pub info: Arc>, } impl Entry { pub fn new (root: &impl AsRef, path: &impl AsRef, depth: usize) -> Perhaps { let path = path.as_ref(); if path.is_dir() { Self::new_dir(root, &path, depth) } else if path.is_file() { Self::new_file(root, &path, depth) } else { Ok(None) } } fn new_dir (_: &impl AsRef, path: &Path, depth: usize) -> Perhaps { Ok(Some(Self { depth, path: path.to_path_buf().into(), info: Arc::new(RwLock::new(Metadata::Directory { hash_file: None, catalog_file: None, artist_file: None, release_file: None, })), })) } fn new_file (_: &impl AsRef, path: &Path, depth: usize) -> Perhaps { Ok(Some(Self { depth, path: path.to_path_buf().into(), info: Arc::new(RwLock::new(Metadata::new(path, false)?)), })) } pub fn is_directory (&self) -> bool { matches!(&*self.info.read().unwrap(), Metadata::Directory { .. }) } pub fn is_music (&self) -> bool { matches!(&*self.info.read().unwrap(), Metadata::Music { .. }) } pub fn is_image (&self) -> bool { matches!(&*self.info.read().unwrap(), Metadata::Image { .. }) } pub fn name (&self) -> Option> { let indent = "".pad_to_width((self.depth - 1) * 2); let icon = self.icon(); let name = self.path.iter().last().expect("empty path").display(); Some(format!("{indent}{icon} {name}").into()) } fn icon (&self) -> &'static str { if self.is_directory() { ICON_DIRECTORY } else if self.is_image() { ICON_IMAGE } else if self.is_music() { ICON_MUSIC } else { ICON_UNKNOWN } } pub fn is_modified (&self) -> bool { match &*self.info.read().unwrap() { Metadata::Music { modified_tag, .. } => modified_tag.is_some(), _ => false, } } } impl Eq for Entry {} impl PartialEq for Entry { fn eq (&self, other: &Self) -> bool { self.path.eq(&other.path) } } impl Ord for Entry { fn cmp (&self, other: &Self) -> Ordering { self.path.cmp(&other.path) } } impl PartialOrd for Entry { fn partial_cmp (&self, other: &Self) -> Option { self.path.partial_cmp(&other.path) } } pub enum Metadata { Directory { hash_file: Option<()>, catalog_file: Option<()>, artist_file: Option<()>, release_file: Option<()>, }, Music { invalid: bool, hash: Arc, size: Arc, original_tag: Option>, modified_tag: Option>>, }, Image { invalid: bool, hash: Arc, size: Arc, title: Option, author: Option, }, Unknown { hash: Arc, size: Arc, } } impl Metadata { pub fn new (path: &Path, strict: bool) -> Usually { let probe = Probe::new(BufReader::new(File::open(path)?)) .options(ParseOptions::new().parsing_mode(if strict { ParsingMode::Strict } else { ParsingMode::BestAttempt })) .guess_file_type()?; if probe.file_type().is_some() { let file = lofty::read_from_path(path)?; let tag = file.primary_tag(); let data = std::fs::read(path)?; let hash = hex::encode(xxh3_64(data.as_slice()).to_be_bytes()).into(); let size = Byte::from_u64(data.len() as u64).get_adjusted_unit(MB); Ok(Self::Music { hash, size: format!("{:#>8.2}", size).into(), invalid: false, original_tag: tag.map(|t|t.clone().into()), modified_tag: None, }) } else { Self::new_fallback(path) } } pub fn new_fallback (path: &Path) -> Usually { let file = File::open(path)?; let size = Byte::from_u64(file.metadata()?.len() as u64).get_adjusted_unit(MB); let mut reader = BufReader::new(file); let mut bytes = vec![0;16]; reader.read(&mut bytes)?; // PNG if bytes.starts_with(MAGIC_PNG) { let mut bytes = vec![]; BufReader::new(File::open(path)?).read(&mut bytes)?; return Ok(Self::Image { hash: hex::encode(xxh3_64(&bytes).to_be_bytes()).into(), size: format!("{:#>8.2}", size).into(), title: None, author: None, invalid: false, }) } // JPG if bytes.starts_with(MAGIC_JPG_1) || bytes.starts_with(MAGIC_JPG_2) || bytes.starts_with(MAGIC_JPG_3) || (bytes.starts_with(MAGIC_JPG_4A) && bytes.get(6) == Some(&0x45) && bytes.get(7) == Some(&0x78) && bytes.get(8) == Some(&0x69) && bytes.get(9) == Some(&0x66) && bytes.get(10) == Some(&0x00) && bytes.get(11) == Some(&0x00)) { return Ok(Self::Image { hash: hex::encode(xxh3_64(&bytes).to_be_bytes()).into(), size: format!("{:#>8.2}", size).into(), title: None, author: None, invalid: false, }) } Ok(Self::Unknown { size: format!("{:#>8.2}", size).into(), hash: hex::encode(xxh3_64(&bytes).to_be_bytes()).into(), }) } } macro_rules! generated_field { ($get:ident = |$self:ident| $expr:expr) => { impl Entry { pub fn $get (&$self) -> Option> { $self.info.read().unwrap().$get() } } impl Metadata { pub fn $get (&$self) -> Option> { $expr } } } } macro_rules! music_tag_field { (string: $get:ident $set:ident $del:ident $key:expr) => { impl Entry { pub fn $get (&self) -> Option> { self.info.read().unwrap().$get() } pub fn $set (&mut self, value: &impl AsRef) -> bool { self.info.write().unwrap().$set(value) } } impl Metadata { pub fn $get (&self) -> Option> { if let Metadata::Music { original_tag, modified_tag, .. } = self { return modified_tag.as_ref() .map(|tag|tag.read().unwrap().$get().map(|t|t.into())) .flatten() .or_else(||original_tag.as_ref() .map(|tag|tag.$get().map(|t|t.into())) .flatten()) } None } pub fn $set (&mut self, value: &impl AsRef) -> bool { let value = value.as_ref().trim(); if let &mut Metadata::Music { ref mut modified_tag, .. } = self { match (value.len(), &modified_tag) { (0, Some(new_tag)) => { if new_tag.read().unwrap().item_count() <= 1 { // removing last entry removes modified_tag *modified_tag = None; } else { // remove entry from modified_tag new_tag.write().unwrap().$del(); } return true; }, (_, Some(new_tag)) => { // add entry to modified_tag new_tag.write().unwrap().$set(value.into()); return true; }, (0, None) => { // leave modified_tag empty return false; }, (_, None) => { // first entry creates modified_tag let mut new_tag = Tag::new(TagType::Id3v2); // FIXME other types new_tag.$set(value.into()); *modified_tag = Some(Arc::new(RwLock::new(new_tag))); return true; }, } } false } } }; (number: $get:ident $set:ident $del:ident $key:expr) => { impl Entry { pub fn $get (&self) -> Option> { self.info.read().unwrap().$get() } pub fn $set (&mut self, value: &impl AsRef) -> bool { self.info.write().unwrap().$set(value) } } impl Metadata { pub fn $get (&self) -> Option> { if let Metadata::Music { original_tag, modified_tag, .. } = self { return modified_tag.as_ref() .map(|tag|tag.read().unwrap().$get().map(|t|format!("{t}").into())) .flatten() .or_else(||original_tag.as_ref() .map(|tag|tag.$get().map(|t|format!("{t}").into())) .flatten()) } None } pub fn $set (&mut self, value: &impl AsRef) -> bool { let value = value.as_ref().trim(); if let &mut Metadata::Music { ref mut modified_tag, .. } = self && let Ok(numeric_value) = value.parse::() { match (value.len(), &modified_tag) { (0, Some(new_tag)) => { if new_tag.read().unwrap().item_count() <= 1 { // removing last entry removes modified_tag *modified_tag = None; } else { // remove entry from modified_tag new_tag.write().unwrap().$del(); } return true; }, (_, Some(new_tag)) => { // add entry to modified_tag new_tag.write().unwrap().$set(numeric_value); return true; }, (0, None) => { // leave modified_tag empty return false; }, (_, None) => { // first entry creates modified_tag let mut new_tag = Tag::new(TagType::Id3v2); // FIXME other types new_tag.$set(numeric_value); *modified_tag = Some(Arc::new(RwLock::new(new_tag))); return true; }, } } false } } }; } generated_field!(hash = |self|match self { Metadata::Image { hash, .. } => Some(hash.clone()), Metadata::Music { hash, .. } => Some(hash.clone()), Metadata::Unknown { hash, .. } => Some(hash.clone()), _ => None }); generated_field!(size = |self|match self { Metadata::Image { size, .. } => Some(size.clone()), Metadata::Music { size, .. } => Some(size.clone()), Metadata::Unknown { size, .. } => Some(size.clone()), _ => None }); music_tag_field!(string: artist set_artist remove_artist ItemKey::TrackArtist); music_tag_field!(number: year set_year remove_year ItemKey::Year); music_tag_field!(string: album set_album remove_album ItemKey::AlbumTitle); music_tag_field!(number: track set_track remove_track ItemKey::TrackNumber); music_tag_field!(string: title set_title remove_title ItemKey::TrackTitle);