mirror of
https://codeberg.org/unspeaker/perch.git
synced 2025-12-08 18:46:43 +01:00
refactor; EntryInfo -> Metadata
This commit is contained in:
parent
95aa9f8b02
commit
cf18e32e13
5 changed files with 384 additions and 351 deletions
359
src/model.rs
359
src/model.rs
|
|
@ -1,9 +1,8 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
use std::fs::File;
|
|
||||||
use std::io::{BufReader, Read};
|
mod column; pub use self::column::*;
|
||||||
use std::cmp::{Eq, PartialEq, Ord, PartialOrd, Ordering};
|
mod entry; pub use self::entry::*;
|
||||||
use lofty::{file::TaggedFileExt, probe::Probe, tag::Accessor};
|
mod metadata; pub use self::metadata::*;
|
||||||
use byte_unit::{Byte, Unit::MB};
|
|
||||||
|
|
||||||
pub struct Taggart {
|
pub struct Taggart {
|
||||||
pub _root: PathBuf,
|
pub _root: PathBuf,
|
||||||
|
|
@ -11,71 +10,11 @@ pub struct Taggart {
|
||||||
pub cursor: usize,
|
pub cursor: usize,
|
||||||
pub offset: usize,
|
pub offset: usize,
|
||||||
pub column: usize,
|
pub column: usize,
|
||||||
pub columns: Columns<Entry>,
|
pub columns: Columns<Entry, fn(&Entry)->Option<Arc<str>>, fn(&mut [Entry], usize, &str)>,
|
||||||
pub display: Measure<TuiOut>,
|
pub display: Measure<TuiOut>,
|
||||||
pub editing: Option<(usize, String)>,
|
pub editing: Option<(usize, String)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Entry {
|
|
||||||
pub path: PathBuf,
|
|
||||||
pub depth: usize,
|
|
||||||
pub info: Arc<RwLock<EntryInfo>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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>,
|
|
||||||
size: Arc<str>,
|
|
||||||
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>,
|
|
||||||
size: Arc<str>,
|
|
||||||
title: Option<String>,
|
|
||||||
author: Option<String>,
|
|
||||||
invalid: bool,
|
|
||||||
},
|
|
||||||
Unknown {
|
|
||||||
hash: Arc<str>,
|
|
||||||
size: Arc<str>,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Taggart {
|
impl Taggart {
|
||||||
pub fn new (root: &impl AsRef<Path>, paths: Vec<Entry>) -> Usually<Self> {
|
pub fn new (root: &impl AsRef<Path>, paths: Vec<Entry>) -> Usually<Self> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
|
@ -91,37 +30,11 @@ impl Taggart {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Column<T> {
|
pub(crate) fn paths_under (
|
||||||
pub title: Arc<str>,
|
entries: &mut [Entry], index: usize
|
||||||
pub width: usize,
|
) -> Option<Vec<Arc<RwLock<Metadata>>>> {
|
||||||
pub getter: fn(&T)->Option<Arc<str>>,
|
|
||||||
pub setter: Option<fn(&mut [T], usize, &str)>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Column<T> {
|
|
||||||
pub fn new (
|
|
||||||
title: &impl AsRef<str>,
|
|
||||||
width: usize,
|
|
||||||
getter: fn(&T)->Option<Arc<str>>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
width,
|
|
||||||
title: title.as_ref().into(),
|
|
||||||
getter,
|
|
||||||
setter: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn setter (mut self, setter: fn(&mut [T], usize, &str)) -> Self {
|
|
||||||
self.setter = Some(setter);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Columns<T>(pub Vec<Column<T>>);
|
|
||||||
|
|
||||||
fn paths_under (entries: &mut [Entry], index: usize) -> Option<Vec<Arc<RwLock<EntryInfo>>>> {
|
|
||||||
let path = if let Some(Entry { path, info, .. }) = entries.get(index)
|
let path = if let Some(Entry { path, info, .. }) = entries.get(index)
|
||||||
&& let EntryInfo::Directory { .. } = &*info.read().unwrap()
|
&& let Metadata::Directory { .. } = &*info.read().unwrap()
|
||||||
{
|
{
|
||||||
Some(path.clone())
|
Some(path.clone())
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -139,257 +52,3 @@ fn paths_under (entries: &mut [Entry], index: usize) -> Option<Vec<Arc<RwLock<En
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Columns<Entry> {
|
|
||||||
fn default () -> Self {
|
|
||||||
Self(vec![
|
|
||||||
|
|
||||||
Column::new(&"HASH", 16, |entry: &Entry|entry.hash()),
|
|
||||||
|
|
||||||
Column::new(&"SIZE", 8, |entry: &Entry|entry.size()),
|
|
||||||
|
|
||||||
Column::new(&"FILE", 80, |entry: &Entry|entry.name()),
|
|
||||||
|
|
||||||
Column::new(&"ARTIST", 30, |entry: &Entry|entry.artist())
|
|
||||||
.setter(|entries: &mut [Entry], index: usize, value: &str|{
|
|
||||||
if let Some(entries) = paths_under(entries, index) {
|
|
||||||
for entry in entries.iter() {
|
|
||||||
entry.write().unwrap().set_artist(&value);
|
|
||||||
}
|
|
||||||
} else if let Some(entry) = entries.get_mut(index) {
|
|
||||||
entry.set_artist(&value)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
Column::new(&"RELEASE", 30, |entry: &Entry|entry.album())
|
|
||||||
.setter(|entries: &mut [Entry], index: usize, value: &str|{
|
|
||||||
if let Some(entries) = paths_under(entries, index) {
|
|
||||||
for entry in entries.iter() {
|
|
||||||
entry.write().unwrap().set_album(&value);
|
|
||||||
}
|
|
||||||
} else if let Some(entry) = entries.get_mut(index) {
|
|
||||||
entry.set_album(&value)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
Column::new(&"TRACK", 5, |entry: &Entry|entry.track())
|
|
||||||
.setter(|entries: &mut [Entry], index: usize, value: &str|{
|
|
||||||
if let Some(entries) = paths_under(entries, index) {
|
|
||||||
for entry in entries.iter() {
|
|
||||||
entry.write().unwrap().set_track(&value);
|
|
||||||
}
|
|
||||||
} else if let Some(entry) = entries.get_mut(index) {
|
|
||||||
entry.set_track(&value)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
Column::new(&"TITLE", 80, |entry: &Entry|entry.title())
|
|
||||||
.setter(|entries: &mut [Entry], index: usize, value: &str|{
|
|
||||||
if let Some(entries) = paths_under(entries, index) {
|
|
||||||
for entry in entries.iter() {
|
|
||||||
entry.write().unwrap().set_title(&value);
|
|
||||||
}
|
|
||||||
} else if let Some(entry) = entries.get_mut(index) {
|
|
||||||
entry.set_title(&value)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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: Arc::new(RwLock::new(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: Arc::new(RwLock::new(EntryInfo::new(path)?)),
|
|
||||||
path: path.into(),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
pub fn is_dir (&self) -> bool {
|
|
||||||
matches!(&*self.info.read().unwrap(), EntryInfo::Directory { .. })
|
|
||||||
}
|
|
||||||
pub fn is_mus (&self) -> bool {
|
|
||||||
matches!(&*self.info.read().unwrap(), EntryInfo::Music { .. })
|
|
||||||
}
|
|
||||||
pub fn is_img (&self) -> bool {
|
|
||||||
matches!(&*self.info.read().unwrap(), EntryInfo::Image { .. })
|
|
||||||
}
|
|
||||||
pub fn hash (&self) -> Option<Arc<str>> {
|
|
||||||
match &*self.info.read().unwrap() {
|
|
||||||
EntryInfo::Image { hash, .. } => Some(hash.clone()),
|
|
||||||
EntryInfo::Music { hash, .. } => Some(hash.clone()),
|
|
||||||
EntryInfo::Unknown { hash, .. } => Some(hash.clone()),
|
|
||||||
_ => None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn size (&self) -> Option<Arc<str>> {
|
|
||||||
match &*self.info.read().unwrap() {
|
|
||||||
EntryInfo::Image { size, .. } => Some(size.clone()),
|
|
||||||
EntryInfo::Music { size, .. } => Some(size.clone()),
|
|
||||||
EntryInfo::Unknown { size, .. } => Some(size.clone()),
|
|
||||||
_ => None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn artist (&self) -> Option<Arc<str>> {
|
|
||||||
match &*self.info.read().unwrap() {
|
|
||||||
EntryInfo::Music { artist, .. } => artist.clone(),
|
|
||||||
_ => None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn album (&self) -> Option<Arc<str>> {
|
|
||||||
match &*self.info.read().unwrap() {
|
|
||||||
EntryInfo::Music { album, .. } => album.clone(),
|
|
||||||
_ => None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn title (&self) -> Option<Arc<str>> {
|
|
||||||
match &*self.info.read().unwrap() {
|
|
||||||
EntryInfo::Music { title, .. } => title.clone(),
|
|
||||||
_ => None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn track (&self) -> Option<Arc<str>> {
|
|
||||||
match &*self.info.read().unwrap() {
|
|
||||||
EntryInfo::Music { track, .. } => track.map(|t|format!("{t}").into()).clone(),
|
|
||||||
_ => None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn set_artist (&self, value: &impl AsRef<str> ) {
|
|
||||||
self.info.write().unwrap().set_artist(value)
|
|
||||||
}
|
|
||||||
pub fn set_album (&self, value: &impl AsRef<str> ) {
|
|
||||||
self.info.write().unwrap().set_album(value)
|
|
||||||
}
|
|
||||||
pub fn set_title (&self, value: &impl AsRef<str> ) {
|
|
||||||
self.info.write().unwrap().set_title(value)
|
|
||||||
}
|
|
||||||
pub fn set_track (&self, value: &impl AsRef<str> ) {
|
|
||||||
self.info.write().unwrap().set_track(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 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(),
|
|
||||||
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 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(&[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 {
|
|
||||||
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(&[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 {
|
|
||||||
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(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
pub fn set_artist (&mut self, value: &impl AsRef<str> ) {
|
|
||||||
match self {
|
|
||||||
EntryInfo::Music { artist, .. } => *artist = Some(value.as_ref().into()),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn set_album (&mut self, value: &impl AsRef<str> ) {
|
|
||||||
match self {
|
|
||||||
EntryInfo::Music { album, .. } => *album = Some(value.as_ref().into()),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn set_title (&mut self, value: &impl AsRef<str> ) {
|
|
||||||
match self {
|
|
||||||
EntryInfo::Music { title, .. } => *title = Some(value.as_ref().into()),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn set_track (&mut self, value: &impl AsRef<str> ) {
|
|
||||||
match self {
|
|
||||||
EntryInfo::Directory { .. } => todo!("set track for whole directory"),
|
|
||||||
EntryInfo::Music { track, .. } => {
|
|
||||||
if let Ok(value) = value.as_ref().trim().parse::<u32>() {
|
|
||||||
*track = Some(value)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
89
src/model/column.rs
Normal file
89
src/model/column.rs
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
pub struct Columns<T, G, S>(pub Vec<Column<T, G, S>>);
|
||||||
|
|
||||||
|
pub struct Column<T, G, S> {
|
||||||
|
__: std::marker::PhantomData<T>,
|
||||||
|
pub title: Arc<str>,
|
||||||
|
pub width: usize,
|
||||||
|
pub getter: G,
|
||||||
|
pub setter: Option<S>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Column<T, fn(&T)->Option<Arc<str>>, fn(&mut [T], usize, &str)> {
|
||||||
|
pub fn new (
|
||||||
|
title: &impl AsRef<str>,
|
||||||
|
width: usize,
|
||||||
|
getter: fn(&T)->Option<Arc<str>>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
width,
|
||||||
|
title: title.as_ref().into(),
|
||||||
|
getter,
|
||||||
|
setter: None,
|
||||||
|
__: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn setter (mut self, setter: fn(&mut [T], usize, &str)) -> Self {
|
||||||
|
self.setter = Some(setter);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Columns<Entry, fn(&Entry)->Option<Arc<str>>, fn(&mut [Entry], usize, &str)> {
|
||||||
|
fn default () -> Self {
|
||||||
|
Self(vec![
|
||||||
|
|
||||||
|
Column::new(&"HASH", 16, |entry: &Entry|entry.hash()),
|
||||||
|
|
||||||
|
Column::new(&"SIZE", 8, |entry: &Entry|entry.size()),
|
||||||
|
|
||||||
|
Column::new(&"FILE", 80, |entry: &Entry|entry.name()),
|
||||||
|
|
||||||
|
Column::new(&"ARTIST", 30, |entry: &Entry|entry.artist())
|
||||||
|
.setter(|entries: &mut [Entry], index: usize, value: &str|{
|
||||||
|
if let Some(entries) = paths_under(entries, index) {
|
||||||
|
for entry in entries.iter() {
|
||||||
|
entry.write().unwrap().set_artist(&value);
|
||||||
|
}
|
||||||
|
} else if let Some(entry) = entries.get_mut(index) {
|
||||||
|
entry.set_artist(&value)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
Column::new(&"RELEASE", 30, |entry: &Entry|entry.album())
|
||||||
|
.setter(|entries: &mut [Entry], index: usize, value: &str|{
|
||||||
|
if let Some(entries) = paths_under(entries, index) {
|
||||||
|
for entry in entries.iter() {
|
||||||
|
entry.write().unwrap().set_album(&value);
|
||||||
|
}
|
||||||
|
} else if let Some(entry) = entries.get_mut(index) {
|
||||||
|
entry.set_album(&value)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
Column::new(&"TRACK", 5, |entry: &Entry|entry.track())
|
||||||
|
.setter(|entries: &mut [Entry], index: usize, value: &str|{
|
||||||
|
if let Some(entries) = paths_under(entries, index) {
|
||||||
|
for entry in entries.iter() {
|
||||||
|
entry.write().unwrap().set_track(&value);
|
||||||
|
}
|
||||||
|
} else if let Some(entry) = entries.get_mut(index) {
|
||||||
|
entry.set_track(&value)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
Column::new(&"TITLE", 80, |entry: &Entry|entry.title())
|
||||||
|
.setter(|entries: &mut [Entry], index: usize, value: &str|{
|
||||||
|
if let Some(entries) = paths_under(entries, index) {
|
||||||
|
for entry in entries.iter() {
|
||||||
|
entry.write().unwrap().set_title(&value);
|
||||||
|
}
|
||||||
|
} else if let Some(entry) = entries.get_mut(index) {
|
||||||
|
entry.set_title(&value)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
100
src/model/entry.rs
Normal file
100
src/model/entry.rs
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
use crate::*;
|
||||||
|
use std::cmp::{Eq, PartialEq, Ord, PartialOrd, Ordering};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Entry {
|
||||||
|
pub path: PathBuf,
|
||||||
|
pub depth: usize,
|
||||||
|
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.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,
|
||||||
|
info: Arc::new(RwLock::new(Metadata::new(path)?)),
|
||||||
|
path: path.into(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
pub fn is_dir (&self) -> bool {
|
||||||
|
matches!(&*self.info.read().unwrap(), Metadata::Directory { .. })
|
||||||
|
}
|
||||||
|
pub fn is_mus (&self) -> bool {
|
||||||
|
matches!(&*self.info.read().unwrap(), Metadata::Music { .. })
|
||||||
|
}
|
||||||
|
pub fn is_img (&self) -> bool {
|
||||||
|
matches!(&*self.info.read().unwrap(), Metadata::Image { .. })
|
||||||
|
}
|
||||||
|
pub fn hash (&self) -> Option<Arc<str>> {
|
||||||
|
self.info.read().unwrap().hash()
|
||||||
|
}
|
||||||
|
pub fn size (&self) -> Option<Arc<str>> {
|
||||||
|
self.info.read().unwrap().size()
|
||||||
|
}
|
||||||
|
pub fn artist (&self) -> Option<Arc<str>> {
|
||||||
|
self.info.read().unwrap().artist()
|
||||||
|
}
|
||||||
|
pub fn album (&self) -> Option<Arc<str>> {
|
||||||
|
self.info.read().unwrap().album()
|
||||||
|
}
|
||||||
|
pub fn title (&self) -> Option<Arc<str>> {
|
||||||
|
self.info.read().unwrap().title()
|
||||||
|
}
|
||||||
|
pub fn track (&self) -> Option<Arc<str>> {
|
||||||
|
self.info.read().unwrap().track()
|
||||||
|
}
|
||||||
|
pub fn set_artist (&self, value: &impl AsRef<str> ) {
|
||||||
|
self.info.write().unwrap().set_artist(value)
|
||||||
|
}
|
||||||
|
pub fn set_album (&self, value: &impl AsRef<str> ) {
|
||||||
|
self.info.write().unwrap().set_album(value)
|
||||||
|
}
|
||||||
|
pub fn set_title (&self, value: &impl AsRef<str> ) {
|
||||||
|
self.info.write().unwrap().set_title(value)
|
||||||
|
}
|
||||||
|
pub fn set_track (&self, value: &impl AsRef<str> ) {
|
||||||
|
self.info.write().unwrap().set_track(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
185
src/model/metadata.rs
Normal file
185
src/model/metadata.rs
Normal file
|
|
@ -0,0 +1,185 @@
|
||||||
|
use crate::*;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::{BufReader, Read};
|
||||||
|
use lofty::{file::TaggedFileExt, probe::Probe, tag::Accessor};
|
||||||
|
use byte_unit::{Byte, Unit::MB};
|
||||||
|
|
||||||
|
#[derive(Ord, Eq, PartialEq, PartialOrd, Debug)]
|
||||||
|
pub enum Metadata {
|
||||||
|
Directory {
|
||||||
|
hash_file: Option<()>,
|
||||||
|
catalog_file: Option<()>,
|
||||||
|
artist_file: Option<()>,
|
||||||
|
release_file: Option<()>,
|
||||||
|
},
|
||||||
|
Music {
|
||||||
|
hash: Arc<str>,
|
||||||
|
size: Arc<str>,
|
||||||
|
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>,
|
||||||
|
size: Arc<str>,
|
||||||
|
title: Option<String>,
|
||||||
|
author: Option<String>,
|
||||||
|
invalid: bool,
|
||||||
|
},
|
||||||
|
Unknown {
|
||||||
|
hash: Arc<str>,
|
||||||
|
size: Arc<str>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Metadata {
|
||||||
|
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 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(),
|
||||||
|
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 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(&[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 {
|
||||||
|
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(&[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 {
|
||||||
|
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(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn hash (&self) -> Option<Arc<str>> {
|
||||||
|
match self {
|
||||||
|
Metadata::Image { hash, .. } => Some(hash.clone()),
|
||||||
|
Metadata::Music { hash, .. } => Some(hash.clone()),
|
||||||
|
Metadata::Unknown { hash, .. } => Some(hash.clone()),
|
||||||
|
_ => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn size (&self) -> Option<Arc<str>> {
|
||||||
|
match self {
|
||||||
|
Metadata::Image { size, .. } => Some(size.clone()),
|
||||||
|
Metadata::Music { size, .. } => Some(size.clone()),
|
||||||
|
Metadata::Unknown { size, .. } => Some(size.clone()),
|
||||||
|
_ => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn artist (&self) -> Option<Arc<str>> {
|
||||||
|
match self {
|
||||||
|
Metadata::Music { artist, .. } => artist.clone(),
|
||||||
|
_ => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn album (&self) -> Option<Arc<str>> {
|
||||||
|
match self {
|
||||||
|
Metadata::Music { album, .. } => album.clone(),
|
||||||
|
_ => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn title (&self) -> Option<Arc<str>> {
|
||||||
|
match self {
|
||||||
|
Metadata::Music { title, .. } => title.clone(),
|
||||||
|
_ => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn track (&self) -> Option<Arc<str>> {
|
||||||
|
match self {
|
||||||
|
Metadata::Music { track, .. } => track.map(|t|format!("{t}").into()).clone(),
|
||||||
|
_ => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn set_artist (&mut self, value: &impl AsRef<str> ) {
|
||||||
|
match self {
|
||||||
|
Metadata::Music { artist, .. } => *artist = Some(value.as_ref().into()),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn set_album (&mut self, value: &impl AsRef<str> ) {
|
||||||
|
match self {
|
||||||
|
Metadata::Music { album, .. } => *album = Some(value.as_ref().into()),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn set_title (&mut self, value: &impl AsRef<str> ) {
|
||||||
|
match self {
|
||||||
|
Metadata::Music { title, .. } => *title = Some(value.as_ref().into()),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn set_track (&mut self, value: &impl AsRef<str> ) {
|
||||||
|
match self {
|
||||||
|
Metadata::Directory { .. } => todo!("set track for whole directory"),
|
||||||
|
Metadata::Music { track, .. } => {
|
||||||
|
if let Ok(value) = value.as_ref().trim().parse::<u32>() {
|
||||||
|
*track = Some(value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -72,7 +72,7 @@ impl<'a> TreeTable<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Columns<T> {
|
impl<T, G, S> Columns<T, G, S> {
|
||||||
pub fn header (&self) -> Arc<str> {
|
pub fn header (&self) -> Arc<str> {
|
||||||
let mut output = String::new();
|
let mut output = String::new();
|
||||||
for Column { width, title, .. } in self.0.iter() {
|
for Column { width, title, .. } in self.0.iter() {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue