mirror of
https://codeberg.org/unspeaker/perch.git
synced 2025-12-06 09:36:42 +01:00
251 lines
8.2 KiB
Rust
251 lines
8.2 KiB
Rust
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<Entry>,
|
|
pub cursor: usize,
|
|
pub offset: usize,
|
|
pub column: usize,
|
|
pub columns: Columns<Entry>,
|
|
pub size: Measure<TuiOut>,
|
|
pub editing: Option<(usize, String)>,
|
|
}
|
|
|
|
#[derive(Ord, Eq, PartialEq, PartialOrd, Debug)]
|
|
pub struct Entry {
|
|
pub path: PathBuf,
|
|
pub depth: usize,
|
|
pub info: EntryInfo,
|
|
}
|
|
|
|
#[derive(Ord, Eq, PartialEq, PartialOrd, Debug)]
|
|
pub enum EntryInfo {
|
|
Directory {
|
|
hash_file: Option<()>,
|
|
catalog_file: Option<()>,
|
|
artist_file: Option<()>,
|
|
release_file: Option<()>,
|
|
},
|
|
Music {
|
|
hash: Arc<str>,
|
|
file_type: Option<&'static FileType>,
|
|
artist: Option<Arc<str>>,
|
|
album: Option<Arc<str>>,
|
|
track: Option<u32>,
|
|
title: Option<Arc<str>>,
|
|
date: Option<Arc<str>>,
|
|
year: Option<u32>,
|
|
people: Option<Vec<Arc<str>>>,
|
|
publisher: Option<Arc<str>>,
|
|
key: Option<Arc<str>>,
|
|
bpm: Option<Arc<str>>,
|
|
invalid: bool,
|
|
},
|
|
Image {
|
|
hash: Arc<str>,
|
|
file_type: Option<&'static FileType>,
|
|
title: Option<String>,
|
|
author: Option<String>,
|
|
invalid: bool,
|
|
},
|
|
Unknown {
|
|
hash: Arc<str>,
|
|
file_type: Option<&'static FileType>,
|
|
}
|
|
}
|
|
|
|
impl Taggart {
|
|
pub fn new (root: &impl AsRef<Path>, paths: Vec<Entry>) -> Usually<Self> {
|
|
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>, path: &impl AsRef<Path>, depth: usize) -> Perhaps<Self> {
|
|
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: &Path, depth: usize) -> Perhaps<Self> {
|
|
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: &Path, depth: usize) -> Perhaps<Self> {
|
|
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<Arc<str>> {
|
|
match self.info {
|
|
EntryInfo::Image { ref hash, .. } => Some(hash.clone()),
|
|
EntryInfo::Music { ref hash, .. } => Some(hash.clone()),
|
|
_ => None
|
|
}
|
|
}
|
|
pub fn artist (&self) -> Option<Arc<str>> {
|
|
match self.info {
|
|
EntryInfo::Music { ref artist, .. } => artist.clone(),
|
|
_ => None
|
|
}
|
|
}
|
|
pub fn album (&self) -> Option<Arc<str>> {
|
|
match self.info {
|
|
EntryInfo::Music { ref album, .. } => album.clone(),
|
|
_ => None
|
|
}
|
|
}
|
|
pub fn title (&self) -> Option<Arc<str>> {
|
|
match self.info {
|
|
EntryInfo::Music { ref title, .. } => title.clone(),
|
|
_ => None
|
|
}
|
|
}
|
|
pub fn track (&self) -> Option<Arc<str>> {
|
|
match self.info {
|
|
EntryInfo::Music { ref track, .. } => track.map(|t|format!("{t}").into()).clone(),
|
|
_ => None
|
|
}
|
|
}
|
|
pub fn set_artist (&mut self, value: &impl AsRef<str> ) {
|
|
match self.info {
|
|
EntryInfo::Directory { .. } => todo!("set artist for whole directory"),
|
|
EntryInfo::Music { ref mut artist, .. } => *artist = Some(value.as_ref().into()),
|
|
_ => {}
|
|
}
|
|
}
|
|
pub fn set_album (&mut self, value: &impl AsRef<str> ) {
|
|
match self.info {
|
|
EntryInfo::Directory { .. } => todo!("set album for whole directory"),
|
|
EntryInfo::Music { ref mut album, .. } => *album = Some(value.as_ref().into()),
|
|
_ => {}
|
|
}
|
|
}
|
|
pub fn set_title (&mut self, value: &impl AsRef<str> ) {
|
|
match self.info {
|
|
EntryInfo::Directory { .. } => todo!("set title for whole directory"),
|
|
EntryInfo::Music { ref mut title, .. } => *title = Some(value.as_ref().into()),
|
|
_ => {}
|
|
}
|
|
}
|
|
pub fn set_track (&mut self, value: &impl AsRef<str> ) {
|
|
match self.info {
|
|
EntryInfo::Directory { .. } => todo!("set track for whole directory"),
|
|
EntryInfo::Music { ref mut track, .. } => {
|
|
if let Ok(value) = value.as_ref().trim().parse::<u32>() {
|
|
*track = Some(value)
|
|
}
|
|
},
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
impl EntryInfo {
|
|
pub fn new (path: &Path) -> Usually<Self> {
|
|
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<Self> {
|
|
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(),
|
|
})
|
|
}
|
|
}
|