use crate::*; use std::fs::File; use std::io::{BufReader, Read}; use lofty::{ file::TaggedFileExt, probe::Probe, tag::Accessor, }; pub struct Taggart { pub _root: PathBuf, pub paths: Vec, pub cursor: usize, pub offset: usize, pub column: usize, pub columns: Columns, pub size: Measure, pub editing: Option<(usize, String)>, } #[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: Option<&'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: Option<&'static FileType>, title: Option, author: Option, invalid: bool, }, Unknown { hash: Arc, file_type: Option<&'static FileType>, } } impl Taggart { pub fn new (root: &impl AsRef, paths: Vec) -> Usually { Ok(Self { _root: root.as_ref().into(), cursor: 0, offset: 0, column: 0, size: Measure::new(), editing: None, columns: Columns::default(), paths, }) } } 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.into(), info: EntryInfo::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, info: EntryInfo::new(path)?, path: path.into(), })) } 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 } } pub fn track (&self) -> Option> { match self.info { EntryInfo::Music { ref track, .. } => track.map(|t|format!("{t}").into()).clone(), _ => None } } } impl EntryInfo { pub fn new (path: &Path) -> Usually { let reader = BufReader::new(File::open(path)?); let probe = Probe::new(reader) //.options(ParseOptions::new().parsing_mode(ParsingMode::Strict)) .guess_file_type()?; if probe.file_type().is_some() { let file = lofty::read_from_path(path)?; let tag = file.primary_tag(); let hash = hex::encode(xxh3_64(std::fs::read(path)?.as_slice()).to_be_bytes()).into(); Ok(Self::Music { hash, file_type: None, artist: tag.map(|t|t.artist().map(|t|t.into())).flatten(), album: tag.map(|t|t.album().map(|t|t.into())).flatten(), track: tag.map(|t|t.track().map(|t|t.into())).flatten(), title: tag.map(|t|t.title().map(|t|t.into())).flatten(), year: tag.map(|t|t.year().map(|t|t.into())).flatten(), date: None, people: None, publisher: None, key: None, bpm: None, invalid: false, }) } else { Self::new_fallback(path) } } pub fn new_fallback (path: &Path) -> Usually { let mut reader = BufReader::new(File::open(path)?); let mut bytes = vec![0;16]; reader.read(&mut bytes)?; // PNG if bytes.starts_with(&[0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A]) { let mut bytes = vec![]; BufReader::new(File::open(path)?).read(&mut bytes)?; return Ok(Self::Image { file_type: None, hash: hex::encode(xxh3_64(&bytes).to_be_bytes()).into(), title: None, author: None, invalid: false, }) } // JPG if bytes.starts_with(&[0xFF, 0xD8, 0xFF, 0xDB]) || bytes.starts_with(&[0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01]) || bytes.starts_with(&[0xFF, 0xD8, 0xFF, 0xEE]) || (bytes.starts_with(&[0xFF, 0xD8, 0xFF, 0xE1]) && 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 { file_type: None, hash: hex::encode(xxh3_64(&bytes).to_be_bytes()).into(), title: None, author: None, invalid: false, }) } Ok(Self::Unknown { file_type: None, hash: hex::encode(xxh3_64(&bytes).to_be_bytes()).into(), }) } }