From 28fe2040586bb46ebb6a82f420376d22466da764 Mon Sep 17 00:00:00 2001 From: Serial <69764315+Serial-ATA@users.noreply.github.com> Date: Fri, 10 Jun 2022 10:24:00 -0400 Subject: [PATCH] Accessor: Add `{set_, remove_}track` --- CHANGELOG.md | 2 + src/ape/tag/mod.rs | 90 +++++++++++++++++++----------- src/id3/v1/tag.rs | 12 ++++ src/id3/v2/flags.rs | 2 +- src/id3/v2/tag.rs | 101 +++++++++++++++++++++------------ src/iff/wav/tag/mod.rs | 58 ++++++++++++------- src/mp4/ilst/mod.rs | 82 ++++++++++++++++----------- src/ogg/tag.rs | 58 ++++++++++++------- src/traits.rs | 123 ++++++++++++++++++++++++++--------------- 9 files changed, 341 insertions(+), 187 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf4bc8d3..c17e61b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - **WavPack** support +- `Accessor::{set_, remove_}track` ### Changed - Bitrates in properties will be rounded up, similar to FFmpeg and TagLib @@ -21,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `RiffInfo` -> `RIFFInfo` - `AiffText` -> `AIFFText` - All types implementing `PartialEq` now implement `Eq` +- `Ilst::track_number` has been moved to the `Accessor::track` implementation ## [0.6.3] - 2022-05-18 diff --git a/src/ape/tag/mod.rs b/src/ape/tag/mod.rs index 90c2da31..4251ff31 100644 --- a/src/ape/tag/mod.rs +++ b/src/ape/tag/mod.rs @@ -16,35 +16,33 @@ use std::path::Path; macro_rules! impl_accessor { ($($name:ident => $($key:literal)|+;)+) => { paste::paste! { - impl Accessor for ApeTag { - $( - fn $name(&self) -> Option<&str> { - $( - if let Some(i) = self.get_key($key) { - if let ItemValue::Text(val) = i.value() { - return Some(val) - } + $( + fn $name(&self) -> Option<&str> { + $( + if let Some(i) = self.get_key($key) { + if let ItemValue::Text(val) = i.value() { + return Some(val) } - )+ + } + )+ - None - } + None + } - fn [](&mut self, value: String) { - self.insert(ApeItem { - read_only: false, - key: String::from(crate::tag::item::first_key!($($key)|*)), - value: ItemValue::Text(value) - }) - } + fn [](&mut self, value: String) { + self.insert(ApeItem { + read_only: false, + key: String::from(crate::tag::item::first_key!($($key)|*)), + value: ItemValue::Text(value) + }) + } - fn [](&mut self) { - $( - self.remove_key($key); - )+ - } - )+ - } + fn [](&mut self) { + $( + self.remove_key($key); + )+ + } + )+ } } } @@ -79,13 +77,6 @@ pub struct ApeTag { pub(super) items: Vec, } -impl_accessor!( - artist => "Artist"; - title => "Title"; - album => "Album"; - genre => "GENRE"; -); - impl ApeTag { /// Get an [`ApeItem`] by key /// @@ -121,6 +112,41 @@ impl ApeTag { } } +impl Accessor for ApeTag { + impl_accessor!( + artist => "Artist"; + title => "Title"; + album => "Album"; + genre => "GENRE"; + ); + + fn track(&self) -> Option { + if let Some(ApeItem { + value: ItemValue::Text(ref text), + .. + }) = self.get_key("Track") + { + if let Ok(ret) = text.parse::() { + return Some(ret); + } + } + + None + } + + fn set_track(&mut self, value: u32) { + self.insert(ApeItem { + read_only: false, + key: String::from("Track"), + value: ItemValue::Text(value.to_string()), + }) + } + + fn remove_track(&mut self) { + self.remove_key("Track"); + } +} + impl TagExt for ApeTag { type Err = LoftyError; diff --git a/src/id3/v1/tag.rs b/src/id3/v1/tag.rs index c75b6ff4..0b967cb2 100644 --- a/src/id3/v1/tag.rs +++ b/src/id3/v1/tag.rs @@ -115,6 +115,18 @@ impl Accessor for Id3v1Tag { fn remove_genre(&mut self) { self.genre = None } + + fn track(&self) -> Option { + self.track_number.map(u32::from) + } + + fn set_track(&mut self, value: u32) { + self.track_number = Some(value as u8); + } + + fn remove_track(&mut self) { + self.track_number = None; + } } impl TagExt for Id3v1Tag { diff --git a/src/id3/v2/flags.rs b/src/id3/v2/flags.rs index 552c1e97..c754fccd 100644 --- a/src/id3/v2/flags.rs +++ b/src/id3/v2/flags.rs @@ -1,7 +1,7 @@ #[cfg(feature = "id3v2_restrictions")] use super::restrictions::TagRestrictions; -#[derive(Default, Copy, Clone, Debug, PartialEq)] +#[derive(Default, Copy, Clone, Debug, PartialEq, Eq)] #[allow(clippy::struct_excessive_bools)] /// Flags that apply to the entire tag pub struct Id3v2TagFlags { diff --git a/src/id3/v2/tag.rs b/src/id3/v2/tag.rs index fa7b5374..0bcac57b 100644 --- a/src/id3/v2/tag.rs +++ b/src/id3/v2/tag.rs @@ -19,39 +19,37 @@ use std::io::Write; use std::path::Path; macro_rules! impl_accessor { - ($($name:ident, $id:literal;)+) => { + ($($name:ident => $id:literal;)+) => { paste::paste! { - impl Accessor for Id3v2Tag { - $( - fn $name(&self) -> Option<&str> { - if let Some(f) = self.get($id) { - if let FrameValue::Text { - ref value, - .. - } = f.content() { - return Some(value) - } + $( + fn $name(&self) -> Option<&str> { + if let Some(f) = self.get($id) { + if let FrameValue::Text { + ref value, + .. + } = f.content() { + return Some(value) } - - None } - fn [](&mut self, value: String) { - self.insert(Frame { - id: FrameID::Valid(String::from($id)), - value: FrameValue::Text { - encoding: TextEncoding::UTF8, - value, - }, - flags: FrameFlags::default() - }); - } + None + } - fn [](&mut self) { - self.remove($id) - } - )+ - } + fn [](&mut self, value: String) { + self.insert(Frame { + id: FrameID::Valid(String::from($id)), + value: FrameValue::Text { + encoding: TextEncoding::UTF8, + value, + }, + flags: FrameFlags::default() + }); + } + + fn [](&mut self) { + self.remove($id) + } + )+ } } } @@ -99,13 +97,6 @@ pub struct Id3v2Tag { frames: Vec, } -impl_accessor!( - title, "TIT2"; - artist, "TPE1"; - album, "TALB"; - genre, "TCON"; -); - impl IntoIterator for Id3v2Tag { type Item = Frame; type IntoIter = std::vec::IntoIter; @@ -272,6 +263,46 @@ impl Id3v2Tag { } } +impl Accessor for Id3v2Tag { + impl_accessor!( + title => "TIT2"; + artist => "TPE1"; + album => "TALB"; + genre => "TCON"; + ); + + fn track(&self) -> Option { + if let Some(Frame { + value: FrameValue::Text { ref value, .. }, + .. + }) = self.get("TRCK") + { + if let Some(track_num) = value.split(&['\0', '/'][..]).next() { + if let Ok(ret) = track_num.parse::() { + return Some(ret); + } + } + } + + None + } + + fn set_track(&mut self, value: u32) { + self.insert(Frame { + id: FrameID::Valid(String::from("TRCK")), + value: FrameValue::Text { + value: value.to_string(), + encoding: TextEncoding::UTF8, + }, + flags: FrameFlags::default(), + }); + } + + fn remove_track(&mut self) { + self.remove("TRCK"); + } +} + impl TagExt for Id3v2Tag { type Err = LoftyError; diff --git a/src/iff/wav/tag/mod.rs b/src/iff/wav/tag/mod.rs index 563c29ab..bb73d328 100644 --- a/src/iff/wav/tag/mod.rs +++ b/src/iff/wav/tag/mod.rs @@ -11,23 +11,21 @@ use std::io::Write; use std::path::Path; macro_rules! impl_accessor { - ($($name:ident, $key:literal;)+) => { + ($($name:ident => $key:literal;)+) => { paste::paste! { - impl Accessor for RiffInfoList { - $( - fn $name(&self) -> Option<&str> { - self.get($key) - } + $( + fn $name(&self) -> Option<&str> { + self.get($key) + } - fn [](&mut self, value: String) { - self.insert(String::from($key), value) - } + fn [](&mut self, value: String) { + self.insert(String::from($key), value) + } - fn [](&mut self) { - self.remove($key) - } - )+ - } + fn [](&mut self) { + self.remove($key) + } + )+ } } } @@ -52,13 +50,6 @@ pub struct RiffInfoList { pub(crate) items: Vec<(String, String)>, } -impl_accessor!( - artist, "IART"; - title, "INAM"; - album, "IPRD"; - genre, "IGNR"; -); - impl RiffInfoList { /// Get an item by key pub fn get(&self, key: &str) -> Option<&str> { @@ -99,6 +90,31 @@ impl RiffInfoList { } } +impl Accessor for RiffInfoList { + impl_accessor!( + artist => "IART"; + title => "INAM"; + album => "IPRD"; + genre => "IGNR"; + ); + + fn track(&self) -> Option { + if let Some(item) = self.get("IPRT") { + return item.parse::().ok(); + } + + None + } + + fn set_track(&mut self, value: u32) { + self.insert(String::from("IPRT"), value.to_string()); + } + + fn remove_track(&mut self) { + self.remove("IPRT"); + } +} + impl TagExt for RiffInfoList { type Err = LoftyError; diff --git a/src/mp4/ilst/mod.rs b/src/mp4/ilst/mod.rs index af2fba76..89116651 100644 --- a/src/mp4/ilst/mod.rs +++ b/src/mp4/ilst/mod.rs @@ -24,32 +24,30 @@ const ALBUM: AtomIdent = AtomIdent::Fourcc(*b"\xa9alb"); const GENRE: AtomIdent = AtomIdent::Fourcc(*b"\xa9gen"); macro_rules! impl_accessor { - ($($name:ident, $const:ident;)+) => { + ($($name:ident => $const:ident;)+) => { paste::paste! { - impl Accessor for Ilst { - $( - fn $name(&self) -> Option<&str> { - if let Some(atom) = self.atom(&$const) { - if let AtomData::UTF8(val) | AtomData::UTF16(val) = atom.data() { - return Some(val) - } + $( + fn $name(&self) -> Option<&str> { + if let Some(atom) = self.atom(&$const) { + if let AtomData::UTF8(val) | AtomData::UTF16(val) = atom.data() { + return Some(val) } - - None } - fn [](&mut self, value: String) { - self.replace_atom(Atom { - ident: $const, - data: AtomDataStorage::Single(AtomData::UTF8(value)), - }) - } + None + } - fn [](&mut self) { - self.remove_atom(&$const) - } - )+ - } + fn [](&mut self, value: String) { + self.replace_atom(Atom { + ident: $const, + data: AtomDataStorage::Single(AtomData::UTF8(value)), + }) + } + + fn [](&mut self) { + self.remove_atom(&$const) + } + )+ } } } @@ -85,13 +83,6 @@ pub struct Ilst { pub(crate) atoms: Vec, } -impl_accessor!( - artist, ARTIST; - title, TITLE; - album, ALBUM; - genre, GENRE; -); - impl Ilst { /// Returns all of the tag's atoms pub fn atoms(&self) -> &[Atom] { @@ -190,11 +181,6 @@ impl Ilst { }) } - /// Returns the track number - pub fn track_number(&self) -> Option { - self.extract_number(*b"trkn", 4) - } - /// Returns the total number of tracks pub fn track_total(&self) -> Option { self.extract_number(*b"trkn", 6) @@ -228,6 +214,36 @@ impl Ilst { } } +impl Accessor for Ilst { + impl_accessor!( + artist => ARTIST; + title => TITLE; + album => ALBUM; + genre => GENRE; + ); + + fn track(&self) -> Option { + self.extract_number(*b"trkn", 4).map(u32::from) + } + + fn set_track(&mut self, value: u32) { + let value = (value as u16).to_be_bytes(); + let track_total = self.track_total().unwrap_or(0).to_be_bytes(); + + self.replace_atom(Atom { + ident: AtomIdent::Fourcc(*b"trkn"), + data: AtomDataStorage::Single(AtomData::Unknown { + code: 0, + data: vec![0, 0, value[0], value[1], track_total[0], track_total[1]], + }), + }); + } + + fn remove_track(&mut self) { + self.remove_atom(&AtomIdent::Fourcc(*b"trkn")); + } +} + impl TagExt for Ilst { type Err = LoftyError; diff --git a/src/ogg/tag.rs b/src/ogg/tag.rs index 5d77afc0..d95846ab 100644 --- a/src/ogg/tag.rs +++ b/src/ogg/tag.rs @@ -13,23 +13,21 @@ use std::io::{Cursor, Write}; use std::path::Path; macro_rules! impl_accessor { - ($($name:ident, $key:literal;)+) => { + ($($name:ident => $key:literal;)+) => { paste::paste! { - impl Accessor for VorbisComments { - $( - fn $name(&self) -> Option<&str> { - self.get($key) - } + $( + fn $name(&self) -> Option<&str> { + self.get($key) + } - fn [](&mut self, value: String) { - self.insert(String::from($key), value, true) - } + fn [](&mut self, value: String) { + self.insert(String::from($key), value, true) + } - fn [](&mut self) { - let _ = self.remove($key); - } - )+ - } + fn [](&mut self) { + let _ = self.remove($key); + } + )+ } } } @@ -52,13 +50,6 @@ pub struct VorbisComments { pub(crate) pictures: Vec<(Picture, PictureInformation)>, } -impl_accessor!( - artist, "ARTIST"; - title, "TITLE"; - album, "ALBUM"; - genre, "GENRE"; -); - impl VorbisComments { /// Returns the vendor string pub fn vendor(&self) -> &str { @@ -157,6 +148,31 @@ impl VorbisComments { } } +impl Accessor for VorbisComments { + impl_accessor!( + artist => "ARTIST"; + title => "TITLE"; + album => "ALBUM"; + genre => "GENRE"; + ); + + fn track(&self) -> Option { + if let Some(item) = self.get("TRACKNUMBER") { + return item.parse::().ok(); + } + + None + } + + fn set_track(&mut self, value: u32) { + self.insert(String::from("TRACKNUMBER"), value.to_string(), true); + } + + fn remove_track(&mut self) { + let _ = self.remove("TRACKNUMBER"); + } +} + impl TagExt for VorbisComments { type Err = LoftyError; diff --git a/src/traits.rs b/src/traits.rs index eb6610fb..33b4c84c 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -1,5 +1,22 @@ +// This defines the `Accessor` trait, used to define unified getters/setters for commonly +// accessed tag values. +// +// Usage: +// +// accessor_trait! { +// field_name +// } +// +// Where `type` is the return type for `Accessor::field_name`. By default, this type will also be used +// in the setter. +// +// An owned type can also be specified for the setter: +// +// accessor_trait! { +// field_name +// } macro_rules! accessor_trait { - ($($name:ident),+) => { + ($($name:ident < $($ty:ty),+ >),+ $(,)?) => { /// Provides accessors for common items /// /// This attempts to only provide methods for items that all tags have in common, @@ -7,56 +24,74 @@ macro_rules! accessor_trait { pub trait Accessor { paste::paste! { $( - #[doc = "Returns the " $name] - /// # Example - /// - /// ```rust - /// use lofty::{Tag, Accessor}; - /// # let tag_type = lofty::TagType::ID3v2; - /// - /// let mut tag = Tag::new(tag_type); - /// - #[doc = "assert_eq!(tag." $name "(), None);"] - /// ``` - fn $name(&self) -> Option<&str> { None } - #[doc = "Sets the " $name] - /// # Example - /// - /// ```rust - /// use lofty::{Tag, Accessor}; - /// # let tag_type = lofty::TagType::ID3v2; - /// - #[doc = "let mut tag = Tag::new(tag_type);\ntag.set_" $name "(String::from(\"Foo " $name "\"));"] - /// - #[doc = "assert_eq!(tag." $name "(), Some(\"Foo " $name "\"));"] - /// ``` - fn [](&mut self, _value: String) {} - #[doc = "Removes the " $name] - /// - /// # Example - /// - /// ```rust - /// use lofty::{Tag, Accessor}; - /// # let tag_type = lofty::TagType::ID3v2; - /// - #[doc = "let mut tag = Tag::new(tag_type);\ntag.set_" $name "(String::from(\"Foo " $name "\"));"] - /// - #[doc = "assert_eq!(tag." $name "(), Some(\"Foo " $name "\"));"] - /// - #[doc = "tag.remove_" $name "();"] - /// - #[doc = "assert_eq!(tag." $name "(), None);"] - /// ``` - fn [](&mut self) {} + accessor_trait! { @GETTER $name $($ty),+ } + + accessor_trait! { @SETTER $name $($ty),+ } + + accessor_trait! { @REMOVE $name $($ty),+ } )+ } } }; + (@GETTER $name:ident $ty:ty $(, $_ty:tt)?) => { + paste::paste! { + #[doc = "Returns the " $name] + /// # Example + /// + /// ```rust,ignore + /// use lofty::{Tag, Accessor}; + /// + /// let mut tag = Tag::new(tag_type); + /// + #[doc = "assert_eq!(tag." $name "(), None);"] + /// ``` + fn $name(&self) -> Option<$ty> { None } + } + }; + (@SETTER $name:ident $_ty:ty, $owned_ty:tt) => { + accessor_trait! { @SETTER $name $owned_ty } + }; + (@SETTER $name:ident $ty:ty $(, $_ty:tt)?) => { + paste::paste! { + #[doc = "Sets the " $name] + /// # Example + /// + /// ```rust,ignore + /// use lofty::{Tag, Accessor}; + /// + #[doc = "let mut tag = Tag::new(tag_type);\ntag.set_" $name "(value);"] + /// + #[doc = "assert_eq!(tag." $name "(), Some(value));"] + /// ``` + fn [](&mut self, _value: $ty) {} + } + }; + (@REMOVE $name:ident $ty:ty $(, $_ty:tt)?) => { + paste::paste! { + #[doc = "Removes the " $name] + /// + /// # Example + /// + /// ```rust,ignore + /// use lofty::{Tag, Accessor}; + /// + #[doc = "let mut tag = Tag::new(tag_type);\ntag.set_" $name "(value);"] + /// + #[doc = "assert_eq!(tag." $name "(), Some(value));"] + /// + #[doc = "tag.remove_" $name "();"] + /// + #[doc = "assert_eq!(tag." $name "(), None);"] + /// ``` + fn [](&mut self) {} + } + }; } accessor_trait! { - artist, title, - album, genre + artist<&str, String>, title<&str, String>, + album<&str, String>, genre<&str, String>, + track } use crate::tag::Tag;