diff --git a/src/components/logic/aiff.rs b/src/components/logic/aiff.rs index 6d8694cd..b0f2f9cf 100644 --- a/src/components/logic/aiff.rs +++ b/src/components/logic/aiff.rs @@ -1,16 +1,17 @@ use crate::{LoftyError, Result}; use byteorder::{BigEndian, LittleEndian, ReadBytesExt}; -use std::cmp::{max, min}; + use std::fs::File; use std::io::{Read, Seek, SeekFrom, Write}; -pub(crate) fn read_from(data: &mut T) -> Result<(Option, Option)> +pub(crate) fn read_from(data: &mut T) -> Result<(Option, Option, Option)> where T: Read + Seek, { let mut name_id: Option = None; let mut author_id: Option = None; + let mut copyright_id: Option = None; data.seek(SeekFrom::Start(12))?; @@ -31,22 +32,28 @@ where author_id = Some(String::from_utf8(auth)?); }, + f if f == b"(c) " && copyright_id.is_none() => { + let mut copy = vec![0; size as usize]; + data.read_exact(&mut copy)?; + + copyright_id = Some(String::from_utf8(copy)?); + }, _ => { data.seek(SeekFrom::Current(i64::from(size)))?; }, } } - if (&None, &None) == (&name_id, &author_id) { + if (&None, &None, &None) == (&name_id, &author_id, ©right_id) { return Err(LoftyError::InvalidData("AIFF file contains no text chunks")); } - Ok((name_id, author_id)) + Ok((name_id, author_id, copyright_id)) } pub(crate) fn write_to( data: &mut File, - metadata: (Option<&String>, Option<&String>), + metadata: (Option<&String>, Option<&String>, Option<&String>), ) -> Result<()> { let mut text_chunks = Vec::new(); @@ -66,10 +73,19 @@ pub(crate) fn write_to( text_chunks.extend(author_id.as_bytes().iter()); } + if let Some(copyright_id) = metadata.2 { + let len = (copyright_id.len() as u32).to_be_bytes(); + + text_chunks.extend(b"(c) ".iter()); + text_chunks.extend(len.iter()); + text_chunks.extend(copyright_id.as_bytes().iter()); + } + data.seek(SeekFrom::Start(12))?; let mut name: Option<(usize, usize)> = None; let mut auth: Option<(usize, usize)> = None; + let mut copy: Option<(usize, usize)> = None; while let (Ok(fourcc), Ok(size)) = ( data.read_u32::(), @@ -80,6 +96,7 @@ pub(crate) fn write_to( match &fourcc.to_le_bytes() { f if f == b"NAME" && name.is_none() => name = Some((pos, (pos + 8 + size as usize))), f if f == b"AUTH" && auth.is_none() => auth = Some((pos, (pos + 8 + size as usize))), + f if f == b"(c) " && copy.is_none() => copy = Some((pos, (pos + 8 + size as usize))), _ => { data.seek(SeekFrom::Current(i64::from(size)))?; }, @@ -91,21 +108,8 @@ pub(crate) fn write_to( let mut file_bytes = Vec::new(); data.read_to_end(&mut file_bytes)?; - match (name, auth) { - (Some((n_pos, n_end)), Some((a_pos, a_end))) => { - let first_start = min(n_pos, a_pos); - let first_end = min(n_end, a_end); - - let last_start = max(n_pos, a_pos); - let last_end = max(n_end, a_end); - - file_bytes.drain(last_start..last_end); - file_bytes.splice(first_start..first_end, text_chunks); - }, - (Some((start, end)), None) | (None, Some((start, end))) => { - file_bytes.splice(start..end, text_chunks); - }, - (None, None) => { + match (name, auth, copy) { + (None, None, None) => { data.seek(SeekFrom::Start(16))?; let mut size = [0; 4]; @@ -114,6 +118,23 @@ pub(crate) fn write_to( let comm_end = (20 + u32::from_le_bytes(size)) as usize; file_bytes.splice(comm_end..comm_end, text_chunks); }, + (Some(single_value), None, None) + | (None, Some(single_value), None) + | (None, None, Some(single_value)) => { + file_bytes.splice(single_value.0..single_value.1, text_chunks); + }, + (title, author, copyright) => { + let items: Vec<(usize, usize)> = vec![title, author, copyright] + .iter() + .filter(|i| i.is_some()) + .map(|v| v.unwrap()) + .collect(); + + if let (Some(first), Some(last)) = (items.iter().min(), items.iter().max()) { + file_bytes.drain(last.0..last.1); + file_bytes.splice(first.0..first.1, text_chunks); + } + }, } let total_size = ((file_bytes.len() - 8) as u32).to_be_bytes(); diff --git a/src/components/logic/riff.rs b/src/components/logic/riff.rs index 74c2c57b..0ea08be6 100644 --- a/src/components/logic/riff.rs +++ b/src/components/logic/riff.rs @@ -28,12 +28,15 @@ where #[allow(clippy::cast_lossless)] while cursor.position() < info_list_size as u64 { + if cursor.read_u8()? != 0 { + cursor.seek(SeekFrom::Current(-1))?; + } + let mut fourcc = vec![0; 4]; cursor.read_exact(&mut fourcc)?; - let size = cursor.read_u32::()?; - let key = String::from_utf8(fourcc)?; + let size = cursor.read_u32::()?; let mut buf = vec![0; size as usize]; cursor.read_exact(&mut buf)?; diff --git a/src/components/tags/aiff_tag.rs b/src/components/tags/aiff_tag.rs index 6d41fa03..a989ef4c 100644 --- a/src/components/tags/aiff_tag.rs +++ b/src/components/tags/aiff_tag.rs @@ -12,6 +12,7 @@ use lofty_attr::impl_tag; struct AiffInnerTag { name_id: Option, author_id: Option, + copyright_id: Option, } #[impl_tag(AiffInnerTag, TagType::AiffText)] @@ -24,10 +25,14 @@ impl AiffTag { where R: Read + Seek, { - let (name_id, author_id) = aiff::read_from(reader)?; + let (name_id, author_id, copyright_id) = aiff::read_from(reader)?; Ok(Self { - inner: AiffInnerTag { name_id, author_id }, + inner: AiffInnerTag { + name_id, + author_id, + copyright_id, + }, }) } } @@ -56,13 +61,27 @@ impl AudioTagEdit for AiffTag { fn remove_artist(&mut self) { self.inner.author_id = None } + + fn copyright(&self) -> Option<&str> { + self.inner.copyright_id.as_deref() + } + fn set_copyright(&mut self, copyright: &str) { + self.inner.copyright_id = Some(copyright.to_string()) + } + fn remove_copyright(&mut self) { + self.inner.copyright_id = None + } } impl AudioTagWrite for AiffTag { fn write_to(&self, file: &mut File) -> Result<()> { aiff::write_to( file, - (self.inner.name_id.as_ref(), self.inner.author_id.as_ref()), + ( + self.inner.name_id.as_ref(), + self.inner.author_id.as_ref(), + self.inner.copyright_id.as_ref(), + ), ) } } diff --git a/src/components/tags/ape_tag.rs b/src/components/tags/ape_tag.rs index 8d9a2e7b..fde14669 100644 --- a/src/components/tags/ape_tag.rs +++ b/src/components/tags/ape_tag.rs @@ -120,6 +120,16 @@ impl AudioTagEdit for ApeTag { self.remove_key("Year") } + fn copyright(&self) -> Option<&str> { + self.get_value("Copyright") + } + fn set_copyright(&mut self, copyright: &str) { + self.set_value("Copyright", copyright) + } + fn remove_copyright(&mut self) { + self.remove_key("Copyright") + } + fn album_title(&self) -> Option<&str> { self.get_value("Album") } diff --git a/src/components/tags/id3_tag.rs b/src/components/tags/id3_tag.rs index e69d37cb..fd8f4162 100644 --- a/src/components/tags/id3_tag.rs +++ b/src/components/tags/id3_tag.rs @@ -105,15 +105,17 @@ impl AudioTagEdit for Id3v2Tag { fn date(&self) -> Option { if let Some(released) = self.inner.get("TDRL") { - if let id3::frame::Content::Text(date) = &released.content() { - return Some(date.clone()); - } + return released + .content() + .text() + .map(std::string::ToString::to_string); } if let Some(recorded) = self.inner.get("TRDC") { - if let id3::frame::Content::Text(date) = &recorded.content() { - return Some(date.clone()); - } + return recorded + .content() + .text() + .map(std::string::ToString::to_string); } None @@ -140,6 +142,20 @@ impl AudioTagEdit for Id3v2Tag { self.inner.remove_year() } + fn copyright(&self) -> Option<&str> { + if let Some(frame) = self.inner.get("TCOP") { + return frame.content().text(); + } + + None + } + fn set_copyright(&mut self, copyright: &str) { + self.inner.set_text("TCOP", copyright) + } + fn remove_copyright(&mut self) { + self.inner.remove("TCOP") + } + fn album_title(&self) -> Option<&str> { self.inner.album() } diff --git a/src/components/tags/mp4_tag.rs b/src/components/tags/mp4_tag.rs index 07984b0c..f5b439df 100644 --- a/src/components/tags/mp4_tag.rs +++ b/src/components/tags/mp4_tag.rs @@ -61,17 +61,16 @@ impl AudioTagEdit for Mp4Tag { fn set_title(&mut self, title: &str) { self.inner.set_title(title) } - fn remove_title(&mut self) { self.inner.remove_title(); } + fn artist_str(&self) -> Option<&str> { self.inner.artist() } fn set_artist(&mut self, artist: &str) { self.inner.set_artist(artist) } - fn remove_artist(&mut self) { self.inner.remove_artists(); } @@ -82,14 +81,23 @@ impl AudioTagEdit for Mp4Tag { fn set_year(&mut self, year: i32) { self.inner.set_year(year.to_string()) } - fn remove_year(&mut self) { self.inner.remove_year(); } + + fn copyright(&self) -> Option<&str> { + self.inner.copyright() + } + fn set_copyright(&mut self, copyright: &str) { + self.inner.set_copyright(copyright) + } + fn remove_copyright(&mut self) { + self.inner.remove_copyright() + } + fn album_title(&self) -> Option<&str> { self.inner.album() } - fn set_album_title(&mut self, title: &str) { self.inner.set_album(title) } @@ -100,11 +108,9 @@ impl AudioTagEdit for Mp4Tag { fn album_artist_str(&self) -> Option<&str> { self.inner.album_artist() } - fn set_album_artist(&mut self, artists: &str) { self.inner.set_album_artist(artists) } - fn remove_album_artists(&mut self) { self.inner.remove_album_artists(); } @@ -155,11 +161,9 @@ impl AudioTagEdit for Mp4Tag { fn back_cover(&self) -> Option { self.front_cover() } - fn set_back_cover(&mut self, cover: Picture) { self.set_front_cover(cover) } - fn remove_back_cover(&mut self) { self.inner.remove_artwork(); } diff --git a/src/components/tags/ogg_tag.rs b/src/components/tags/ogg_tag.rs index c646604f..074b52ae 100644 --- a/src/components/tags/ogg_tag.rs +++ b/src/components/tags/ogg_tag.rs @@ -236,6 +236,16 @@ impl AudioTagEdit for OggTag { self.inner.remove_key("YEAR"); } + fn copyright(&self) -> Option<&str> { + self.inner.get_value("COPYRIGHT") + } + fn set_copyright(&mut self, copyright: &str) { + self.inner.set_value("COPYRIGHT", copyright) + } + fn remove_copyright(&mut self) { + self.inner.remove_key("COPYRIGHT") + } + fn album_title(&self) -> Option<&str> { self.inner.get_value("ALBUM") } diff --git a/src/components/tags/riff_tag.rs b/src/components/tags/riff_tag.rs index eef38e68..755a5b7c 100644 --- a/src/components/tags/riff_tag.rs +++ b/src/components/tags/riff_tag.rs @@ -60,11 +60,9 @@ impl AudioTagEdit for RiffTag { fn title(&self) -> Option<&str> { self.get_value("INAM") } - fn set_title(&mut self, title: &str) { self.set_value("INAM", title) } - fn remove_title(&mut self) { self.remove_key("INAM") } @@ -72,23 +70,29 @@ impl AudioTagEdit for RiffTag { fn artist_str(&self) -> Option<&str> { self.get_value("IART") } - fn set_artist(&mut self, artist: &str) { self.set_value("IART", artist) } - fn remove_artist(&mut self) { self.remove_key("IART") } + fn copyright(&self) -> Option<&str> { + self.get_value("ICOP") + } + fn set_copyright(&mut self, copyright: &str) { + self.set_value("ICOP", copyright) + } + fn remove_copyright(&mut self) { + self.remove_key("ICOP") + } + fn album_title(&self) -> Option<&str> { self.get_value("IPRD").or_else(|| self.get_value("ALBU")) } - fn set_album_title(&mut self, title: &str) { self.set_value("IPRD", title) } - fn remove_album_title(&mut self) { self.remove_key("IPRD") } diff --git a/src/traits.rs b/src/traits.rs index b12acd7e..3afdbfdf 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -55,6 +55,15 @@ pub trait AudioTagEdit { /// Removes the track year fn remove_year(&mut self) {} + /// Returns the copyright + fn copyright(&self) -> Option<&str> { + None + } + /// Sets the copyright + fn set_copyright(&mut self, _copyright: &str) {} + /// Removes the copyright + fn remove_copyright(&mut self) {} + /// Returns the track's [`Album`] fn album(&self) -> Album<'_> { Album { diff --git a/tests/io.rs b/tests/io.rs index b7c44756..ea13149e 100644 --- a/tests/io.rs +++ b/tests/io.rs @@ -31,6 +31,9 @@ macro_rules! add_tags { println!("Setting year"); tag.set_year(2020); + println!("Setting copyright"); + tag.set_copyright("1988"); + println!("Setting album title"); tag.set_album_title("foo album title"); @@ -103,6 +106,9 @@ macro_rules! verify_write { assert_eq!(tag.year(), Some(2020)); } + println!("Verifying copyright"); + assert_eq!(tag.copyright(), Some("1988")); + println!("Verifying album title"); assert_eq!(tag.album_title(), Some("foo album title")); @@ -174,6 +180,11 @@ macro_rules! remove_tags { assert!(tag.year().is_none()); tag.remove_year(); + println!("Removing copyright"); + tag.remove_copyright(); + assert!(tag.copyright().is_none()); + tag.remove_copyright(); + println!("Removing album title"); tag.remove_album_title(); assert!(tag.album_title().is_none()); @@ -224,11 +235,14 @@ fn test_aiff_text() { tag.set_title("foo title"); println!("Setting artist"); tag.set_artist("foo artist"); + println!("Setting copyright"); + tag.set_copyright("1988"); println!("Writing"); tag.write_to_path(file).unwrap(); println!("-- Verifying tags --"); + println!("Reading file"); let mut tag = Tag::default().read_from_path_signature(file).unwrap(); @@ -236,6 +250,8 @@ fn test_aiff_text() { assert_eq!(tag.title(), Some("foo title")); println!("Verifying artist"); assert_eq!(tag.artist_str(), Some("foo artist")); + println!("Verifying copyright"); + assert_eq!(tag.copyright(), Some("1988")); println!("-- Removing tags --"); @@ -243,6 +259,8 @@ fn test_aiff_text() { tag.remove_title(); println!("Removing artist"); tag.remove_artist(); + println!("Removing copyright"); + tag.remove_copyright(); println!("Writing"); tag.write_to_path(file).unwrap()