From c1f117f5a46cc7a3e6d161741eedcbd1633de6dd Mon Sep 17 00:00:00 2001 From: Serial <69764315+Serial-ATA@users.noreply.github.com> Date: Sat, 27 Apr 2024 11:48:53 -0400 Subject: [PATCH] Tag: Support `ItemKey::ParentalAdvisory` for Ilst and ID3v2 --- CHANGELOG.md | 5 +++++ lofty/src/id3/v2/tag.rs | 35 +++++++++++++++++++++++++++++++++++ lofty/src/id3/v2/tag/tests.rs | 22 ++++++++++++++++++++++ lofty/src/mp4/ilst/mod.rs | 28 ++++++++++++++++++++++++++++ lofty/src/tag/item.rs | 1 + 5 files changed, 91 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6ce7c91..18876100 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- **Tag**: Support `ItemKey::ParentalAdvisory` for `Ilst` and `Id3v2Tag` ([issue](https://github.com/Serial-ATA/lofty-rs/issues/99)) ([PR](https://github.com/Serial-ATA/lofty-rs/pull/388)) + - This will allow for generic edits to the iTunes-style parental advisory tag. Note that this will use the + numeric representation. For more information, see: https://docs.mp3tag.de/mapping/#itunesadvisory. + ### Changed - **VorbisComments**/**ApeTag**: Verify contents of `ItemKey::FlagCompilation` during `Tag` merge ([PR](https://github.com/Serial-ATA/lofty-rs/pull/387)) diff --git a/lofty/src/id3/v2/tag.rs b/lofty/src/id3/v2/tag.rs index 5f20d435..55cc9de9 100644 --- a/lofty/src/id3/v2/tag.rs +++ b/lofty/src/id3/v2/tag.rs @@ -17,6 +17,7 @@ use crate::id3::v2::util::pairs::{ format_number_pair, set_number, NUMBER_PAIR_KEYS, NUMBER_PAIR_SEPARATOR, }; use crate::id3::v2::KeyValueFrame; +use crate::mp4::AdvisoryRating; use crate::picture::{Picture, PictureType, TOMBSTONE_PICTURE}; use crate::tag::{ try_parse_year, Accessor, ItemKey, ItemValue, MergeTag, SplitTag, Tag, TagExt, TagItem, TagType, @@ -713,6 +714,18 @@ fn new_text_frame(id: FrameId<'_>, value: String, flags: FrameFlags) -> Frame<'_ } } +fn new_user_text_frame(description: String, content: String, flags: FrameFlags) -> Frame<'static> { + Frame { + id: FrameId::Valid(Cow::Borrowed(USER_DEFINED_TEXT_FRAME_ID)), + value: FrameValue::UserText(ExtendedTextFrame { + encoding: TextEncoding::UTF8, + description, + content, + }), + flags, + } +} + fn new_comment_frame(content: String, flags: FrameFlags) -> Frame<'static> { Frame { id: FrameId::Valid(Cow::Borrowed(COMMENT_FRAME_ID)), @@ -1407,6 +1420,28 @@ impl MergeTag for SplitTagRemainder { )); } + 'rate: { + if let Some(advisory_rating) = tag.take_strings(&ItemKey::ParentalAdvisory).next() { + let Ok(rating) = advisory_rating.parse::() else { + log::warn!( + "Parental advisory rating is not a number: {advisory_rating}, discarding" + ); + break 'rate; + }; + + let Ok(parsed_rating) = AdvisoryRating::try_from(rating) else { + log::warn!("Parental advisory rating is out of range: {rating}, discarding"); + break 'rate; + }; + + merged.frames.push(new_user_text_frame( + "ITUNESADVISORY".to_string(), + parsed_rating.as_u8().to_string(), + FrameFlags::default(), + )); + } + } + // Insert all remaining items as single frames and deduplicate as needed for item in tag.items { merged.insert_item(item); diff --git a/lofty/src/id3/v2/tag/tests.rs b/lofty/src/id3/v2/tag/tests.rs index 1cc12422..bfc0d776 100644 --- a/lofty/src/id3/v2/tag/tests.rs +++ b/lofty/src/id3/v2/tag/tests.rs @@ -1332,3 +1332,25 @@ fn flag_item_conversion() { Some("0") ); } + +#[test] +fn itunes_advisory_roundtrip() { + use crate::mp4::{AdvisoryRating, Ilst}; + + let mut tag = Ilst::new(); + tag.set_advisory_rating(AdvisoryRating::Explicit); + + let tag: Tag = tag.into(); + let tag: Id3v2Tag = tag.into(); + + assert_eq!(tag.frames.len(), 1); + + let frame = tag.get_user_text("ITUNESADVISORY"); + assert!(frame.is_some()); + assert_eq!(frame.unwrap(), "1"); + + let tag: Tag = tag.into(); + let tag: Ilst = tag.into(); + + assert_eq!(tag.advisory_rating(), Some(AdvisoryRating::Explicit)); +} diff --git a/lofty/src/mp4/ilst/mod.rs b/lofty/src/mp4/ilst/mod.rs index 8690e38d..513ee0b8 100644 --- a/lofty/src/mp4/ilst/mod.rs +++ b/lofty/src/mp4/ilst/mod.rs @@ -654,6 +654,11 @@ impl SplitTag for Ilst { false // Atom consumed }); + if let Some(rating) = self.advisory_rating() { + tag.insert_text(ItemKey::ParentalAdvisory, rating.as_u8().to_string()); + let _ = self.remove(&ADVISORY_RATING); + } + (SplitTagRemainder(self), tag) } } @@ -715,6 +720,29 @@ impl MergeTag for SplitTagRemainder { data: AtomDataStorage::Single(AtomData::Bool(data)), }) }, + ItemKey::ParentalAdvisory => { + let Ok(rating) = text.parse::() else { + log::warn!( + "Parental advisory rating is not a number: {}, discarding", + text + ); + continue; + }; + + let Ok(parsed_rating) = AdvisoryRating::try_from(rating) else { + log::warn!( + "Parental advisory rating is out of range: {rating}, discarding" + ); + continue; + }; + + merged.atoms.push(Atom { + ident: ident.into_owned(), + data: AtomDataStorage::Single(AtomData::SignedInteger(i32::from( + parsed_rating.as_u8(), + ))), + }) + }, _ => merged.atoms.push(Atom { ident: ident.into_owned(), data: AtomDataStorage::Single(AtomData::UTF8(text)), diff --git a/lofty/src/tag/item.rs b/lofty/src/tag/item.rs index 2f6beec0..87bfcc4c 100644 --- a/lofty/src/tag/item.rs +++ b/lofty/src/tag/item.rs @@ -169,6 +169,7 @@ gen_map!( "TRCK" => TrackNumber, "TRCK" => TrackTotal, "POPM" => Popularimeter, + "ITUNESADVISORY" => ParentalAdvisory, "TDRC" => RecordingDate, "TDOR" => OriginalReleaseDate, "TSRC" => Isrc,