mirror of
https://github.com/Serial-ATA/lofty-rs
synced 2024-11-10 06:34:18 +00:00
APE: Add Picture interface to ApeTag
This commit is contained in:
parent
04ad40381b
commit
b697ccee97
7 changed files with 151 additions and 15 deletions
|
@ -6,10 +6,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- **APE**: Picture interface for `ApeTag` to easily insert, remove, and iterate over pictures ([issue](https://github.com/Serial-ATA/lofty-rs/issues/280)) ([PR](https://github.com/Serial-ATA/lofty-rs/pull/348))
|
||||
|
||||
## [0.18.2] - 2024-01-23
|
||||
|
||||
### Fixed
|
||||
- **MP4**: Padding for shrinking tags will no longer overwrite unrelated data ([PR](https://github.com/Serial-ATA/lofty-rs/pull/346))
|
||||
- **MP4**: Padding for shrinking tags will no longer overwrite unrelated data ([PR](https://github.com/Serial-ATA/lofty-rs/pull/347))
|
||||
|
||||
## [0.18.1] - 2024-01-20 (YANKED)
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ byteorder = "1.5.0"
|
|||
# ID3 compressed frames
|
||||
flate2 = { version = "1.0.28", optional = true }
|
||||
# Proc macros
|
||||
lofty_attr = "0.9.0"
|
||||
lofty_attr = { path = "lofty_attr" }
|
||||
# Debug logging
|
||||
log = "0.4.20"
|
||||
# OGG Vorbis/Opus
|
||||
|
|
|
@ -44,9 +44,11 @@ pub(crate) fn init_write_lookup(
|
|||
}
|
||||
|
||||
insert!(map, Ape, {
|
||||
let (items, pictures) = lofty::ape::tag::tagitems_into_ape(tag);
|
||||
lofty::ape::tag::ApeTagRef {
|
||||
read_only: false,
|
||||
items: lofty::ape::tag::tagitems_into_ape(tag),
|
||||
items,
|
||||
pictures,
|
||||
}
|
||||
.write_to(data)
|
||||
});
|
||||
|
|
|
@ -5,6 +5,7 @@ mod write;
|
|||
use crate::ape::tag::item::{ApeItem, ApeItemRef};
|
||||
use crate::error::{LoftyError, Result};
|
||||
use crate::id3::v2::util::pairs::{format_number_pair, set_number, NUMBER_PAIR_KEYS};
|
||||
use crate::picture::{Picture, PictureType};
|
||||
use crate::tag::item::{ItemKey, ItemValue, ItemValueRef, TagItem};
|
||||
use crate::tag::{try_parse_year, Tag, TagType};
|
||||
use crate::traits::{Accessor, MergeTag, SplitTag, TagExt};
|
||||
|
@ -80,6 +81,7 @@ pub struct ApeTag {
|
|||
/// Whether or not to mark the tag as read only
|
||||
pub read_only: bool,
|
||||
pub(super) items: Vec<ApeItem>,
|
||||
pub(super) pictures: Vec<Picture>,
|
||||
}
|
||||
|
||||
impl ApeTag {
|
||||
|
@ -171,6 +173,85 @@ impl ApeTag {
|
|||
};
|
||||
}
|
||||
|
||||
/// Inserts a [`Picture`]
|
||||
///
|
||||
/// According to spec, there can only be one picture of type [`PictureType::Icon`] and [`PictureType::OtherIcon`].
|
||||
/// When attempting to insert these types, if another is found it will be removed and returned.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use lofty::ape::ApeTag;
|
||||
/// use lofty::Picture;
|
||||
/// use std::fs::File;
|
||||
///
|
||||
/// # fn main() -> lofty::Result<()> {
|
||||
/// # let picture_path: &str = "tests/files/assets/issue_37.jpg";
|
||||
///
|
||||
/// let mut tag = ApeTag::new();
|
||||
///
|
||||
/// let mut picture_file = File::open(picture_path)?;
|
||||
/// tag.insert_picture(Picture::from_reader(&mut picture_file)?);
|
||||
/// # Ok(()) }
|
||||
pub fn insert_picture(&mut self, picture: Picture) -> Option<Picture> {
|
||||
let ret = match picture.pic_type {
|
||||
PictureType::Icon | PictureType::OtherIcon => self
|
||||
.pictures
|
||||
.iter()
|
||||
.position(|p| p.pic_type == picture.pic_type)
|
||||
.map(|pos| self.pictures.remove(pos)),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
self.pictures.push(picture);
|
||||
ret
|
||||
}
|
||||
|
||||
/// Gets all [`Picture`]s
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use lofty::ape::ApeTag;
|
||||
/// use lofty::Picture;
|
||||
/// use std::fs::File;
|
||||
///
|
||||
/// # fn main() -> lofty::Result<()> {
|
||||
/// # let picture_path: &str = "tests/files/assets/issue_37.jpg";
|
||||
///
|
||||
/// let mut tag = ApeTag::new();
|
||||
///
|
||||
/// let mut picture_file = File::open(picture_path)?;
|
||||
/// tag.insert_picture(Picture::from_reader(&mut picture_file)?);
|
||||
///
|
||||
/// let pictures: Vec<&Picture> = tag.pictures().collect();
|
||||
/// assert_eq!(pictures.len(), 1);
|
||||
/// # Ok(()) }
|
||||
pub fn pictures(&self) -> impl Iterator<Item = &Picture> {
|
||||
self.pictures.iter()
|
||||
}
|
||||
|
||||
/// Removes all [`Picture`]s of a certain [`PictureType`]
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use lofty::ape::ApeTag;
|
||||
/// use lofty::Picture;
|
||||
/// use std::fs::File;
|
||||
///
|
||||
/// # fn main() -> lofty::Result<()> {
|
||||
/// # let picture_path: &str = "tests/files/assets/issue_37.jpg";
|
||||
///
|
||||
/// let mut tag = ApeTag::new();
|
||||
///
|
||||
/// let mut picture_file = File::open(picture_path)?;
|
||||
/// tag.insert_picture(Picture::from_reader(&mut picture_file)?);
|
||||
/// # Ok(()) }
|
||||
pub fn remove_picture_type(&mut self, picture_type: PictureType) {
|
||||
self.pictures.retain(|p| p.pic_type != picture_type)
|
||||
}
|
||||
|
||||
fn split_num_pair(&self, key: &str) -> (Option<u32>, Option<u32>) {
|
||||
if let Some(ApeItem {
|
||||
value: ItemValue::Text(ref text),
|
||||
|
@ -325,6 +406,7 @@ impl TagExt for ApeTag {
|
|||
ApeTagRef {
|
||||
read_only: self.read_only,
|
||||
items: self.items.iter().map(Into::into),
|
||||
pictures: self.pictures.iter(),
|
||||
}
|
||||
.write_to(file)
|
||||
}
|
||||
|
@ -338,6 +420,7 @@ impl TagExt for ApeTag {
|
|||
ApeTagRef {
|
||||
read_only: self.read_only,
|
||||
items: self.items.iter().map(Into::into),
|
||||
pictures: self.pictures.iter(),
|
||||
}
|
||||
.dump_to(writer)
|
||||
}
|
||||
|
@ -471,17 +554,20 @@ impl From<Tag> for ApeTag {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) struct ApeTagRef<'a, I>
|
||||
pub(crate) struct ApeTagRef<'a, I, P>
|
||||
where
|
||||
I: Iterator<Item = ApeItemRef<'a>>,
|
||||
P: Iterator<Item = &'a Picture>,
|
||||
{
|
||||
pub(crate) read_only: bool,
|
||||
pub(crate) items: I,
|
||||
pub(crate) pictures: P,
|
||||
}
|
||||
|
||||
impl<'a, I> ApeTagRef<'a, I>
|
||||
impl<'a, I, P> ApeTagRef<'a, I, P>
|
||||
where
|
||||
I: Iterator<Item = ApeItemRef<'a>>,
|
||||
P: Iterator<Item = &'a Picture>,
|
||||
{
|
||||
pub(crate) fn write_to(&mut self, file: &mut File) -> Result<()> {
|
||||
write::write_to(file, self)
|
||||
|
@ -495,7 +581,12 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn tagitems_into_ape(tag: &Tag) -> impl Iterator<Item = ApeItemRef<'_>> {
|
||||
pub(crate) fn tagitems_into_ape(
|
||||
tag: &Tag,
|
||||
) -> (
|
||||
impl Iterator<Item = ApeItemRef<'_>>,
|
||||
impl Iterator<Item = &Picture>,
|
||||
) {
|
||||
fn create_apeitemref_for_number_pair<'a>(
|
||||
number: Option<&str>,
|
||||
total: Option<&str>,
|
||||
|
@ -508,7 +599,8 @@ pub(crate) fn tagitems_into_ape(tag: &Tag) -> impl Iterator<Item = ApeItemRef<'_
|
|||
})
|
||||
}
|
||||
|
||||
tag.items()
|
||||
let items = tag
|
||||
.items()
|
||||
.filter(|item| !NUMBER_PAIR_KEYS.contains(item.key()))
|
||||
.filter_map(|i| {
|
||||
i.key().map_key(TagType::Ape, true).map(|key| ApeItemRef {
|
||||
|
@ -526,7 +618,12 @@ pub(crate) fn tagitems_into_ape(tag: &Tag) -> impl Iterator<Item = ApeItemRef<'_
|
|||
tag.get_string(&ItemKey::DiscNumber),
|
||||
tag.get_string(&ItemKey::DiscTotal),
|
||||
"Disk",
|
||||
))
|
||||
));
|
||||
let pictures = tag
|
||||
.pictures
|
||||
.iter()
|
||||
.filter(|p| p.pic_type.as_ape_key().is_some());
|
||||
(items, pictures)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use super::item::ApeItem;
|
||||
use super::ApeTag;
|
||||
use crate::ape::constants::{APE_PREAMBLE, INVALID_KEYS};
|
||||
use crate::picture::{APE_PICTURE_TYPES, Picture};
|
||||
use crate::ape::header::{self, ApeHeader};
|
||||
use crate::error::Result;
|
||||
use crate::macros::{decode_err, err, try_vec};
|
||||
|
@ -57,6 +58,11 @@ where
|
|||
let mut value = try_vec![0; value_size as usize];
|
||||
data.read_exact(&mut value)?;
|
||||
|
||||
if APE_PICTURE_TYPES.contains(&&**&key) {
|
||||
tag.pictures.push(Picture::from_ape_bytes(&key, &value)?);
|
||||
continue;
|
||||
}
|
||||
|
||||
let parsed_value = match item_type {
|
||||
0 => ItemValue::Text(utf8_decode(value).map_err(|_| {
|
||||
decode_err!(Ape, "Failed to convert text item into a UTF-8 string")
|
||||
|
|
|
@ -11,12 +11,14 @@ use crate::tag::item::ItemValueRef;
|
|||
use std::fs::File;
|
||||
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
|
||||
|
||||
use crate::Picture;
|
||||
use byteorder::{LittleEndian, WriteBytesExt};
|
||||
|
||||
#[allow(clippy::shadow_unrelated)]
|
||||
pub(crate) fn write_to<'a, I>(data: &mut File, tag: &mut ApeTagRef<'a, I>) -> Result<()>
|
||||
pub(crate) fn write_to<'a, I, P>(data: &mut File, tag: &mut ApeTagRef<'a, I, P>) -> Result<()>
|
||||
where
|
||||
I: Iterator<Item = ApeItemRef<'a>>,
|
||||
P: Iterator<Item = &'a Picture>,
|
||||
{
|
||||
let probe = Probe::new(data).guess_file_type()?;
|
||||
|
||||
|
@ -93,6 +95,7 @@ where
|
|||
create_ape_tag(&mut ApeTagRef {
|
||||
read_only: read_only.read_only,
|
||||
items: read_only.items.iter().map(Into::into),
|
||||
pictures: read_only.pictures.iter(),
|
||||
})?
|
||||
} else {
|
||||
create_ape_tag(tag)?
|
||||
|
@ -122,9 +125,10 @@ where
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn create_ape_tag<'a, I>(tag: &mut ApeTagRef<'a, I>) -> Result<Vec<u8>>
|
||||
pub(super) fn create_ape_tag<'a, I, P>(tag: &mut ApeTagRef<'a, I, P>) -> Result<Vec<u8>>
|
||||
where
|
||||
I: Iterator<Item = ApeItemRef<'a>>,
|
||||
P: Iterator<Item = &'a Picture>,
|
||||
{
|
||||
let items = &mut tag.items;
|
||||
let mut peek = items.peekable();
|
||||
|
@ -169,6 +173,26 @@ where
|
|||
item_count += 1;
|
||||
}
|
||||
|
||||
for picture in &mut tag.pictures {
|
||||
let Some(ape_picture_key) = picture.pic_type.as_ape_key() else {
|
||||
log::warn!(
|
||||
"APE: Discarding unsupported picture type: `{:?}`",
|
||||
picture.pic_type
|
||||
);
|
||||
continue;
|
||||
};
|
||||
|
||||
let value = picture.as_ape_bytes();
|
||||
|
||||
tag_write.write_u32::<LittleEndian>(value.len() as u32)?;
|
||||
tag_write.write_u32::<LittleEndian>(1_u32 << 1)?;
|
||||
tag_write.write_all(ape_picture_key.as_bytes())?;
|
||||
tag_write.write_u8(0)?;
|
||||
tag_write.write_all(&value)?;
|
||||
|
||||
item_count += 1;
|
||||
}
|
||||
|
||||
let size = tag_write.get_ref().len();
|
||||
|
||||
if size as u64 + 32 > u64::from(u32::MAX) {
|
||||
|
|
|
@ -40,11 +40,15 @@ pub(crate) fn write_tag(tag: &Tag, file: &mut File, file_type: FileType) -> Resu
|
|||
#[allow(unreachable_patterns)]
|
||||
pub(crate) fn dump_tag<W: Write>(tag: &Tag, writer: &mut W) -> Result<()> {
|
||||
match tag.tag_type() {
|
||||
TagType::Ape => ApeTagRef {
|
||||
read_only: false,
|
||||
items: ape::tag::tagitems_into_ape(tag),
|
||||
}
|
||||
.dump_to(writer),
|
||||
TagType::Ape => {
|
||||
let (items, pictures) = ape::tag::tagitems_into_ape(tag);
|
||||
ApeTagRef {
|
||||
read_only: false,
|
||||
items,
|
||||
pictures,
|
||||
}
|
||||
.dump_to(writer)
|
||||
},
|
||||
TagType::Id3v1 => Into::<Id3v1TagRef<'_>>::into(tag).dump_to(writer),
|
||||
TagType::Id3v2 => Id3v2TagRef {
|
||||
flags: Id3v2TagFlags::default(),
|
||||
|
|
Loading…
Reference in a new issue