mirror of
https://codeberg.org/unspeaker/perch.git
synced 2025-12-06 09:36:42 +01:00
339 lines
12 KiB
Rust
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);
|