perch/src/model/entry.rs
2025-04-06 19:46:01 +03:00

339 lines
12 KiB
Rust

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<PathBuf>,
/// Type-specific metadata
pub info: Arc<RwLock<Metadata>>,
}
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.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: &Path, depth: usize) -> Perhaps<Self> {
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<Arc<str>> {
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<Ordering> {
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<str>,
size: Arc<str>,
original_tag: Option<Arc<Tag>>,
modified_tag: Option<Arc<RwLock<Tag>>>,
},
Image {
invalid: bool,
hash: Arc<str>,
size: Arc<str>,
title: Option<String>,
author: Option<String>,
},
Unknown {
hash: Arc<str>,
size: Arc<str>,
}
}
impl Metadata {
pub fn new (path: &Path, strict: bool) -> Usually<Self> {
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<Self> {
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<Arc<str>> {
$self.info.read().unwrap().$get()
}
}
impl Metadata {
pub fn $get (&$self) -> Option<Arc<str>> {
$expr
}
}
}
}
macro_rules! music_tag_field {
(string: $get:ident $set:ident $del:ident $key:expr) => {
impl Entry {
pub fn $get (&self) -> Option<Arc<str>> {
self.info.read().unwrap().$get()
}
pub fn $set (&mut self, value: &impl AsRef<str>) -> bool {
self.info.write().unwrap().$set(value)
}
}
impl Metadata {
pub fn $get (&self) -> Option<Arc<str>> {
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<str>) -> 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<Arc<str>> {
self.info.read().unwrap().$get()
}
pub fn $set (&mut self, value: &impl AsRef<str>) -> bool {
self.info.write().unwrap().$set(value)
}
}
impl Metadata {
pub fn $get (&self) -> Option<Arc<str>> {
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<str>) -> bool {
let value = value.as_ref().trim();
if let &mut Metadata::Music { ref mut modified_tag, .. } = self
&& let Ok(numeric_value) = value.parse::<u32>()
{
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);