From 47f67e019a5ef9e81bcc47e95530b447a14037cf Mon Sep 17 00:00:00 2001 From: Serial <69764315+Serial-ATA@users.noreply.github.com> Date: Sat, 18 Sep 2021 12:22:24 -0400 Subject: [PATCH] Break up logic::id3::v1, improve doc comments --- src/lib.rs | 127 +++++++++++++++++---------- src/logic/ape/write.rs | 2 +- src/logic/id3/mod.rs | 2 - src/logic/id3/{ => v1}/constants.rs | 2 +- src/logic/id3/v1/mod.rs | 41 +++++++++ src/logic/id3/v1/read.rs | 63 +++++++++++++ src/logic/id3/{v1.rs => v1/write.rs} | 101 +-------------------- src/logic/id3/v2/frame/mod.rs | 4 +- src/logic/id3/v2/frame/read.rs | 1 - src/logic/mpeg/read.rs | 2 +- src/types/item.rs | 8 +- src/types/tag.rs | 2 +- 12 files changed, 198 insertions(+), 157 deletions(-) rename src/logic/id3/{ => v1}/constants.rs (98%) create mode 100644 src/logic/id3/v1/mod.rs create mode 100644 src/logic/id3/v1/read.rs rename src/logic/id3/{v1.rs => v1/write.rs} (52%) diff --git a/src/lib.rs b/src/lib.rs index cbdda3e7..2ccab40c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -105,7 +105,7 @@ //! ## Utilities //! * `id3v2_restrictions` - Parses ID3v2 extended headers and exposes flags for fine grained control //! -//! # Notes on ID3v2 +//! # Notes on ID3 //! //! See [`id3`](crate::id3) for important warnings and notes on reading tags. @@ -158,52 +158,85 @@ pub mod files { #[cfg(any(feature = "id3v1", feature = "id3v2"))] /// ID3v1/v2 specific items pub mod id3 { - //! # ID3v2 notes and warnings - //! - //! ID3v2 does things differently than other formats. - //! - //! ## Unknown Keys - //! - //! ID3v2 **does not** support [`ItemKey::Unknown`](crate::ItemKey::Unknown) and they will be ignored. - //! Instead, [`ItemKey::Id3v2Specific`](crate::ItemKey::Id3v2Specific) with an [`Id3v2Frame`](crate::id3::Id3v2Frame) variant must be used. - //! - //! ## Frame ID mappings - //! - //! Certain [`ItemKey`](crate::ItemKey)s are unable to map to an ID3v2 frame, as they are a part of a larger - //! collection (such as `TIPL` and `TMCL`). - //! - //! For example, if the key is `Arranger` (part of `TIPL`), there is no mapping available. - //! - //! In this case, the caller is expected to build these lists. If these [`ItemKey`](crate::ItemKey)s are inserted - //! using [`Tag::insert_item_unchecked`], they will simply be ignored. - //! - //! ## Special frames - //! - //! ID3v2 has multiple frames that have no equivalent in other formats: - //! - //! * COMM - Comments (Unlike comments in other formats) - //! * USLT - Unsynchronized text (Unlike lyrics/text in other formats) - //! * TXXX - User defined text - //! * WXXX - User defined URL - //! * SYLT - Synchronized text - //! * GEOB - Encapsulated object (file) - //! - //! These frames all require different amounts of information, so they cannot be mapped to a traditional [`ItemKey`](crate::ItemKey) variant. - //! The solution is to use [`ItemKey::Id3v2Specific`](crate::ItemKey::Id3v2Specific) alongside [`Id3v2Frame`](crate::id3::Id3v2Frame). - //! - //! NOTE: Unlike the above issue, this one does not require unchecked insertion. - pub use crate::logic::id3::v2::frame::{Id3v2Frame, LanguageSpecificFrame}; - pub use crate::logic::id3::v2::items::encapsulated_object::{ - GEOBInformation, GeneralEncapsulatedObject, - }; - #[cfg(feature = "id3v2_restrictions")] - pub use crate::logic::id3::v2::items::restrictions::*; - pub use crate::logic::id3::v2::items::sync_text::{ - SyncTextContentType, SyncTextInformation, SynchronizedText, TimestampFormat, - }; - pub use crate::logic::id3::v2::util::text_utils::TextEncoding; - pub use crate::logic::id3::v2::util::upgrade::{upgrade_v2, upgrade_v3}; - pub use crate::logic::id3::v2::Id3v2Version; + //! ID3 does things differently than other tags, making working with them a little more effort than other formats. + //! Check the other modules for important notes and/or warnings. + + #[cfg(feature = "id3v2")] + pub mod v2 { + //! ID3v2 items and utilities + //! + //! # ID3v2 notes and warnings + //! + //! ID3v2 does things differently than other formats. + //! + //! ## Unknown Keys + //! + //! ID3v2 **does not** support [`ItemKey::Unknown`](crate::ItemKey::Unknown) and they will be ignored. + //! Instead, [`ItemKey::Id3v2Specific`](crate::ItemKey::Id3v2Specific) with an [`Id3v2Frame`](crate::id3::v2::Id3v2Frame) variant must be used. + //! + //! ## Frame ID mappings + //! + //! Certain [`ItemKey`](crate::ItemKey)s are unable to map to an ID3v2 frame, as they are a part of a larger + //! collection (such as `TIPL` and `TMCL`). + //! + //! For example, if the key is `Arranger` (part of `TIPL`), there is no mapping available. + //! + //! In this case, the caller is expected to build these lists. If these [`ItemKey`](crate::ItemKey)s are inserted + //! using [`Tag::insert_item_unchecked`](crate::Tag::insert_item_unchecked), they will simply be ignored. + //! + //! ## Special frames + //! + //! ID3v2 has multiple frames that have no equivalent in other formats: + //! + //! * COMM - Comments (Unlike comments in other formats) + //! * USLT - Unsynchronized text (Unlike lyrics/text in other formats) + //! * TXXX - User defined text + //! * WXXX - User defined URL + //! * SYLT - Synchronized text + //! * GEOB - Encapsulated object (file) + //! + //! These frames all require different amounts of information, so they cannot be mapped to a traditional [`ItemKey`](crate::ItemKey) variant. + //! The solution is to use [`ItemKey::Id3v2Specific`](crate::ItemKey::Id3v2Specific) alongside [`Id3v2Frame`](crate::id3::v2::Id3v2Frame). + //! + //! NOTE: Unlike the above issue, this one does not require unchecked insertion. + + pub use { + crate::logic::id3::v2::frame::{Id3v2Frame, LanguageSpecificFrame}, + crate::logic::id3::v2::items::encapsulated_object::{ + GEOBInformation, GeneralEncapsulatedObject, + }, + crate::logic::id3::v2::items::sync_text::{ + SyncTextContentType, SyncTextInformation, SynchronizedText, TimestampFormat, + }, + crate::logic::id3::v2::util::text_utils::TextEncoding, + crate::logic::id3::v2::util::upgrade::{upgrade_v2, upgrade_v3}, + crate::logic::id3::v2::Id3v2Version, + }; + + #[cfg(feature = "id3v2_restrictions")] + pub use crate::logic::id3::v2::items::restrictions::*; + } + + #[cfg(feature = "id3v1")] + pub mod v1 { + //! ID3v1 items + //! + //! # ID3v1 notes + //! + //! ## Genres + //! + //! ID3v1 stores the genre in a single byte ranging from 0 to 192. + //! The number can be stored in any of the following [`ItemValue`](crate::ItemValue) variants: `Text, UInt, UInt64, Int, Int64`, and will be discarded if it is unable to parse or is too big. + //! All possible genres have been stored in the [`GENRES`](crate::id3::v1::GENRES) constant. + //! + //! ## Track Numbers + //! + //! ID3v1 stores the track number in a non-zero byte. + //! A track number of 0 will be treated as an empty field. + //! Additionally, there is no track total field. + + pub use crate::logic::id3::v1::constants::GENRES; + } } /// Various items related to [`Picture`](crate::picture::Picture)s diff --git a/src/logic/ape/write.rs b/src/logic/ape/write.rs index f2c50ac8..4f0e603c 100644 --- a/src/logic/ape/write.rs +++ b/src/logic/ape/write.rs @@ -6,7 +6,7 @@ use std::fs::File; pub(crate) fn write_to(data: &mut File, tag: &Tag) -> Result<()> { match tag.tag_type() { TagType::Ape => super::tag::write::write_to(data, tag), - TagType::Id3v1 => crate::logic::id3::v1::write_id3v1(data, tag), + TagType::Id3v1 => crate::logic::id3::v1::write::write_id3v1(data, tag), TagType::Id3v2 => todo!(), _ => Err(LoftyError::UnsupportedTag), } diff --git a/src/logic/id3/mod.rs b/src/logic/id3/mod.rs index 47d8aa24..e5e08630 100644 --- a/src/logic/id3/mod.rs +++ b/src/logic/id3/mod.rs @@ -1,5 +1,3 @@ -mod constants; - #[cfg(feature = "id3v1")] pub(crate) mod v1; diff --git a/src/logic/id3/constants.rs b/src/logic/id3/v1/constants.rs similarity index 98% rename from src/logic/id3/constants.rs rename to src/logic/id3/v1/constants.rs index 742ca142..efbccf4c 100644 --- a/src/logic/id3/constants.rs +++ b/src/logic/id3/v1/constants.rs @@ -1,4 +1,4 @@ -#[cfg(feature = "id3v1")] +/// All possible genres for ID3v1 pub const GENRES: [&str; 192] = [ "Blues", "Classic rock", diff --git a/src/logic/id3/v1/mod.rs b/src/logic/id3/v1/mod.rs new file mode 100644 index 00000000..b24e75e7 --- /dev/null +++ b/src/logic/id3/v1/mod.rs @@ -0,0 +1,41 @@ +pub(crate) mod constants; +pub(in crate::logic) mod read; +pub(in crate::logic) mod write; + +use crate::error::Result; +use crate::types::tag::Tag; + +use std::io::{Read, Seek, SeekFrom}; + +pub(in crate::logic) fn find_id3v1(data: &mut R, read: bool) -> Result<(bool, Option)> +where + R: Read + Seek, +{ + let mut id3v1 = None; + let mut exists = false; + + data.seek(SeekFrom::End(-128))?; + + let mut id3v1_header = [0; 3]; + data.read_exact(&mut id3v1_header)?; + + data.seek(SeekFrom::Current(-3))?; + + if &id3v1_header == b"TAG" { + exists = true; + + if read { + let mut id3v1_tag = [0; 128]; + data.read_exact(&mut id3v1_tag)?; + + data.seek(SeekFrom::End(-128))?; + + id3v1 = Some(read::parse_id3v1(id3v1_tag)) + } + } else { + // No ID3v1 tag found + data.seek(SeekFrom::End(0))?; + } + + Ok((exists, id3v1)) +} diff --git a/src/logic/id3/v1/read.rs b/src/logic/id3/v1/read.rs new file mode 100644 index 00000000..4036d9ed --- /dev/null +++ b/src/logic/id3/v1/read.rs @@ -0,0 +1,63 @@ +use super::constants::GENRES; +use crate::types::item::{ItemKey, ItemValue, TagItem}; +use crate::types::tag::{Tag, TagType}; + +pub fn parse_id3v1(reader: [u8; 128]) -> Tag { + let mut tag = Tag::new(TagType::Id3v1); + + let reader = &reader[3..]; + + if let Some(title) = decode_text(ItemKey::TrackTitle, &reader[..30]) { + tag.insert_item_unchecked(title); + } + + if let Some(artist) = decode_text(ItemKey::TrackArtist, &reader[30..60]) { + tag.insert_item_unchecked(artist); + } + + if let Some(album) = decode_text(ItemKey::AlbumTitle, &reader[60..90]) { + tag.insert_item_unchecked(album); + } + + if let Some(year) = decode_text(ItemKey::Year, &reader[90..94]) { + tag.insert_item_unchecked(year); + } + + let range = if reader[119] == 0 && reader[122] != 0 { + tag.insert_item_unchecked(TagItem::new( + ItemKey::TrackNumber, + ItemValue::UInt(u32::from(reader[122])), + )); + + 94_usize..123 + } else { + 94..124 + }; + + if let Some(comment) = decode_text(ItemKey::Comment, &reader[range]) { + tag.insert_item_unchecked(comment); + } + + if reader[124] < GENRES.len() as u8 { + tag.insert_item_unchecked(TagItem::new( + ItemKey::Genre, + ItemValue::Text(GENRES[reader[125] as usize].to_string()), + )); + } + + tag +} + +fn decode_text(key: ItemKey, data: &[u8]) -> Option { + let read = data + .iter() + .filter(|c| **c != 0) + .map(|c| *c as char) + .collect::(); + + if read.is_empty() { + None + } else { + Some(TagItem::new(key, ItemValue::Text(read))) + } +} diff --git a/src/logic/id3/v1.rs b/src/logic/id3/v1/write.rs similarity index 52% rename from src/logic/id3/v1.rs rename to src/logic/id3/v1/write.rs index 3244d372..00cb9f1c 100644 --- a/src/logic/id3/v1.rs +++ b/src/logic/id3/v1/write.rs @@ -1,112 +1,19 @@ -use super::constants::GENRES; use crate::error::Result; use crate::types::item::{ItemKey, ItemValue, TagItem}; -use crate::types::tag::{Tag, TagType}; +use crate::types::tag::Tag; -use byteorder::WriteBytesExt; use std::io::{Cursor, Read, Seek, SeekFrom, Write}; -pub(in crate::logic) fn find_id3v1(data: &mut R, read: bool) -> Result<(bool, Option)> -where - R: Read + Seek, -{ - let mut id3v1 = None; - let mut exists = false; +use byteorder::WriteBytesExt; - data.seek(SeekFrom::End(-128))?; - - let mut id3v1_header = [0; 3]; - data.read_exact(&mut id3v1_header)?; - - data.seek(SeekFrom::Current(-3))?; - - if &id3v1_header == b"TAG" { - exists = true; - - if read { - let mut id3v1_tag = [0; 128]; - data.read_exact(&mut id3v1_tag)?; - - data.seek(SeekFrom::End(-128))?; - - id3v1 = Some(parse_id3v1(id3v1_tag)) - } - } else { - // No ID3v1 tag found - data.seek(SeekFrom::End(0))?; - } - - Ok((exists, id3v1)) -} - -pub(in crate::logic) fn parse_id3v1(reader: [u8; 128]) -> Tag { - let mut tag = Tag::new(TagType::Id3v1); - - let reader = &reader[3..]; - - if let Some(title) = decode_text(ItemKey::TrackTitle, &reader[..30]) { - tag.insert_item_unchecked(title); - } - - if let Some(artist) = decode_text(ItemKey::TrackArtist, &reader[30..60]) { - tag.insert_item_unchecked(artist); - } - - if let Some(album) = decode_text(ItemKey::AlbumTitle, &reader[60..90]) { - tag.insert_item_unchecked(album); - } - - if let Some(year) = decode_text(ItemKey::Year, &reader[90..94]) { - tag.insert_item_unchecked(year); - } - - let range = if reader[119] == 0 && reader[122] != 0 { - tag.insert_item_unchecked(TagItem::new( - ItemKey::TrackNumber, - ItemValue::UInt(u32::from(reader[122])), - )); - - 94_usize..123 - } else { - 94..124 - }; - - if let Some(comment) = decode_text(ItemKey::Comment, &reader[range]) { - tag.insert_item_unchecked(comment); - } - - if reader[124] < GENRES.len() as u8 { - tag.insert_item_unchecked(TagItem::new( - ItemKey::Genre, - ItemValue::Text(GENRES[reader[125] as usize].to_string()), - )); - } - - tag -} - -fn decode_text(key: ItemKey, data: &[u8]) -> Option { - let read = data - .iter() - .filter(|c| **c != 0) - .map(|c| *c as char) - .collect::(); - - if read.is_empty() { - None - } else { - Some(TagItem::new(key, ItemValue::Text(read))) - } -} - -pub(in crate::logic) fn write_id3v1(writer: &mut W, tag: &Tag) -> Result<()> +pub fn write_id3v1(writer: &mut W, tag: &Tag) -> Result<()> where W: Write + Read + Seek, { let tag = encode(tag)?; // This will seek us to the writing position - find_id3v1(writer, false)?; + super::find_id3v1(writer, false)?; writer.write_all(&tag)?; diff --git a/src/logic/id3/v2/frame/mod.rs b/src/logic/id3/v2/frame/mod.rs index 89f37d59..4f136cfe 100644 --- a/src/logic/id3/v2/frame/mod.rs +++ b/src/logic/id3/v2/frame/mod.rs @@ -51,11 +51,11 @@ pub enum Id3v2Frame { UserURL(TextEncoding, String), /// Represents a "SYLT" frame /// - /// Nothing is required here, the entire frame is stored as [`ItemValue::Binary`](crate::ItemValue::Binary). For parsing see [`SynchronizedText::parse`](crate::id3::SynchronizedText::parse) + /// Nothing is required here, the entire frame is stored as [`ItemValue::Binary`](crate::ItemValue::Binary). For parsing see [`SynchronizedText::parse`](crate::id3::v2::SynchronizedText::parse) SyncText, /// Represents a "GEOB" frame /// - /// Nothing is required here, the entire frame is stored as [`ItemValue::Binary`](crate::ItemValue::Binary). For parsing see [`GeneralEncapsulatedObject::parse`](crate::id3::GeneralEncapsulatedObject::parse) + /// Nothing is required here, the entire frame is stored as [`ItemValue::Binary`](crate::ItemValue::Binary). For parsing see [`GeneralEncapsulatedObject::parse`](crate::id3::v2::GeneralEncapsulatedObject::parse) EncapsulatedObject, /// When an ID3v2.2 key couldn't be upgraded /// diff --git a/src/logic/id3/v2/frame/read.rs b/src/logic/id3/v2/frame/read.rs index 267b6326..cc2e54ed 100644 --- a/src/logic/id3/v2/frame/read.rs +++ b/src/logic/id3/v2/frame/read.rs @@ -6,7 +6,6 @@ use crate::types::item::TagItemFlags; use std::io::Read; -use crate::TagItem; use byteorder::{BigEndian, ReadBytesExt}; pub(crate) struct Frame { diff --git a/src/logic/mpeg/read.rs b/src/logic/mpeg/read.rs index 544d371e..c2d8cdf5 100644 --- a/src/logic/mpeg/read.rs +++ b/src/logic/mpeg/read.rs @@ -115,7 +115,7 @@ where let mut id3v1_read = [0; 128]; data.read_exact(&mut id3v1_read)?; - mpeg_file.id3v1 = Some(crate::logic::id3::v1::parse_id3v1(id3v1_read)); + mpeg_file.id3v1 = Some(crate::logic::id3::v1::read::parse_id3v1(id3v1_read)); continue; }, [b'A', b'P', b'E', b'T'] => { diff --git a/src/types/item.rs b/src/types/item.rs index 70e8e77e..4f91f109 100644 --- a/src/types/item.rs +++ b/src/types/item.rs @@ -43,7 +43,7 @@ macro_rules! item_keys { /// Map a format specific key to an ItemKey /// /// NOTE: If used with ID3v2, this will only check against the ID3v2.4 keys. - /// If you wish to use a V2 or V3 key, see [`upgrade_v2`](crate::id3::upgrade_v2) and [`upgrade_v3`](crate::id3::upgrade_v3) + /// If you wish to use a V2 or V3 key, see [`upgrade_v2`](crate::id3::v2::upgrade_v2) and [`upgrade_v3`](crate::id3::v2::upgrade_v3) pub fn from_key(tag_type: &TagType, key: &str) -> Option { match tag_type { $( @@ -60,7 +60,7 @@ macro_rules! item_keys { /// Maps the variant to a format-specific key /// - /// NOTE: Since all ID3v2 tags are upgraded to [`Id3v2Version::V4`](crate::id3::Id3v2Version), the + /// NOTE: Since all ID3v2 tags are upgraded to [`Id3v2Version::V4`](crate::id3::v2::Id3v2Version), the /// version provided does not matter. They cannot be downgraded. pub fn map_key(&self, tag_type: &TagType) -> Option<&str> { match (tag_type, self) { @@ -436,8 +436,8 @@ pub enum ItemValue { Locator(String), /// **(APE/ID3v2/MP4 ONLY)** Binary information /// - /// In the case of ID3v2, this is the type of a [`Id3v2Frame::EncapsulatedObject`](crate::id3::Id3v2Frame::EncapsulatedObject), - /// [`Id3v2Frame::SyncText`](crate::id3::Id3v2Frame::SyncText), and any unknown frame. + /// In the case of ID3v2, this is the type of a [`Id3v2Frame::EncapsulatedObject`](crate::id3::v2::Id3v2Frame::EncapsulatedObject), + /// [`Id3v2Frame::SyncText`](crate::id3::v2::Id3v2Frame::SyncText), and any unknown frame. /// /// For APEv2 and MP4, the only use is for unknown items. Binary(Vec), diff --git a/src/types/tag.rs b/src/types/tag.rs index d97fb9a4..32f9e755 100644 --- a/src/types/tag.rs +++ b/src/types/tag.rs @@ -207,7 +207,7 @@ impl Tag { /// # Warning /// /// When dealing with ID3v2, it may be necessary to use [`insert_item_unchecked`](Tag::insert_item_unchecked). - /// See [`id3`](crate::id3) for an explanation. + /// See [`id3`](crate::id3::v2) for an explanation. pub fn insert_item(&mut self, item: TagItem) -> bool { if item.re_map(&self.tag_type).is_some() { self.insert_item_unchecked(item);