EBML: Add generic tag conversion test

This commit is contained in:
Serial 2024-10-18 22:29:21 -04:00
parent 0dd9557447
commit 756f022584
No known key found for this signature in database
GPG key ID: DA95198DC17C4568
8 changed files with 224 additions and 107 deletions

View file

@ -4,7 +4,7 @@
use super::{Language, MatroskaTag, SimpleTag, TargetType, TOMBSTONE_SIMPLE_TAG};
use crate::tag::items::Lang;
use crate::tag::{ItemKey, Tag, TagItem, TagType};
use crate::tag::{ItemKey, ItemValue, Tag, TagItem, TagType};
use std::collections::HashMap;
use std::sync::LazyLock;
@ -140,7 +140,9 @@ pub(super) fn split_tag(mut matroska_tag: MatroskaTag) -> (MatroskaTag, Tag) {
let mut tag = Tag::new(TagType::Matroska);
// TODO: Pictures, can they be handled in a generic way?
// What about the uid and referral?
// - What about the uid and referral?
// - It seems like the "standard" way of adding cover art is to name it "cover.{ext}"
// - Maybe only support front covers? who knows.
matroska_tag.tags.retain_mut(|t| {
let target_type = match &t.target {
@ -168,15 +170,17 @@ fn split_simple_tags(
tag: &mut Tag,
) -> bool {
let lang: Lang;
match &simple_tag.language {
Some(Language::Iso639_2(l)) if l.len() == 3 => {
lang = l.as_bytes().try_into().unwrap(); // Infallible
},
None => lang = *b"und",
// `Lang` doesn't support anything outside of a 3 character ISO-639-2 code.
_ => return TAG_RETAINED,
let Language::Iso639_2(l) = &simple_tag.language else {
return TAG_RETAINED;
};
// `Lang` doesn't support anything outside of a 3 character ISO-639-2 code.
if l.len() != 3 {
return TAG_CONSUMED;
}
lang = l.as_bytes().try_into().unwrap(); // Infallible
let Some(item_key) = MAPPINGS.get(&(target_type, &*simple_tag.name)).cloned() else {
return TAG_RETAINED;
};
@ -197,6 +201,44 @@ fn split_simple_tags(
return TAG_CONSUMED;
}
pub(super) fn merge_tag(tag: Tag, matroska_tag: MatroskaTag) -> MatroskaTag {
todo!()
pub(super) fn merge_tag(tag: Tag, mut matroska_tag: MatroskaTag) -> MatroskaTag {
for item in tag.items {
let Some((simple_tag, target_type)) = simple_tag_for_item(item) else {
continue;
};
let tag = matroska_tag.get_or_insert_tag_for_type(target_type);
tag.simple_tags.push(simple_tag);
}
matroska_tag
}
fn simple_tag_for_item(item: TagItem) -> Option<(SimpleTag<'static>, TargetType)> {
let TagItem {
mut lang,
item_key,
item_value: ItemValue::Text(text) | ItemValue::Locator(text),
..
} = item
else {
return None;
};
let Some((target_type, simple_tag_name)) = REVERSE_MAPPINGS.get(&item_key) else {
return None;
};
// Matroska uses "und" for unknown languages
if lang == *b"XXX" {
lang = *b"und";
}
let lang_str = std::str::from_utf8(lang.as_slice()).unwrap_or("und");
let mut simple_tag = SimpleTag::new(simple_tag_name.to_string(), text);
simple_tag.language = Language::Iso639_2(lang_str.to_string());
Some((simple_tag, *target_type))
}

View file

@ -4,6 +4,8 @@ mod simple_tag;
mod tag;
mod tag_name;
mod target;
#[cfg(test)]
mod tests;
mod write;
pub use attached_file::*;
@ -70,26 +72,12 @@ pub struct MatroskaTagKey<'a>(TargetType, Cow<'a, str>);
impl MatroskaTag {
fn get(&self, key: MatroskaTagKey<'_>) -> Option<&SimpleTag<'_>> {
fn tag_matches_target(tag: &Tag<'_>, target_type: TargetType) -> bool {
let Some(target) = &tag.target else {
// An empty target is implicitly `Album`
return target_type == TargetType::Album;
};
target.is_candidate_for_type(target_type)
}
let MatroskaTagKey(target, key) = key;
let applicable_tags = self
.tags
.iter()
.filter(|tag| tag_matches_target(tag, target));
let applicable_tags = self.tags.iter().filter(|tag| tag.matches_target(target));
for applicable_tag in applicable_tags {
for item in applicable_tag.simple_tags.iter() {
if item.name == key
&& (item.language.is_none()
|| matches!(&item.language, Some(Language::Iso639_2(l)) if l == "und"))
if item.name == key && matches!(&item.language, Language::Iso639_2(l) if l == "und")
{
return Some(item);
}
@ -99,6 +87,33 @@ impl MatroskaTag {
None
}
fn get_or_insert_tag_for_type<'a>(
&'a mut self,
target_type: TargetType,
) -> &'a mut Tag<'static> {
let mut pos = None;
if let Some(applicable_tag_pos) = self
.tags
.iter()
.position(|tag| tag.matches_target(target_type))
{
pos = Some(applicable_tag_pos);
}
if pos.is_none() {
pos = Some(self.tags.len());
let mut new_tag = Tag::default();
if target_type != TargetType::Album {
new_tag.target = Some(Target::from(target_type));
}
self.tags.push(new_tag);
}
self.tags.get_mut(pos.unwrap()).unwrap()
}
fn get_str(&self, key: MatroskaTagKey<'_>) -> Option<Cow<'_, str>> {
let simple_tag = self.get(key)?;
simple_tag.get_str().map(Cow::from)
@ -229,8 +244,12 @@ impl Accessor for MatroskaTag {
);
fn track(&self) -> Option<u32> {
// `PART_NUMBER` at level Track
todo!()
self.get(MatroskaTagKey(
TargetType::Track,
Cow::Borrowed("PART_NUMBER"),
))
.and_then(SimpleTag::get_str)
.and_then(|val| val.parse::<u32>().ok())
}
fn set_track(&mut self, _value: u32) {
@ -242,8 +261,12 @@ impl Accessor for MatroskaTag {
}
fn track_total(&self) -> Option<u32> {
// `TOTAL_PARTS` at level album
todo!()
self.get(MatroskaTagKey(
TargetType::Album,
Cow::Borrowed("TOTAL_PARTS"),
))
.and_then(SimpleTag::get_str)
.and_then(|val| val.parse::<u32>().ok())
}
fn set_track_total(&mut self, _value: u32) {
@ -318,19 +341,6 @@ impl TagExt for MatroskaTag {
todo!()
}
fn remove_from_path<P: AsRef<Path>>(&self, _path: P) -> std::result::Result<(), Self::Err> {
todo!()
}
fn remove_from<F>(&self, _file: &mut F) -> std::result::Result<(), Self::Err>
where
F: FileLike,
LoftyError: From<<F as Truncate>::Error>,
LoftyError: From<<F as Length>::Error>,
{
todo!()
}
fn clear(&mut self) {
self.tags.clear();
self.attached_files.clear();

View file

@ -1,7 +1,7 @@
use std::borrow::Cow;
use crate::tag::ItemValue;
use std::borrow::Cow;
/// The language of a [`SimpleTag`] or chapter
///
/// Notes:
@ -157,7 +157,7 @@ pub struct SimpleTag<'a> {
/// The language of the tag
///
/// See [`Language`] for more information.
pub language: Option<Language>,
pub language: Language,
/// Whether [`language`] is the default/original language to use
///
/// This is used when multiple languages are present in a file. Otherwise, this
@ -188,7 +188,7 @@ impl<'a> SimpleTag<'a> {
{
Self {
name: name.into(),
language: None,
language: Language::default(),
default: false,
value: Some(value.into()),
}
@ -241,7 +241,7 @@ impl<'a> SimpleTag<'a> {
// Used in conversions
pub(super) const TOMBSTONE_SIMPLE_TAG: SimpleTag<'static> = SimpleTag {
name: Cow::Borrowed(""),
language: None,
language: Language::Iso639_2(String::new()),
default: false,
value: None,
};

View file

@ -1,4 +1,5 @@
use super::{Language, SimpleTag, Target};
use super::simple_tag::SimpleTag;
use super::target::{Target, TargetDescriptor, TargetType};
/// A single metadata descriptor.
///
@ -24,7 +25,7 @@ pub struct Tag<'a> {
}
impl<'a> Tag<'a> {
/// Get all [`SimpleTag`]s with `name` and `language`
/// Get all [`SimpleTag`]s with `name`
///
/// # Example
///
@ -40,18 +41,12 @@ impl<'a> Tag<'a> {
/// ],
/// };
///
/// assert_eq!(tag.get("TITLE", None).count(), 1);
/// assert_eq!(tag.get("ARTIST", None).count(), 1);
/// assert_eq!(tag.get("SOMETHING_ELSE", None).count(), 0);
/// assert_eq!(tag.get("TITLE").count(), 1);
/// assert_eq!(tag.get("ARTIST").count(), 1);
/// assert_eq!(tag.get("SOMETHING_ELSE").count(), 0);
/// ```
pub fn get(
&'a self,
name: &'a str,
language: Option<Language>,
) -> impl Iterator<Item = &'a SimpleTag<'a>> {
self.simple_tags
.iter()
.filter(move |tag| tag.name == name && tag.language == language)
pub fn get(&'a self, name: &'a str) -> impl Iterator<Item = &'a SimpleTag<'a>> {
self.simple_tags.iter().filter(move |tag| tag.name == name)
}
/// Get the number of simple tags in this tag.
@ -92,6 +87,18 @@ impl<'a> Tag<'a> {
self.simple_tags.is_empty()
}
/// Whether the tag can be used solely by the TargetType (its target is not bound to any uids)
///
/// This is used by `MatroskaTag::get` to find applicable tags for `Accessor` methods
pub(crate) fn matches_target(&self, target_type: TargetType) -> bool {
let Some(target) = &self.target else {
// An empty target is implicitly `Album`
return target_type == TargetType::Album;
};
target.is_candidate_for_type(target_type)
}
pub(crate) fn into_owned(self) -> Tag<'static> {
Tag {
target: self.target,

View file

@ -131,3 +131,37 @@ impl Target {
|| self.attachment_uids.is_some()
}
}
/// Used to simplify conversions when writing a generic `Tag`, where extra Target information
/// will, of course, not be available.
pub(crate) enum TargetDescriptor<'a> {
Basic(TargetType),
Full(&'a Target),
}
impl TargetDescriptor<'_> {
pub(crate) fn target_type(&self) -> TargetType {
match self {
Self::Basic(ty) => *ty,
Self::Full(target) => target.target_type,
}
}
pub(crate) fn is_empty_candidate(&self) -> bool {
match self {
Self::Basic(ty) if *ty == TargetType::Album => true,
Self::Full(target) => target.is_empty_candidate(),
_ => false,
}
}
}
impl<'a> From<&'a Target> for TargetDescriptor<'a> {
fn from(target: &'a Target) -> Self {
if !target.has_uids() {
return TargetDescriptor::Basic(target.target_type);
}
TargetDescriptor::Full(target)
}
}

View file

@ -0,0 +1,22 @@
use crate::ebml::MatroskaTag;
use crate::prelude::ItemKey;
use crate::tag::{Accessor, Tag, TagType};
#[test_log::test]
fn tag_to_matroska_tag() {
let mut tag = Tag::new(TagType::Matroska);
tag.insert_text(ItemKey::TrackArtist, String::from("Foo artist"));
tag.insert_text(ItemKey::TrackTitle, String::from("Bar title"));
tag.insert_text(ItemKey::AlbumTitle, String::from("Baz album"));
tag.insert_text(ItemKey::TrackNumber, String::from("1"));
tag.insert_text(ItemKey::TrackTotal, String::from("2"));
let matroska_tag: MatroskaTag = tag.into();
assert_eq!(matroska_tag.artist().as_deref(), Some("Foo artist"));
assert_eq!(matroska_tag.title().as_deref(), Some("Bar title"));
assert_eq!(matroska_tag.album().as_deref(), Some("Baz album"));
assert_eq!(matroska_tag.track(), Some(1));
assert_eq!(matroska_tag.track_total(), Some(2));
}

View file

@ -20,23 +20,19 @@ impl WriteableElement for SimpleTag<'_> {
let mut element_children = Vec::new();
write_element(ctx, TagName_ID, &self.name.as_ref(), &mut element_children)?;
if let Some(lang) = &self.language {
match lang {
Language::Iso639_2(iso_639_2) => write_element(
ctx,
TagLanguage_ID,
&iso_639_2.as_str(),
&mut element_children,
)?,
Language::Bcp47(bcp47) => write_element(
ctx,
TagLanguageBcp47_ID,
&bcp47.as_str(),
&mut element_children,
)?,
}
} else {
write_element(ctx, TagLanguage_ID, &"und", &mut element_children)?;
match &self.language {
Language::Iso639_2(iso_639_2) => write_element(
ctx,
TagLanguage_ID,
&iso_639_2.as_str(),
&mut element_children,
)?,
Language::Bcp47(bcp47) => write_element(
ctx,
TagLanguageBcp47_ID,
&bcp47.as_str(),
&mut element_children,
)?,
}
write_element(ctx, TagDefault_ID, &self.default, &mut element_children)?;

View file

@ -1,5 +1,5 @@
use crate::ebml::tag::write::{write_element, EbmlWriteExt, ElementWriterCtx, WriteableElement};
use crate::ebml::{ElementId, Target, TargetType, VInt};
use crate::ebml::{ElementId, TargetDescriptor, TargetType, VInt};
use crate::io::FileLike;
const TargetTypeValue_ID: ElementId = ElementId(0x68CA);
@ -9,7 +9,7 @@ const TagEditionUID_ID: ElementId = ElementId(0x63C9);
const TagChapterUID_ID: ElementId = ElementId(0x63C4);
const TagAttachmentUID_ID: ElementId = ElementId(0x63C6);
impl WriteableElement for Target {
impl WriteableElement for TargetDescriptor<'_> {
const ID: ElementId = ElementId(0x63C0);
fn write_element<F: FileLike>(
@ -24,7 +24,9 @@ impl WriteableElement for Target {
}
let mut element_children = Vec::new();
if self.target_type == TargetType::Album {
let target_type = self.target_type();
if target_type == TargetType::Album {
write_element(
ctx,
TargetTypeValue_ID,
@ -32,39 +34,41 @@ impl WriteableElement for Target {
&mut element_children,
)?;
} else {
let vint = VInt::<u64>::try_from(self.target_type as u64)?;
let vint = VInt::<u64>::try_from(target_type as u64)?;
write_element(ctx, TargetTypeValue_ID, &vint, &mut element_children)?;
}
if let Some(name) = &self.name {
write_element(ctx, TargetType_ID, &name.as_str(), &mut element_children)?;
}
if let Some(track_uids) = &self.track_uids {
for &uid in track_uids {
let vint = VInt::<u64>::try_from(uid)?;
write_element(ctx, TagTrackUID_ID, &vint, &mut element_children)?;
if let TargetDescriptor::Full(target) = self {
if let Some(name) = &target.name {
write_element(ctx, TargetType_ID, &name.as_str(), &mut element_children)?;
}
}
if let Some(edition_uids) = &self.edition_uids {
for &uid in edition_uids {
let vint = VInt::<u64>::try_from(uid)?;
write_element(ctx, TagEditionUID_ID, &vint, &mut element_children)?;
if let Some(track_uids) = &target.track_uids {
for &uid in track_uids {
let vint = VInt::<u64>::try_from(uid)?;
write_element(ctx, TagTrackUID_ID, &vint, &mut element_children)?;
}
}
}
if let Some(chapter_uids) = &self.chapter_uids {
for &uid in chapter_uids {
let vint = VInt::<u64>::try_from(uid)?;
write_element(ctx, TagChapterUID_ID, &vint, &mut element_children)?;
if let Some(edition_uids) = &target.edition_uids {
for &uid in edition_uids {
let vint = VInt::<u64>::try_from(uid)?;
write_element(ctx, TagEditionUID_ID, &vint, &mut element_children)?;
}
}
}
if let Some(attachment_uids) = &self.attachment_uids {
for &uid in attachment_uids {
let vint = VInt::<u64>::try_from(uid)?;
write_element(ctx, TagAttachmentUID_ID, &vint, &mut element_children)?;
if let Some(chapter_uids) = &target.chapter_uids {
for &uid in chapter_uids {
let vint = VInt::<u64>::try_from(uid)?;
write_element(ctx, TagChapterUID_ID, &vint, &mut element_children)?;
}
}
if let Some(attachment_uids) = &target.attachment_uids {
for &uid in attachment_uids {
let vint = VInt::<u64>::try_from(uid)?;
write_element(ctx, TagAttachmentUID_ID, &vint, &mut element_children)?;
}
}
}
@ -77,6 +81,7 @@ impl WriteableElement for Target {
#[cfg(test)]
mod tests {
use super::*;
use crate::ebml::Target;
use std::io::Cursor;
@ -85,7 +90,8 @@ mod tests {
let target = Target::default();
let mut buf = Cursor::new(Vec::new());
target
let target_descriptor = TargetDescriptor::from(&target);
target_descriptor
.write_element(
ElementWriterCtx {
max_id_len: 4,