├── .gitignore ├── assets ├── test.mp3 ├── id3v2.mp3 ├── double_id.mp3 └── trunc_test.mp3 ├── .github ├── FUNDING.yml └── workflows │ └── CI.yml ├── tests ├── error_check.rs ├── common.rs ├── truncate.rs ├── id3v2.rs ├── invalid_time.rs └── basic.rs ├── src ├── lib.rs ├── consts.rs ├── utils.rs ├── enums.rs ├── types.rs └── metadata.rs ├── examples ├── Cargo.toml └── src │ └── basic.rs ├── Cargo.toml ├── README.md ├── benches └── basic.rs └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | /Cargo.lock 2 | /target/ 3 | **.mp3 4 | -------------------------------------------------------------------------------- /assets/test.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuillaumeGomez/mp3-metadata/HEAD/assets/test.mp3 -------------------------------------------------------------------------------- /assets/id3v2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuillaumeGomez/mp3-metadata/HEAD/assets/id3v2.mp3 -------------------------------------------------------------------------------- /assets/double_id.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuillaumeGomez/mp3-metadata/HEAD/assets/double_id.mp3 -------------------------------------------------------------------------------- /assets/trunc_test.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuillaumeGomez/mp3-metadata/HEAD/assets/trunc_test.mp3 -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [GuillaumeGomez] 4 | patreon: GuillaumeGomez 5 | -------------------------------------------------------------------------------- /tests/error_check.rs: -------------------------------------------------------------------------------- 1 | extern crate mp3_metadata; 2 | 3 | mod common; 4 | 5 | #[test] 6 | fn error_check() { 7 | common::get_file("assets/error.mp3"); 8 | let _meta = mp3_metadata::read_from_file("assets/error.mp3"); //.expect("File error"); 9 | } 10 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use enums::{ChannelType, Copyright, Emphasis, Error, Genre, Layer, Status, Version, CRC}; 2 | pub use metadata::{read_from_file, read_from_slice}; 3 | pub use types::{AudioTag, Frame, MP3Metadata, OptionalAudioTags, Url}; 4 | 5 | mod consts; 6 | mod enums; 7 | mod metadata; 8 | mod types; 9 | mod utils; 10 | -------------------------------------------------------------------------------- /examples/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mp3-metadata-example" 3 | authors = ["Guillaume Gomez "] 4 | version = "0.2.0" 5 | 6 | description = "Metadata parser for MP3 files" 7 | repository = "https://github.com/GuillaumeGomez/mp3-metadata" 8 | license = "MIT" 9 | 10 | [[bin]] 11 | name = "basic" 12 | 13 | [dependencies] 14 | mp3-metadata = { path = "../" } 15 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mp3-metadata" 3 | version = "0.4.0" 4 | authors = ["Guillaume Gomez "] 5 | rust-version = "1.56" 6 | edition = "2021" 7 | 8 | description = "Metadata parser for MP3 files" 9 | repository = "https://github.com/GuillaumeGomez/mp3-metadata" 10 | license = "MIT" 11 | 12 | keywords = ["mp3", "metadata"] 13 | 14 | [lib] 15 | name = "mp3_metadata" 16 | 17 | [dev-dependencies] 18 | reqwest = { version = "0.11", features = ["blocking"] } 19 | simplemad = "0.9" 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mp3-metadata [![Build Status](https://travis-ci.org/GuillaumeGomez/mp3-metadata.svg?branch=master)](https://travis-ci.org/GuillaumeGomez/mp3-metadata) [![Build status](https://ci.appveyor.com/api/projects/status/g21vliyvouvsg92n/branch/master?svg=true)](https://ci.appveyor.com/project/GuillaumeGomez/mp3-metadata/branch/master) 2 | 3 | 4 | mp3 metadata parser in rust. 5 | 6 | For an example, take a look into examples folder. 7 | 8 | ## Doc 9 | 10 | You can have access to an online doc [here](https://docs.rs/mp3-metadata/). 11 | -------------------------------------------------------------------------------- /benches/basic.rs: -------------------------------------------------------------------------------- 1 | #![feature(test)] 2 | 3 | extern crate test; 4 | 5 | use std::fs::File; 6 | use std::io::Read; 7 | 8 | #[bench] 9 | fn bench_read_from_slice(b: &mut test::Bencher) { 10 | let mut buf = Vec::new(); 11 | match File::open("assets/test.mp3") { 12 | Ok(mut fd) => { 13 | fd.read_to_end(&mut buf).expect("read_to_end failed"); 14 | } 15 | Err(e) => panic!("File::open failed: {:?}", e), 16 | } 17 | b.iter(|| { 18 | mp3_metadata::read_from_slice(&buf).expect("read_from_slice failed"); 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [master] 4 | pull_request: 5 | 6 | name: CI 7 | 8 | jobs: 9 | build-linux: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | rust: 14 | - 1.56.0 15 | - nightly 16 | steps: 17 | - uses: actions/checkout@v2 18 | - uses: actions-rs/toolchain@v1 19 | with: 20 | profile: minimal 21 | toolchain: ${{ matrix.rust }} 22 | override: true 23 | components: clippy, rustfmt 24 | - run: cargo fmt -- --check 25 | - run: cargo clippy -- -D warnings 26 | - run: cargo build 27 | - run: RUST_BACKTRACE=1 cargo test 28 | - run: cd examples && cargo run -- ../assets/test.mp3 29 | -------------------------------------------------------------------------------- /src/consts.rs: -------------------------------------------------------------------------------- 1 | #[rustfmt::skip] 2 | pub const BITRATES: [[u16; 16]; 5] = [ 3 | [0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, 0], 4 | [0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 0], 5 | [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0], 6 | [0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256, 0], 7 | [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0], 8 | ]; 9 | #[rustfmt::skip] 10 | pub const SAMPLING_FREQ: [[u16; 4]; 4] = [ 11 | [44100, 48000, 32000, 0], 12 | [22050, 24000, 16000, 0], 13 | [11025, 12000, 8000, 0], 14 | [0, 0, 0, 0], 15 | ]; 16 | #[rustfmt::skip] 17 | pub const SAMPLES_PER_FRAME: [[u32; 4]; 2] = [ 18 | [384, 1152, 1152, 0], 19 | [384, 1152, 576, 0], 20 | ]; 21 | -------------------------------------------------------------------------------- /tests/common.rs: -------------------------------------------------------------------------------- 1 | extern crate reqwest; 2 | 3 | use std::fs::File; 4 | use std::io::prelude::*; 5 | use std::path::Path; 6 | 7 | pub fn get_file>(p: P) { 8 | let p = p.as_ref(); 9 | if p.exists() { 10 | return; 11 | } 12 | let mut resp = reqwest::blocking::get(format!( 13 | "https://guillaume-gomez.fr/rust-test/{}", 14 | p.file_name() 15 | .expect("file_name() failed") 16 | .to_str() 17 | .expect("to_str() failed") 18 | )) 19 | .expect("reqwest::get() failed"); 20 | assert!(resp.status().is_success()); 21 | 22 | let mut content = Vec::new(); 23 | resp.read_to_end(&mut content) 24 | .expect("read_to_string() failed"); 25 | 26 | let mut file = File::create(p).expect("cannot create file"); 27 | file.write_all(&content).expect("write_all() failed"); 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Guillaume Gomez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/truncate.rs: -------------------------------------------------------------------------------- 1 | extern crate mp3_metadata; 2 | 3 | use std::time::Duration; 4 | 5 | #[test] 6 | fn truncate() { 7 | let meta = mp3_metadata::read_from_file("assets/trunc_test.mp3").expect("File error"); 8 | if let Some(frame) = meta.frames.first() { 9 | assert_eq!(frame.size, 417); 10 | assert_eq!(frame.version, mp3_metadata::Version::MPEG1); 11 | assert_eq!(frame.layer, mp3_metadata::Layer::Layer3); 12 | assert_eq!(frame.crc, mp3_metadata::CRC::Added); 13 | assert_eq!(frame.bitrate, 128); 14 | assert_eq!(frame.sampling_freq, 44100); 15 | assert!(!frame.padding); 16 | assert!(!frame.private_bit); 17 | assert_eq!(frame.chan_type, mp3_metadata::ChannelType::SingleChannel); 18 | assert!(!frame.intensity_stereo); 19 | assert!(!frame.ms_stereo); 20 | assert_eq!(frame.copyright, mp3_metadata::Copyright::None); 21 | assert_eq!(frame.status, mp3_metadata::Status::Copy); 22 | assert_eq!(frame.emphasis, mp3_metadata::Emphasis::None); 23 | } 24 | assert_eq!(meta.duration, Duration::new(12, 120815872)); 25 | assert_eq!(meta.tag, None); 26 | } 27 | -------------------------------------------------------------------------------- /examples/src/basic.rs: -------------------------------------------------------------------------------- 1 | extern crate mp3_metadata; 2 | 3 | use std::env; 4 | 5 | fn main() { 6 | let file = match env::args().skip(1).next() { 7 | Some(f) => f, 8 | None => { 9 | println!("Need a music file parameter:"); 10 | println!("./basic [music_file].mp3\n"); 11 | println!("If you're using cargo, run it like this:"); 12 | println!("cargo run -- [music_file].mp3"); 13 | return 14 | } 15 | }; 16 | let meta = mp3_metadata::read_from_file(file).expect("File error"); 17 | 18 | println!("Number of frames: {}", meta.frames.len()); 19 | println!("\nShowing 5 first frames information:"); 20 | for frame in meta.frames[0..5].iter() { 21 | println!("========== NEW FRAME =========="); 22 | println!("size: {}", frame.size); 23 | println!("version: {:?}", frame.version); 24 | println!("layer: {:?}", frame.layer); 25 | println!("bitrate: {} Kb/s", frame.bitrate); 26 | println!("sampling frequency: {} Hz", frame.sampling_freq); 27 | println!("channel type: {:?}", frame.chan_type); 28 | println!("copyright: {:?}", frame.copyright); 29 | println!("status: {:?}", frame.status); 30 | println!("emphasis: {:?}", frame.emphasis); 31 | } 32 | 33 | println!("\n========== TAGS =========="); 34 | if let Some(tag) = meta.tag { 35 | println!("title: {}", tag.title); 36 | println!("artist: {}", tag.artist); 37 | println!("album: {}", tag.album); 38 | println!("year: {}", tag.year); 39 | println!("comment: {}", tag.comment); 40 | println!("genre: {:?}", tag.genre); 41 | } else { 42 | println!("No tag"); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/id3v2.rs: -------------------------------------------------------------------------------- 1 | extern crate mp3_metadata; 2 | 3 | #[test] 4 | fn id3v2() { 5 | let meta = mp3_metadata::read_from_file("assets/id3v2.mp3").expect("File error"); 6 | assert_eq!(meta.optional_info[0].position, 0); 7 | assert_eq!(meta.optional_info[0].major_version, 4); 8 | assert_eq!(meta.optional_info[0].minor_version, 0); 9 | assert_eq!( 10 | meta.optional_info[0].album_movie_show, 11 | Some("éൣø§".to_owned()) 12 | ); 13 | assert_eq!(meta.optional_info[0].bpm, None); 14 | assert_eq!( 15 | meta.optional_info[0].composers, 16 | vec!("not Mozart".to_owned(), "not Beethoven".to_owned()) 17 | ); 18 | assert_eq!( 19 | meta.optional_info[0].content_type, 20 | vec!(mp3_metadata::Genre::InstrumentalPop) 21 | ); 22 | assert_eq!( 23 | meta.optional_info[0].copyright, 24 | Some("Is there?".to_owned()) 25 | ); 26 | assert_eq!(meta.optional_info[0].date, None); 27 | assert_eq!(meta.optional_info[0].playlist_delay, None); 28 | assert_eq!( 29 | meta.optional_info[0].encoded_by, 30 | Some("some website...".to_owned()) 31 | ); 32 | assert_eq!(meta.optional_info[0].text_writers.len(), 0); 33 | assert_eq!(meta.optional_info[0].file_type, None); 34 | assert_eq!(meta.optional_info[0].time, None); 35 | assert_eq!(meta.optional_info[0].content_group_description, None); 36 | assert_eq!(meta.optional_info[0].subtitle_refinement_description, None); 37 | assert_eq!( 38 | meta.optional_info[0].title, 39 | Some("This is a wonderful title isn't it?".to_owned()) 40 | ); 41 | assert_eq!( 42 | meta.optional_info[0].performers, 43 | vec!("Someone".to_owned(), "Someone else".to_owned()) 44 | ); 45 | assert_eq!( 46 | meta.optional_info[0].band, 47 | Some("I like artists! But who to choose? So many of them...".to_owned()) 48 | ); 49 | assert_eq!(meta.optional_info[0].track_number, Some("01".to_owned())); 50 | 51 | assert_eq!( 52 | meta.tag, 53 | Some(mp3_metadata::AudioTag { 54 | title: "This is a wonderful title isn'".to_owned(), 55 | artist: "Someone/Someone else ".to_owned(), 56 | album: "".to_owned(), 57 | year: 2015, 58 | comment: "Some random comment because ".to_owned(), 59 | genre: mp3_metadata::Genre::Other, 60 | }) 61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /tests/invalid_time.rs: -------------------------------------------------------------------------------- 1 | extern crate mp3_metadata; 2 | extern crate simplemad; 3 | 4 | use std::time::Duration; 5 | 6 | use std::fs::File; 7 | 8 | mod common; 9 | 10 | // Still invalid for the moment as it seems... 11 | 12 | #[test] 13 | fn invalid_time() { 14 | common::get_file("assets/invalid_time.mp3"); 15 | let meta = mp3_metadata::read_from_file("assets/invalid_time.mp3").expect("File error"); 16 | let file = File::open("assets/invalid_time.mp3").unwrap(); 17 | let decoder = simplemad::Decoder::decode(file).unwrap(); 18 | let mut i = 0; 19 | let mut sum = Duration::new(0, 0); 20 | for decoding_result in decoder { 21 | match decoding_result { 22 | Err(_) => { 23 | //println!("Error: {:?} {:?}", e, meta.frames[i]); 24 | } 25 | Ok(frame) => { 26 | if i >= meta.frames.len() { 27 | println!("==> {} > {}", i, meta.frames.len()); 28 | i += 1; 29 | continue; 30 | } 31 | if meta.frames[i].sampling_freq as u32 != frame.sample_rate { 32 | println!( 33 | "[{}] [SAMPLE_RATE] {} != {}", 34 | i, meta.frames[i].sampling_freq, frame.sample_rate 35 | ); 36 | } 37 | if meta.frames[i].bitrate as u32 * 1000 != frame.bit_rate { 38 | println!( 39 | "[{}] [BIT_RATE] {} != {}", 40 | i, 41 | meta.frames[i].bitrate as u32 * 1000, 42 | frame.bit_rate 43 | ); 44 | } 45 | if meta.frames[i].duration.unwrap() != frame.duration { 46 | println!( 47 | "[{}] [DURATION] {:?} != {:?}", 48 | i, meta.frames[i].duration, frame.duration 49 | ); 50 | } 51 | if meta.frames[i].position != frame.position { 52 | println!( 53 | "[{}] [POSITION] {:?} != {:?}", 54 | i, meta.frames[i].position, frame.position 55 | ); 56 | } 57 | sum += frame.duration; 58 | } 59 | } 60 | i += 1; 61 | } 62 | //assert_eq!(meta.duration, Duration::new(162, 611095984)); 63 | //assert_eq!(meta.duration, sum); 64 | } 65 | -------------------------------------------------------------------------------- /tests/basic.rs: -------------------------------------------------------------------------------- 1 | extern crate mp3_metadata; 2 | extern crate simplemad; 3 | 4 | use std::time::Duration; 5 | 6 | use std::fs::File; 7 | 8 | #[test] 9 | fn basic() { 10 | let meta = mp3_metadata::read_from_file("assets/test.mp3").expect("File error"); 11 | let file = File::open("assets/test.mp3").unwrap(); 12 | let decoder = simplemad::Decoder::decode(file).unwrap(); 13 | let mut sum = Duration::new(0, 0); 14 | for (i, decoding_result) in decoder.enumerate() { 15 | match decoding_result { 16 | Err(_) => {} 17 | Ok(frame) => { 18 | if i >= meta.frames.len() { 19 | println!( 20 | "==> {} > {} {:?} {:?}", 21 | i, 22 | meta.frames.len(), 23 | meta.frames.last().unwrap().duration, 24 | frame.duration 25 | ); 26 | } else { 27 | if meta.frames[i].sampling_freq as u32 != frame.sample_rate { 28 | println!( 29 | "[{}] [SAMPLE_RATE] {} != {}", 30 | i, meta.frames[i].sampling_freq, frame.sample_rate 31 | ); 32 | panic!(); 33 | } 34 | if meta.frames[i].bitrate as u32 * 1000 != frame.bit_rate { 35 | println!( 36 | "[{}] [BIT_RATE] {} != {}", 37 | i, 38 | meta.frames[i].bitrate as u32 * 1000, 39 | frame.bit_rate 40 | ); 41 | panic!(); 42 | } 43 | if meta.frames[i].duration.unwrap() != frame.duration { 44 | println!( 45 | "[{}] [DURATION] {:?} != {:?}", 46 | i, meta.frames[i].duration, frame.duration 47 | ); 48 | panic!(); 49 | } 50 | if meta.frames[i].position != frame.position { 51 | println!( 52 | "[{}] [POSITION] {:?} != {:?}", 53 | i, meta.frames[i].position, frame.position 54 | ); 55 | panic!(); 56 | } 57 | } 58 | sum += frame.duration; 59 | } 60 | } 61 | } 62 | if let Some(frame) = meta.frames.first() { 63 | assert_eq!(frame.size, 417, "frame size"); 64 | assert_eq!(frame.version, mp3_metadata::Version::MPEG1, "version"); 65 | assert_eq!(frame.layer, mp3_metadata::Layer::Layer3, "layer"); 66 | assert_eq!(frame.crc, mp3_metadata::CRC::Added, "crc"); 67 | assert_eq!(frame.bitrate, 128, "bitrate"); 68 | assert_eq!(frame.sampling_freq, 44100, "sampling freq"); 69 | assert!(!frame.padding, "padding"); 70 | assert!(!frame.private_bit, "private bit"); 71 | assert_eq!( 72 | frame.chan_type, 73 | mp3_metadata::ChannelType::SingleChannel, 74 | "channel type" 75 | ); 76 | assert!(!frame.intensity_stereo, "intensity stereo"); 77 | assert!(!frame.ms_stereo, "ms stereo"); 78 | assert_eq!(frame.copyright, mp3_metadata::Copyright::None, "copyright"); 79 | assert_eq!(frame.status, mp3_metadata::Status::Copy, "status"); 80 | assert_eq!(frame.emphasis, mp3_metadata::Emphasis::None, "emphasis"); 81 | } 82 | assert_eq!(meta.frames.len(), 475, "number of frames"); 83 | assert_eq!(meta.duration, Duration::new(12, 408162800), "duration"); 84 | assert_eq!( 85 | meta.tag, 86 | Some(mp3_metadata::AudioTag { 87 | title: "Test of MP3 File ".to_owned(), 88 | artist: "Me ".to_owned(), 89 | album: "Me ".to_owned(), 90 | year: 2006, 91 | comment: "test ".to_owned(), 92 | genre: mp3_metadata::Genre::Other, 93 | }), 94 | "tag" 95 | ); 96 | assert_eq!(meta.duration, sum, "time check"); 97 | } 98 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use crate::consts::SAMPLES_PER_FRAME; 4 | use crate::enums::{Layer, Version}; 5 | use crate::types::Url; 6 | 7 | pub fn compute_duration(v: Version, l: Layer, sample_rate: u16) -> Option { 8 | if sample_rate == 0 { 9 | return None; 10 | } 11 | let mut big = match v { 12 | Version::MPEG1 => SAMPLES_PER_FRAME[0][get_layer_value(l)] as u64 * 1_000_000_000, 13 | Version::MPEG2 | Version::MPEG2_5 => { 14 | SAMPLES_PER_FRAME[1][get_layer_value(l)] as u64 * 1_000_000_000 15 | } 16 | _ => return None, 17 | }; 18 | big /= sample_rate as u64; 19 | Some(Duration::new( 20 | big / 1_000_000_000, 21 | (big % 1_000_000_000) as u32, 22 | )) 23 | } 24 | 25 | pub fn get_line(v: Version, l: Layer) -> usize { 26 | match (v, l) { 27 | (Version::MPEG1, Layer::Layer1) => 0, 28 | (Version::MPEG1, Layer::Layer2) => 1, 29 | (Version::MPEG1, Layer::Layer3) => 2, 30 | (Version::MPEG2, Layer::Layer1) | (Version::MPEG2_5, Layer::Layer1) => 3, 31 | _ => 4, 32 | } 33 | } 34 | 35 | pub fn get_layer_value(l: Layer) -> usize { 36 | match l { 37 | Layer::Layer1 => 0, 38 | Layer::Layer2 => 1, 39 | Layer::Layer3 => 2, 40 | _ => 3, 41 | } 42 | } 43 | 44 | pub fn get_samp_line(v: Version) -> usize { 45 | match v { 46 | Version::MPEG1 => 0, 47 | Version::MPEG2 => 1, 48 | Version::MPEG2_5 => 2, 49 | _ => 1, 50 | } 51 | } 52 | 53 | pub fn create_latin1_str(buf: &[u8]) -> String { 54 | // interpret each byte as full codepoint. UTF-16 is big enough to 55 | // represent those, surrogate pairs can't be created that way 56 | let utf16 = buf.iter().map(|c| *c as u16).collect::>(); 57 | String::from_utf16_lossy(utf16.as_ref()) 58 | } 59 | 60 | pub fn create_utf16_str(buf: &[u8]) -> String { 61 | let mut v = Vec::::new(); 62 | if buf.len() >= 2 { 63 | // BOM: \u{feff} 64 | if buf[0] == 0xfe && buf[1] == 0xff { 65 | // UTF-16BE 66 | v.reserve(buf.len() / 2 - 1); 67 | for i in 1..buf.len() / 2 { 68 | v.push(((buf[2 * i] as u16) << 8) | (buf[2 * i + 1] as u16)); 69 | } 70 | return String::from_utf16_lossy(v.as_ref()); 71 | } else if buf[0] == 0xff && buf[1] == 0xfe { 72 | // UTF-16LE 73 | v.reserve(buf.len() / 2 - 1); 74 | for i in 1..buf.len() / 2 { 75 | v.push(((buf[2 * i + 1] as u16) << 8) | (buf[2 * i] as u16)); 76 | } 77 | return String::from_utf16_lossy(v.as_ref()); 78 | } 79 | } 80 | // try as UTF-16LE 81 | v.reserve(buf.len() / 2); 82 | for i in 0..buf.len() / 2 { 83 | v.push(((buf[2 * i + 1] as u16) << 8) | (buf[2 * i] as u16)) 84 | } 85 | String::from_utf16_lossy(v.as_ref()) 86 | } 87 | 88 | pub fn create_utf8_str(mut buf: &[u8]) -> String { 89 | // Remove trailing NUL bytes from the input 90 | while let [rest @ .., last] = buf { 91 | if *last == 0 { 92 | buf = rest; 93 | } else { 94 | break; 95 | } 96 | } 97 | 98 | // String::from_utf8_lossy(buf).into_owned() 99 | String::from_utf8(buf.to_owned()).unwrap_or_default() 100 | } 101 | 102 | pub fn get_url_field( 103 | buf: &[u8], 104 | pos: usize, 105 | size: u32, 106 | changes: &mut bool, 107 | value: &mut Option, 108 | ) { 109 | if value.is_some() || size < 2 { 110 | return; 111 | } 112 | if !(*changes) { 113 | *changes = true; 114 | } 115 | let tmp_v = buf[pos..pos + size as usize].to_vec(); 116 | *value = Some(Url(String::from_utf8(tmp_v).unwrap_or_default())); 117 | } 118 | 119 | pub fn get_url_fields(buf: &[u8], pos: usize, size: u32, changes: &mut bool, value: &mut Vec) { 120 | let mut tmp = None; 121 | get_url_field(buf, pos, size, changes, &mut tmp); 122 | if let Some(tmp) = tmp { 123 | value.push(tmp); 124 | } 125 | } 126 | 127 | pub fn get_field(buf: &[u8], pos: usize, size: u32) -> String { 128 | let buf = &buf[pos..][..size as usize]; 129 | if buf.is_empty() { 130 | String::new() 131 | } else if buf[0] == 0 { 132 | // ISO-8859-1 133 | create_latin1_str(&buf[1..]) 134 | } else if buf[0] == 1 { 135 | // UTF-16, requires a BOM 136 | create_utf16_str(&buf[1..]) 137 | } else if buf[0] == 3 { 138 | // UTF-8 139 | create_utf8_str(&buf[1..]) 140 | } else { 141 | String::new() 142 | } 143 | } 144 | 145 | pub fn get_text_field( 146 | buf: &[u8], 147 | pos: usize, 148 | size: u32, 149 | changes: &mut bool, 150 | value: &mut Option, 151 | ) { 152 | if value.is_some() || size < 2 { 153 | return; 154 | } 155 | if !(*changes) { 156 | *changes = true; 157 | } 158 | *value = Some(get_field(buf, pos, size)); 159 | } 160 | 161 | pub fn get_text_fields( 162 | buf: &[u8], 163 | pos: usize, 164 | size: u32, 165 | changes: &mut bool, 166 | value: &mut Vec, 167 | ) { 168 | let tmp = get_field(buf, pos, size); 169 | let tmp_v = tmp.split('/'); 170 | for entry in tmp_v { 171 | if !entry.is_empty() { 172 | value.push(entry.to_owned()); 173 | } 174 | } 175 | if !(*changes) { 176 | *changes = true; 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/enums.rs: -------------------------------------------------------------------------------- 1 | use std::convert::From; 2 | use std::default::Default; 3 | use std::fmt; 4 | 5 | #[derive(Copy, Clone, Debug, Eq, PartialEq)] 6 | pub enum Error { 7 | FileError, 8 | NotMP3, 9 | NoHeader, 10 | DuplicatedIDV3, 11 | InvalidData, 12 | } 13 | 14 | impl fmt::Display for Error { 15 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 16 | let err = match *self { 17 | Error::FileError => "An I/O error occurred", 18 | Error::NotMP3 => "The file is not a valid MP3 file", 19 | Error::NoHeader => "The file is missing an MP3 header", 20 | Error::DuplicatedIDV3 => "The MP3 file contains a duplicate IDv3 frame", 21 | Error::InvalidData => "The MP3 metadata is invalid", 22 | }; 23 | err.fmt(f) 24 | } 25 | } 26 | 27 | #[allow(non_camel_case_types)] 28 | #[derive(Copy, Clone, Debug, Eq, PartialEq)] 29 | pub enum Version { 30 | Reserved, 31 | MPEG1, 32 | MPEG2, 33 | MPEG2_5, 34 | Unknown, 35 | } 36 | 37 | impl Default for Version { 38 | fn default() -> Version { 39 | Version::Unknown 40 | } 41 | } 42 | 43 | impl From for Version { 44 | fn from(c: u32) -> Version { 45 | match c { 46 | 0x00 => Version::MPEG2_5, 47 | 0x01 => Version::Reserved, 48 | 0x02 => Version::MPEG2, 49 | 0x03 => Version::MPEG1, 50 | _ => unreachable!(), 51 | } 52 | } 53 | } 54 | 55 | #[derive(Copy, Clone, Debug, Eq, PartialEq)] 56 | pub enum Layer { 57 | Reserved, 58 | Layer1, 59 | Layer2, 60 | Layer3, 61 | Unknown, 62 | } 63 | 64 | impl Default for Layer { 65 | fn default() -> Layer { 66 | Layer::Unknown 67 | } 68 | } 69 | 70 | impl From for Layer { 71 | fn from(c: u32) -> Layer { 72 | match c { 73 | 0x0 => Layer::Reserved, 74 | 0x1 => Layer::Layer3, 75 | 0x2 => Layer::Layer2, 76 | 0x3 => Layer::Layer1, 77 | _ => unreachable!(), 78 | } 79 | } 80 | } 81 | 82 | #[derive(Copy, Clone, Debug, Eq, PartialEq)] 83 | pub enum CRC { 84 | /// Redundancy added. 85 | Added, 86 | /// Redundancy not added. 87 | NotAdded, 88 | } 89 | 90 | impl Default for CRC { 91 | fn default() -> CRC { 92 | CRC::NotAdded 93 | } 94 | } 95 | 96 | impl From for CRC { 97 | fn from(c: u32) -> CRC { 98 | match c { 99 | 0x00 => CRC::Added, 100 | 0x01 => CRC::NotAdded, 101 | _ => unreachable!(), 102 | } 103 | } 104 | } 105 | 106 | #[derive(Copy, Clone, Debug, Eq, PartialEq)] 107 | pub enum ChannelType { 108 | Stereo, 109 | JointStereo, 110 | DualChannel, 111 | SingleChannel, 112 | Unknown, 113 | } 114 | 115 | impl Default for ChannelType { 116 | fn default() -> ChannelType { 117 | ChannelType::Unknown 118 | } 119 | } 120 | 121 | impl From for ChannelType { 122 | fn from(c: u32) -> ChannelType { 123 | match c { 124 | 0x0 => ChannelType::Stereo, 125 | 0x1 => ChannelType::JointStereo, 126 | 0x2 => ChannelType::DualChannel, 127 | 0x3 => ChannelType::SingleChannel, 128 | _ => unreachable!(), 129 | } 130 | } 131 | } 132 | 133 | #[derive(Copy, Clone, Debug, Eq, PartialEq)] 134 | pub enum Copyright { 135 | None, 136 | Some, 137 | } 138 | 139 | impl Default for Copyright { 140 | fn default() -> Copyright { 141 | Copyright::Some 142 | } 143 | } 144 | 145 | impl From for Copyright { 146 | fn from(c: u32) -> Copyright { 147 | match c { 148 | 0x0 => Copyright::None, 149 | 0x1 => Copyright::Some, 150 | _ => unreachable!(), 151 | } 152 | } 153 | } 154 | 155 | #[derive(Copy, Clone, Debug, Eq, PartialEq)] 156 | pub enum Status { 157 | Copy, 158 | Original, 159 | Unknown, 160 | } 161 | 162 | impl Default for Status { 163 | fn default() -> Status { 164 | Status::Unknown 165 | } 166 | } 167 | 168 | impl From for Status { 169 | fn from(c: u32) -> Status { 170 | match c { 171 | 0x0 => Status::Copy, 172 | 0x1 => Status::Original, 173 | _ => unreachable!(), 174 | } 175 | } 176 | } 177 | 178 | #[derive(Copy, Clone, Debug, Eq, PartialEq)] 179 | pub enum Emphasis { 180 | /// No emphasis 181 | None, 182 | /// 50/15 Micro seconds 183 | MicroSeconds, 184 | /// Reserved 185 | Reserved, 186 | /// CCIT J.17 187 | CCITT, 188 | Unknown, 189 | } 190 | 191 | impl Default for Emphasis { 192 | fn default() -> Emphasis { 193 | Emphasis::Unknown 194 | } 195 | } 196 | 197 | impl From for Emphasis { 198 | fn from(c: u32) -> Emphasis { 199 | match c { 200 | 0x0 => Emphasis::None, 201 | 0x1 => Emphasis::MicroSeconds, 202 | 0x2 => Emphasis::Reserved, 203 | 0x3 => Emphasis::CCITT, 204 | _ => unreachable!(), 205 | } 206 | } 207 | } 208 | 209 | #[derive(Clone, Debug, Eq, PartialEq)] 210 | pub enum Genre { 211 | Blues, 212 | ClassicRock, 213 | Country, 214 | Dance, 215 | Disco, 216 | Funk, 217 | Grunge, 218 | HipHop, 219 | Jazz, 220 | Metal, 221 | NewAge, 222 | Oldies, 223 | Other, 224 | Pop, 225 | RAndB, 226 | Rap, 227 | Reggae, 228 | Rock, 229 | Techno, 230 | Industrial, 231 | Alternative, 232 | Ska, 233 | DeathMetal, 234 | Pranks, 235 | Soundtrack, 236 | EuroTechno, 237 | Ambient, 238 | TripHop, 239 | Vocal, 240 | JazzFunk, 241 | Fusion, 242 | Trance, 243 | Classical, 244 | Instrumental, 245 | Acid, 246 | House, 247 | Game, 248 | SoundClip, 249 | Gospel, 250 | Noise, 251 | AlternRock, 252 | Bass, 253 | Soul, 254 | Punk, 255 | Space, 256 | Meditative, 257 | InstrumentalPop, 258 | InstrumentalRock, 259 | Ethnic, 260 | Gothic, 261 | Darkwave, 262 | TechnoIndustrial, 263 | Electronic, 264 | PopFolk, 265 | Eurodance, 266 | Dream, 267 | SouthernRock, 268 | Comedy, 269 | Cult, 270 | Gangsta, 271 | Top40, 272 | ChristianRap, 273 | PopFunk, 274 | Jungle, 275 | NativeAmerican, 276 | Cabaret, 277 | NewWave, 278 | Psychadelic, 279 | Rave, 280 | Showtunes, 281 | Trailer, 282 | LoFi, 283 | Tribal, 284 | AcidPunk, 285 | AcidJazz, 286 | Polka, 287 | Retro, 288 | Musical, 289 | RockAndRoll, 290 | HardRock, 291 | // Extension from here 292 | Folk, 293 | FolkRock, 294 | NationalFolk, 295 | Swing, 296 | FastFusion, 297 | Bebob, 298 | Latin, 299 | Revival, 300 | Celtic, 301 | Bluegrass, 302 | Avantgarde, 303 | GothicRock, 304 | ProgressiveRock, 305 | PsychedelicRock, 306 | SymphonicRock, 307 | SlowRock, 308 | BigBand, 309 | Chorus, 310 | EasyListening, 311 | Acoustic, 312 | Humour, 313 | Speech, 314 | Chanson, 315 | Opera, 316 | ChamberMusic, 317 | Sonata, 318 | Symphony, 319 | BootyBrass, 320 | Primus, 321 | PornGroove, 322 | Satire, 323 | SlowJam, 324 | Club, 325 | Tango, 326 | Samba, 327 | Folklore, 328 | Ballad, 329 | PowerBallad, 330 | RhytmicSoul, 331 | Freestyle, 332 | Duet, 333 | PunkRock, 334 | DrumSolo, 335 | ACapela, 336 | EuroHouse, 337 | DanceHall, 338 | Something(String), 339 | Unknown, 340 | } 341 | 342 | impl Default for Genre { 343 | fn default() -> Genre { 344 | Genre::Unknown 345 | } 346 | } 347 | 348 | impl<'a> From<&'a str> for Genre { 349 | fn from(c: &'a str) -> Genre { 350 | match c.parse::() { 351 | Ok(nb) => Genre::from(nb), 352 | Err(_) => Genre::Something(c.to_owned()), 353 | } 354 | } 355 | } 356 | 357 | impl From for Genre { 358 | fn from(c: u8) -> Genre { 359 | match c { 360 | 0 => Genre::Blues, 361 | 1 => Genre::ClassicRock, 362 | 2 => Genre::Country, 363 | 3 => Genre::Dance, 364 | 4 => Genre::Disco, 365 | 5 => Genre::Funk, 366 | 6 => Genre::Grunge, 367 | 7 => Genre::HipHop, 368 | 8 => Genre::Jazz, 369 | 9 => Genre::Metal, 370 | 10 => Genre::NewAge, 371 | 11 => Genre::Oldies, 372 | 12 => Genre::Other, 373 | 13 => Genre::Pop, 374 | 14 => Genre::RAndB, 375 | 15 => Genre::Rap, 376 | 16 => Genre::Reggae, 377 | 17 => Genre::Rock, 378 | 18 => Genre::Techno, 379 | 19 => Genre::Industrial, 380 | 20 => Genre::Alternative, 381 | 21 => Genre::Ska, 382 | 22 => Genre::DeathMetal, 383 | 23 => Genre::Pranks, 384 | 24 => Genre::Soundtrack, 385 | 25 => Genre::EuroTechno, 386 | 26 => Genre::Ambient, 387 | 27 => Genre::TripHop, 388 | 28 => Genre::Vocal, 389 | 29 => Genre::JazzFunk, 390 | 30 => Genre::Fusion, 391 | 31 => Genre::Trance, 392 | 32 => Genre::Classical, 393 | 33 => Genre::Instrumental, 394 | 34 => Genre::Acid, 395 | 35 => Genre::House, 396 | 36 => Genre::Game, 397 | 37 => Genre::SoundClip, 398 | 38 => Genre::Gospel, 399 | 39 => Genre::Noise, 400 | 40 => Genre::AlternRock, 401 | 41 => Genre::Bass, 402 | 42 => Genre::Soul, 403 | 43 => Genre::Punk, 404 | 44 => Genre::Space, 405 | 45 => Genre::Meditative, 406 | 46 => Genre::InstrumentalPop, 407 | 47 => Genre::InstrumentalRock, 408 | 48 => Genre::Ethnic, 409 | 49 => Genre::Gothic, 410 | 50 => Genre::Darkwave, 411 | 51 => Genre::TechnoIndustrial, 412 | 52 => Genre::Electronic, 413 | 53 => Genre::PopFolk, 414 | 54 => Genre::Eurodance, 415 | 55 => Genre::Dream, 416 | 56 => Genre::SouthernRock, 417 | 57 => Genre::Comedy, 418 | 58 => Genre::Cult, 419 | 59 => Genre::Gangsta, 420 | 60 => Genre::Top40, 421 | 61 => Genre::ChristianRap, 422 | 62 => Genre::PopFunk, 423 | 63 => Genre::Jungle, 424 | 64 => Genre::NativeAmerican, 425 | 65 => Genre::Cabaret, 426 | 66 => Genre::NewWave, 427 | 67 => Genre::Psychadelic, 428 | 68 => Genre::Rave, 429 | 69 => Genre::Showtunes, 430 | 70 => Genre::Trailer, 431 | 71 => Genre::LoFi, 432 | 72 => Genre::Tribal, 433 | 73 => Genre::AcidPunk, 434 | 74 => Genre::AcidJazz, 435 | 75 => Genre::Polka, 436 | 76 => Genre::Retro, 437 | 77 => Genre::Musical, 438 | 78 => Genre::RockAndRoll, 439 | 79 => Genre::HardRock, 440 | 80 => Genre::Folk, 441 | 81 => Genre::FolkRock, 442 | 82 => Genre::NationalFolk, 443 | 83 => Genre::Swing, 444 | 84 => Genre::FastFusion, 445 | 85 => Genre::Bebob, 446 | 86 => Genre::Latin, 447 | 87 => Genre::Revival, 448 | 88 => Genre::Celtic, 449 | 89 => Genre::Bluegrass, 450 | 90 => Genre::Avantgarde, 451 | 91 => Genre::GothicRock, 452 | 92 => Genre::ProgressiveRock, 453 | 93 => Genre::PsychedelicRock, 454 | 94 => Genre::SymphonicRock, 455 | 95 => Genre::SlowRock, 456 | 96 => Genre::BigBand, 457 | 97 => Genre::Chorus, 458 | 98 => Genre::EasyListening, 459 | 99 => Genre::Acoustic, 460 | 100 => Genre::Humour, 461 | 101 => Genre::Speech, 462 | 102 => Genre::Chanson, 463 | 103 => Genre::Opera, 464 | 104 => Genre::ChamberMusic, 465 | 105 => Genre::Sonata, 466 | 106 => Genre::Symphony, 467 | 107 => Genre::BootyBrass, 468 | 108 => Genre::Primus, 469 | 109 => Genre::PornGroove, 470 | 110 => Genre::Satire, 471 | 111 => Genre::SlowJam, 472 | 112 => Genre::Club, 473 | 113 => Genre::Tango, 474 | 114 => Genre::Samba, 475 | 115 => Genre::Folklore, 476 | 116 => Genre::Ballad, 477 | 117 => Genre::PowerBallad, 478 | 118 => Genre::RhytmicSoul, 479 | 119 => Genre::Freestyle, 480 | 120 => Genre::Duet, 481 | 121 => Genre::PunkRock, 482 | 122 => Genre::DrumSolo, 483 | 123 => Genre::ACapela, 484 | 124 => Genre::EuroHouse, 485 | 125 => Genre::DanceHall, 486 | _ => Genre::Unknown, 487 | } 488 | } 489 | } 490 | 491 | impl fmt::Display for Genre { 492 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 493 | match self { 494 | Self::Something(s) => write!(f, "{}", s), 495 | _ => fmt::Debug::fmt(self, f), 496 | } 497 | } 498 | } 499 | 500 | #[cfg(test)] 501 | mod tests { 502 | use super::*; 503 | #[test] 504 | fn fmt_genre() { 505 | assert_eq!(Genre::Club.to_string(), "Club"); 506 | assert_eq!(Genre::Something("Foo".to_string()).to_string(), "Foo"); 507 | } 508 | } 509 | -------------------------------------------------------------------------------- /src/types.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use crate::enums::{ChannelType, Copyright, Emphasis, Genre, Layer, Status, Version, CRC}; 4 | 5 | #[derive(Debug, Default, Eq, PartialEq)] 6 | pub struct Frame { 7 | pub size: u32, 8 | pub version: Version, 9 | pub layer: Layer, 10 | pub crc: CRC, 11 | pub bitrate: u16, 12 | pub sampling_freq: u16, 13 | pub padding: bool, 14 | pub private_bit: bool, 15 | pub chan_type: ChannelType, 16 | pub intensity_stereo: bool, 17 | pub ms_stereo: bool, 18 | pub copyright: Copyright, 19 | pub status: Status, 20 | pub emphasis: Emphasis, 21 | pub duration: Option, 22 | pub position: Duration, 23 | pub offset: u32, 24 | } 25 | 26 | #[derive(Debug, Eq, PartialEq)] 27 | pub struct MP3Metadata { 28 | pub duration: Duration, 29 | pub frames: Vec, 30 | pub tag: Option, 31 | pub optional_info: Vec, 32 | } 33 | 34 | #[derive(Debug, Default, Eq, PartialEq)] 35 | pub struct AudioTag { 36 | pub title: String, 37 | pub artist: String, 38 | pub album: String, 39 | pub year: u16, 40 | pub comment: String, 41 | pub genre: Genre, 42 | } 43 | 44 | #[derive(Debug, Default, Eq, PartialEq)] 45 | pub struct Url(pub String); 46 | 47 | // TODO: Add picture support 48 | /// id3.org/id3v2.3.0#Declared_ID3v2_frames#Text_information_frames_-_details 49 | #[derive(Debug, Default, Eq, PartialEq)] 50 | pub struct OptionalAudioTags { 51 | /// Corresponds to the nth frames `MP3Metadata.frame`. 52 | pub position: u32, 53 | pub major_version: u8, 54 | pub minor_version: u8, 55 | /// The 'Album/Movie/Show title' frame is intended for the title of the 56 | /// recording(/source of sound) which the audio in the file is taken from. 57 | pub album_movie_show: Option, 58 | /// The 'BPM' frame contains the number of beats per minute in the mainpart 59 | /// of the audio. The BPM is an integer and represented as a numerical string. 60 | pub bpm: Option, 61 | /// The 'Composer(s)' frame is intended for the name of the composer(s). 62 | pub composers: Vec, 63 | /// The 'Content type', which previously was stored as a one byte numeric value 64 | /// only, is now a numeric string. You may use one or several of the types as 65 | /// ID3v1.1 did or, since the category list would be impossible to maintain with 66 | /// accurate and up to date categories, define your own. 67 | /// 68 | /// References to the ID3v1 genres can be made by, as first byte, enter "(" 69 | /// followed by a number from the genres list (appendix A) and ended with a ")" 70 | /// character. This is optionally followed by a refinement, e.g. "(21)" or 71 | /// "(4)Eurodisco". Several references can be made in the same frame, e.g. 72 | /// "(51)(39)". If the refinement should begin with a "(" character it should be 73 | /// replaced with "((", e.g. "((I can figure out any genre)" or "(55)((I think...)". 74 | /// The following new content types is defined in ID3v2 and is implemented in the 75 | /// same way as the numerig content types, e.g. "(RX)". 76 | pub content_type: Vec, 77 | /// The 'Copyright message' frame, which must begin with a year and a space character 78 | /// (making five characters), is intended for the copyright holder of the original 79 | /// sound, not the audio file itself. The absence of this frame means only that the 80 | /// copyright information is unavailable or has been removed, and must not be 81 | /// interpreted to mean that the sound is public domain. Every time this field is 82 | /// displayed the field must be preceded with "Copyright © ". 83 | pub copyright: Option, 84 | /// The 'Date' frame is a numeric string in the DDMM format containing the date for 85 | /// the recording. This field is always four characters long. 86 | pub date: Option, 87 | /// The 'Playlist delay' defines the numbers of milliseconds of silence between 88 | /// every song in a playlist. The player should use the "ETC" frame, if present, 89 | /// to skip initial silence and silence at the end of the audio to match the 90 | /// 'Playlist delay' time. The time is represented as a numeric string. 91 | pub playlist_delay: Option, 92 | /// The 'Encoded by' frame contains the name of the person or organisation that 93 | /// encoded the audio file. This field may contain a copyright message, if the 94 | /// audio file also is copyrighted by the encoder. 95 | pub encoded_by: Option, 96 | /// The 'Lyricist(s)/Text writer(s)' frame is intended for the writer(s) of the text 97 | /// or lyrics in the recording. 98 | pub text_writers: Vec, 99 | /// The 'File type' frame indicates which type of audio this tag defines. The 100 | /// following type and refinements are defined: 101 | /// 102 | /// * MPG MPEG Audio 103 | /// * /1 MPEG 1/2 layer I 104 | /// * /2 MPEG 1/2 layer II 105 | /// * /3 MPEG 1/2 layer III 106 | /// * /2.5 MPEG 2.5 107 | /// * /AAC Advanced audio compression 108 | /// * VQF Transform-domain Weighted Interleave Vector Quantization 109 | /// * PCM Pulse Code Modulated audio 110 | /// 111 | /// but other types may be used, not for these types though. This is used in a 112 | /// similar way to the predefined types in the "TMED" frame, but without 113 | /// parentheses. If this frame is not present audio type is assumed to be "MPG". 114 | pub file_type: Option, 115 | /// The 'Time' frame is a numeric string in the HHMM format containing the time 116 | /// for the recording. This field is always four characters long. 117 | pub time: Option, 118 | /// The 'Content group description' frame is used if the sound belongs to a larger 119 | /// category of sounds/music. For example, classical music is often sorted in 120 | /// different musical sections (e.g. "Piano Concerto", "Weather - Hurricane"). 121 | pub content_group_description: Option, 122 | /// The 'Subtitle/Description refinement' frame is used for information directly 123 | /// related to the contents title (e.g. "Op. 16" or "Performed live at Wembley"). 124 | pub subtitle_refinement_description: Option, 125 | /// The 'Title/Songname/Content description' frame is the actual name of the 126 | /// piece (e.g. "Adagio", "Hurricane Donna"). 127 | pub title: Option, 128 | /// The 'Initial key' frame contains the musical key in which the sound starts. It is 129 | /// represented as a string with a maximum length of three characters. The ground 130 | /// keys are represented with "A","B","C","D","E", "F" and "G" and halfkeys 131 | /// represented with "b" and "#". Minor is represented as "m". Example "Cbm". Off 132 | /// key is represented with an "o" only. 133 | pub initial_key: Option, 134 | /// The 'Language(s)' frame should contain the languages of the text or lyrics spoken 135 | /// or sung in the audio. The language is represented with three characters according 136 | /// to ISO-639-2. If more than one language is used in the text their language codes 137 | /// should follow according to their usage. 138 | pub language: Option, 139 | /// The 'Length' frame contains the length of the audiofile in milliseconds, 140 | /// represented as a numeric string. 141 | pub length: Option, 142 | /// The 'Media type' frame describes from which media the sound originated. This may 143 | /// be a text string or a reference to the predefined media types found in the list 144 | /// below. References are made within "(" and ")" and are optionally followed by a 145 | /// text refinement, e.g. "(MC) with four channels". If a text refinement should 146 | /// begin with a "(" character it should be replaced with "((" in the same way as in 147 | /// the "TCO" frame. Predefined refinements is appended after the media type, e.g. 148 | /// "(CD/A)" or "(VID/PAL/VHS)". 149 | /// 150 | /// DIG Other digital media 151 | /// /A Analog transfer from media 152 | /// /// 153 | /// ANA Other analog media 154 | /// /WAC Wax cylinder 155 | /// /8CA 8-track tape cassette 156 | /// 157 | /// CD CD 158 | /// /A Analog transfer from media 159 | /// /DD DDD 160 | /// /AD ADD 161 | /// /AA AAD 162 | /// 163 | /// LD Laserdisc 164 | /// /A Analog transfer from media 165 | /// 166 | /// TT Turntable records 167 | /// /33 33.33 rpm 168 | /// /45 45 rpm 169 | /// /71 71.29 rpm 170 | /// /76 76.59 rpm 171 | /// /78 78.26 rpm 172 | /// /80 80 rpm 173 | /// 174 | /// MD MiniDisc 175 | /// /A Analog transfer from media 176 | /// 177 | /// DAT DAT 178 | /// /A Analog transfer from media 179 | /// /1 standard, 48 kHz/16 bits, linear 180 | /// /2 mode 2, 32 kHz/16 bits, linear 181 | /// /3 mode 3, 32 kHz/12 bits, nonlinear, low speed 182 | /// /4 mode 4, 32 kHz/12 bits, 4 channels 183 | /// /5 mode 5, 44.1 kHz/16 bits, linear 184 | /// /6 mode 6, 44.1 kHz/16 bits, 'wide track' play 185 | /// 186 | /// DCC DCC 187 | /// /A Analog transfer from media 188 | /// 189 | /// DVD DVD 190 | /// /A Analog transfer from media 191 | /// 192 | /// TV Television 193 | /// /PAL PAL 194 | /// /NTSC NTSC 195 | /// /SECAM SECAM 196 | /// 197 | /// VID Video 198 | /// /PAL PAL 199 | /// /NTSC NTSC 200 | /// /SECAM SECAM 201 | /// /VHS VHS 202 | /// /SVHS S-VHS 203 | /// /BETA BETAMAX 204 | /// 205 | /// RAD Radio 206 | /// /FM FM 207 | /// /AM AM 208 | /// /LW LW 209 | /// /MW MW 210 | /// 211 | /// TEL Telephone 212 | /// /I ISDN 213 | /// 214 | /// MC MC (normal cassette) 215 | /// /4 4.75 cm/s (normal speed for a two sided cassette) 216 | /// /9 9.5 cm/s 217 | /// /I Type I cassette (ferric/normal) 218 | /// /II Type II cassette (chrome) 219 | /// /III Type III cassette (ferric chrome) 220 | /// /IV Type IV cassette (metal) 221 | /// 222 | /// REE Reel 223 | /// /9 9.5 cm/s 224 | /// /19 19 cm/s 225 | /// /38 38 cm/s 226 | /// /76 76 cm/s 227 | /// /I Type I cassette (ferric/normal) 228 | /// /II Type II cassette (chrome) 229 | /// /III Type III cassette (ferric chrome) 230 | /// /IV Type IV cassette (metal) 231 | pub media_type: Option, 232 | /// The 'Original album/movie/show title' frame is intended for the title of the 233 | /// original recording (or source of sound), if for example the music in the file 234 | /// should be a cover of a previously released song. 235 | pub original_album_move_show_title: Option, 236 | /// The 'Original filename' frame contains the preferred filename for the file, 237 | /// since some media doesn't allow the desired length of the filename. The 238 | /// filename is case sensitive and includes its suffix. 239 | pub original_filename: Option, 240 | /// The 'Original lyricist(s)/text writer(s)' frame is intended for the text 241 | /// writer(s) of the original recording, if for example the music in the file should 242 | /// be a cover of a previously released song. 243 | pub original_text_writers: Vec, 244 | /// The 'Original artist(s)/performer(s)' frame is intended for the performer(s) of 245 | /// the original recording, if for example the music in the file should be a cover 246 | /// of a previously released song. 247 | pub original_artists: Vec, 248 | /// The 'Original release year' frame is intended for the year when the original 249 | /// recording, if for example the music in the file should be a cover of a 250 | /// previously released song, was released. The field is formatted as in the 251 | /// `year` field. 252 | pub original_release_year: Option, 253 | /// The 'File owner/licensee' frame contains the name of the owner or licensee 254 | /// of the file and it's contents. 255 | pub file_owner: Option, 256 | /// The 'Lead artist(s)/Lead performer(s)/Soloist(s)/Performing group' is used 257 | /// for the main artist(s). 258 | pub performers: Vec, 259 | /// The 'Band/Orchestra/Accompaniment' frame is used for additional information 260 | /// about the performers in the recording. 261 | pub band: Option, 262 | /// The 'Conductor' frame is used for the name of the conductor. 263 | pub conductor: Option, 264 | /// The 'Interpreted, remixed, or otherwise modified by' frame contains more 265 | /// information about the people behind a remix and similar interpretations of 266 | /// another existing piece. 267 | pub interpreted: Option, 268 | /// The 'Part of a set' frame is a numeric string that describes which part of a 269 | /// set the audio came from. This frame is used if the source described in the 270 | /// "TALB" frame is divided into several mediums, e.g. a double CD. The value may 271 | /// be extended with a "/" character and a numeric string containing the total 272 | /// number of parts in the set. E.g. "1/2". 273 | pub part_of_a_set: Option, 274 | /// The 'Publisher' frame simply contains the name of the label or publisher. 275 | pub publisher: Option, 276 | /// The 'Track number/Position in set' frame is a numeric string containing the 277 | /// order number of the audio-file on its original recording. This may be extended 278 | /// with a "/" character and a numeric string containing the total numer of 279 | /// tracks/elements on the original recording. E.g. "4/9". 280 | pub track_number: Option, 281 | /// The 'Recording dates' frame is a intended to be used as complement to the 282 | /// "TYER", "TDAT" and "TIME" frames. E.g. "4th-7th June, 12th June" in 283 | /// combination with the "TYER" frame. 284 | pub recording_dates: Option, 285 | /// The 'Internet radio station name' frame contains the name of the internet 286 | /// radio station from which the audio is streamed. 287 | pub internet_radio_station_name: Option, 288 | /// The 'Internet radio station owner' frame contains the name of the owner of 289 | /// the internet radio station from which the audio is streamed. 290 | pub internet_radio_station_owner: Option, 291 | /// The 'Size' frame contains the size of the audiofile in bytes, excluding the 292 | /// ID3v2 tag, represented as a numeric string. 293 | pub size: Option, 294 | /// The 'ISRC' frame should contain the International Standard Recording Code 295 | /// (ISRC) (12 characters). 296 | pub international_standard_recording_code: Option, 297 | /// The 'Software/Hardware and settings used for encoding' frame includes the 298 | /// used audio encoder and its settings when the file was encoded. Hardware 299 | /// refers to hardware encoders, not the computer on which a program was run. 300 | pub soft_hard_setting: Option, 301 | /// The 'Year' frame is a numeric string with a year of the recording. This 302 | /// frames is always four characters long (until the year 10000). 303 | pub year: Option, 304 | /// Since there might be a lot of people contributing to an audio file in 305 | /// various ways, such as musicians and technicians, the 'Text information 306 | /// frames' are often insufficient to list everyone involved in a project. 307 | /// The 'Involved people list' is a frame containing the names of those 308 | /// involved, and how they were involved. The body simply contains a terminated 309 | /// string with the involvement directly followed by a terminated string with 310 | /// the involvee followed by a new involvement and so on. 311 | pub involved_people: Option, 312 | 313 | /// The 'Commercial information' frame is a URL pointing at a webpage with 314 | /// information such as where the album can be bought. There may be more than 315 | /// one "WCOM" frame in a tag, but not with the same content. 316 | pub commercial_info_url: Vec, 317 | /// The 'Copyright/Legal information' frame is a URL pointing at a webpage 318 | /// where the terms of use and ownership of the file is described. 319 | pub copyright_info_url: Option, 320 | /// The 'Official audio file webpage' frame is a URL pointing at a file specific 321 | /// webpage. 322 | pub official_webpage: Option, 323 | /// The 'Official artist/performer webpage' frame is a URL pointing at the 324 | /// artists official webpage. There may be more than one "WOAR" frame in a tag 325 | /// if the audio contains more than one performer, but not with the same content. 326 | pub official_artist_webpage: Vec, 327 | /// The 'Official audio source webpage' frame is a URL pointing at the official 328 | /// webpage for the source of the audio file, e.g. a movie. 329 | pub official_audio_source_webpage: Option, 330 | /// The 'Official internet radio station homepage' contains a URL pointing at the 331 | /// homepage of the internet radio station. 332 | pub official_internet_radio_webpage: Option, 333 | /// The 'Payment' frame is a URL pointing at a webpage that will handle the 334 | /// process of paying for this file. 335 | pub payment_url: Option, 336 | /// The 'Publishers official webpage' frame is a URL pointing at the official 337 | /// wepage for the publisher. 338 | pub publishers_official_webpage: Option, 339 | } 340 | -------------------------------------------------------------------------------- /src/metadata.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::io::Read; 3 | use std::path::Path; 4 | use std::time::Duration; 5 | 6 | use crate::consts::{BITRATES, SAMPLING_FREQ}; 7 | use crate::enums::{ChannelType, Copyright, Emphasis, Error, Genre, Layer, Status, Version, CRC}; 8 | use crate::types::{AudioTag, Frame, MP3Metadata, OptionalAudioTags}; 9 | use crate::utils::{ 10 | compute_duration, create_utf8_str, get_line, get_samp_line, get_text_field, get_text_fields, 11 | }; 12 | use crate::utils::{get_url_field, get_url_fields}; 13 | 14 | fn get_id3(i: &mut u32, buf: &[u8], meta: &mut MP3Metadata) -> Result<(), Error> { 15 | let mut x = *i as usize; 16 | // Get extended information 17 | if buf.len() > 32 && x + 32 < buf.len() && &buf[x..x + 8] == b"APETAGEX" { 18 | // APE 19 | *i += 31; // skip APE header / footer 20 | Ok(()) 21 | } else if buf.len() > 127 && x + 127 < buf.len() && &buf[x..x + 3] == b"TAG" { 22 | // V1 23 | if meta.tag.is_some() { 24 | return Err(Error::DuplicatedIDV3); 25 | } 26 | if let Some(last) = meta.frames.last_mut() { 27 | if *i <= last.size { 28 | return Ok(()); 29 | } 30 | last.size = *i - last.size - 1; 31 | } 32 | *i += 126; 33 | // tag v1 34 | meta.tag = Some(AudioTag { 35 | title: create_utf8_str(&buf[x + 3..][..30]), 36 | artist: create_utf8_str(&buf[x + 33..][..30]), 37 | album: create_utf8_str(&buf[x + 63..][..30]), 38 | year: create_utf8_str(&buf[x + 93..][..4]) 39 | .parse::() 40 | .unwrap_or(0), 41 | comment: create_utf8_str(&buf[x + 97..][..if buf[x + 97 + 28] != 0 { 30 } else { 28 }]), 42 | genre: Genre::from(buf[x + 127]), 43 | }); 44 | Ok(()) 45 | } else if buf.len() > x + 13 && &buf[x..x + 3] == b"ID3" { 46 | // V2 and above 47 | let maj_version = buf[x + 3]; 48 | let min_version = buf[x + 4]; 49 | 50 | if maj_version > 4 { 51 | return Ok(()); 52 | } 53 | 54 | let tag_size = ((buf[x + 9] as usize) & 0xFF) 55 | | (((buf[x + 8] as usize) & 0xFF) << 7) 56 | | (((buf[x + 7] as usize) & 0xFF) << 14) 57 | | ((((buf[x + 6] as usize) & 0xFF) << 21) + 10); 58 | let use_sync = buf[x + 5] & 0x80 != 0; 59 | let has_extended_header = buf[x + 5] & 0x40 != 0; 60 | 61 | x += 10; 62 | 63 | if has_extended_header { 64 | if x + 4 >= buf.len() { 65 | *i = x as u32; 66 | return Ok(()); 67 | } 68 | let header_size = ((buf[x] as u32) << 21) 69 | | ((buf[x + 1] as u32) << 14) 70 | | ((buf[x + 2] as u32) << 7) 71 | | buf[x + 3] as u32; 72 | if header_size < 4 { 73 | return Ok(()); 74 | } 75 | x += header_size as usize - 4; 76 | } 77 | 78 | *i = x as u32 + tag_size as u32; 79 | if x + tag_size >= buf.len() { 80 | return Ok(()); 81 | } 82 | 83 | // Recreate the tag if desynchronization is used inside; we need to replace 84 | // 0xFF 0x00 with 0xFF 85 | let mut v = Vec::new(); 86 | let (buf, length) = if use_sync { 87 | let mut new_pos = 0; 88 | let mut skip = false; 89 | v.reserve(tag_size); 90 | 91 | for i in 0..tag_size { 92 | if skip { 93 | skip = false; 94 | continue; 95 | } 96 | if i + 1 >= buf.len() { 97 | return Ok(()); 98 | } 99 | if i + 1 < tag_size && buf[i] == 0xFF && buf[i + 1] == 0 { 100 | if let Some(elem) = v.get_mut(new_pos) { 101 | *elem = 0xFF; 102 | } else { 103 | return Err(Error::InvalidData); 104 | } 105 | new_pos += 1; 106 | skip = true; 107 | continue; 108 | } 109 | if new_pos >= v.len() { 110 | return Ok(()); 111 | } 112 | v[new_pos] = buf[i]; 113 | new_pos += 1; 114 | } 115 | (v.as_slice(), new_pos) 116 | } else { 117 | (buf, tag_size) 118 | }; 119 | 120 | let mut pos = x; 121 | let id3_frame_size = if maj_version < 3 { 6 } else { 10 }; 122 | let mut op = OptionalAudioTags::default(); 123 | let mut changes = false; 124 | loop { 125 | if pos + id3_frame_size > x + length { 126 | break; 127 | } 128 | 129 | // Check if there is there a frame. 130 | let c = buf[pos]; 131 | #[allow(clippy::manual_range_contains)] 132 | if c < b'A' || c > b'Z' { 133 | break; 134 | } 135 | 136 | // Frame name is 3 chars in pre-ID3v3 and 4 chars after 137 | let (frame_name, frame_size) = if maj_version < 3 { 138 | ( 139 | &buf[pos..pos + 3], 140 | (buf[pos + 5] as u32 & 0xFF) 141 | | ((buf[pos + 4] as u32 & 0xFF) << 8) 142 | | ((buf[pos + 3] as u32 & 0xFF) << 16), 143 | ) 144 | } else if maj_version < 4 { 145 | ( 146 | &buf[pos..pos + 4], 147 | (buf[pos + 7] as u32 & 0xFF) 148 | | ((buf[pos + 6] as u32 & 0xFF) << 8) 149 | | ((buf[pos + 5] as u32 & 0xFF) << 16) 150 | | ((buf[pos + 4] as u32 & 0xFF) << 24), 151 | ) 152 | } else { 153 | ( 154 | &buf[pos..pos + 4], 155 | (buf[pos + 7] as u32 & 0xFF) 156 | | ((buf[pos + 6] as u32 & 0xFF) << 7) 157 | | ((buf[pos + 5] as u32 & 0xFF) << 14) 158 | | ((buf[pos + 4] as u32 & 0xFF) << 21), 159 | ) 160 | }; 161 | 162 | pos += id3_frame_size; 163 | if pos + frame_size as usize > x + length { 164 | break; 165 | } 166 | 167 | // http://id3.org/id3v2.3.0#Declared_ID3v2_frames 168 | match frame_name { 169 | // ----------------------- 170 | // ----- TEXT FRAMES ----- 171 | // ----------------------- 172 | b"TALB" => { 173 | get_text_field(buf, pos, frame_size, &mut changes, &mut op.album_movie_show) 174 | } 175 | b"TBPM" => get_text_field(buf, pos, frame_size, &mut changes, &mut op.bpm), 176 | b"TCOM" => get_text_fields(buf, pos, frame_size, &mut changes, &mut op.composers), 177 | b"TCON" => { 178 | let mut s = None; 179 | get_text_field(buf, pos, frame_size, &mut changes, &mut s); 180 | if let Some(s) = s { 181 | if !s.is_empty() { 182 | if s.starts_with('(') && s.ends_with(')') { 183 | let v = s 184 | .split(')') 185 | .collect::>() 186 | .into_iter() 187 | .filter_map(|a| match a.replace('(', "").parse::() { 188 | Ok(num) => Some(Genre::from(num)), 189 | _ => None, 190 | }) 191 | .collect::>(); 192 | if !v.is_empty() { 193 | for entry in v { 194 | op.content_type.push(entry); 195 | } 196 | } else { 197 | op.content_type.push(Genre::from(s.as_str())); 198 | } 199 | } else { 200 | op.content_type.push(Genre::from(s.as_str())); 201 | } 202 | } 203 | } 204 | } 205 | b"TCOP" => get_text_field(buf, pos, frame_size, &mut changes, &mut op.copyright), 206 | b"TDAT" => get_text_field(buf, pos, frame_size, &mut changes, &mut op.date), 207 | b"TDLY" => { 208 | get_text_field(buf, pos, frame_size, &mut changes, &mut op.playlist_delay) 209 | } 210 | b"TENC" => get_text_field(buf, pos, frame_size, &mut changes, &mut op.encoded_by), 211 | b"TEXT" => { 212 | get_text_fields(buf, pos, frame_size, &mut changes, &mut op.text_writers) 213 | } 214 | b"TFLT" => get_text_field(buf, pos, frame_size, &mut changes, &mut op.file_type), 215 | b"TIME" => get_text_field(buf, pos, frame_size, &mut changes, &mut op.time), 216 | b"TIT" | b"TIT2" => { 217 | get_text_field(buf, pos, frame_size, &mut changes, &mut op.title) 218 | } 219 | b"TIT1" => get_text_field( 220 | buf, 221 | pos, 222 | frame_size, 223 | &mut changes, 224 | &mut op.content_group_description, 225 | ), 226 | b"TIT3" => get_text_field( 227 | buf, 228 | pos, 229 | frame_size, 230 | &mut changes, 231 | &mut op.subtitle_refinement_description, 232 | ), 233 | b"TKEY" => get_text_field(buf, pos, frame_size, &mut changes, &mut op.initial_key), 234 | b"TLAN" => get_text_field(buf, pos, frame_size, &mut changes, &mut op.language), 235 | b"TLEN" => get_text_field(buf, pos, frame_size, &mut changes, &mut op.length), 236 | b"TMED" => get_text_field(buf, pos, frame_size, &mut changes, &mut op.media_type), 237 | b"TOAL" => get_text_field( 238 | buf, 239 | pos, 240 | frame_size, 241 | &mut changes, 242 | &mut op.original_album_move_show_title, 243 | ), 244 | b"TOFN" => get_text_field( 245 | buf, 246 | pos, 247 | frame_size, 248 | &mut changes, 249 | &mut op.original_filename, 250 | ), 251 | b"TOLY" => get_text_fields( 252 | buf, 253 | pos, 254 | frame_size, 255 | &mut changes, 256 | &mut op.original_text_writers, 257 | ), 258 | b"TOPE" => { 259 | get_text_fields(buf, pos, frame_size, &mut changes, &mut op.original_artists) 260 | } 261 | b"TORY" => get_text_field( 262 | buf, 263 | pos, 264 | frame_size, 265 | &mut changes, 266 | &mut op.original_release_year, 267 | ), 268 | b"TOWN" => get_text_field(buf, pos, frame_size, &mut changes, &mut op.file_owner), 269 | b"TPE1" => get_text_fields(buf, pos, frame_size, &mut changes, &mut op.performers), 270 | b"TPE2" => get_text_field(buf, pos, frame_size, &mut changes, &mut op.band), 271 | b"TPE3" => get_text_field(buf, pos, frame_size, &mut changes, &mut op.conductor), 272 | b"TPE4" => get_text_field(buf, pos, frame_size, &mut changes, &mut op.interpreted), 273 | b"TPOS" => { 274 | get_text_field(buf, pos, frame_size, &mut changes, &mut op.part_of_a_set) 275 | } 276 | b"TPUB" => get_text_field(buf, pos, frame_size, &mut changes, &mut op.publisher), 277 | b"TRCK" => get_text_field(buf, pos, frame_size, &mut changes, &mut op.track_number), 278 | b"TRDA" => { 279 | get_text_field(buf, pos, frame_size, &mut changes, &mut op.recording_dates) 280 | } 281 | b"TRSN" => get_text_field( 282 | buf, 283 | pos, 284 | frame_size, 285 | &mut changes, 286 | &mut op.internet_radio_station_name, 287 | ), 288 | b"TRSO" => get_text_field( 289 | buf, 290 | pos, 291 | frame_size, 292 | &mut changes, 293 | &mut op.internet_radio_station_owner, 294 | ), 295 | b"TSIZ" => get_text_field(buf, pos, frame_size, &mut changes, &mut op.size), 296 | b"TSRC" => get_text_field( 297 | buf, 298 | pos, 299 | frame_size, 300 | &mut changes, 301 | &mut op.international_standard_recording_code, 302 | ), 303 | b"TSSE" => get_text_field( 304 | buf, 305 | pos, 306 | frame_size, 307 | &mut changes, 308 | &mut op.soft_hard_setting, 309 | ), 310 | b"TYER" => get_text_field(buf, pos, frame_size, &mut changes, &mut op.year), 311 | b"IPLS" => { 312 | get_text_field(buf, pos, frame_size, &mut changes, &mut op.involved_people) 313 | } 314 | // ---------------------- 315 | // ----- URL FRAMES ----- 316 | // ---------------------- 317 | b"WCOM" => get_url_fields( 318 | buf, 319 | pos, 320 | frame_size, 321 | &mut changes, 322 | &mut op.commercial_info_url, 323 | ), 324 | b"WCOP" => get_url_field( 325 | buf, 326 | pos, 327 | frame_size, 328 | &mut changes, 329 | &mut op.copyright_info_url, 330 | ), 331 | b"WOAF" => { 332 | get_url_field(buf, pos, frame_size, &mut changes, &mut op.official_webpage) 333 | } 334 | b"WOAR" => get_url_fields( 335 | buf, 336 | pos, 337 | frame_size, 338 | &mut changes, 339 | &mut op.official_artist_webpage, 340 | ), 341 | b"WOAS" => get_url_field( 342 | buf, 343 | pos, 344 | frame_size, 345 | &mut changes, 346 | &mut op.official_audio_source_webpage, 347 | ), 348 | b"WORS" => get_url_field( 349 | buf, 350 | pos, 351 | frame_size, 352 | &mut changes, 353 | &mut op.official_internet_radio_webpage, 354 | ), 355 | b"WPAY" => get_url_field(buf, pos, frame_size, &mut changes, &mut op.payment_url), 356 | b"WPUB" => get_url_field( 357 | buf, 358 | pos, 359 | frame_size, 360 | &mut changes, 361 | &mut op.publishers_official_webpage, 362 | ), 363 | _ => { 364 | // TODO: handle other type of fields, like picture 365 | } 366 | }; 367 | 368 | pos += frame_size as usize; 369 | } 370 | if changes { 371 | op.position = meta.frames.len() as u32; 372 | op.minor_version = min_version; 373 | op.major_version = maj_version; 374 | meta.optional_info.push(op); 375 | } 376 | Ok(()) 377 | } else { 378 | Ok(()) 379 | } 380 | } 381 | 382 | fn read_header(buf: &[u8], i: &mut u32, meta: &mut MP3Metadata) -> Result { 383 | let header = ((buf[*i as usize] as u32) << 24) 384 | | ((buf[*i as usize + 1] as u32) << 16) 385 | | ((buf[*i as usize + 2] as u32) << 8) 386 | | (buf[*i as usize + 3] as u32); 387 | if header & 0xffe00000 == 0xffe00000 388 | && header & (3 << 17) != 0 389 | && header & (0xf << 12) != (0xf << 12) 390 | && header & (3 << 10) != (3 << 10) 391 | { 392 | let mut frame: Frame = Default::default(); 393 | 394 | frame.version = Version::from((header >> 19) & 3); 395 | frame.layer = Layer::from((header >> 17) & 3); 396 | frame.crc = CRC::from((header >> 16) & 1); 397 | 398 | frame.bitrate = 399 | BITRATES[get_line(frame.version, frame.layer)][((header >> 12) & 0xF) as usize]; 400 | frame.sampling_freq = 401 | SAMPLING_FREQ[get_samp_line(frame.version)][((header >> 10) & 0x3) as usize]; 402 | frame.padding = (header >> 9) & 1 == 1; 403 | frame.private_bit = (header >> 8) & 1 == 1; 404 | 405 | frame.chan_type = ChannelType::from((header >> 6) & 3); 406 | let (intensity, ms_stereo) = match (header >> 4) & 3 { 407 | 0x1 => (true, false), 408 | 0x2 => (false, true), 409 | 0x3 => (true, true), 410 | /*0x00*/ _ => (false, false), 411 | }; 412 | frame.intensity_stereo = intensity; 413 | frame.ms_stereo = ms_stereo; 414 | frame.copyright = Copyright::from((header >> 3) & 1); 415 | frame.status = Status::from((header >> 2) & 1); 416 | frame.emphasis = Emphasis::from(header & 0x03); 417 | frame.duration = compute_duration(frame.version, frame.layer, frame.sampling_freq); 418 | frame.position = meta.duration; 419 | frame.offset = *i; 420 | 421 | if let Some(dur) = frame.duration { 422 | meta.duration += dur; 423 | } 424 | /*frame.size = if frame.layer == Layer::Layer1 && frame.sampling_freq > 0 { 425 | /*println!("{:4}: (12000 * {} / {} + {}) * 4 = {}", i, frame.bitrate as u64, frame.sampling_freq as u64, 426 | if frame.slot { 1 } else { 0 }, 427 | (12000 * frame.bitrate as u64 / frame.sampling_freq as u64 + 428 | if frame.slot { 1 } else { 0 }) * 4);*/ 429 | 430 | (12000 * frame.bitrate as u64 / frame.sampling_freq as u64 + 431 | if frame.slot { 1 } else { 0 }) * 4 432 | } else if (frame.layer == Layer::Layer2 || frame.layer == Layer::Layer3) && frame.sampling_freq > 0 { 433 | /*println!("{:4}: 144000 * {} / {} + {} = {}", i, frame.bitrate as u64, frame.sampling_freq as u64, 434 | if frame.slot { 1 } else { 0 }, 435 | 144000 * frame.bitrate as u64 / frame.sampling_freq as u64 + 436 | if frame.slot { 1 } else { 0 });*/ 437 | 438 | 144000 * frame.bitrate as u64 / frame.sampling_freq as u64 + 439 | if frame.slot { 1 } else { 0 } 440 | } else { 441 | continue 'a; 442 | } as u32;*/ 443 | let samples_per_frame = match frame.layer { 444 | Layer::Layer3 => { 445 | if frame.version == Version::MPEG1 { 446 | 1152 447 | } else { 448 | 576 449 | } 450 | } 451 | Layer::Layer2 => 1152, 452 | Layer::Layer1 => 384, 453 | _ => unreachable!(), 454 | }; 455 | frame.size = (samples_per_frame as u64 / 8 * frame.bitrate as u64 * 1000 456 | / frame.sampling_freq as u64) as u32; 457 | if frame.size < 1 { 458 | return Ok(false); 459 | } 460 | if frame.padding { 461 | frame.size += 1; 462 | } 463 | *i += frame.size; 464 | meta.frames.push(frame); 465 | Ok(true) 466 | } else { 467 | Ok(false) 468 | } 469 | } 470 | 471 | pub fn read_from_file

(file: P) -> Result 472 | where 473 | P: AsRef, 474 | { 475 | if let Ok(mut fd) = File::open(file) { 476 | let mut buf = Vec::new(); 477 | 478 | match fd.read_to_end(&mut buf) { 479 | Ok(_) => read_from_slice(&buf), 480 | Err(_) => Err(Error::FileError), 481 | } 482 | } else { 483 | Err(Error::FileError) 484 | } 485 | } 486 | 487 | pub fn read_from_slice(buf: &[u8]) -> Result { 488 | let mut meta = MP3Metadata { 489 | frames: Vec::new(), 490 | duration: Duration::new(0, 0), 491 | tag: None, 492 | optional_info: Vec::new(), 493 | }; 494 | let mut i = 0u32; 495 | 496 | 'a: while i < buf.len() as u32 { 497 | loop { 498 | get_id3(&mut i, buf, &mut meta)?; 499 | if i + 3 >= buf.len() as u32 { 500 | break 'a; 501 | } 502 | match read_header(buf, &mut i, &mut meta) { 503 | Ok(true) => continue 'a, 504 | Err(e) => return Err(e), 505 | _ => {} 506 | } 507 | let old_i = i; 508 | get_id3(&mut i, buf, &mut meta)?; 509 | if i == old_i { 510 | i += 1; 511 | } 512 | if i >= buf.len() as u32 { 513 | break 'a; 514 | } 515 | } 516 | } 517 | if meta.tag.is_none() { 518 | if let Some(last) = meta.frames.last_mut() { 519 | if i <= last.size { 520 | return Err(Error::InvalidData); 521 | } 522 | } 523 | } 524 | if meta.frames.is_empty() { 525 | Err(Error::NotMP3) 526 | } else { 527 | Ok(meta) 528 | } 529 | } 530 | 531 | #[cfg(test)] 532 | mod tests { 533 | use super::*; 534 | 535 | #[test] 536 | fn not_mp3() { 537 | let ret = read_from_file("src/lib.rs"); 538 | 539 | match ret { 540 | Ok(_) => panic!("Wasn't supposed to be ok!"), 541 | Err(e) => assert_eq!(e, Error::NotMP3), 542 | } 543 | } 544 | 545 | #[test] 546 | fn double_id() { 547 | let ret = read_from_file("assets/double_id.mp3"); 548 | 549 | match ret { 550 | Ok(_) => panic!("Wasn't supposed to be ok!"), 551 | Err(e) => assert_eq!(e, Error::DuplicatedIDV3), 552 | } 553 | } 554 | 555 | #[test] 556 | fn wrong_data() { 557 | let data = [ 558 | 255, 0, 0, 16, 0, 12, 0, 5, 43, 51, 61, 61, 90, 0, 0, 50, 5, 255, 239, 32, 61, 61, 61, 559 | 61, 61, 61, 92, 61, 65, 51, 255, 230, 255, 5, 61, 61, 5, 255, 255, 5, 43, 51, 61, 61, 560 | 5, 255, 255, 5, 169, 169, 73, 68, 51, 0, 0, 187, 0, 0, 0, 0, 0, 0, 0, 50, 5, 255, 255, 561 | 5, 169, 169, 73, 68, 51, 0, 0, 187, 0, 0, 0, 0, 0, 0, 0, 0, 51, 180, 255, 0, 0, 51, 5, 562 | 255, 252, 5, 43, 51, 51, 0, 1, 32, 31, 0, 0, 51, 51, 148, 255, 255, 16, 51, 51, 53, 563 | 250, 0, 1, 61, 61, 61, 0, 51, 180, 255, 0, 0, 51, 5, 255, 252, 5, 43, 51, 51, 0, 1, 32, 564 | 31, 0, 0, 51, 5, 255, 255, 5, 169, 169, 73, 68, 51, 0, 0, 187, 0, 0, 0, 0, 0, 0, 0, 50, 565 | 5, 255, 255, 5, 169, 169, 73, 68, 51, 0, 0, 187, 0, 0, 0, 0, 0, 0, 0, 0, 51, 180, 255, 566 | 0, 0, 51, 5, 255, 252, 5, 43, 51, 148, 255, 255, 16, 567 | ]; 568 | assert!(read_from_slice(&data).is_err()); 569 | } 570 | } 571 | --------------------------------------------------------------------------------