symphonia-metadata-0.5.2/.cargo_vcs_info.json0000644000000001600000000000100146220ustar { "git": { "sha1": "412f44daab39920beeb81d78b0e4271b263d33e9" }, "path_in_vcs": "symphonia-metadata" }symphonia-metadata-0.5.2/Cargo.toml0000644000000022500000000000100126220ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2018" rust-version = "1.53" name = "symphonia-metadata" version = "0.5.2" authors = ["Philip Deljanov "] description = "Project Symphonia multimedia tag and metadata readers." homepage = "https://github.com/pdeljanov/Symphonia" readme = "README.md" keywords = [ "multimedia", "media", "metadata", "id3v1", "id3v2", ] categories = [ "multimedia", "multimedia::audio", "multimedia::encoding", ] license = "MPL-2.0" repository = "https://github.com/pdeljanov/Symphonia" [dependencies.encoding_rs] version = "0.8.17" [dependencies.lazy_static] version = "1.4.0" [dependencies.log] version = "0.4" [dependencies.symphonia-core] version = "0.5.2" symphonia-metadata-0.5.2/Cargo.toml.orig000064400000000000000000000012101046102023000162760ustar 00000000000000[package] name = "symphonia-metadata" version = "0.5.2" description = "Project Symphonia multimedia tag and metadata readers." homepage = "https://github.com/pdeljanov/Symphonia" repository = "https://github.com/pdeljanov/Symphonia" authors = ["Philip Deljanov "] license = "MPL-2.0" readme = "README.md" categories = ["multimedia", "multimedia::audio", "multimedia::encoding"] keywords = ["multimedia", "media", "metadata", "id3v1", "id3v2"] edition = "2018" rust-version = "1.53" [dependencies] encoding_rs = "0.8.17" lazy_static = "1.4.0" log = "0.4" symphonia-core = { version = "0.5.2", path = "../symphonia-core" }symphonia-metadata-0.5.2/README.md000064400000000000000000000014271046102023000147000ustar 00000000000000# Symphonia Metadata Utilities [![Docs](https://docs.rs/symphonia-metadata/badge.svg)](https://docs.rs/symphonia-metadata) Common metadata readers, helpers, and utilities for Project Symphonia. **Note:** This crate is part of Symphonia. Please use the [`symphonia`](https://crates.io/crates/symphonia) crate instead of this one directly. ## License Symphonia is provided under the MPL v2.0 license. Please refer to the LICENSE file for more details. ## Contributing Symphonia is an open-source project and contributions are very welcome! If you would like to make a large contribution, please raise an issue ahead of time to make sure your efforts fit into the project goals, and that no duplication of efforts occurs. All contributors will be credited within the CONTRIBUTORS file. symphonia-metadata-0.5.2/src/id3v1.rs000064400000000000000000000135201046102023000155010ustar 00000000000000// Symphonia // Copyright (c) 2019-2022 The Project Symphonia Developers. // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. //! An ID3v1 metadata reader. use symphonia_core::errors::{unsupported_error, Result}; use symphonia_core::io::ReadBytes; use symphonia_core::meta::{MetadataBuilder, StandardTagKey, Tag, Value}; const GENRES: &[&str] = &[ // Standard Genres as per ID3v1 specificaation "Blues", "Classic rock", "Country", "Dance", "Disco", "Funk", "Grunge", "Hip-Hop", "Jazz", "Metal", "New Age", "Oldies", "Other", "Pop", "Rhythm and Blues", "Rap", "Reggae", "Rock", "Techno", "Industrial", "Alternative", "Ska", "Death metal", "Pranks", "Soundtrack", "Euro-Techno", "Ambient", "Trip-Hop", "Vocal", "Jazz & Funk", "Fusion", "Trance", "Classical", "Instrumental", "Acid", "House", "Game", "Sound clip", "Gospel", "Noise", "Alternative Rock", "Bass", "Soul", "Punk", "Space", "Meditative", "Instrumental Pop", "Instrumental Rock", "Ethnic", "Gothic", "Darkwave", "Techno-Industrial", "Electronic", "Pop-Folk", "Eurodance", "Dream", "Southern Rock", "Comedy", "Cult", "Gangsta", "Top 40", "Christian Rap", "Pop/Funk", "Jungle", "Native US", "Cabaret", "New Wave", "Psychedelic", "Rave", "Show tunes", "Trailer", "Lo-Fi", "Tribal", "Acid Punk", "Acid Jazz", "Polka", "Retro", "Musical", "Rock 'n Roll", "Hard Rock", // Winamp 1.91+ Extended Genres "Folk", "Folk-Rock", "National Folk", "Swing", "Fast Fusion", "Bebop", "Latin", "Revival", "Celtic", "Bluegrass", "Avantgarde", "Gothic Rock", "Progressive Rock", "Psychedelic Rock", "Symphonic Rock", "Slow rock", "Big Band", "Chorus", "Easy Listening", "Acoustic", "Humour", "Speech", "Chanson", "Opera", "Chamber music", "Symphonia", "Symphony", "Booty bass", "Primus", "Porn groove", "Satire", "Slow jam", "Club", "Tango", "Samba", "Folklore", "Ballad", "Power ballad", "Rhythmic Soul", "Freestyle", "Duet", "Punk Rock", "Drum solo", "A cappella", "Euro-House", "Dance Hall", "Goa", "Drum & Bass", "Club-House", "Hardcore Techno", "Terror", "Indie", "BritPop", "(133)", "Polsk Punk", "Beat", "Christian Gangsta Rap", "Heavy Metal", "Black Metal", "Crossover", "Contemporary Christian", "Christian rock", "Merengue", "Salsa", "Thrash Metal", "Anime", "Jpop", "Synthpop", // Winamp 5.0+ Extended Genres "Abstract", "Art Rock", "Baroque", "Bhangra", "Big beat", "Breakbeat", "Chillout", "Downtempo", "Dub", "EBM", "Eclectic", "Electro", "Electroclash", "Emo", "Experimental", "Garage", "Global", "IDM", "Illbient", "Industro-Goth", "Jam Band", "Krautrock", "Leftfield", "Lounge", "Math Rock", "New Romantic", "Nu-Breakz", "Post-Punk", "Post-Rock", "Psytrance", "Shoegaze", "Space Rock", "Trop Rock", "World Music", "Neoclassical", "Audiobook", "Audio theatre", "Neue Deutsche Welle", "Podcast", "Indie-Rock", "G-Funk", "Dubstep", "Garage Rock", "Psybient", ]; pub fn read_id3v1(reader: &mut B, metadata: &mut MetadataBuilder) -> Result<()> { // Read the "TAG" header. let marker = reader.read_triple_bytes()?; if marker != *b"TAG" { return unsupported_error("id3v1: Not an ID3v1 tag"); } let buf = reader.read_boxed_slice_exact(125)?; let title = decode_iso8859_text(&buf[0..30]); if !title.is_empty() { metadata.add_tag(Tag::new(Some(StandardTagKey::TrackTitle), "TITLE", Value::from(title))); } let artist = decode_iso8859_text(&buf[30..60]); if !artist.is_empty() { metadata.add_tag(Tag::new(Some(StandardTagKey::Artist), "ARTIST", Value::from(artist))); } let album = decode_iso8859_text(&buf[60..90]); if !album.is_empty() { metadata.add_tag(Tag::new(Some(StandardTagKey::Album), "ALBUM", Value::from(album))); } let year = decode_iso8859_text(&buf[90..94]); if !year.is_empty() { metadata.add_tag(Tag::new(Some(StandardTagKey::Date), "DATE", Value::from(year))); } let comment = if buf[122] == 0 { let track = buf[123]; metadata.add_tag(Tag::new(Some(StandardTagKey::TrackNumber), "TRACK", Value::from(track))); decode_iso8859_text(&buf[94..122]) } else { decode_iso8859_text(&buf[94..124]) }; if !comment.is_empty() { metadata.add_tag(Tag::new(Some(StandardTagKey::Comment), "COMMENT", Value::from(comment))); } let genre_idx = buf[124] as usize; // Convert the genre index to an actual genre name using the GENRES lookup table. Genre #133 is // an offensive term and is excluded from Symphonia. if genre_idx < GENRES.len() && genre_idx != 133 { metadata.add_tag(Tag::new( Some(StandardTagKey::Genre), "GENRE", Value::from(GENRES[genre_idx]), )); } Ok(()) } fn decode_iso8859_text(data: &[u8]) -> String { data.iter().filter(|&b| *b > 0x1f).map(|&b| b as char).collect() } pub mod util { use super::GENRES; /// Try to get the genre name for the ID3v1 genre index. pub fn genre_name(index: u8) -> Option<&'static &'static str> { GENRES.get(usize::from(index)) } } symphonia-metadata-0.5.2/src/id3v2/frames.rs000064400000000000000000001273301046102023000167640ustar 00000000000000// Symphonia // Copyright (c) 2019-2022 The Project Symphonia Developers. // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. use std::borrow::Cow; use std::collections::HashMap; use std::io; use std::str; use symphonia_core::errors::{decode_error, unsupported_error, Result}; use symphonia_core::io::{BufReader, FiniteStream, ReadBytes}; use symphonia_core::meta::{StandardTagKey, Tag, Value, Visual}; use encoding_rs::UTF_16BE; use lazy_static::lazy_static; use log::warn; use super::unsync::{decode_unsynchronisation, read_syncsafe_leq32}; use super::util; // The following is a list of all standardized ID3v2.x frames for all ID3v2 major versions and their // implementation status ("S" column) in Symphonia. // // ID3v2.2 uses 3 character frame identifiers as opposed to the 4 character identifiers used in // subsequent versions. This table may be used to map equivalent frames between the two versions. // // All ID3v2.3 frames are officially part of ID3v2.4 with the exception of those marked "n/a". // However, it is likely that ID3v2.3-only frames appear in some real-world ID3v2.4 tags. // // - ---- ---- ---- ---------------- ------------------------------------------------ // S v2.2 v2.3 v2.4 Std. Key Description // - ---- ---- ---- ---------------- ------------------------------------------------ // CRA AENC Audio encryption // CRM Encrypted meta frame // x PIC APIC Attached picture // ASPI Audio seek point index // x COM COMM Comment Comments // COMR Commercial frame // ENCR Encryption method registration // EQU EQUA Equalisation // EQU2 Equalisation (2) // ETC ETCO Event timing codes // GEO GEOB General encapsulated object // GRID Group identification registration // x IPL IPLS TIPL Involved people list // LNK LINK Linked information // x MCI MCDI Music CD identifier // MLL MLLT MPEG location lookup table // OWNE Ownership frame // x PRIV Private frame // x CNT PCNT Play counter // x POP POPM Rating Popularimeter // POSS Position synchronisation frame // BUF RBUF Recommended buffer size // RVA RVAD Relative volume adjustment // RVA2 Relative volume adjustment (2) // REV RVRB Reverb // SEEK Seek frame // SIGN Signature frame // SLT SYLT Synchronized lyric/text // STC SYTC Synchronized tempo codes // x TAL TALB Album Album/Movie/Show title // x TBP TBPM Bpm BPM (beats per minute) // x TCM TCOM Composer Composer // x TCO TCON Genre Content type // x TCR TCOP Copyright Copyright message // x TDA TDAT Date Date // x TDEN EncodingDate Encoding time // x TDY TDLY Playlist delay // x TDOR OriginalDate Original release time // x TDRC Date Recording time // x TDRL ReleaseDate Release time // x TDTG TaggingDate Tagging time // x TEN TENC EncodedBy Encoded by // x TXT TEXT Writer Lyricist/Text writer // x TFT TFLT File type // x TIM TIME n/a Date Time // x TT1 TIT1 ContentGroup Content group description // x TT2 TIT2 TrackTitle Title/songname/content description // x TT3 TIT3 TrackSubtitle Subtitle/Description refinement // x TKE TKEY Initial key // x TLA TLAN Language Language(s) // x TLE TLEN Length // x TMCL Musician credits list // x TMT TMED MediaFormat Media type // x TMOO Mood Mood // x TOT TOAL OriginalAlbum Original album/movie/show title // x TOF TOFN OriginalFile Original filename // x TOL TOLY OriginalWriter Original lyricist(s)/text writer(s) // x TOA TOPE OriginalArtist Original artist(s)/performer(s) // x TOR TORY n/a OriginalDate Original release year // x TOWN File owner/licensee // x TP1 TPE1 Artist Lead performer(s)/Soloist(s) // x TP2 TPE2 AlbumArtist Band/orchestra/accompaniment // x TP3 TPE3 Performer Conductor/performer refinement // x TP4 TPE4 Remixer Interpreted, remixed, or otherwise modified by // x TPA TPOS TrackNumber Part of a set // x TPRO Produced notice // x TPB TPUB Label Publisher // x TRK TRCK TrackNumber Track number/Position in set // x TRD TRDA n/a Date Recording dates // x TRSN Internet radio station name // x TRSO Internet radio station owner // x TSOA SortAlbum Album sort order // x TSOP SortArtist Performer sort order // x TSOT SortTrackTitle Title sort order // x TSI TSIZ n/a Size // x TRC TSRC IdentIsrc ISRC (international standard recording code) // x TSS TSSE Encoder Software/Hardware and settings used for encoding // x TSST Set subtitle // x TYE TYER n/a Date Year // x TXX TXXX User defined text information frame // UFI UFID Unique file identifier // USER Terms of use // x ULT USLT Lyrics Unsychronized lyric/text transcription // x WCM WCOM UrlPurchase Commercial information // x WCP WCOP UrlCopyright Copyright/Legal information // x WAF WOAF UrlOfficial Official audio file webpage // x WAR WOAR UrlArtist Official artist/performer webpage // x WAS WOAS UrlSource Official audio source webpage // x WORS UrlInternetRadio Official internet radio station homepage // x WPAY UrlPayment Payment // x WPB WPUB UrlLabel Publishers official webpage // x WXX WXXX Url User defined URL link frame // x GRP1 (Apple iTunes) Grouping // x MVNM MovementName (Apple iTunes) Movement name // x MVIN MovementNumber (Apple iTunes) Movement number // PCS PCST (Apple iTunes) Podcast flag // x TCAT PodcastCategory (Apple iTunes) Podcast category // x TDES PodcastDescription (Apple iTunes) Podcast description // x TGID IdentPodcast (Apple iTunes) Podcast identifier // x TKWD PodcastKeywords (Apple iTunes) Podcast keywords // x WFED UrlPodcast (Apple iTunes) Podcast url // x TST SortTrackTitle (Apple iTunes) Title sort order // x TSP SortArtist (Apple iTunes) Artist order order // x TSA SortAlbum (Apple iTunes) Album sort order // x TS2 TSO2 SortAlbumArtist (Apple iTunes) Album artist sort order // x TSC TSOC SortComposer (Apple iTunes) Composer sort order // // Information on these frames can be found at: // // ID3v2.2: http://id3.org/id3v2-00 // ID3v2.3: http://id3.org/d3v2.3.0 // ID3v2.4: http://id3.org/id3v2.4.0-frames /// The result of parsing a frame. pub enum FrameResult { /// Padding was encountered instead of a frame. The remainder of the ID3v2 Tag may be skipped. Padding, /// An unknown frame was found and its body skipped. UnsupportedFrame(String), /// The frame was invalid and its body skipped. InvalidData(String), /// A frame was parsed and yielded a single `Tag`. Tag(Tag), /// A frame was parsed and yielded a single `Visual`. Visual(Visual), /// A frame was parsed and yielded many `Tag`s. MultipleTags(Vec), } /// Makes a frame result for a frame containing invalid data. fn invalid_data(id: &[u8]) -> Result { Ok(FrameResult::InvalidData(as_ascii_str(id).to_string())) } /// Makes a frame result for an unsupported frame. fn unsupported_frame(id: &[u8]) -> Result { Ok(FrameResult::UnsupportedFrame(as_ascii_str(id).to_string())) } type FrameParser = fn(&mut BufReader<'_>, Option, &str) -> Result; lazy_static! { static ref LEGACY_FRAME_MAP: HashMap<&'static [u8; 3], &'static [u8; 4]> = { let mut m = HashMap::new(); m.insert(b"BUF", b"RBUF"); m.insert(b"CNT", b"PCNT"); m.insert(b"COM", b"COMM"); m.insert(b"CRA", b"AENC"); m.insert(b"EQU", b"EQUA"); m.insert(b"ETC", b"ETCO"); m.insert(b"GEO", b"GEOB"); m.insert(b"IPL", b"IPLS"); m.insert(b"LNK", b"LINK"); m.insert(b"MCI", b"MCDI"); m.insert(b"MLL", b"MLLT"); m.insert(b"PCS", b"PCST"); m.insert(b"PIC", b"APIC"); m.insert(b"POP", b"POPM"); m.insert(b"REV", b"RVRB"); m.insert(b"RVA", b"RVAD"); m.insert(b"SLT", b"SYLT"); m.insert(b"STC", b"SYTC"); m.insert(b"TAL", b"TALB"); m.insert(b"TBP", b"TBPM"); m.insert(b"TCM", b"TCOM"); m.insert(b"TCO", b"TCON"); m.insert(b"TCR", b"TCOP"); m.insert(b"TDA", b"TDAT"); m.insert(b"TDY", b"TDLY"); m.insert(b"TEN", b"TENC"); m.insert(b"TFT", b"TFLT"); m.insert(b"TIM", b"TIME"); m.insert(b"TKE", b"TKEY"); m.insert(b"TLA", b"TLAN"); m.insert(b"TLE", b"TLEN"); m.insert(b"TMT", b"TMED"); m.insert(b"TOA", b"TOPE"); m.insert(b"TOF", b"TOFN"); m.insert(b"TOL", b"TOLY"); m.insert(b"TOR", b"TORY"); m.insert(b"TOT", b"TOAL"); m.insert(b"TP1", b"TPE1"); m.insert(b"TP2", b"TPE2"); m.insert(b"TP3", b"TPE3"); m.insert(b"TP4", b"TPE4"); m.insert(b"TPA", b"TPOS"); m.insert(b"TPB", b"TPUB"); m.insert(b"TRC", b"TSRC"); m.insert(b"TRD", b"TRDA"); m.insert(b"TRK", b"TRCK"); m.insert(b"TS2", b"TSO2"); m.insert(b"TSA", b"TSOA"); m.insert(b"TSC", b"TSOC"); m.insert(b"TSI", b"TSIZ"); m.insert(b"TSP", b"TSOP"); m.insert(b"TSS", b"TSSE"); m.insert(b"TST", b"TSOT"); m.insert(b"TT1", b"TIT1"); m.insert(b"TT2", b"TIT2"); m.insert(b"TT3", b"TIT3"); m.insert(b"TXT", b"TEXT"); m.insert(b"TXX", b"TXXX"); m.insert(b"TYE", b"TYER"); m.insert(b"UFI", b"UFID"); m.insert(b"ULT", b"USLT"); m.insert(b"WAF", b"WOAF"); m.insert(b"WAR", b"WOAR"); m.insert(b"WAS", b"WOAS"); m.insert(b"WCM", b"WCOM"); m.insert(b"WCP", b"WCOP"); m.insert(b"WPB", b"WPUB"); m.insert(b"WXX", b"WXXX"); m }; } lazy_static! { static ref FRAME_PARSERS: HashMap<&'static [u8; 4], (FrameParser, Option)> = { let mut m = HashMap::new(); // m.insert(b"AENC", read_null_frame); m.insert(b"APIC", (read_apic_frame as FrameParser, None)); // m.insert(b"ASPI", read_null_frame); m.insert(b"COMM", (read_comm_uslt_frame, Some(StandardTagKey::Comment))); // m.insert(b"COMR", read_null_frame); // m.insert(b"ENCR", read_null_frame); // m.insert(b"EQU2", read_null_frame); // m.insert(b"EQUA", read_null_frame); // m.insert(b"ETCO", read_null_frame); // m.insert(b"GEOB", read_null_frame); // m.insert(b"GRID", read_null_frame); m.insert(b"IPLS", (read_text_frame, None)); // m.insert(b"LINK", read_null_frame); m.insert(b"MCDI", (read_mcdi_frame, None)); // m.insert(b"MLLT", read_null_frame); // m.insert(b"OWNE", read_null_frame); m.insert(b"PCNT", (read_pcnt_frame, None)); m.insert(b"POPM", (read_popm_frame, Some(StandardTagKey::Rating))); // m.insert(b"POSS", read_null_frame); m.insert(b"PRIV", (read_priv_frame, None)); // m.insert(b"RBUF", read_null_frame); // m.insert(b"RVA2", read_null_frame); // m.insert(b"RVAD", read_null_frame); // m.insert(b"RVRB", read_null_frame); // m.insert(b"SEEK", read_null_frame); // m.insert(b"SIGN", read_null_frame); // m.insert(b"SYLT", read_null_frame); // m.insert(b"SYTC", read_null_frame); m.insert(b"TALB", (read_text_frame, Some(StandardTagKey::Album))); m.insert(b"TBPM", (read_text_frame, Some(StandardTagKey::Bpm))); m.insert(b"TCOM", (read_text_frame, Some(StandardTagKey::Composer))); m.insert(b"TCON", (read_text_frame, Some(StandardTagKey::Genre))); m.insert(b"TCOP", (read_text_frame, Some(StandardTagKey::Copyright))); m.insert(b"TDAT", (read_text_frame, Some(StandardTagKey::Date))); m.insert(b"TDEN", (read_text_frame, Some(StandardTagKey::EncodingDate))); m.insert(b"TDLY", (read_text_frame, None)); m.insert(b"TDOR", (read_text_frame, Some(StandardTagKey::OriginalDate))); m.insert(b"TDRC", (read_text_frame, Some(StandardTagKey::Date))); m.insert(b"TDRL", (read_text_frame, Some(StandardTagKey::ReleaseDate))); m.insert(b"TDTG", (read_text_frame, Some(StandardTagKey::TaggingDate))); m.insert(b"TENC", (read_text_frame, Some(StandardTagKey::EncodedBy))); // Also Writer? m.insert(b"TEXT", (read_text_frame, Some(StandardTagKey::Writer))); m.insert(b"TFLT", (read_text_frame, None)); m.insert(b"TIME", (read_text_frame, Some(StandardTagKey::Date))); m.insert(b"TIPL", (read_text_frame, None)); m.insert(b"TIT1", (read_text_frame, Some(StandardTagKey::ContentGroup))); m.insert(b"TIT2", (read_text_frame, Some(StandardTagKey::TrackTitle))); m.insert(b"TIT3", (read_text_frame, Some(StandardTagKey::TrackSubtitle))); m.insert(b"TKEY", (read_text_frame, None)); m.insert(b"TLAN", (read_text_frame, Some(StandardTagKey::Language))); m.insert(b"TLEN", (read_text_frame, None)); m.insert(b"TMCL", (read_text_frame, None)); m.insert(b"TMED", (read_text_frame, Some(StandardTagKey::MediaFormat))); m.insert(b"TMOO", (read_text_frame, Some(StandardTagKey::Mood))); m.insert(b"TOAL", (read_text_frame, Some(StandardTagKey::OriginalAlbum))); m.insert(b"TOFN", (read_text_frame, Some(StandardTagKey::OriginalFile))); m.insert(b"TOLY", (read_text_frame, Some(StandardTagKey::OriginalWriter))); m.insert(b"TOPE", (read_text_frame, Some(StandardTagKey::OriginalArtist))); m.insert(b"TORY", (read_text_frame, Some(StandardTagKey::OriginalDate))); m.insert(b"TOWN", (read_text_frame, None)); m.insert(b"TPE1", (read_text_frame, Some(StandardTagKey::Artist))); m.insert(b"TPE2", (read_text_frame, Some(StandardTagKey::AlbumArtist))); m.insert(b"TPE3", (read_text_frame, Some(StandardTagKey::Conductor))); m.insert(b"TPE4", (read_text_frame, Some(StandardTagKey::Remixer))); // May be "disc number / total discs" m.insert(b"TPOS", (read_text_frame, Some(StandardTagKey::DiscNumber))); m.insert(b"TPRO", (read_text_frame, None)); m.insert(b"TPUB", (read_text_frame, Some(StandardTagKey::Label))); // May be "track number / total tracks" m.insert(b"TRCK", (read_text_frame, Some(StandardTagKey::TrackNumber))); m.insert(b"TRDA", (read_text_frame, Some(StandardTagKey::Date))); m.insert(b"TRSN", (read_text_frame, None)); m.insert(b"TRSO", (read_text_frame, None)); m.insert(b"TSIZ", (read_text_frame, None)); m.insert(b"TSOA", (read_text_frame, Some(StandardTagKey::SortAlbum))); m.insert(b"TSOP", (read_text_frame, Some(StandardTagKey::SortArtist))); m.insert(b"TSOT", (read_text_frame, Some(StandardTagKey::SortTrackTitle))); m.insert(b"TSRC", (read_text_frame, Some(StandardTagKey::IdentIsrc))); m.insert(b"TSSE", (read_text_frame, Some(StandardTagKey::Encoder))); m.insert(b"TSST", (read_text_frame, None)); m.insert(b"TXXX", (read_txxx_frame, None)); m.insert(b"TYER", (read_text_frame, Some(StandardTagKey::Date))); // m.insert(b"UFID", read_null_frame); // m.insert(b"USER", read_null_frame); m.insert(b"USLT", (read_comm_uslt_frame, Some(StandardTagKey::Lyrics))); m.insert(b"WCOM", (read_url_frame, Some(StandardTagKey::UrlPurchase))); m.insert(b"WCOP", (read_url_frame, Some(StandardTagKey::UrlCopyright))); m.insert(b"WOAF", (read_url_frame, Some(StandardTagKey::UrlOfficial))); m.insert(b"WOAR", (read_url_frame, Some(StandardTagKey::UrlArtist))); m.insert(b"WOAS", (read_url_frame, Some(StandardTagKey::UrlSource))); m.insert(b"WORS", (read_url_frame, Some(StandardTagKey::UrlInternetRadio))); m.insert(b"WPAY", (read_url_frame, Some(StandardTagKey::UrlPayment))); m.insert(b"WPUB", (read_url_frame, Some(StandardTagKey::UrlLabel))); m.insert(b"WXXX", (read_wxxx_frame, Some(StandardTagKey::Url))); // Apple iTunes frames // m.insert(b"PCST", (read_null_frame, None)); m.insert(b"GRP1", (read_text_frame, None)); m.insert(b"MVIN", (read_text_frame, Some(StandardTagKey::MovementNumber))); m.insert(b"MVNM", (read_text_frame, Some(StandardTagKey::MovementName))); m.insert(b"TCAT", (read_text_frame, Some(StandardTagKey::PodcastCategory))); m.insert(b"TDES", (read_text_frame, Some(StandardTagKey::PodcastDescription))); m.insert(b"TGID", (read_text_frame, Some(StandardTagKey::IdentPodcast))); m.insert(b"TKWD", (read_text_frame, Some(StandardTagKey::PodcastKeywords))); m.insert(b"TSO2", (read_text_frame, Some(StandardTagKey::SortAlbumArtist))); m.insert(b"TSOC", (read_text_frame, Some(StandardTagKey::SortComposer))); m.insert(b"WFED", (read_text_frame, Some(StandardTagKey::UrlPodcast))); m }; } lazy_static! { static ref TXXX_FRAME_STD_KEYS: HashMap<&'static str, StandardTagKey> = { let mut m = HashMap::new(); m.insert("ACOUSTID FINGERPRINT", StandardTagKey::AcoustidFingerprint); m.insert("ACOUSTID ID", StandardTagKey::AcoustidId); m.insert("BARCODE", StandardTagKey::IdentBarcode); m.insert("CATALOGNUMBER", StandardTagKey::IdentCatalogNumber); m.insert("LICENSE", StandardTagKey::License); m.insert("MUSICBRAINZ ALBUM ARTIST ID", StandardTagKey::MusicBrainzAlbumArtistId); m.insert("MUSICBRAINZ ALBUM ID", StandardTagKey::MusicBrainzAlbumId); m.insert("MUSICBRAINZ ARTIST ID", StandardTagKey::MusicBrainzArtistId); m.insert("MUSICBRAINZ RELEASE GROUP ID", StandardTagKey::MusicBrainzReleaseGroupId); m.insert("MUSICBRAINZ WORK ID", StandardTagKey::MusicBrainzWorkId); m.insert("REPLAYGAIN_ALBUM_GAIN", StandardTagKey::ReplayGainAlbumGain); m.insert("REPLAYGAIN_ALBUM_PEAK", StandardTagKey::ReplayGainAlbumPeak); m.insert("REPLAYGAIN_TRACK_GAIN", StandardTagKey::ReplayGainTrackGain); m.insert("REPLAYGAIN_TRACK_PEAK", StandardTagKey::ReplayGainTrackPeak); m.insert("SCRIPT", StandardTagKey::Script); m }; } /// Validates that a frame id only contains the uppercase letters A-Z, and digits 0-9. fn validate_frame_id(id: &[u8]) -> bool { // Only frame IDs with 3 or 4 characters are valid. if id.len() != 4 && id.len() != 3 { return false; } // Character: '/' [ '0' ... '9' ] ':' ... '@' [ 'A' ... 'Z' ] '[' // ASCII Code: 0x2f [ 0x30 ... 0x39 ] 0x3a ... 0x40 [ 0x41 ... 0x5a ] 0x5b id.iter().filter(|&b| !((*b >= b'0' && *b <= b'9') || (*b >= b'A' && *b <= b'Z'))).count() == 0 } /// Validates that a language code conforms to the ISO-639-2 standard. That is to say, the code is /// composed of 3 characters, each character being between lowercase letters a-z. fn validate_lang_code(code: [u8; 3]) -> bool { code.iter().filter(|&c| *c < b'a' || *c > b'z').count() == 0 } /// Gets a slice of ASCII bytes as a string slice. /// /// Assumes the bytes are valid ASCII characters. Panics otherwise. fn as_ascii_str(id: &[u8]) -> &str { std::str::from_utf8(id).unwrap() } /// Finds a frame parser for "modern" ID3v2.3 or ID3v2.4 tags. fn find_parser(id: [u8; 4]) -> Option<&'static (FrameParser, Option)> { FRAME_PARSERS.get(&id) } /// Finds a frame parser for a "legacy" ID3v2.2 tag by finding an equivalent "modern" ID3v2.3+ frame /// parser. fn find_parser_legacy(id: [u8; 3]) -> Option<&'static (FrameParser, Option)> { match LEGACY_FRAME_MAP.get(&id) { Some(id) => find_parser(**id), _ => None, } } /// Read an ID3v2.2 frame. pub fn read_id3v2p2_frame(reader: &mut B) -> Result { let id = reader.read_triple_bytes()?; // Check if the frame id contains valid characters. If it does not, then assume the rest of the // tag is padding. As per the specification, padding should be all 0s, but there are some tags // which don't obey the specification. if !validate_frame_id(&id) { // As per the specification, padding should be all 0s, but there are some tags which don't // obey the specification. if id != [0, 0, 0] { warn!("padding bytes not zero"); } return Ok(FrameResult::Padding); } let size = u64::from(reader.read_be_u24()?); // Find a parser for the frame. If there is none, skip over the remainder of the frame as it // cannot be parsed. let (parser, std_key) = match find_parser_legacy(id) { Some(p) => p, None => { reader.ignore_bytes(size)?; return unsupported_frame(&id); } }; // A frame must be atleast 1 byte as per the specification. if size == 0 { return invalid_data(&id); } let data = reader.read_boxed_slice_exact(size as usize)?; parser(&mut BufReader::new(&data), *std_key, as_ascii_str(&id)) } /// Read an ID3v2.3 frame. pub fn read_id3v2p3_frame(reader: &mut B) -> Result { let id = reader.read_quad_bytes()?; // Check if the frame id contains valid characters. If it does not, then assume the rest of the // tag is padding. As per the specification, padding should be all 0s, but there are some tags // which don't obey the specification. if !validate_frame_id(&id) { // As per the specification, padding should be all 0s, but there are some tags which don't // obey the specification. if id != [0, 0, 0, 0] { warn!("padding bytes not zero"); } return Ok(FrameResult::Padding); } let mut size = u64::from(reader.read_be_u32()?); let flags = reader.read_be_u16()?; // Unused flag bits must be cleared. if flags & 0x1f1f != 0x0 { return decode_error("id3v2: unused flag bits are not cleared"); } // Find a parser for the frame. If there is none, skip over the remainder of the frame as it // cannot be parsed. let (parser, std_key) = match find_parser(id) { Some(p) => p, None => { reader.ignore_bytes(size)?; return unsupported_frame(&id); } }; // Frame zlib DEFLATE compression usage flag. // TODO: Implement decompression if it is actually used in the real world. if flags & 0x80 != 0x0 { reader.ignore_bytes(size)?; return unsupported_error("id3v2: compressed frames are not supported"); } // Frame encryption usage flag. This will likely never be supported since encryption methods are // vendor-specific. if flags & 0x4 != 0x0 { reader.ignore_bytes(size)?; return unsupported_error("id3v2: encrypted frames are not supported"); } // Frame group identifier byte. Used to group a set of frames. There is no analogue in // Symphonia. if size >= 1 && (flags & 0x20) != 0x0 { reader.read_byte()?; size -= 1; } // A frame must be atleast 1 byte as per the specification. if size == 0 { return invalid_data(&id); } let data = reader.read_boxed_slice_exact(size as usize)?; parser(&mut BufReader::new(&data), *std_key, as_ascii_str(&id)) } /// Read an ID3v2.4 frame. pub fn read_id3v2p4_frame(reader: &mut B) -> Result { let id = reader.read_quad_bytes()?; // Check if the frame id contains valid characters. If it does not, then assume the rest of the // tag is padding. if !validate_frame_id(&id) { // As per the specification, padding should be all 0s, but there are some tags which don't // obey the specification. if id != [0, 0, 0, 0] { warn!("padding bytes not zero"); } return Ok(FrameResult::Padding); } let mut size = u64::from(read_syncsafe_leq32(reader, 28)?); let flags = reader.read_be_u16()?; // Unused flag bits must be cleared. if flags & 0x8fb0 != 0x0 { return decode_error("id3v2: unused flag bits are not cleared"); } // Find a parser for the frame. If there is none, skip over the remainder of the frame as it // cannot be parsed. let (parser, std_key) = match find_parser(id) { Some(p) => p, None => { reader.ignore_bytes(size)?; return unsupported_frame(&id); } }; // Frame zlib DEFLATE compression usage flag. // TODO: Implement decompression if it is actually used in the real world. if flags & 0x8 != 0x0 { reader.ignore_bytes(size)?; return unsupported_error("id3v2: compressed frames are not supported"); } // Frame encryption usage flag. This will likely never be supported since encryption methods are // vendor-specific. if flags & 0x4 != 0x0 { reader.ignore_bytes(size)?; return unsupported_error("id3v2: encrypted frames are not supported"); } // Frame group identifier byte. Used to group a set of frames. There is no analogue in // Symphonia. if size >= 1 && (flags & 0x40) != 0x0 { reader.read_byte()?; size -= 1; } // The data length indicator is optional in the frame header. This field indicates the original // size of the frame body before compression, encryption, and/or unsynchronisation. It is // mandatory if encryption or compression are used, but only encouraged for unsynchronisation. // It's not that helpful, so we just ignore it. if size >= 4 && (flags & 0x1) != 0x0 { read_syncsafe_leq32(reader, 28)?; size -= 4; } // A frame must be atleast 1 byte as per the specification. if size == 0 { return invalid_data(&id); } // Read the frame body into a new buffer. This is, unfortunate. The original plan was to use an // UnsyncStream to transparently decode the unsynchronisation stream, however, the format does // not make this easy. For one, the decoded data length field is optional. This is fine.. // sometimes. For example, text frames should have their text field terminated by 0x00 or // 0x0000, so it /should/ be possible to scan for the termination. However, despite being // mandatory per the specification, not all tags have terminated text fields. It gets even worse // when your text field is actually a list. The condition to continue scanning for terminations // is if there is more data left in the frame body. However, the frame body length is the // unsynchronised length, not the decoded length (that part is optional). If we scan for a // termination, we know the length of the /decoded/ data, not how much data we actually consumed // to obtain that decoded data. Therefore we exceed the bounds of the frame. With this in mind, // the easiest thing to do is just load frame body into memory, subject to a memory limit, and // decode it before passing it to a parser. Therefore we always know the decoded data length and // the typical algorithms work. It should be noted this isn't necessarily worse. Scanning for a // termination still would've required a buffer to scan into with the UnsyncStream, whereas we // can just get references to the decoded data buffer we create here. // // You win some, you lose some. :) let mut raw_data = reader.read_boxed_slice_exact(size as usize)?; // The frame body is unsynchronised. Decode the unsynchronised data back to it's original form // in-place before wrapping the decoded data in a BufStream for the frame parsers. if flags & 0x2 != 0x0 { let unsync_data = decode_unsynchronisation(&mut raw_data); parser(&mut BufReader::new(unsync_data), *std_key, as_ascii_str(&id)) } // The frame body has not been unsynchronised. Wrap the raw data buffer in BufStream without any // additional decoding. else { parser(&mut BufReader::new(&raw_data), *std_key, as_ascii_str(&id)) } } /// Reads all text frames frame except for `TXXX`. fn read_text_frame( reader: &mut BufReader<'_>, std_key: Option, id: &str, ) -> Result { // The first byte of the frame is the encoding. let encoding = match Encoding::parse(reader.read_byte()?) { Some(encoding) => encoding, _ => return decode_error("id3v2: invalid text encoding"), }; // Since a text frame can have a null-terminated list of values, and Symphonia allows multiple // tags with the same key, create one Tag per listed value. let mut tags = Vec::::new(); // The remainder of the frame is one or more null-terminated strings. loop { let len = reader.bytes_available() as usize; if len > 0 { // Scan for text, and create a Tag. let text = scan_text(reader, encoding, len)?; tags.push(Tag::new(std_key, id, Value::from(text))); } else { break; } } Ok(FrameResult::MultipleTags(tags)) } /// Reads a `TXXX` (user defined) text frame. fn read_txxx_frame( reader: &mut BufReader<'_>, _: Option, _: &str, ) -> Result { // The first byte of the frame is the encoding. let encoding = match Encoding::parse(reader.read_byte()?) { Some(encoding) => encoding, _ => return decode_error("id3v2: invalid TXXX text encoding"), }; // Read the description string. let desc = scan_text(reader, encoding, reader.bytes_available() as usize)?; // Some TXXX frames may be mapped to standard keys. Check if a standard key exists for the // description. let std_key = TXXX_FRAME_STD_KEYS.get(desc.as_ref()).copied(); // Generate a key name using the description. let key = format!("TXXX:{}", desc); // Since a TXXX frame can have a null-terminated list of values, and Symphonia allows multiple // tags with the same key, create one Tag per listed value. let mut tags = Vec::::new(); // The remainder of the frame is one or more null-terminated strings. loop { let len = reader.bytes_available() as usize; if len > 0 { let text = scan_text(reader, encoding, len)?; tags.push(Tag::new(std_key, &key, Value::from(text))); } else { break; } } Ok(FrameResult::MultipleTags(tags)) } /// Reads all URL frames except for `WXXX`. fn read_url_frame( reader: &mut BufReader<'_>, std_key: Option, id: &str, ) -> Result { // Scan for a ISO-8859-1 URL string. let url = scan_text(reader, Encoding::Iso8859_1, reader.bytes_available() as usize)?; // Create a Tag. let tag = Tag::new(std_key, id, Value::from(url)); Ok(FrameResult::Tag(tag)) } /// Reads a `WXXX` (user defined) URL frame. fn read_wxxx_frame( reader: &mut BufReader<'_>, std_key: Option, _: &str, ) -> Result { // The first byte of the WXXX frame is the encoding of the description. let encoding = match Encoding::parse(reader.read_byte()?) { Some(encoding) => encoding, _ => return decode_error("id3v2: invalid WXXX URL description encoding"), }; // Scan for the the description string. let desc = format!("WXXX:{}", &scan_text(reader, encoding, reader.bytes_available() as usize)?); // Scan for a ISO-8859-1 URL string. let url = scan_text(reader, Encoding::Iso8859_1, reader.bytes_available() as usize)?; // Create a Tag. let tag = Tag::new(std_key, &desc, Value::from(url)); Ok(FrameResult::Tag(tag)) } /// Reads a `PRIV` (private) frame. fn read_priv_frame( reader: &mut BufReader<'_>, std_key: Option, _: &str, ) -> Result { // Scan for a ISO-8859-1 owner identifier. let owner = format!( "PRIV:{}", &scan_text(reader, Encoding::Iso8859_1, reader.bytes_available() as usize)? ); // The remainder of the frame is binary data. let data_buf = reader.read_buf_bytes_ref(reader.bytes_available() as usize)?; // Create a Tag. let tag = Tag::new(std_key, &owner, Value::from(data_buf)); Ok(FrameResult::Tag(tag)) } /// Reads a `COMM` (comment) or `USLT` (unsynchronized comment) frame. fn read_comm_uslt_frame( reader: &mut BufReader<'_>, std_key: Option, id: &str, ) -> Result { // The first byte of the frame is the encoding of the description. let encoding = match Encoding::parse(reader.read_byte()?) { Some(encoding) => encoding, _ => return decode_error("id3v2: invalid text encoding"), }; // The next three bytes are the language. let lang = reader.read_triple_bytes()?; // Encode the language into the key of the comment Tag. Since many files don't use valid // ISO-639-2 language codes, we'll just skip the language code if it doesn't validate. Returning // an error would break far too many files to be worth it. let key = if validate_lang_code(lang) { format!("{}!{}", id, as_ascii_str(&lang)) } else { id.to_string() }; // Short text (content description) is next, but since there is no way to represent this in // Symphonia, skip it. scan_text(reader, encoding, reader.bytes_available() as usize)?; // Full text (lyrics) is last. let text = scan_text(reader, encoding, reader.bytes_available() as usize)?; // Create the tag. let tag = Tag::new(std_key, &key, Value::from(text)); Ok(FrameResult::Tag(tag)) } /// Reads a `PCNT` (total file play count) frame. fn read_pcnt_frame( reader: &mut BufReader<'_>, std_key: Option, id: &str, ) -> Result { let len = reader.byte_len() as usize; // The play counter must be a minimum of 4 bytes long. if len < 4 { return decode_error("id3v2: play counters must be a minimum of 32bits"); } // However it may be extended by an arbitrary amount of bytes (or so it would seem). // Practically, a 4-byte (32-bit) count is way more than enough, but we'll support up-to an // 8-byte (64bit) count. if len > 8 { return unsupported_error("id3v2: play counters greater than 64bits are not supported"); } // The play counter is stored as an N-byte big-endian integer. Read N bytes into an 8-byte // buffer, making sure the missing bytes are zeroed, and then reinterpret as a 64-bit integer. let mut buf = [0u8; 8]; reader.read_buf_exact(&mut buf[8 - len..])?; let play_count = u64::from_be_bytes(buf); // Create the tag. let tag = Tag::new(std_key, id, Value::from(play_count)); Ok(FrameResult::Tag(tag)) } /// Reads a `POPM` (popularimeter) frame. fn read_popm_frame( reader: &mut BufReader<'_>, std_key: Option, id: &str, ) -> Result { let email = scan_text(reader, Encoding::Iso8859_1, reader.bytes_available() as usize)?; let key = format!("{}:{}", id, &email); let rating = reader.read_u8()?; // There's a personalized play counter here, but there is no analogue in Symphonia so don't do // anything with it. // Create the tag. let tag = Tag::new(std_key, &key, Value::from(rating)); Ok(FrameResult::Tag(tag)) } /// Reads a `MCDI` (music CD identifier) frame. fn read_mcdi_frame( reader: &mut BufReader<'_>, std_key: Option, id: &str, ) -> Result { // The entire frame is a binary dump of a CD-DA TOC. let buf = reader.read_buf_bytes_ref(reader.byte_len() as usize)?; // Create the tag. let tag = Tag::new(std_key, id, Value::from(buf)); Ok(FrameResult::Tag(tag)) } fn read_apic_frame( reader: &mut BufReader<'_>, _: Option, _: &str, ) -> Result { // The first byte of the frame is the encoding of the text description. let encoding = match Encoding::parse(reader.read_byte()?) { Some(encoding) => encoding, _ => return decode_error("id3v2: invalid text encoding"), }; // ASCII media (MIME) type. let media_type = scan_text(reader, Encoding::Iso8859_1, reader.bytes_available() as usize)?.into_owned(); // Image usage. let usage = util::apic_picture_type_to_visual_key(u32::from(reader.read_u8()?)); // Textual image description. let desc = scan_text(reader, encoding, reader.bytes_available() as usize)?; let tags = vec![Tag::new(Some(StandardTagKey::Description), "", Value::from(desc))]; // The remainder of the APIC frame is the image data. // TODO: Apply a limit. let data = Box::from(reader.read_buf_bytes_available_ref()); let visual = Visual { media_type, dimensions: None, bits_per_pixel: None, color_mode: None, usage, tags, data, }; Ok(FrameResult::Visual(visual)) } /// Enumeration of valid encodings for text fields in ID3v2 tags #[derive(Copy, Clone, Debug)] enum Encoding { /// ISO-8859-1 (aka Latin-1) characters in the range 0x20-0xFF. Iso8859_1, /// UTF-16 (or UCS-2) with a byte-order-mark (BOM). If the BOM is missing, big-endian encoding /// is assumed. Utf16Bom, /// UTF-16 big-endian without a byte-order-mark (BOM). Utf16Be, /// UTF-8. Utf8, } impl Encoding { fn parse(encoding: u8) -> Option { match encoding { // ISO-8859-1 terminated with 0x00. 0 => Some(Encoding::Iso8859_1), // UTF-16 with byte order marker (BOM), terminated with 0x00 0x00. 1 => Some(Encoding::Utf16Bom), // UTF-16BE without byte order marker (BOM), terminated with 0x00 0x00. 2 => Some(Encoding::Utf16Be), // UTF-8 terminated with 0x00. 3 => Some(Encoding::Utf8), // Invalid encoding. _ => None, } } } /// Scans up-to `scan_len` bytes from the provided `BufStream` for a string that is terminated with /// the appropriate null terminator for the given encoding as per the ID3v2 specification. A /// copy-on-write reference to the string excluding the null terminator is returned or an error. If /// the scanned string is valid UTF-8, or is equivalent to UTF-8, then no copies will occur. If a /// null terminator is not found, and `scan_len` is reached, or the stream is exhausted, all the /// scanned bytes up-to that point are interpreted as the string. fn scan_text<'a>( reader: &'a mut BufReader<'_>, encoding: Encoding, scan_len: usize, ) -> io::Result> { let buf = match encoding { Encoding::Iso8859_1 | Encoding::Utf8 => reader.scan_bytes_aligned_ref(&[0x00], 1, scan_len), Encoding::Utf16Bom | Encoding::Utf16Be => { reader.scan_bytes_aligned_ref(&[0x00, 0x00], 2, scan_len) } }?; Ok(decode_text(encoding, buf)) } /// Decodes a slice of bytes containing encoded text into a UTF-8 `str`. Trailing null terminators /// are removed, and any invalid characters are replaced with the [U+FFFD REPLACEMENT CHARACTER]. fn decode_text(encoding: Encoding, data: &[u8]) -> Cow<'_, str> { let mut end = data.len(); match encoding { Encoding::Iso8859_1 => { // The ID3v2 specification says that only ISO-8859-1 characters between 0x20 to 0xFF, // inclusive, are considered valid. Any null terminator(s) (trailing 0x00 byte for // ISO-8859-1) will also be removed. // // TODO: Improve this conversion by returning a copy-on-write str sliced from data if // all characters are > 0x1F and < 0x80. Fallback to the iterator approach otherwise. data.iter().filter(|&b| *b > 0x1f).map(|&b| b as char).collect() } Encoding::Utf8 => { // Remove any null terminator(s) (trailing 0x00 byte for UTF-8). while end > 0 { if data[end - 1] != 0 { break; } end -= 1; } String::from_utf8_lossy(&data[..end]) } Encoding::Utf16Bom | Encoding::Utf16Be => { // Remove any null terminator(s) (trailing [0x00, 0x00] bytes for UTF-16 variants). while end > 1 { if data[end - 2] != 0x0 || data[end - 1] != 0x0 { break; } end -= 2; } // Decode UTF-16 to UTF-8. If a byte-order-mark is present, UTF_16BE.decode() will use // the indicated endianness. Otherwise, big endian is assumed. UTF_16BE.decode(&data[..end]).0 } } } symphonia-metadata-0.5.2/src/id3v2/mod.rs000064400000000000000000000314331046102023000162640ustar 00000000000000// Symphonia // Copyright (c) 2019-2022 The Project Symphonia Developers. // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. //! An ID3v2 metadata reader. use symphonia_core::errors::{decode_error, unsupported_error, Result}; use symphonia_core::io::*; use symphonia_core::meta::{MetadataBuilder, MetadataOptions, MetadataReader, MetadataRevision}; use symphonia_core::probe::{Descriptor, Instantiate, QueryDescriptor}; use symphonia_core::support_metadata; use log::{info, trace, warn}; mod frames; mod unsync; use frames::*; use unsync::{read_syncsafe_leq32, UnsyncStream}; #[derive(Debug)] #[allow(clippy::enum_variant_names)] enum TagSizeRestriction { Max128Frames1024KiB, Max64Frames128KiB, Max32Frames40KiB, Max32Frames4KiB, } #[derive(Debug)] enum TextEncodingRestriction { None, Utf8OrIso88591, } #[derive(Debug)] enum TextFieldSize { None, Max1024Characters, Max128Characters, Max30Characters, } #[derive(Debug)] enum ImageEncodingRestriction { None, PngOrJpegOnly, } #[derive(Debug)] enum ImageSizeRestriction { None, LessThan256x256, LessThan64x64, Exactly64x64, } #[derive(Debug)] #[allow(dead_code)] struct Header { major_version: u8, minor_version: u8, size: u32, unsynchronisation: bool, has_extended_header: bool, experimental: bool, has_footer: bool, } #[derive(Debug)] #[allow(dead_code)] struct Restrictions { tag_size: TagSizeRestriction, text_encoding: TextEncodingRestriction, text_field_size: TextFieldSize, image_encoding: ImageEncodingRestriction, image_size: ImageSizeRestriction, } #[derive(Debug)] #[allow(dead_code)] struct ExtendedHeader { /// ID3v2.3 only, the number of padding bytes. padding_size: Option, /// ID3v2.3+, a CRC32 checksum of the Tag. crc32: Option, /// ID3v2.4 only, is this Tag an update to an earlier Tag. is_update: Option, /// ID3v2.4 only, Tag modification restrictions. restrictions: Option, } /// Read the header of an ID3v2 (verions 2.2+) tag. fn read_id3v2_header(reader: &mut B) -> Result
{ let marker = reader.read_triple_bytes()?; if marker != *b"ID3" { return unsupported_error("id3v2: not an ID3v2 tag"); } let major_version = reader.read_u8()?; let minor_version = reader.read_u8()?; let flags = reader.read_u8()?; let size = unsync::read_syncsafe_leq32(reader, 28)?; let mut header = Header { major_version, minor_version, size, unsynchronisation: false, has_extended_header: false, experimental: false, has_footer: false, }; // Major and minor version numbers should never equal 0xff as per the specification. if major_version == 0xff || minor_version == 0xff { return decode_error("id3v2: invalid version number(s)"); } // Only support versions 2.2.x (first version) to 2.4.x (latest version as of May 2019) of the // specification. if major_version < 2 || major_version > 4 { return unsupported_error("id3v2: unsupported ID3v2 version"); } // Version 2.2 of the standard specifies a compression flag bit, but does not specify a // compression standard. Future versions of the standard remove this feature and repurpose this // bit for other features. Since there is no way to know how to handle the remaining tag data, // return an unsupported error. if major_version == 2 && (flags & 0x40) != 0 { return unsupported_error("id3v2: ID3v2.2 compression is not supported"); } // With the exception of the compression flag in version 2.2, flags were added sequentially each // major version. Check each bit sequentially as they appear in each version. if major_version >= 2 { header.unsynchronisation = flags & 0x80 != 0; } if major_version >= 3 { header.has_extended_header = flags & 0x40 != 0; header.experimental = flags & 0x20 != 0; } if major_version >= 4 { header.has_footer = flags & 0x10 != 0; } Ok(header) } /// Read the extended header of an ID3v2.3 tag. fn read_id3v2p3_extended_header(reader: &mut B) -> Result { let size = reader.read_be_u32()?; let flags = reader.read_be_u16()?; let padding_size = reader.read_be_u32()?; if !(size == 6 || size == 10) { return decode_error("id3v2: invalid extended header size"); } let mut header = ExtendedHeader { padding_size: Some(padding_size), crc32: None, is_update: None, restrictions: None, }; // CRC32 flag. if size == 10 && flags & 0x8000 != 0 { header.crc32 = Some(reader.read_be_u32()?); } Ok(header) } /// Read the extended header of an ID3v2.4 tag. fn read_id3v2p4_extended_header(reader: &mut B) -> Result { let _size = read_syncsafe_leq32(reader, 28)?; if reader.read_u8()? != 1 { return decode_error("id3v2: extended flags should have a length of 1"); } let flags = reader.read_u8()?; let mut header = ExtendedHeader { padding_size: None, crc32: None, is_update: Some(false), restrictions: None, }; // Tag is an update flag. if flags & 0x40 != 0x0 { let len = reader.read_u8()?; if len != 1 { return decode_error("id3v2: is update extended flag has invalid size"); } header.is_update = Some(true); } // CRC32 flag. if flags & 0x20 != 0x0 { let len = reader.read_u8()?; if len != 5 { return decode_error("id3v2: CRC32 extended flag has invalid size"); } header.crc32 = Some(read_syncsafe_leq32(reader, 32)?); } // Restrictions flag. if flags & 0x10 != 0x0 { let len = reader.read_u8()?; if len != 1 { return decode_error("id3v2: restrictions extended flag has invalid size"); } let restrictions = reader.read_u8()?; let tag_size = match (restrictions & 0xc0) >> 6 { 0 => TagSizeRestriction::Max128Frames1024KiB, 1 => TagSizeRestriction::Max64Frames128KiB, 2 => TagSizeRestriction::Max32Frames40KiB, 3 => TagSizeRestriction::Max32Frames4KiB, _ => unreachable!(), }; let text_encoding = match (restrictions & 0x40) >> 5 { 0 => TextEncodingRestriction::None, 1 => TextEncodingRestriction::Utf8OrIso88591, _ => unreachable!(), }; let text_field_size = match (restrictions & 0x18) >> 3 { 0 => TextFieldSize::None, 1 => TextFieldSize::Max1024Characters, 2 => TextFieldSize::Max128Characters, 3 => TextFieldSize::Max30Characters, _ => unreachable!(), }; let image_encoding = match (restrictions & 0x04) >> 2 { 0 => ImageEncodingRestriction::None, 1 => ImageEncodingRestriction::PngOrJpegOnly, _ => unreachable!(), }; let image_size = match restrictions & 0x03 { 0 => ImageSizeRestriction::None, 1 => ImageSizeRestriction::LessThan256x256, 2 => ImageSizeRestriction::LessThan64x64, 3 => ImageSizeRestriction::Exactly64x64, _ => unreachable!(), }; header.restrictions = Some(Restrictions { tag_size, text_encoding, text_field_size, image_encoding, image_size, }) } Ok(header) } fn read_id3v2_body( reader: &mut B, header: &Header, metadata: &mut MetadataBuilder, ) -> Result<()> { // If there is an extended header, read and parse it based on the major version of the tag. if header.has_extended_header { let extended = match header.major_version { 3 => read_id3v2p3_extended_header(reader)?, 4 => read_id3v2p4_extended_header(reader)?, _ => unreachable!(), }; trace!("{:#?}", &extended); } let min_frame_size = match header.major_version { 2 => 6, 3 | 4 => 10, _ => unreachable!(), }; loop { // Read frames based on the major version of the tag. let frame = match header.major_version { 2 => read_id3v2p2_frame(reader), 3 => read_id3v2p3_frame(reader), 4 => read_id3v2p4_frame(reader), _ => break, }?; match frame { // The padding has been reached, don't parse any further. FrameResult::Padding => break, // A frame was parsed into a tag, add it to the tag collection. FrameResult::Tag(tag) => { metadata.add_tag(tag); } // A frame was parsed into multiple tags, add them all to the tag collection. FrameResult::MultipleTags(multi_tags) => { for tag in multi_tags { metadata.add_tag(tag); } } // A frame was parsed into a visual, add it to the visual collection. FrameResult::Visual(visual) => { metadata.add_visual(visual); } // An unknown frame was encountered. FrameResult::UnsupportedFrame(ref id) => { info!("unsupported frame {}", id); } // The frame contained invalid data. FrameResult::InvalidData(ref id) => { warn!("invalid data for {} frame", id); } } // Read frames until there is not enough bytes available in the ID3v2 tag for another frame. if reader.bytes_available() < min_frame_size { break; } } Ok(()) } pub fn read_id3v2(reader: &mut B, metadata: &mut MetadataBuilder) -> Result<()> { // Read the (sorta) version agnostic tag header. let header = read_id3v2_header(reader)?; // If the unsynchronisation flag is set in the header, all tag data must be passed through the // unsynchronisation decoder before being read for verions < 4 of ID3v2. let mut scoped = if header.unsynchronisation && header.major_version < 4 { let mut unsync = UnsyncStream::new(ScopedStream::new(reader, u64::from(header.size))); read_id3v2_body(&mut unsync, &header, metadata)?; unsync.into_inner() } // Otherwise, read the data as-is. Individual frames may be unsynchronised for major versions // >= 4. else { let mut scoped = ScopedStream::new(reader, u64::from(header.size)); read_id3v2_body(&mut scoped, &header, metadata)?; scoped }; // Ignore any remaining data in the tag. scoped.ignore()?; Ok(()) } pub mod util { use symphonia_core::meta::StandardVisualKey; /// Try to get a `StandardVisualKey` from the APIC picture type identifier. pub fn apic_picture_type_to_visual_key(apic: u32) -> Option { match apic { 0x01 => Some(StandardVisualKey::FileIcon), 0x02 => Some(StandardVisualKey::OtherIcon), 0x03 => Some(StandardVisualKey::FrontCover), 0x04 => Some(StandardVisualKey::BackCover), 0x05 => Some(StandardVisualKey::Leaflet), 0x06 => Some(StandardVisualKey::Media), 0x07 => Some(StandardVisualKey::LeadArtistPerformerSoloist), 0x08 => Some(StandardVisualKey::ArtistPerformer), 0x09 => Some(StandardVisualKey::Conductor), 0x0a => Some(StandardVisualKey::BandOrchestra), 0x0b => Some(StandardVisualKey::Composer), 0x0c => Some(StandardVisualKey::Lyricist), 0x0d => Some(StandardVisualKey::RecordingLocation), 0x0e => Some(StandardVisualKey::RecordingSession), 0x0f => Some(StandardVisualKey::Performance), 0x10 => Some(StandardVisualKey::ScreenCapture), 0x12 => Some(StandardVisualKey::Illustration), 0x13 => Some(StandardVisualKey::BandArtistLogo), 0x14 => Some(StandardVisualKey::PublisherStudioLogo), _ => None, } } } pub struct Id3v2Reader; impl QueryDescriptor for Id3v2Reader { fn query() -> &'static [Descriptor] { &[support_metadata!("id3v2", "ID3v2", &[], &[], &[b"ID3"])] } fn score(_context: &[u8]) -> u8 { 255 } } impl MetadataReader for Id3v2Reader { fn new(_options: &MetadataOptions) -> Self { Id3v2Reader {} } fn read_all(&mut self, reader: &mut MediaSourceStream) -> Result { let mut builder = MetadataBuilder::new(); read_id3v2(reader, &mut builder)?; Ok(builder.metadata()) } } symphonia-metadata-0.5.2/src/id3v2/unsync.rs000064400000000000000000000137401046102023000170250ustar 00000000000000// Symphonia // Copyright (c) 2019-2022 The Project Symphonia Developers. // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. use std::io; use symphonia_core::errors::Result; use symphonia_core::io::{FiniteStream, ReadBytes}; pub fn read_syncsafe_leq32(reader: &mut B, bit_width: u8) -> Result { debug_assert!(bit_width <= 32); let mut result = 0u32; let mut bits_read = 0; while bits_read < bit_width { // Ensure bits_read never exceeds the bit width which will cause an overflow let next_read = (bit_width - bits_read).min(7); bits_read += next_read; // The mask should have of the bits below 2 ^ nex_read set to 1 let mask = (1 << next_read) - 1; result |= u32::from(reader.read_u8()? & mask) << (bit_width - bits_read); } Ok(result) } pub fn decode_unsynchronisation(buf: &mut [u8]) -> &mut [u8] { let len = buf.len(); let mut src = 0; let mut dst = 0; // Decode the unsynchronisation scheme in-place. while src < len - 1 { buf[dst] = buf[src]; dst += 1; src += 1; if buf[src - 1] == 0xff && buf[src] == 0x00 { src += 1; } } if src < len { buf[dst] = buf[src]; dst += 1; } &mut buf[..dst] } pub struct UnsyncStream { inner: B, byte: u8, } impl UnsyncStream { pub fn new(inner: B) -> Self { UnsyncStream { inner, byte: 0 } } /// Convert the `UnsyncStream` to the inner stream. pub fn into_inner(self) -> B { self.inner } } impl FiniteStream for UnsyncStream { #[inline(always)] fn byte_len(&self) -> u64 { self.inner.byte_len() } #[inline(always)] fn bytes_read(&self) -> u64 { self.inner.bytes_read() } #[inline(always)] fn bytes_available(&self) -> u64 { self.inner.bytes_available() } } impl ReadBytes for UnsyncStream { fn read_byte(&mut self) -> io::Result { let last = self.byte; self.byte = self.inner.read_byte()?; // If the last byte was 0xff, and the current byte is 0x00, the current byte should be // dropped and the next byte read instead. if last == 0xff && self.byte == 0x00 { self.byte = self.inner.read_byte()?; } Ok(self.byte) } fn read_double_bytes(&mut self) -> io::Result<[u8; 2]> { Ok([self.read_byte()?, self.read_byte()?]) } fn read_triple_bytes(&mut self) -> io::Result<[u8; 3]> { Ok([self.read_byte()?, self.read_byte()?, self.read_byte()?]) } fn read_quad_bytes(&mut self) -> io::Result<[u8; 4]> { Ok([self.read_byte()?, self.read_byte()?, self.read_byte()?, self.read_byte()?]) } fn read_buf(&mut self, _: &mut [u8]) -> io::Result { // Not required. unimplemented!(); } fn read_buf_exact(&mut self, buf: &mut [u8]) -> io::Result<()> { let len = buf.len(); if len > 0 { // Fill the provided buffer directly from the underlying reader. self.inner.read_buf_exact(buf)?; // If the last seen byte was 0xff, and the first byte in buf is 0x00, skip the first // byte of buf. let mut src = usize::from(self.byte == 0xff && buf[0] == 0x00); let mut dst = 0; // Record the last byte in buf to continue unsychronisation streaming later. self.byte = buf[len - 1]; // Decode the unsynchronisation scheme in-place. while src < len - 1 { buf[dst] = buf[src]; dst += 1; src += 1; if buf[src - 1] == 0xff && buf[src] == 0x00 { src += 1; } } // When the final two src bytes are [ 0xff, 0x00 ], src will always equal len. // Therefore, if src < len, then the final byte should always be copied to dst. if src < len { buf[dst] = buf[src]; dst += 1; } // If dst < len, then buf is not full. Read the remaining bytes manually to completely // fill buf. while dst < len { buf[dst] = self.read_byte()?; dst += 1; } } Ok(()) } fn scan_bytes_aligned<'a>( &mut self, _: &[u8], _: usize, _: &'a mut [u8], ) -> io::Result<&'a mut [u8]> { // Not required. unimplemented!(); } fn ignore_bytes(&mut self, count: u64) -> io::Result<()> { for _ in 0..count { self.inner.read_byte()?; } Ok(()) } fn pos(&self) -> u64 { // Not required. unimplemented!(); } } #[cfg(test)] mod tests { use super::read_syncsafe_leq32; use symphonia_core::io::BufReader; #[test] fn verify_read_syncsafe_leq32() { let mut stream = BufReader::new(&[3, 4, 80, 1, 15]); assert_eq!(101875743, read_syncsafe_leq32(&mut stream, 32).unwrap()); // Special case: for a bit depth that is not a multiple of 7 such as 32 // we need to ensure the mask is correct. // In this case, the final iteration should read 4 bits and have a mask of 0b0000_1111. // 0b0000_1111 has a 0 in 16's place so testing mask & 16 will ensure this is working. let mut stream = BufReader::new(&[16, 16, 16, 16, 16]); assert_eq!(541098240, read_syncsafe_leq32(&mut stream, 32).unwrap()); let mut stream = BufReader::new(&[3, 4, 80, 1]); assert_eq!(6367233, read_syncsafe_leq32(&mut stream, 28).unwrap()); let mut stream = BufReader::new(&[3, 4, 80, 1]); assert_eq!(0, read_syncsafe_leq32(&mut stream, 0).unwrap()); } } symphonia-metadata-0.5.2/src/itunes.rs000064400000000000000000000065771046102023000161000ustar 00000000000000// Symphonia // Copyright (c) 2019-2022 The Project Symphonia Developers. // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. //! iTunes metadata support. use symphonia_core::meta::StandardTagKey; use std::collections::HashMap; use lazy_static::lazy_static; lazy_static! { static ref ITUNES_TAG_MAP: HashMap<&'static str, StandardTagKey> = { let mut m = HashMap::new(); m.insert("com.apple.iTunes:ARTISTS", StandardTagKey::Artist); m.insert("com.apple.iTunes:ASIN", StandardTagKey::IdentAsin); m.insert("com.apple.iTunes:BARCODE", StandardTagKey::IdentBarcode); m.insert("com.apple.iTunes:CATALOGNUMBER", StandardTagKey::IdentCatalogNumber); m.insert("com.apple.iTunes:CONDUCTOR", StandardTagKey::Conductor); m.insert("com.apple.iTunes:DISCSUBTITLE", StandardTagKey::DiscSubtitle); m.insert("com.apple.iTunes:DJMIXER", StandardTagKey::MixDj); m.insert("com.apple.iTunes:ENGINEER", StandardTagKey::Engineer); m.insert("com.apple.iTunes:ISRC", StandardTagKey::IdentIsrc); m.insert("com.apple.iTunes:LABEL", StandardTagKey::Label); m.insert("com.apple.iTunes:LANGUAGE", StandardTagKey::Language); m.insert("com.apple.iTunes:LICENSE", StandardTagKey::License); m.insert("com.apple.iTunes:LYRICIST", StandardTagKey::Lyricist); m.insert("com.apple.iTunes:MEDIA", StandardTagKey::MediaFormat); m.insert("com.apple.iTunes:MIXER", StandardTagKey::MixEngineer); m.insert("com.apple.iTunes:MOOD", StandardTagKey::Mood); m.insert( "com.apple.iTunes:MusicBrainz Album Artist Id", StandardTagKey::MusicBrainzAlbumArtistId, ); m.insert("com.apple.iTunes:MusicBrainz Album Id", StandardTagKey::MusicBrainzAlbumId); m.insert( "com.apple.iTunes:MusicBrainz Album Release Country", StandardTagKey::ReleaseCountry, ); m.insert( "com.apple.iTunes:MusicBrainz Album Status", StandardTagKey::MusicBrainzReleaseStatus, ); m.insert("com.apple.iTunes:MusicBrainz Album Type", StandardTagKey::MusicBrainzReleaseType); m.insert("com.apple.iTunes:MusicBrainz Artist Id", StandardTagKey::MusicBrainzArtistId); m.insert( "com.apple.iTunes:MusicBrainz Release Group Id", StandardTagKey::MusicBrainzReleaseGroupId, ); m.insert( "com.apple.iTunes:MusicBrainz Release Track Id", StandardTagKey::MusicBrainzReleaseTrackId, ); m.insert("com.apple.iTunes:MusicBrainz Track Id", StandardTagKey::MusicBrainzTrackId); m.insert("com.apple.iTunes:MusicBrainz Work Id", StandardTagKey::MusicBrainzWorkId); m.insert("com.apple.iTunes:originaldate", StandardTagKey::OriginalDate); m.insert("com.apple.iTunes:PRODUCER", StandardTagKey::Producer); m.insert("com.apple.iTunes:REMIXER", StandardTagKey::Remixer); m.insert("com.apple.iTunes:SCRIPT", StandardTagKey::Script); m.insert("com.apple.iTunes:SUBTITLE", StandardTagKey::TrackSubtitle); m }; } /// Try to map the iTunes `tag` name to a `StandardTagKey`. pub fn std_key_from_tag(key: &str) -> Option { ITUNES_TAG_MAP.get(key).copied() } symphonia-metadata-0.5.2/src/lib.rs000064400000000000000000000012331046102023000153170ustar 00000000000000// Symphonia // Copyright (c) 2019-2022 The Project Symphonia Developers. // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. #![warn(rust_2018_idioms)] #![forbid(unsafe_code)] // The following lints are allowed in all Symphonia crates. Please see clippy.toml for their // justification. #![allow(clippy::comparison_chain)] #![allow(clippy::excessive_precision)] #![allow(clippy::identity_op)] #![allow(clippy::manual_range_contains)] pub mod id3v1; pub mod id3v2; pub mod itunes; pub mod riff; pub mod vorbis; symphonia-metadata-0.5.2/src/riff.rs000064400000000000000000000056211046102023000155040ustar 00000000000000// Symphonia // Copyright (c) 2019-2022 The Project Symphonia Developers. // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. //! A RIFF INFO metadata reader. use lazy_static::lazy_static; use std::collections::HashMap; use symphonia_core::meta::{StandardTagKey, Tag, Value}; lazy_static! { static ref RIFF_INFO_MAP: HashMap<&'static str, StandardTagKey> = { let mut m = HashMap::new(); m.insert("ages", StandardTagKey::Rating); m.insert("cmnt", StandardTagKey::Comment); // Is this the same as a cmnt? m.insert("comm", StandardTagKey::Comment); m.insert("dtim", StandardTagKey::OriginalDate); m.insert("genr", StandardTagKey::Genre); m.insert("iart", StandardTagKey::Artist); // Is this also the same as cmnt? m.insert("icmt", StandardTagKey::Comment); m.insert("icop", StandardTagKey::Copyright); m.insert("icrd", StandardTagKey::Date); m.insert("idit", StandardTagKey::OriginalDate); m.insert("ienc", StandardTagKey::EncodedBy); m.insert("ieng", StandardTagKey::Engineer); m.insert("ifrm", StandardTagKey::TrackTotal); m.insert("ignr", StandardTagKey::Genre); m.insert("ilng", StandardTagKey::Language); m.insert("imus", StandardTagKey::Composer); m.insert("inam", StandardTagKey::TrackTitle); m.insert("iprd", StandardTagKey::Album); m.insert("ipro", StandardTagKey::Producer); m.insert("iprt", StandardTagKey::TrackNumber); m.insert("irtd", StandardTagKey::Rating); m.insert("isft", StandardTagKey::Encoder); m.insert("isgn", StandardTagKey::Genre); m.insert("isrf", StandardTagKey::MediaFormat); m.insert("itch", StandardTagKey::EncodedBy); m.insert("iwri", StandardTagKey::Writer); m.insert("lang", StandardTagKey::Language); m.insert("prt1", StandardTagKey::TrackNumber); m.insert("prt2", StandardTagKey::TrackTotal); // Same as inam? m.insert("titl", StandardTagKey::TrackTitle); m.insert("torg", StandardTagKey::Label); m.insert("trck", StandardTagKey::TrackNumber); m.insert("tver", StandardTagKey::Version); m.insert("year", StandardTagKey::Date); m }; } /// Parse the RIFF INFO block into a `Tag` using the block's identifier tag and a slice /// containing the block's contents. pub fn parse(tag: [u8; 4], buf: &[u8]) -> Tag { // TODO: Key should be checked that it only contains ASCII characters. let key = String::from_utf8_lossy(&tag); let value = String::from_utf8_lossy(buf); // Attempt to assign a standardized tag key. let std_tag = RIFF_INFO_MAP.get(key.to_lowercase().as_str()).copied(); Tag::new(std_tag, &key, Value::from(value)) } symphonia-metadata-0.5.2/src/vorbis.rs000064400000000000000000000224771046102023000160720ustar 00000000000000// Symphonia // Copyright (c) 2019-2022 The Project Symphonia Developers. // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. //! A Vorbic COMMENT metadata reader for FLAC or OGG formats. use lazy_static::lazy_static; use std::collections::HashMap; use symphonia_core::errors::Result; use symphonia_core::io::ReadBytes; use symphonia_core::meta::{MetadataBuilder, StandardTagKey, Tag, Value}; lazy_static! { static ref VORBIS_COMMENT_MAP: HashMap<&'static str, StandardTagKey> = { let mut m = HashMap::new(); m.insert("album artist" , StandardTagKey::AlbumArtist); m.insert("album" , StandardTagKey::Album); m.insert("albumartist" , StandardTagKey::AlbumArtist); m.insert("albumartistsort" , StandardTagKey::SortAlbumArtist); m.insert("albumsort" , StandardTagKey::SortAlbum); m.insert("arranger" , StandardTagKey::Arranger); m.insert("artist" , StandardTagKey::Artist); m.insert("artistsort" , StandardTagKey::SortArtist); // TODO: Is Author a synonym for Writer? m.insert("author" , StandardTagKey::Writer); m.insert("barcode" , StandardTagKey::IdentBarcode); m.insert("bpm" , StandardTagKey::Bpm); m.insert("catalog #" , StandardTagKey::IdentCatalogNumber); m.insert("catalog" , StandardTagKey::IdentCatalogNumber); m.insert("catalognumber" , StandardTagKey::IdentCatalogNumber); m.insert("catalogue #" , StandardTagKey::IdentCatalogNumber); m.insert("comment" , StandardTagKey::Comment); m.insert("compileation" , StandardTagKey::Compilation); m.insert("composer" , StandardTagKey::Composer); m.insert("conductor" , StandardTagKey::Conductor); m.insert("copyright" , StandardTagKey::Copyright); m.insert("date" , StandardTagKey::Date); m.insert("description" , StandardTagKey::Description); m.insert("disc" , StandardTagKey::DiscNumber); m.insert("discnumber" , StandardTagKey::DiscNumber); m.insert("discsubtitle" , StandardTagKey::DiscSubtitle); m.insert("disctotal" , StandardTagKey::DiscTotal); m.insert("disk" , StandardTagKey::DiscNumber); m.insert("disknumber" , StandardTagKey::DiscNumber); m.insert("disksubtitle" , StandardTagKey::DiscSubtitle); m.insert("disktotal" , StandardTagKey::DiscTotal); m.insert("djmixer" , StandardTagKey::MixDj); m.insert("ean/upn" , StandardTagKey::IdentEanUpn); m.insert("encoded-by" , StandardTagKey::EncodedBy); m.insert("encoder settings" , StandardTagKey::EncoderSettings); m.insert("encoder" , StandardTagKey::Encoder); m.insert("encoding" , StandardTagKey::EncoderSettings); m.insert("engineer" , StandardTagKey::Engineer); m.insert("ensemble" , StandardTagKey::Ensemble); m.insert("genre" , StandardTagKey::Genre); m.insert("isrc" , StandardTagKey::IdentIsrc); m.insert("language" , StandardTagKey::Language); m.insert("label" , StandardTagKey::Label); m.insert("license" , StandardTagKey::License); m.insert("lyricist" , StandardTagKey::Lyricist); m.insert("lyrics" , StandardTagKey::Lyrics); m.insert("media" , StandardTagKey::MediaFormat); m.insert("mixer" , StandardTagKey::MixEngineer); m.insert("mood" , StandardTagKey::Mood); m.insert("musicbrainz_albumartistid" , StandardTagKey::MusicBrainzAlbumArtistId); m.insert("musicbrainz_albumid" , StandardTagKey::MusicBrainzAlbumId); m.insert("musicbrainz_artistid" , StandardTagKey::MusicBrainzArtistId); m.insert("musicbrainz_discid" , StandardTagKey::MusicBrainzDiscId); m.insert("musicbrainz_originalalbumid" , StandardTagKey::MusicBrainzOriginalAlbumId); m.insert("musicbrainz_originalartistid", StandardTagKey::MusicBrainzOriginalArtistId); m.insert("musicbrainz_recordingid" , StandardTagKey::MusicBrainzRecordingId); m.insert("musicbrainz_releasegroupid" , StandardTagKey::MusicBrainzReleaseGroupId); m.insert("musicbrainz_releasetrackid" , StandardTagKey::MusicBrainzReleaseTrackId); m.insert("musicbrainz_trackid" , StandardTagKey::MusicBrainzTrackId); m.insert("musicbrainz_workid" , StandardTagKey::MusicBrainzWorkId); m.insert("opus" , StandardTagKey::Opus); m.insert("organization" , StandardTagKey::Label); m.insert("originaldate" , StandardTagKey::OriginalDate); m.insert("part" , StandardTagKey::Part); m.insert("performer" , StandardTagKey::Performer); m.insert("producer" , StandardTagKey::Producer); m.insert("productnumber" , StandardTagKey::IdentPn); // TODO: Is Publisher a synonym for Label? m.insert("publisher" , StandardTagKey::Label); m.insert("rating" , StandardTagKey::Rating); m.insert("releasecountry" , StandardTagKey::ReleaseCountry); m.insert("remixer" , StandardTagKey::Remixer); m.insert("replaygain_album_gain" , StandardTagKey::ReplayGainAlbumGain); m.insert("replaygain_album_peak" , StandardTagKey::ReplayGainAlbumPeak); m.insert("replaygain_track_gain" , StandardTagKey::ReplayGainTrackGain); m.insert("replaygain_track_peak" , StandardTagKey::ReplayGainTrackPeak); m.insert("script" , StandardTagKey::Script); m.insert("subtitle" , StandardTagKey::TrackSubtitle); m.insert("title" , StandardTagKey::TrackTitle); m.insert("titlesort" , StandardTagKey::SortTrackTitle); m.insert("totaldiscs" , StandardTagKey::DiscTotal); m.insert("totaltracks" , StandardTagKey::TrackTotal); m.insert("tracknumber" , StandardTagKey::TrackNumber); m.insert("tracktotal" , StandardTagKey::TrackTotal); m.insert("unsyncedlyrics" , StandardTagKey::Lyrics); m.insert("upc" , StandardTagKey::IdentUpc); m.insert("version" , StandardTagKey::Remixer); m.insert("version" , StandardTagKey::Version); m.insert("writer" , StandardTagKey::Writer); m.insert("year" , StandardTagKey::Date); m }; } /// Parse the given Vorbis Comment string into a `Tag`. fn parse(tag: &str) -> Tag { // Vorbis Comments (aka tags) are stored as = where is // a reduced ASCII-only identifier and is a UTF8 value. // // must only contain ASCII 0x20 through 0x7D, with 0x3D ('=') excluded. // ASCII 0x41 through 0x5A inclusive (A-Z) is to be considered equivalent to // ASCII 0x61 through 0x7A inclusive (a-z) for tag matching. let field: Vec<&str> = tag.splitn(2, '=').collect(); // Attempt to assign a standardized tag key. let std_tag = VORBIS_COMMENT_MAP.get(field[0].to_lowercase().as_str()).copied(); // The value field was empty so only the key field exists. Create an empty tag for the given // key field. if field.len() == 1 { return Tag::new(std_tag, field[0], Value::from("")); } Tag::new(std_tag, field[0], Value::from(field[1])) } pub fn read_comment_no_framing( reader: &mut B, metadata: &mut MetadataBuilder, ) -> Result<()> { // Read the vendor string length in bytes. let vendor_length = reader.read_u32()?; // Ignore the vendor string. reader.ignore_bytes(u64::from(vendor_length))?; // Read the number of comments. let n_comments = reader.read_u32()? as usize; for _ in 0..n_comments { // Read the comment string length in bytes. let comment_length = reader.read_u32()?; // Read the comment string. let mut comment_byte = vec![0; comment_length as usize]; reader.read_buf_exact(&mut comment_byte)?; // Parse the comment string into a Tag and insert it into the parsed tag list. metadata.add_tag(parse(&String::from_utf8_lossy(&comment_byte))); } Ok(()) }