use crate::testing_prelude::*;
use lofty::file::{AudioFile, TaggedFile, TaggedFileExt};
use lofty::picture::Picture;
use lofty::probe::Probe;
use lofty::properties::{ChannelMask, FileProperties};
use lofty::tag::{ItemValue, Tag};
use sha2::{Digest, Sha256};
#[derive(Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct AudioSnapshot {
pub duration_ms: u128,
pub overall_bitrate: Option<u32>,
pub audio_bitrate: Option<u32>,
pub sample_rate: Option<u32>,
pub bit_depth: Option<u8>,
pub channels: Option<u8>,
pub channel_mask: Option<u32>,
pub tags: BTreeMap<String, BTreeMap<String, TagValue>>,
pub pictures: Vec<PictureSnapshot>,
}
#[derive(Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct PictureSnapshot {
pub picture_type: String,
pub mime_type: Option<String>,
pub description: Option<String>,
pub size: usize,
pub sha256: String,
}
impl From<&Picture> for PictureSnapshot {
fn from(picture: &Picture) -> Self {
Self {
picture_type: format!("{:?}", picture.pic_type()),
mime_type: picture.mime_type().map(|m| format!("{m:?}")),
description: picture.description().map(String::from),
size: picture.data().len(),
sha256: format!("{:x}", Sha256::digest(picture.data())),
}
}
}
impl From<&FileProperties> for AudioSnapshot {
fn from(props: &FileProperties) -> Self {
Self {
duration_ms: props.duration().as_millis(),
overall_bitrate: props.overall_bitrate(),
audio_bitrate: props.audio_bitrate(),
sample_rate: props.sample_rate(),
bit_depth: props.bit_depth(),
channels: props.channels(),
channel_mask: props.channel_mask().map(ChannelMask::bits),
tags: BTreeMap::new(),
pictures: Vec::new(),
}
}
}
impl From<&TaggedFile> for AudioSnapshot {
fn from(file: &TaggedFile) -> Self {
let mut metadata = Self::from(file.properties());
metadata.tags = get_tag_map(file.tags());
metadata.pictures = get_pictures(file.tags());
metadata
}
}
impl AudioSnapshot {
pub fn from_path(path: &Path) -> Option<Self> {
let file = Probe::open(path).ok()?.read().ok()?;
Some(Self::from(&file))
}
}
#[derive(Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
#[serde(untagged)]
pub enum TagValue {
Text(String),
Binary {
size: usize,
sha256: String,
},
}
impl From<&ItemValue> for TagValue {
fn from(value: &ItemValue) -> Self {
match value {
ItemValue::Text(text) | ItemValue::Locator(text) => Self::Text(text.clone()),
ItemValue::Binary(data) => Self::Binary {
size: data.len(),
sha256: format!("{:x}", Sha256::digest(data)),
},
}
}
}
fn get_tag_map(tags: &[Tag]) -> BTreeMap<String, BTreeMap<String, TagValue>> {
let mut map: BTreeMap<String, BTreeMap<String, TagValue>> = BTreeMap::new();
for tag in tags {
let tag_type = format!("{:?}", tag.tag_type());
let tag_items = map.entry(tag_type).or_default();
for item in tag.items() {
let key = format!("{:?}", item.key());
tag_items.insert(key, TagValue::from(item.value()));
}
}
map
}
fn get_pictures(tags: &[Tag]) -> Vec<PictureSnapshot> {
let mut pictures: Vec<PictureSnapshot> = tags
.iter()
.flat_map(Tag::pictures)
.map(PictureSnapshot::from)
.collect();
pictures.sort();
pictures
}