Skip to content

Commit e37fe44

Browse files
committed
EBML: Add generic tag conversion test
1 parent 62ad31b commit e37fe44

File tree

8 files changed

+223
-106
lines changed

8 files changed

+223
-106
lines changed

lofty/src/ebml/tag/generic.rs

+53-11
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
55
use super::{Language, MatroskaTag, SimpleTag, TargetType, TOMBSTONE_SIMPLE_TAG};
66
use crate::tag::items::Lang;
7-
use crate::tag::{ItemKey, Tag, TagItem, TagType};
7+
use crate::tag::{ItemKey, ItemValue, Tag, TagItem, TagType};
88

99
use std::collections::HashMap;
1010
use std::sync::LazyLock;
@@ -140,7 +140,9 @@ pub(super) fn split_tag(mut matroska_tag: MatroskaTag) -> (MatroskaTag, Tag) {
140140
let mut tag = Tag::new(TagType::Matroska);
141141

142142
// TODO: Pictures, can they be handled in a generic way?
143-
// What about the uid and referral?
143+
// - What about the uid and referral?
144+
// - It seems like the "standard" way of adding cover art is to name it "cover.{ext}"
145+
// - Maybe only support front covers? who knows.
144146

145147
matroska_tag.tags.retain_mut(|t| {
146148
let target_type = match &t.target {
@@ -168,15 +170,17 @@ fn split_simple_tags(
168170
tag: &mut Tag,
169171
) -> bool {
170172
let lang: Lang;
171-
match &simple_tag.language {
172-
Some(Language::Iso639_2(l)) if l.len() == 3 => {
173-
lang = l.as_bytes().try_into().unwrap(); // Infallible
174-
},
175-
None => lang = *b"und",
176-
// `Lang` doesn't support anything outside of a 3 character ISO-639-2 code.
177-
_ => return TAG_RETAINED,
173+
let Language::Iso639_2(l) = &simple_tag.language else {
174+
return TAG_RETAINED;
175+
};
176+
177+
// `Lang` doesn't support anything outside of a 3 character ISO-639-2 code.
178+
if l.len() != 3 {
179+
return TAG_CONSUMED;
178180
}
179181

182+
lang = l.as_bytes().try_into().unwrap(); // Infallible
183+
180184
let Some(item_key) = MAPPINGS.get(&(target_type, &*simple_tag.name)).cloned() else {
181185
return TAG_RETAINED;
182186
};
@@ -197,6 +201,44 @@ fn split_simple_tags(
197201
return TAG_CONSUMED;
198202
}
199203

200-
pub(super) fn merge_tag(tag: Tag, matroska_tag: MatroskaTag) -> MatroskaTag {
201-
todo!()
204+
pub(super) fn merge_tag(tag: Tag, mut matroska_tag: MatroskaTag) -> MatroskaTag {
205+
for item in tag.items {
206+
let Some((simple_tag, target_type)) = simple_tag_for_item(item) else {
207+
continue;
208+
};
209+
210+
let tag = matroska_tag.get_or_insert_tag_for_type(target_type);
211+
212+
tag.simple_tags.push(simple_tag);
213+
}
214+
215+
matroska_tag
216+
}
217+
218+
fn simple_tag_for_item(item: TagItem) -> Option<(SimpleTag<'static>, TargetType)> {
219+
let TagItem {
220+
mut lang,
221+
item_key,
222+
item_value: ItemValue::Text(text) | ItemValue::Locator(text),
223+
..
224+
} = item
225+
else {
226+
return None;
227+
};
228+
229+
let Some((target_type, simple_tag_name)) = REVERSE_MAPPINGS.get(&item_key) else {
230+
return None;
231+
};
232+
233+
// Matroska uses "und" for unknown languages
234+
if lang == *b"XXX" {
235+
lang = *b"und";
236+
}
237+
238+
let lang_str = std::str::from_utf8(lang.as_slice()).unwrap_or("und");
239+
240+
let mut simple_tag = SimpleTag::new(simple_tag_name.to_string(), text);
241+
simple_tag.language = Language::Iso639_2(lang_str.to_string());
242+
243+
Some((simple_tag, *target_type))
202244
}

lofty/src/ebml/tag/mod.rs

+43-33
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ mod simple_tag;
44
mod tag;
55
mod tag_name;
66
mod target;
7+
#[cfg(test)]
8+
mod tests;
79
mod write;
810

911
pub use attached_file::*;
@@ -70,26 +72,12 @@ pub struct MatroskaTagKey<'a>(TargetType, Cow<'a, str>);
7072

7173
impl MatroskaTag {
7274
fn get(&self, key: MatroskaTagKey<'_>) -> Option<&SimpleTag<'_>> {
73-
fn tag_matches_target(tag: &Tag<'_>, target_type: TargetType) -> bool {
74-
let Some(target) = &tag.target else {
75-
// An empty target is implicitly `Album`
76-
return target_type == TargetType::Album;
77-
};
78-
79-
target.is_candidate_for_type(target_type)
80-
}
81-
8275
let MatroskaTagKey(target, key) = key;
8376

84-
let applicable_tags = self
85-
.tags
86-
.iter()
87-
.filter(|tag| tag_matches_target(tag, target));
77+
let applicable_tags = self.tags.iter().filter(|tag| tag.matches_target(target));
8878
for applicable_tag in applicable_tags {
8979
for item in applicable_tag.simple_tags.iter() {
90-
if item.name == key
91-
&& (item.language.is_none()
92-
|| matches!(&item.language, Some(Language::Iso639_2(l)) if l == "und"))
80+
if item.name == key && matches!(&item.language, Language::Iso639_2(l) if l == "und")
9381
{
9482
return Some(item);
9583
}
@@ -99,6 +87,33 @@ impl MatroskaTag {
9987
None
10088
}
10189

90+
fn get_or_insert_tag_for_type<'a>(
91+
&'a mut self,
92+
target_type: TargetType,
93+
) -> &'a mut Tag<'static> {
94+
let mut pos = None;
95+
if let Some(applicable_tag_pos) = self
96+
.tags
97+
.iter()
98+
.position(|tag| tag.matches_target(target_type))
99+
{
100+
pos = Some(applicable_tag_pos);
101+
}
102+
103+
if pos.is_none() {
104+
pos = Some(self.tags.len());
105+
106+
let mut new_tag = Tag::default();
107+
if target_type != TargetType::Album {
108+
new_tag.target = Some(Target::from(target_type));
109+
}
110+
111+
self.tags.push(new_tag);
112+
}
113+
114+
self.tags.get_mut(pos.unwrap()).unwrap()
115+
}
116+
102117
fn get_str(&self, key: MatroskaTagKey<'_>) -> Option<Cow<'_, str>> {
103118
let simple_tag = self.get(key)?;
104119
simple_tag.get_str().map(Cow::from)
@@ -229,8 +244,12 @@ impl Accessor for MatroskaTag {
229244
);
230245

231246
fn track(&self) -> Option<u32> {
232-
// `PART_NUMBER` at level Track
233-
todo!()
247+
self.get(MatroskaTagKey(
248+
TargetType::Track,
249+
Cow::Borrowed("PART_NUMBER"),
250+
))
251+
.and_then(SimpleTag::get_str)
252+
.and_then(|val| val.parse::<u32>().ok())
234253
}
235254

236255
fn set_track(&mut self, _value: u32) {
@@ -242,8 +261,12 @@ impl Accessor for MatroskaTag {
242261
}
243262

244263
fn track_total(&self) -> Option<u32> {
245-
// `TOTAL_PARTS` at level album
246-
todo!()
264+
self.get(MatroskaTagKey(
265+
TargetType::Album,
266+
Cow::Borrowed("TOTAL_PARTS"),
267+
))
268+
.and_then(SimpleTag::get_str)
269+
.and_then(|val| val.parse::<u32>().ok())
247270
}
248271

249272
fn set_track_total(&mut self, _value: u32) {
@@ -318,19 +341,6 @@ impl TagExt for MatroskaTag {
318341
todo!()
319342
}
320343

321-
fn remove_from_path<P: AsRef<Path>>(&self, _path: P) -> std::result::Result<(), Self::Err> {
322-
todo!()
323-
}
324-
325-
fn remove_from<F>(&self, _file: &mut F) -> std::result::Result<(), Self::Err>
326-
where
327-
F: FileLike,
328-
LoftyError: From<<F as Truncate>::Error>,
329-
LoftyError: From<<F as Length>::Error>,
330-
{
331-
todo!()
332-
}
333-
334344
fn clear(&mut self) {
335345
self.tags.clear();
336346
self.attached_files.clear();

lofty/src/ebml/tag/simple_tag.rs

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
use std::borrow::Cow;
2-
31
use crate::tag::ItemValue;
42

3+
use std::borrow::Cow;
4+
55
/// The language of a [`SimpleTag`] or chapter
66
///
77
/// Notes:
@@ -157,7 +157,7 @@ pub struct SimpleTag<'a> {
157157
/// The language of the tag
158158
///
159159
/// See [`Language`] for more information.
160-
pub language: Option<Language>,
160+
pub language: Language,
161161
/// Whether [`language`] is the default/original language to use
162162
///
163163
/// This is used when multiple languages are present in a file. Otherwise, this
@@ -188,7 +188,7 @@ impl<'a> SimpleTag<'a> {
188188
{
189189
Self {
190190
name: name.into(),
191-
language: None,
191+
language: Language::default(),
192192
default: false,
193193
value: Some(value.into()),
194194
}
@@ -241,7 +241,7 @@ impl<'a> SimpleTag<'a> {
241241
// Used in conversions
242242
pub(super) const TOMBSTONE_SIMPLE_TAG: SimpleTag<'static> = SimpleTag {
243243
name: Cow::Borrowed(""),
244-
language: None,
244+
language: Language::Iso639_2(String::new()),
245245
default: false,
246246
value: None,
247247
};

lofty/src/ebml/tag/tag.rs

+20-13
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
use super::{Language, SimpleTag, Target};
1+
use super::simple_tag::SimpleTag;
2+
use super::target::{Target, TargetDescriptor, TargetType};
23

34
/// A single metadata descriptor.
45
///
@@ -24,7 +25,7 @@ pub struct Tag<'a> {
2425
}
2526

2627
impl<'a> Tag<'a> {
27-
/// Get all [`SimpleTag`]s with `name` and `language`
28+
/// Get all [`SimpleTag`]s with `name`
2829
///
2930
/// # Example
3031
///
@@ -40,18 +41,12 @@ impl<'a> Tag<'a> {
4041
/// ],
4142
/// };
4243
///
43-
/// assert_eq!(tag.get("TITLE", None).count(), 1);
44-
/// assert_eq!(tag.get("ARTIST", None).count(), 1);
45-
/// assert_eq!(tag.get("SOMETHING_ELSE", None).count(), 0);
44+
/// assert_eq!(tag.get("TITLE").count(), 1);
45+
/// assert_eq!(tag.get("ARTIST").count(), 1);
46+
/// assert_eq!(tag.get("SOMETHING_ELSE").count(), 0);
4647
/// ```
47-
pub fn get(
48-
&'a self,
49-
name: &'a str,
50-
language: Option<Language>,
51-
) -> impl Iterator<Item = &'a SimpleTag<'a>> {
52-
self.simple_tags
53-
.iter()
54-
.filter(move |tag| tag.name == name && tag.language == language)
48+
pub fn get(&'a self, name: &'a str) -> impl Iterator<Item = &'a SimpleTag<'a>> {
49+
self.simple_tags.iter().filter(move |tag| tag.name == name)
5550
}
5651

5752
/// Get the number of simple tags in this tag.
@@ -92,6 +87,18 @@ impl<'a> Tag<'a> {
9287
self.simple_tags.is_empty()
9388
}
9489

90+
/// Whether the tag can be used solely by the TargetType (its target is not bound to any uids)
91+
///
92+
/// This is used by `MatroskaTag::get` to find applicable tags for `Accessor` methods
93+
pub(crate) fn matches_target(&self, target_type: TargetType) -> bool {
94+
let Some(target) = &self.target else {
95+
// An empty target is implicitly `Album`
96+
return target_type == TargetType::Album;
97+
};
98+
99+
target.is_candidate_for_type(target_type)
100+
}
101+
95102
pub(crate) fn into_owned(self) -> Tag<'static> {
96103
Tag {
97104
target: self.target,

lofty/src/ebml/tag/target.rs

+34
Original file line numberDiff line numberDiff line change
@@ -131,3 +131,37 @@ impl Target {
131131
|| self.attachment_uids.is_some()
132132
}
133133
}
134+
135+
/// Used to simplify conversions when writing a generic `Tag`, where extra Target information
136+
/// will, of course, not be available.
137+
pub(crate) enum TargetDescriptor<'a> {
138+
Basic(TargetType),
139+
Full(&'a Target),
140+
}
141+
142+
impl TargetDescriptor<'_> {
143+
pub(crate) fn target_type(&self) -> TargetType {
144+
match self {
145+
Self::Basic(ty) => *ty,
146+
Self::Full(target) => target.target_type,
147+
}
148+
}
149+
150+
pub(crate) fn is_empty_candidate(&self) -> bool {
151+
match self {
152+
Self::Basic(ty) if *ty == TargetType::Album => true,
153+
Self::Full(target) => target.is_empty_candidate(),
154+
_ => false,
155+
}
156+
}
157+
}
158+
159+
impl<'a> From<&'a Target> for TargetDescriptor<'a> {
160+
fn from(target: &'a Target) -> Self {
161+
if !target.has_uids() {
162+
return TargetDescriptor::Basic(target.target_type);
163+
}
164+
165+
TargetDescriptor::Full(target)
166+
}
167+
}

lofty/src/ebml/tag/tests.rs

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
use crate::ebml::MatroskaTag;
2+
use crate::prelude::ItemKey;
3+
use crate::tag::{Accessor, Tag, TagType};
4+
5+
#[test_log::test]
6+
fn tag_to_matroska_tag() {
7+
let mut tag = Tag::new(TagType::Matroska);
8+
9+
tag.insert_text(ItemKey::TrackArtist, String::from("Foo artist"));
10+
tag.insert_text(ItemKey::TrackTitle, String::from("Bar title"));
11+
tag.insert_text(ItemKey::AlbumTitle, String::from("Baz album"));
12+
tag.insert_text(ItemKey::TrackNumber, String::from("1"));
13+
tag.insert_text(ItemKey::TrackTotal, String::from("2"));
14+
15+
let matroska_tag: MatroskaTag = tag.into();
16+
17+
assert_eq!(matroska_tag.artist().as_deref(), Some("Foo artist"));
18+
assert_eq!(matroska_tag.title().as_deref(), Some("Bar title"));
19+
assert_eq!(matroska_tag.album().as_deref(), Some("Baz album"));
20+
assert_eq!(matroska_tag.track(), Some(1));
21+
assert_eq!(matroska_tag.track_total(), Some(2));
22+
}

0 commit comments

Comments
 (0)