Split and rejoin tags for read/modify/write round trips

This commit is contained in:
Uwe Klotz 2023-01-18 23:01:39 +01:00 committed by Alex
parent 19fe23cbeb
commit 2b562c4a4b
12 changed files with 251 additions and 127 deletions

View file

@ -6,7 +6,7 @@ use crate::ape::tag::item::{ApeItem, ApeItemRef};
use crate::error::{LoftyError, Result}; use crate::error::{LoftyError, Result};
use crate::tag::item::{ItemKey, ItemValue, TagItem}; use crate::tag::item::{ItemKey, ItemValue, TagItem};
use crate::tag::{Tag, TagType}; use crate::tag::{Tag, TagType};
use crate::traits::{Accessor, TagExt}; use crate::traits::{Accessor, SplitAndRejoinTag, TagExt};
use std::borrow::Cow; use std::borrow::Cow;
use std::convert::TryInto; use std::convert::TryInto;
@ -289,8 +289,8 @@ impl TagExt for ApeTag {
} }
} }
impl From<ApeTag> for Tag { impl SplitAndRejoinTag for ApeTag {
fn from(input: ApeTag) -> Self { fn split_tag(&mut self) -> Tag {
fn split_pair( fn split_pair(
content: &str, content: &str,
tag: &mut Tag, tag: &mut Tag,
@ -312,7 +312,7 @@ impl From<ApeTag> for Tag {
let mut tag = Tag::new(TagType::APE); let mut tag = Tag::new(TagType::APE);
for item in input.items { for item in std::mem::take(&mut self.items) {
let item_key = ItemKey::from_key(TagType::APE, item.key()); let item_key = ItemKey::from_key(TagType::APE, item.key());
// The text pairs need some special treatment // The text pairs need some special treatment
@ -321,13 +321,13 @@ impl From<ApeTag> for Tag {
if split_pair(val, &mut tag, ItemKey::TrackNumber, ItemKey::TrackTotal) if split_pair(val, &mut tag, ItemKey::TrackNumber, ItemKey::TrackTotal)
.is_some() => .is_some() =>
{ {
continue continue; // Item consumed
}, },
(ItemKey::DiscNumber | ItemKey::DiscTotal, ItemValue::Text(val)) (ItemKey::DiscNumber | ItemKey::DiscTotal, ItemValue::Text(val))
if split_pair(val, &mut tag, ItemKey::DiscNumber, ItemKey::DiscTotal) if split_pair(val, &mut tag, ItemKey::DiscNumber, ItemKey::DiscTotal)
.is_some() => .is_some() =>
{ {
continue continue; // Item consumed
}, },
(ItemKey::MovementNumber | ItemKey::MovementTotal, ItemValue::Text(val)) (ItemKey::MovementNumber | ItemKey::MovementTotal, ItemValue::Text(val))
if split_pair( if split_pair(
@ -338,36 +338,46 @@ impl From<ApeTag> for Tag {
) )
.is_some() => .is_some() =>
{ {
continue continue; // Item consumed
},
(k, _) => {
tag.items.push(TagItem::new(k, item.value));
}, },
(k, _) => tag.items.push(TagItem::new(k, item.value)),
} }
} }
tag tag
} }
fn rejoin_tag(&mut self, tag: Tag) {
for item in tag.items {
if let Ok(ape_item) = item.try_into() {
self.insert(ape_item)
}
}
for pic in tag.pictures {
if let Some(key) = pic.pic_type.as_ape_key() {
if let Ok(item) =
ApeItem::new(key.to_string(), ItemValue::Binary(pic.as_ape_bytes()))
{
self.insert(item)
}
}
}
}
}
impl From<ApeTag> for Tag {
fn from(mut input: ApeTag) -> Self {
input.split_tag()
}
} }
impl From<Tag> for ApeTag { impl From<Tag> for ApeTag {
fn from(input: Tag) -> Self { fn from(input: Tag) -> Self {
let mut ape_tag = Self::default(); let mut ape_tag = Self::default();
ape_tag.rejoin_tag(input);
for item in input.items {
if let Ok(ape_item) = item.try_into() {
ape_tag.insert(ape_item)
}
}
for pic in input.pictures {
if let Some(key) = pic.pic_type.as_ape_key() {
if let Ok(item) =
ApeItem::new(key.to_string(), ItemValue::Binary(pic.as_ape_bytes()))
{
ape_tag.insert(item)
}
}
}
ape_tag ape_tag
} }
} }

View file

@ -2,7 +2,7 @@ use crate::error::{LoftyError, Result};
use crate::id3::v1::constants::GENRES; use crate::id3::v1::constants::GENRES;
use crate::tag::item::{ItemKey, ItemValue, TagItem}; use crate::tag::item::{ItemKey, ItemValue, TagItem};
use crate::tag::{Tag, TagType}; use crate::tag::{Tag, TagType};
use crate::traits::{Accessor, TagExt}; use crate::traits::{Accessor, SplitAndRejoinTag, TagExt};
use std::borrow::Cow; use std::borrow::Cow;
use std::fs::{File, OpenOptions}; use std::fs::{File, OpenOptions};
@ -238,26 +238,32 @@ impl TagExt for ID3v1Tag {
} }
} }
impl From<ID3v1Tag> for Tag { impl SplitAndRejoinTag for ID3v1Tag {
fn from(input: ID3v1Tag) -> Self { fn split_tag(&mut self) -> Tag {
let mut tag = Self::new(TagType::ID3v1); let mut tag = Tag::new(TagType::ID3v1);
input.title.map(|t| tag.insert_text(ItemKey::TrackTitle, t)); self.title
input .take()
.artist .map(|t| tag.insert_text(ItemKey::TrackTitle, t));
self.artist
.take()
.map(|a| tag.insert_text(ItemKey::TrackArtist, a)); .map(|a| tag.insert_text(ItemKey::TrackArtist, a));
input.album.map(|a| tag.insert_text(ItemKey::AlbumTitle, a)); self.album
input.year.map(|y| tag.insert_text(ItemKey::Year, y)); .take()
input.comment.map(|c| tag.insert_text(ItemKey::Comment, c)); .map(|a| tag.insert_text(ItemKey::AlbumTitle, a));
self.year.take().map(|y| tag.insert_text(ItemKey::Year, y));
self.comment
.take()
.map(|c| tag.insert_text(ItemKey::Comment, c));
if let Some(t) = input.track_number { if let Some(t) = self.track_number.take() {
tag.items.push(TagItem::new( tag.items.push(TagItem::new(
ItemKey::TrackNumber, ItemKey::TrackNumber,
ItemValue::Text(t.to_string()), ItemValue::Text(t.to_string()),
)) ))
} }
if let Some(genre_index) = input.genre { if let Some(genre_index) = self.genre.take() {
if let Some(genre) = GENRES.get(genre_index as usize) { if let Some(genre) = GENRES.get(genre_index as usize) {
tag.insert_text(ItemKey::Genre, (*genre).to_string()); tag.insert_text(ItemKey::Genre, (*genre).to_string());
} }
@ -265,6 +271,16 @@ impl From<ID3v1Tag> for Tag {
tag tag
} }
fn rejoin_tag(&mut self, tag: Tag) {
*self = tag.into();
}
}
impl From<ID3v1Tag> for Tag {
fn from(mut input: ID3v1Tag) -> Self {
input.split_tag()
}
} }
impl From<Tag> for ID3v1Tag { impl From<Tag> for ID3v1Tag {

View file

@ -6,10 +6,10 @@ use crate::error::{LoftyError, Result};
use crate::id3::v2::frame::FrameRef; use crate::id3::v2::frame::FrameRef;
use crate::id3::v2::items::encoded_text_frame::EncodedTextFrame; use crate::id3::v2::items::encoded_text_frame::EncodedTextFrame;
use crate::id3::v2::items::language_frame::LanguageFrame; use crate::id3::v2::items::language_frame::LanguageFrame;
use crate::picture::{Picture, PictureType}; use crate::picture::{Picture, PictureType, TOMBSTONE_PICTURE};
use crate::tag::item::{ItemKey, ItemValue, TagItem}; use crate::tag::item::{ItemKey, ItemValue, TagItem};
use crate::tag::{Tag, TagType}; use crate::tag::{Tag, TagType};
use crate::traits::{Accessor, TagExt}; use crate::traits::{Accessor, SplitAndRejoinTag, TagExt};
use crate::util::text::TextEncoding; use crate::util::text::TextEncoding;
use std::borrow::Cow; use std::borrow::Cow;
@ -533,8 +533,8 @@ impl TagExt for ID3v2Tag {
} }
} }
impl From<ID3v2Tag> for Tag { impl SplitAndRejoinTag for ID3v2Tag {
fn from(input: ID3v2Tag) -> Self { fn split_tag(&mut self) -> Tag {
fn split_pair( fn split_pair(
content: &str, content: &str,
tag: &mut Tag, tag: &mut Tag,
@ -555,13 +555,13 @@ impl From<ID3v2Tag> for Tag {
Some(()) Some(())
} }
let mut tag = Self::new(TagType::ID3v2); let mut tag = Tag::new(TagType::ID3v2);
for frame in input.frames { self.frames.retain_mut(|frame| {
let id = frame.id; let id = &frame.id;
// The text pairs need some special treatment // The text pairs need some special treatment
match (id.as_str(), frame.value) { match (id.as_str(), &mut frame.value) {
("TRCK", FrameValue::Text { value: content, .. }) ("TRCK", FrameValue::Text { value: content, .. })
if split_pair( if split_pair(
&content, &content,
@ -571,13 +571,13 @@ impl From<ID3v2Tag> for Tag {
) )
.is_some() => .is_some() =>
{ {
continue false // Frame consumed
}, },
("TPOS", FrameValue::Text { value: content, .. }) ("TPOS", FrameValue::Text { value: content, .. })
if split_pair(&content, &mut tag, ItemKey::DiscNumber, ItemKey::DiscTotal) if split_pair(&content, &mut tag, ItemKey::DiscNumber, ItemKey::DiscTotal)
.is_some() => .is_some() =>
{ {
continue false // Frame consumed
}, },
("MVIN", FrameValue::Text { value: content, .. }) ("MVIN", FrameValue::Text { value: content, .. })
if split_pair( if split_pair(
@ -588,7 +588,7 @@ impl From<ID3v2Tag> for Tag {
) )
.is_some() => .is_some() =>
{ {
continue false // Frame consumed
}, },
// Store TXXX/WXXX frames by their descriptions, rather than their IDs // Store TXXX/WXXX frames by their descriptions, rather than their IDs
( (
@ -606,6 +606,7 @@ impl From<ID3v2Tag> for Tag {
ItemValue::Text(c.to_string()), ItemValue::Text(c.to_string()),
)); ));
} }
false // Frame consumed
}, },
( (
"WXXX", "WXXX",
@ -622,6 +623,7 @@ impl From<ID3v2Tag> for Tag {
ItemValue::Locator(c.to_string()), ItemValue::Locator(c.to_string()),
)); ));
} }
false // Frame consumed
}, },
(id, value) => { (id, value) => {
let item_key = ItemKey::from_key(TagType::ID3v2, id); let item_key = ItemKey::from_key(TagType::ID3v2, id);
@ -642,13 +644,14 @@ impl From<ID3v2Tag> for Tag {
description, description,
.. ..
}) => { }) => {
if description == EMPTY_CONTENT_DESCRIPTOR { if *description == EMPTY_CONTENT_DESCRIPTOR {
for c in content.split(V4_MULTI_VALUE_SEPARATOR) { for c in content.split(V4_MULTI_VALUE_SEPARATOR) {
tag.items.push(TagItem::new( tag.items.push(TagItem::new(
item_key.clone(), item_key.clone(),
ItemValue::Text(c.to_string()), ItemValue::Text(c.to_string()),
)); ));
} }
return false; // Frame consumed
} }
// ...else do not convert text frames with a non-empty content // ...else do not convert text frames with a non-empty content
// descriptor that would otherwise unintentionally be modified // descriptor that would otherwise unintentionally be modified
@ -656,8 +659,7 @@ impl From<ID3v2Tag> for Tag {
// TODO: How to convert these frames consistently and safely // TODO: How to convert these frames consistently and safely
// such that the content descriptor is preserved during read/write // such that the content descriptor is preserved during read/write
// round trips? // round trips?
return true; // Keep frame
continue;
}, },
FrameValue::Text { value: content, .. } => { FrameValue::Text { value: content, .. } => {
for c in content.split(V4_MULTI_VALUE_SEPARATOR) { for c in content.split(V4_MULTI_VALUE_SEPARATOR) {
@ -666,34 +668,34 @@ impl From<ID3v2Tag> for Tag {
ItemValue::Text(c.to_string()), ItemValue::Text(c.to_string()),
)); ));
} }
return false; // Frame consumed
continue;
}, },
FrameValue::URL(content) FrameValue::URL(content)
| FrameValue::UserURL(EncodedTextFrame { content, .. }) => ItemValue::Locator(content), | FrameValue::UserURL(EncodedTextFrame { content, .. }) => {
ItemValue::Locator(std::mem::take(content))
},
FrameValue::Picture { picture, .. } => { FrameValue::Picture { picture, .. } => {
tag.push_picture(picture); tag.push_picture(std::mem::replace(picture, TOMBSTONE_PICTURE));
continue; return false; // Frame consumed
}, },
FrameValue::Popularimeter(popularimeter) => { FrameValue::Popularimeter(popularimeter) => {
ItemValue::Binary(popularimeter.as_bytes()) ItemValue::Binary(popularimeter.as_bytes())
}, },
FrameValue::Binary(binary) => ItemValue::Binary(binary), FrameValue::Binary(binary) => ItemValue::Binary(std::mem::take(binary)),
}; };
tag.items.push(TagItem::new(item_key, item_value)); tag.items.push(TagItem::new(item_key, item_value));
false // Frame consumed
}, },
} }
} });
tag tag
} }
}
impl From<Tag> for ID3v2Tag { fn rejoin_tag(&mut self, mut tag: Tag) {
fn from(mut input: Tag) -> Self { fn join_items(tag: &mut Tag, key: &ItemKey) -> String {
fn join_items(input: &mut Tag, key: &ItemKey) -> String { let mut iter = tag.take_strings(key);
let mut iter = input.take_strings(key);
match iter.next() { match iter.next() {
None => String::new(), None => String::new(),
@ -710,25 +712,22 @@ impl From<Tag> for ID3v2Tag {
} }
} }
let mut id3v2_tag = ID3v2Tag { self.frames.reserve(tag.item_count() as usize);
frames: Vec::with_capacity(input.item_count() as usize),
..ID3v2Tag::default()
};
let artists = join_items(&mut input, &ItemKey::TrackArtist); let artists = join_items(&mut tag, &ItemKey::TrackArtist);
id3v2_tag.set_artist(artists); self.set_artist(artists);
for item in input.items { for item in tag.items {
let frame: Frame<'_> = match item.into() { let frame: Frame<'_> = match item.into() {
Some(frame) => frame, Some(frame) => frame,
None => continue, None => continue,
}; };
id3v2_tag.insert(frame); self.insert(frame);
} }
for picture in input.pictures { for picture in tag.pictures {
id3v2_tag.frames.push(Frame { self.frames.push(Frame {
id: FrameID::Valid(Cow::Borrowed("APIC")), id: FrameID::Valid(Cow::Borrowed("APIC")),
value: FrameValue::Picture { value: FrameValue::Picture {
encoding: TextEncoding::UTF8, encoding: TextEncoding::UTF8,
@ -737,7 +736,19 @@ impl From<Tag> for ID3v2Tag {
flags: FrameFlags::default(), flags: FrameFlags::default(),
}) })
} }
}
}
impl From<ID3v2Tag> for Tag {
fn from(mut input: ID3v2Tag) -> Self {
input.split_tag()
}
}
impl From<Tag> for ID3v2Tag {
fn from(input: Tag) -> Self {
let mut id3v2_tag = ID3v2Tag::default();
id3v2_tag.rejoin_tag(input);
id3v2_tag id3v2_tag
} }
} }

View file

@ -3,7 +3,7 @@ use crate::iff::chunk::Chunks;
use crate::macros::err; use crate::macros::err;
use crate::tag::item::{ItemKey, ItemValue, TagItem}; use crate::tag::item::{ItemKey, ItemValue, TagItem};
use crate::tag::{Tag, TagType}; use crate::tag::{Tag, TagType};
use crate::traits::{Accessor, TagExt}; use crate::traits::{Accessor, SplitAndRejoinTag, TagExt};
use std::borrow::Cow; use std::borrow::Cow;
use std::convert::TryFrom; use std::convert::TryFrom;
@ -216,9 +216,19 @@ impl TagExt for AIFFTextChunks {
} }
} }
impl SplitAndRejoinTag for AIFFTextChunks {
fn split_tag(&mut self) -> Tag {
std::mem::take(self).into()
}
fn rejoin_tag(&mut self, tag: Tag) {
*self = tag.into();
}
}
impl From<AIFFTextChunks> for Tag { impl From<AIFFTextChunks> for Tag {
fn from(input: AIFFTextChunks) -> Self { fn from(input: AIFFTextChunks) -> Self {
let mut tag = Tag::new(TagType::AIFFText); let mut tag = Self::new(TagType::AIFFText);
let push_item = |field: Option<String>, item_key: ItemKey, tag: &mut Tag| { let push_item = |field: Option<String>, item_key: ItemKey, tag: &mut Tag| {
if let Some(text) = field { if let Some(text) = field {

View file

@ -4,7 +4,7 @@ mod write;
use crate::error::{LoftyError, Result}; use crate::error::{LoftyError, Result};
use crate::tag::item::{ItemKey, ItemValue, TagItem}; use crate::tag::item::{ItemKey, ItemValue, TagItem};
use crate::tag::{Tag, TagType}; use crate::tag::{Tag, TagType};
use crate::traits::{Accessor, TagExt}; use crate::traits::{Accessor, SplitAndRejoinTag, TagExt};
use std::borrow::Cow; use std::borrow::Cow;
use std::fs::{File, OpenOptions}; use std::fs::{File, OpenOptions};
@ -211,9 +211,19 @@ impl TagExt for RIFFInfoList {
} }
} }
impl SplitAndRejoinTag for RIFFInfoList {
fn split_tag(&mut self) -> Tag {
std::mem::take(self).into()
}
fn rejoin_tag(&mut self, tag: Tag) {
*self = tag.into();
}
}
impl From<RIFFInfoList> for Tag { impl From<RIFFInfoList> for Tag {
fn from(input: RIFFInfoList) -> Self { fn from(input: RIFFInfoList) -> Self {
let mut tag = Tag::new(TagType::RIFFInfo); let mut tag = Self::new(TagType::RIFFInfo);
for (k, v) in input.items { for (k, v) in input.items {
let item_key = ItemKey::from_key(TagType::RIFFInfo, &k); let item_key = ItemKey::from_key(TagType::RIFFInfo, &k);

View file

@ -178,7 +178,7 @@ pub use crate::tag::{Tag, TagType};
pub use tag::item::{ItemKey, ItemValue, TagItem}; pub use tag::item::{ItemKey, ItemValue, TagItem};
pub use util::text::TextEncoding; pub use util::text::TextEncoding;
pub use crate::traits::{Accessor, TagExt}; pub use crate::traits::{Accessor, SplitAndRejoinTag, TagExt};
pub use picture::PictureInformation; pub use picture::PictureInformation;

View file

@ -12,10 +12,10 @@ pub(super) enum AtomDataStorage {
} }
impl AtomDataStorage { impl AtomDataStorage {
pub(super) fn take_first(self) -> AtomData { pub(super) fn first_mut(&mut self) -> &mut AtomData {
match self { match self {
AtomDataStorage::Single(val) => val, AtomDataStorage::Single(val) => val,
AtomDataStorage::Multiple(mut data) => data.swap_remove(0), AtomDataStorage::Multiple(data) => data.first_mut().expect("not empty"),
} }
} }
} }

View file

@ -7,10 +7,10 @@ pub(crate) mod write;
use super::AtomIdent; use super::AtomIdent;
use crate::error::LoftyError; use crate::error::LoftyError;
use crate::mp4::ilst::atom::AtomDataStorage; use crate::mp4::ilst::atom::AtomDataStorage;
use crate::picture::{Picture, PictureType}; use crate::picture::{Picture, PictureType, TOMBSTONE_PICTURE};
use crate::tag::item::{ItemKey, ItemValue, TagItem}; use crate::tag::item::{ItemKey, ItemValue, TagItem};
use crate::tag::{Tag, TagType}; use crate::tag::{Tag, TagType};
use crate::traits::{Accessor, TagExt}; use crate::traits::{Accessor, SplitAndRejoinTag, TagExt};
use atom::{AdvisoryRating, Atom, AtomData}; use atom::{AdvisoryRating, Atom, AtomData};
use std::borrow::Cow; use std::borrow::Cow;
@ -377,20 +377,23 @@ impl TagExt for Ilst {
} }
} }
impl From<Ilst> for Tag { impl SplitAndRejoinTag for Ilst {
fn from(input: Ilst) -> Self { fn split_tag(&mut self) -> Tag {
let mut tag = Self::new(TagType::MP4ilst); let mut tag = Tag::new(TagType::MP4ilst);
for atom in input.atoms { self.atoms.retain_mut(|atom| {
let Atom { ident, data } = atom; let Atom { ident, data } = atom;
let value = match data.take_first() { let value = match data.first_mut() {
AtomData::UTF8(text) | AtomData::UTF16(text) => ItemValue::Text(text), AtomData::UTF8(text) | AtomData::UTF16(text) => {
AtomData::Picture(pic) => { ItemValue::Text(std::mem::take(text))
tag.pictures.push(pic); },
continue; AtomData::Picture(picture) => {
tag.pictures
.push(std::mem::replace(picture, TOMBSTONE_PICTURE));
return false; // Atom consumed
}, },
AtomData::Bool(b) => { AtomData::Bool(b) => {
let text = if b { "1".to_owned() } else { "0".to_owned() }; let text = if *b { "1".to_owned() } else { "0".to_owned() };
ItemValue::Text(text) ItemValue::Text(text)
}, },
// We have to special case track/disc numbers since they are stored together // We have to special case track/disc numbers since they are stored together
@ -403,6 +406,7 @@ impl From<Ilst> for Tag {
tag.insert_text(ItemKey::TrackNumber, current.to_string()); tag.insert_text(ItemKey::TrackNumber, current.to_string());
tag.insert_text(ItemKey::TrackTotal, total.to_string()); tag.insert_text(ItemKey::TrackTotal, total.to_string());
return false; // Atom consumed
}, },
b"disk" => { b"disk" => {
let current = u16::from_be_bytes([data[2], data[3]]); let current = u16::from_be_bytes([data[2], data[3]]);
@ -410,14 +414,17 @@ impl From<Ilst> for Tag {
tag.insert_text(ItemKey::DiscNumber, current.to_string()); tag.insert_text(ItemKey::DiscNumber, current.to_string());
tag.insert_text(ItemKey::DiscTotal, total.to_string()); tag.insert_text(ItemKey::DiscTotal, total.to_string());
return false; // Atom consumed
}, },
_ => {}, _ => {},
} }
} }
continue; return true; // Keep atom
},
_ => {
return true; // Keep atom
}, },
_ => continue,
}; };
let key = ItemKey::from_key( let key = ItemKey::from_key(
@ -433,14 +440,13 @@ impl From<Ilst> for Tag {
); );
tag.items.push(TagItem::new(key, value)); tag.items.push(TagItem::new(key, value));
} false // Atom consumed
});
tag tag
} }
}
impl From<Tag> for Ilst { fn rejoin_tag(&mut self, tag: Tag) {
fn from(input: Tag) -> Self {
fn convert_to_uint(space: &mut Option<u16>, cont: &str) { fn convert_to_uint(space: &mut Option<u16>, cont: &str) {
if let Ok(num) = cont.parse::<u16>() { if let Ok(num) = cont.parse::<u16>() {
*space = Some(num); *space = Some(num);
@ -465,13 +471,11 @@ impl From<Tag> for Ilst {
} }
} }
let mut ilst = Self::default();
// Storage for integer pairs // Storage for integer pairs
let mut tracks: (Option<u16>, Option<u16>) = (None, None); let mut tracks: (Option<u16>, Option<u16>) = (None, None);
let mut discs: (Option<u16>, Option<u16>) = (None, None); let mut discs: (Option<u16>, Option<u16>) = (None, None);
for item in input.items { for item in tag.items {
let key = item.item_key; let key = item.item_key;
if let Ok(ident) = TryInto::<AtomIdent<'_>>::try_into(&key) { if let Ok(ident) = TryInto::<AtomIdent<'_>>::try_into(&key) {
@ -495,13 +499,13 @@ impl From<Tag> for Ilst {
continue; continue;
}, },
}; };
ilst.atoms.push(Atom { self.atoms.push(Atom {
ident: ident.into_owned(), ident: ident.into_owned(),
data: AtomDataStorage::Single(AtomData::Bool(data)), data: AtomDataStorage::Single(AtomData::Bool(data)),
}) })
} }
}, },
_ => ilst.atoms.push(Atom { _ => self.atoms.push(Atom {
ident: ident.into_owned(), ident: ident.into_owned(),
data: AtomDataStorage::Single(AtomData::UTF8(data)), data: AtomDataStorage::Single(AtomData::UTF8(data)),
}), }),
@ -509,20 +513,32 @@ impl From<Tag> for Ilst {
} }
} }
for mut picture in input.pictures { for mut picture in tag.pictures {
// Just for correctness, since we can't actually // Just for correctness, since we can't actually
// assign a picture type in this format // assign a picture type in this format
picture.pic_type = PictureType::Other; picture.pic_type = PictureType::Other;
ilst.atoms.push(Atom { self.atoms.push(Atom {
ident: AtomIdent::Fourcc([b'c', b'o', b'v', b'r']), ident: AtomIdent::Fourcc([b'c', b'o', b'v', b'r']),
data: AtomDataStorage::Single(AtomData::Picture(picture)), data: AtomDataStorage::Single(AtomData::Picture(picture)),
}) })
} }
create_int_pair(&mut ilst, *b"trkn", tracks); create_int_pair(self, *b"trkn", tracks);
create_int_pair(&mut ilst, *b"disk", discs); create_int_pair(self, *b"disk", discs);
}
}
impl From<Ilst> for Tag {
fn from(mut input: Ilst) -> Self {
input.split_tag()
}
}
impl From<Tag> for Ilst {
fn from(input: Tag) -> Self {
let mut ilst = Self::default();
ilst.rejoin_tag(input);
ilst ilst
} }
} }

View file

@ -7,7 +7,7 @@ use crate::picture::{Picture, PictureInformation};
use crate::probe::Probe; use crate::probe::Probe;
use crate::tag::item::{ItemKey, ItemValue, TagItem}; use crate::tag::item::{ItemKey, ItemValue, TagItem};
use crate::tag::{Tag, TagType}; use crate::tag::{Tag, TagType};
use crate::traits::{Accessor, TagExt}; use crate::traits::{Accessor, SplitAndRejoinTag, TagExt};
use std::borrow::Cow; use std::borrow::Cow;
use std::fs::{File, OpenOptions}; use std::fs::{File, OpenOptions};
@ -328,11 +328,11 @@ impl TagExt for VorbisComments {
} }
} }
impl From<VorbisComments> for Tag { impl SplitAndRejoinTag for VorbisComments {
fn from(input: VorbisComments) -> Self { fn split_tag(&mut self) -> Tag {
let mut tag = Tag::new(TagType::VorbisComments); let mut tag = Tag::new(TagType::VorbisComments);
for (k, v) in input.items { for (k, v) in std::mem::take(&mut self.items) {
tag.items.push(TagItem::new( tag.items.push(TagItem::new(
ItemKey::from_key(TagType::VorbisComments, &k), ItemKey::from_key(TagType::VorbisComments, &k),
ItemValue::Text(v), ItemValue::Text(v),
@ -347,31 +347,28 @@ impl From<VorbisComments> for Tag {
{ {
tag.items.push(TagItem::new( tag.items.push(TagItem::new(
ItemKey::EncoderSoftware, ItemKey::EncoderSoftware,
ItemValue::Text(input.vendor), // Preserve the original vendor by cloning
ItemValue::Text(self.vendor.clone()),
)); ));
} }
for (pic, _info) in input.pictures { for (pic, _info) in std::mem::take(&mut self.pictures) {
tag.push_picture(pic) tag.push_picture(pic)
} }
tag tag
} }
}
impl From<Tag> for VorbisComments {
fn from(mut input: Tag) -> Self {
let mut vorbis_comments = Self::default();
fn rejoin_tag(&mut self, mut tag: Tag) {
if let Some(TagItem { if let Some(TagItem {
item_value: ItemValue::Text(val), item_value: ItemValue::Text(val),
.. ..
}) = input.take(&ItemKey::EncoderSoftware).next() }) = tag.take(&ItemKey::EncoderSoftware).next()
{ {
vorbis_comments.vendor = val; self.vendor = val;
} }
for item in input.items { for item in tag.items {
let item_key = item.item_key; let item_key = item.item_key;
let item_value = item.item_value; let item_value = item.item_value;
@ -386,15 +383,27 @@ impl From<Tag> for VorbisComments {
Some(k) => k, Some(k) => k,
}; };
vorbis_comments.items.push((key.to_string(), val)); self.items.push((key.to_string(), val));
} }
for picture in input.pictures { for picture in tag.pictures {
if let Ok(information) = PictureInformation::from_picture(&picture) { if let Ok(information) = PictureInformation::from_picture(&picture) {
vorbis_comments.pictures.push((picture, information)) self.pictures.push((picture, information))
} }
} }
}
}
impl From<VorbisComments> for Tag {
fn from(mut input: VorbisComments) -> Self {
input.split_tag()
}
}
impl From<Tag> for VorbisComments {
fn from(input: Tag) -> Self {
let mut vorbis_comments = Self::default();
vorbis_comments.rejoin_tag(input);
vorbis_comments vorbis_comments
} }
} }

View file

@ -502,8 +502,8 @@ impl Picture {
Self { Self {
pic_type, pic_type,
mime_type, mime_type,
description: description.map(Cow::from), description: description.map(Cow::Owned),
data: Cow::from(data), data: Cow::Owned(data),
} }
} }
@ -890,3 +890,11 @@ impl Picture {
} }
} }
} }
// A placeholder that is needed during conversions.
pub(crate) const TOMBSTONE_PICTURE: Picture = Picture {
pic_type: PictureType::Other,
mime_type: MimeType::Unknown(String::new()),
description: None,
data: Cow::Owned(Vec::new()),
};

View file

@ -6,7 +6,7 @@ use crate::file::FileType;
use crate::macros::err; use crate::macros::err;
use crate::picture::{Picture, PictureType}; use crate::picture::{Picture, PictureType};
use crate::probe::Probe; use crate::probe::Probe;
use crate::traits::{Accessor, TagExt}; use crate::traits::{Accessor, SplitAndRejoinTag, TagExt};
use item::{ItemKey, ItemValue, TagItem}; use item::{ItemKey, ItemValue, TagItem};
use std::borrow::Cow; use std::borrow::Cow;
@ -583,6 +583,16 @@ impl TagExt for Tag {
} }
} }
impl SplitAndRejoinTag for Tag {
fn split_tag(&mut self) -> Self {
std::mem::replace(self, Self::new(self.tag_type))
}
fn rejoin_tag(&mut self, tag: Self) {
*self = tag;
}
}
/// The tag's format /// The tag's format
#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[non_exhaustive] #[non_exhaustive]

View file

@ -233,6 +233,30 @@ pub trait TagExt: Accessor + Into<Tag> + Sized {
fn clear(&mut self); fn clear(&mut self);
} }
/// Split and rejoin tags.
///
/// Useful and required for implementing read/modify/write round trips.
pub trait SplitAndRejoinTag {
/// Extract and split generic contents into a [`Tag`].
///
/// Leaves the remainder that cannot be represented in the
/// resulting tag in `self`. This is useful if the modified [`Tag`]
/// is rejoined later using [`SplitAndRejoinTag::rejoin_tag`].
// NOTE: Using the "typestate pattern" (http://cliffle.com/blog/rust-typestate/)
// to represent the intermediate state turned out as less flexible
// and useful than expected.
fn split_tag(&mut self) -> Tag;
/// Rejoin a [`Tag`].
///
/// Rejoin a tag that has previously been obtained with
/// [`SplitAndRejoinTag::split_tag`].
///
/// Restores the original representation merged with the contents of
/// `tag` for further processing, e.g. writing back into a file.
fn rejoin_tag(&mut self, tag: Tag);
}
// TODO: https://github.com/rust-lang/rust/issues/59359 // TODO: https://github.com/rust-lang/rust/issues/59359
pub(crate) trait SeekStreamLen: std::io::Seek { pub(crate) trait SeekStreamLen: std::io::Seek {
fn stream_len(&mut self) -> crate::error::Result<u64> { fn stream_len(&mut self) -> crate::error::Result<u64> {