mirror of
https://github.com/Serial-ATA/lofty-rs
synced 2024-11-10 06:34:18 +00:00
ID3v2: Support building TIPL frames on Tag
merge
This commit is contained in:
parent
be749a48a3
commit
07ddca3762
4 changed files with 140 additions and 6 deletions
|
@ -8,9 +8,11 @@ use crate::id3::v2::items::{
|
|||
AttachedPictureFrame, CommentFrame, ExtendedTextFrame, ExtendedUrlFrame, TextInformationFrame,
|
||||
UniqueFileIdentifierFrame, UnsynchronizedTextFrame, UrlLinkFrame,
|
||||
};
|
||||
use crate::id3::v2::util::mappings::TIPL_MAPPINGS;
|
||||
use crate::id3::v2::util::pairs::{
|
||||
format_number_pair, set_number, NUMBER_PAIR_KEYS, NUMBER_PAIR_SEPARATOR,
|
||||
};
|
||||
use crate::id3::v2::KeyValueFrame;
|
||||
use crate::picture::{Picture, PictureType, TOMBSTONE_PICTURE};
|
||||
use crate::tag::item::{ItemKey, ItemValue, TagItem};
|
||||
use crate::tag::{try_parse_year, Tag, TagType};
|
||||
|
@ -418,6 +420,13 @@ impl Id3v2Tag {
|
|||
self.frames.drain(..split_idx)
|
||||
}
|
||||
|
||||
fn take_first(&mut self, id: &FrameId<'_>) -> Option<Frame<'static>> {
|
||||
self.frames
|
||||
.iter()
|
||||
.position(|f| &f.id == id)
|
||||
.map(|pos| self.frames.remove(pos))
|
||||
}
|
||||
|
||||
/// Retains [`Frame`]s by evaluating the predicate
|
||||
pub fn retain<P>(&mut self, predicate: P)
|
||||
where
|
||||
|
@ -1008,6 +1017,28 @@ impl SplitTag for Id3v2Tag {
|
|||
}
|
||||
false // Frame consumed
|
||||
},
|
||||
(
|
||||
"TIPL",
|
||||
FrameValue::KeyValue(KeyValueFrame {
|
||||
key_value_pairs, ..
|
||||
}),
|
||||
) => {
|
||||
key_value_pairs.retain_mut(|(key, value)| {
|
||||
for (item_key, tipl_key) in TIPL_MAPPINGS.iter() {
|
||||
if key == *tipl_key {
|
||||
tag.items.push(TagItem::new(
|
||||
item_key.clone(),
|
||||
ItemValue::Text(core::mem::take(value)),
|
||||
));
|
||||
return false; // This key-value pair is consumed
|
||||
}
|
||||
}
|
||||
|
||||
true // Keep key-value pair
|
||||
});
|
||||
|
||||
!key_value_pairs.is_empty() // Frame is consumed if we consumed all items
|
||||
},
|
||||
// Store TXXX/WXXX frames by their descriptions, rather than their IDs
|
||||
(
|
||||
"TXXX",
|
||||
|
@ -1207,10 +1238,8 @@ impl MergeTag for SplitTagRemainder {
|
|||
&ItemKey::Conductor,
|
||||
&ItemKey::Writer,
|
||||
&ItemKey::Director,
|
||||
&ItemKey::InvolvedPeople,
|
||||
&ItemKey::Lyricist,
|
||||
&ItemKey::MusicianCredits,
|
||||
&ItemKey::Producer,
|
||||
&ItemKey::InternetRadioStationName,
|
||||
&ItemKey::InternetRadioStationOwner,
|
||||
&ItemKey::Remixer,
|
||||
|
@ -1265,6 +1294,45 @@ impl MergeTag for SplitTagRemainder {
|
|||
merged.frames.push(frame);
|
||||
};
|
||||
|
||||
// TIPL key-value mappings
|
||||
'tipl: {
|
||||
let mut key_value_pairs = Vec::new();
|
||||
for (item_key, tipl_key) in TIPL_MAPPINGS {
|
||||
for value in tag.take_strings(item_key) {
|
||||
key_value_pairs.push(((*tipl_key).to_string(), value));
|
||||
}
|
||||
}
|
||||
|
||||
if key_value_pairs.is_empty() {
|
||||
break 'tipl;
|
||||
}
|
||||
|
||||
// Check for an existing TIPL frame, and simply extend the existing list
|
||||
// to retain the current `TextEncoding` and `FrameFlags`.
|
||||
let existing_tipl = merged.take_first(&FrameId::Valid(Cow::Borrowed("TIPL")));
|
||||
if let Some(mut tipl_frame) = existing_tipl {
|
||||
if let FrameValue::KeyValue(KeyValueFrame {
|
||||
key_value_pairs: ref mut existing,
|
||||
..
|
||||
}) = &mut tipl_frame.value
|
||||
{
|
||||
existing.extend(key_value_pairs);
|
||||
}
|
||||
|
||||
merged.frames.push(tipl_frame);
|
||||
break 'tipl;
|
||||
}
|
||||
|
||||
merged.frames.push(Frame {
|
||||
id: FrameId::Valid(Cow::Borrowed("TIPL")),
|
||||
value: FrameValue::KeyValue(KeyValueFrame {
|
||||
key_value_pairs,
|
||||
encoding: TextEncoding::UTF8,
|
||||
}),
|
||||
flags: FrameFlags::default(),
|
||||
});
|
||||
}
|
||||
|
||||
// Insert all remaining items as single frames and deduplicate as needed
|
||||
for item in tag.items {
|
||||
merged.insert_item(item);
|
||||
|
@ -1376,10 +1444,11 @@ mod tests {
|
|||
use crate::id3::v2::header::{Id3v2Header, Id3v2Version};
|
||||
use crate::id3::v2::items::{ExtendedUrlFrame, Popularimeter, UniqueFileIdentifierFrame};
|
||||
use crate::id3::v2::tag::{filter_comment_frame_by_description, new_text_frame};
|
||||
use crate::id3::v2::util::mappings::TIPL_MAPPINGS;
|
||||
use crate::id3::v2::util::pairs::DEFAULT_NUMBER_IN_PAIR;
|
||||
use crate::id3::v2::{
|
||||
AttachedPictureFrame, CommentFrame, ExtendedTextFrame, Frame, FrameFlags, FrameId,
|
||||
FrameValue, Id3v2Tag, TextInformationFrame, UrlLinkFrame,
|
||||
FrameValue, Id3v2Tag, KeyValueFrame, TextInformationFrame, UrlLinkFrame,
|
||||
};
|
||||
use crate::tag::utils::test_utils::read_path;
|
||||
use crate::util::text::TextEncoding;
|
||||
|
@ -2649,4 +2718,62 @@ mod tests {
|
|||
assert_eq!(genres.next(), Some("Cover"));
|
||||
assert_eq!(genres.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tipl_round_trip() {
|
||||
let mut tag = Id3v2Tag::default();
|
||||
let mut tipl = KeyValueFrame {
|
||||
encoding: TextEncoding::UTF8,
|
||||
key_value_pairs: Vec::new(),
|
||||
};
|
||||
|
||||
// Add all supported keys
|
||||
for (_, key) in TIPL_MAPPINGS {
|
||||
tipl.key_value_pairs
|
||||
.push((String::from(*key), String::from("Serial-ATA")));
|
||||
}
|
||||
|
||||
// Add one unsupported key
|
||||
tipl.key_value_pairs
|
||||
.push((String::from("Foo"), String::from("Bar")));
|
||||
|
||||
tag.insert(
|
||||
Frame::new(
|
||||
"TIPL",
|
||||
FrameValue::KeyValue(tipl.clone()),
|
||||
FrameFlags::default(),
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
let (split_remainder, split_tag) = tag.split_tag();
|
||||
assert_eq!(split_remainder.0.len(), 1); // "Foo" is not supported
|
||||
assert_eq!(split_tag.len(), TIPL_MAPPINGS.len()); // All supported keys are present
|
||||
|
||||
for (item_key, _) in TIPL_MAPPINGS {
|
||||
assert_eq!(
|
||||
split_tag
|
||||
.get(item_key)
|
||||
.map(TagItem::value)
|
||||
.and_then(ItemValue::text),
|
||||
Some("Serial-ATA")
|
||||
);
|
||||
}
|
||||
|
||||
let mut id3v2 = split_remainder.merge_tag(split_tag);
|
||||
assert_eq!(id3v2.frames.len(), 1);
|
||||
match &mut id3v2.frames[..] {
|
||||
[Frame {
|
||||
id: _,
|
||||
value: FrameValue::KeyValue(tipl2),
|
||||
flags: _,
|
||||
}] => {
|
||||
// Order will not be the same, so we have to sort first
|
||||
tipl.key_value_pairs.sort();
|
||||
tipl2.key_value_pairs.sort();
|
||||
assert_eq!(tipl, *tipl2);
|
||||
},
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
9
src/id3/v2/util/mappings.rs
Normal file
9
src/id3/v2/util/mappings.rs
Normal file
|
@ -0,0 +1,9 @@
|
|||
use crate::tag::item::ItemKey;
|
||||
|
||||
pub(crate) const TIPL_MAPPINGS: &[(ItemKey, &str)] = &[
|
||||
(ItemKey::Producer, "producer"),
|
||||
(ItemKey::Arranger, "arranger"),
|
||||
(ItemKey::Engineer, "engineer"),
|
||||
(ItemKey::MixDj, "DJ-mix"),
|
||||
(ItemKey::MixEngineer, "mix"),
|
||||
];
|
|
@ -1,5 +1,6 @@
|
|||
//! Utilities for working with ID3v2 tags
|
||||
|
||||
pub(crate) mod mappings;
|
||||
pub(crate) mod pairs;
|
||||
pub mod synchsafe;
|
||||
pub mod upgrade;
|
||||
|
|
|
@ -157,10 +157,8 @@ gen_map!(
|
|||
"TCOM" => Composer,
|
||||
"TPE3" => Conductor,
|
||||
"DIRECTOR" => Director,
|
||||
"TIPL" => InvolvedPeople,
|
||||
"TEXT" => Lyricist,
|
||||
"TMCL" => MusicianCredits,
|
||||
"IPRO" => Producer,
|
||||
"TPUB" => Publisher,
|
||||
"TPUB" => Label,
|
||||
"TRSN" => InternetRadioStationName,
|
||||
|
@ -522,7 +520,6 @@ gen_item_keys!(
|
|||
Conductor,
|
||||
Director,
|
||||
Engineer,
|
||||
InvolvedPeople,
|
||||
Lyricist,
|
||||
MixDj,
|
||||
MixEngineer,
|
||||
|
|
Loading…
Reference in a new issue