From c285b36629909434c795ffd7454fcbccbfc61e3c Mon Sep 17 00:00:00 2001 From: Serial Date: Fri, 23 Apr 2021 21:15:33 -0400 Subject: [PATCH] Album cover support for ogg/opus/flac --- Cargo.toml | 1 + src/components/tags/id3_tag.rs | 17 ++--- src/components/tags/mp4_tag.rs | 18 +++--- src/components/tags/riff_tag.rs | 2 +- src/components/tags/vorbis_tag.rs | 102 +++++++++++++++++++++++------- src/types/album.rs | 6 +- src/types/picture.rs | 8 +-- tests/io.rs | 24 ++++--- 8 files changed, 114 insertions(+), 64 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5aa3e5fd..d7240a5a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ metaflac = {version = "0.2.4", optional = true} opus_headers = {version = "0.1.2", optional = true} # Errors thiserror = "1.0.24" +base64 = "0.13.0" byteorder = "1.4.3" filepath = "0.1.1" diff --git a/src/components/tags/id3_tag.rs b/src/components/tags/id3_tag.rs index 0710cb4d..c09c5848 100644 --- a/src/components/tags/id3_tag.rs +++ b/src/components/tags/id3_tag.rs @@ -44,19 +44,16 @@ impl Id3v2Tag { } } -impl<'a> std::convert::TryFrom<&'a id3::frame::Picture> for Picture<'a> { +impl std::convert::TryFrom for Picture { type Error = Error; - fn try_from(inp: &'a id3::frame::Picture) -> Result { - let &id3::frame::Picture { + fn try_from(inp: id3::frame::Picture) -> Result { + let id3::frame::Picture { ref mime_type, - ref data, + data, .. } = inp; let mime_type: MimeType = mime_type.as_str().try_into()?; - Ok(Self { - data: &data, - mime_type, - }) + Ok(Self { data, mime_type }) } } @@ -129,7 +126,7 @@ impl AudioTagEdit for Id3v2Tag { .find(|&pic| matches!(pic.picture_type, id3::frame::PictureType::CoverFront)) .and_then(|pic| { Some(Picture { - data: &pic.data, + data: pic.data.clone(), mime_type: (pic.mime_type.as_str()).try_into().ok()?, }) }) @@ -140,7 +137,7 @@ impl AudioTagEdit for Id3v2Tag { mime_type: String::from(cover.mime_type), picture_type: id3::frame::PictureType::CoverFront, description: "".to_owned(), - data: cover.data.to_owned(), + data: cover.data, }); } fn remove_album_cover(&mut self) { diff --git a/src/components/tags/mp4_tag.rs b/src/components/tags/mp4_tag.rs index 9d2e35f9..994d7e02 100644 --- a/src/components/tags/mp4_tag.rs +++ b/src/components/tags/mp4_tag.rs @@ -28,15 +28,15 @@ impl Mp4Tag { } } -impl<'a> std::convert::TryFrom<&'a mp4ameta::Data> for Picture<'a> { +impl std::convert::TryFrom for Picture { type Error = Error; - fn try_from(inp: &'a mp4ameta::Data) -> Result { - Ok(match *inp { - mp4ameta::Data::Png(ref data) => Self { + fn try_from(inp: mp4ameta::Data) -> Result { + Ok(match inp { + mp4ameta::Data::Png(data) => Self { data, mime_type: MimeType::Png, }, - mp4ameta::Data::Jpeg(ref data) => Self { + mp4ameta::Data::Jpeg(data) => Self { data, mime_type: MimeType::Jpeg, }, @@ -104,11 +104,11 @@ impl AudioTagEdit for Mp4Tag { self.inner.artwork().and_then(|data| match data { Jpeg(d) => Some(Picture { - data: d, + data: d.clone(), mime_type: MimeType::Jpeg, }), Png(d) => Some(Picture { - data: d, + data: d.clone(), mime_type: MimeType::Png, }), _ => None, @@ -118,8 +118,8 @@ impl AudioTagEdit for Mp4Tag { fn set_album_cover(&mut self, cover: Picture) { self.remove_album_cover(); self.inner.add_artwork(match cover.mime_type { - MimeType::Png => mp4ameta::Data::Png(cover.data.to_owned()), - MimeType::Jpeg => mp4ameta::Data::Jpeg(cover.data.to_owned()), + MimeType::Png => mp4ameta::Data::Png(cover.data), + MimeType::Jpeg => mp4ameta::Data::Jpeg(cover.data), _ => panic!("Only png and jpeg are supported in m4a"), }); } diff --git a/src/components/tags/riff_tag.rs b/src/components/tags/riff_tag.rs index 9daa7660..a3e52a75 100644 --- a/src/components/tags/riff_tag.rs +++ b/src/components/tags/riff_tag.rs @@ -225,7 +225,7 @@ impl AudioTagWrite for RiffTag { chunk.extend(riff::LIST_ID.value.iter()); - let fourcc = "INFO"; // TODO: ID3 + let fourcc = "INFO"; chunk.extend(fourcc.as_bytes().iter()); for (k, v) in data { diff --git a/src/components/tags/vorbis_tag.rs b/src/components/tags/vorbis_tag.rs index 3fc28d58..bb42eb2f 100644 --- a/src/components/tags/vorbis_tag.rs +++ b/src/components/tags/vorbis_tag.rs @@ -3,11 +3,12 @@ use crate::components::logic; use crate::tag::VorbisFormat; use crate::{ - impl_tag, Album, AnyTag, AudioTag, AudioTagEdit, AudioTagWrite, Picture, Result, TagType, - ToAny, ToAnyTag, + impl_tag, Album, AnyTag, AudioTag, AudioTagEdit, AudioTagWrite, MimeType, Picture, Result, + TagType, ToAny, ToAnyTag, }; use std::collections::HashMap; +use std::convert::{TryFrom, TryInto}; use std::fs::{File, OpenOptions}; use std::io::{Cursor, Read, Seek, SeekFrom, Write}; use std::path::Path; @@ -263,31 +264,84 @@ impl AudioTagEdit for VorbisTag { } fn album_cover(&self) -> Option { - // TODO - // self.inner - // .pictures() - // .filter(|&pic| matches!(pic.picture_type, metaflac::block::PictureType::CoverFront)) - // .next() - // .and_then(|pic| { - // Some(Picture { - // data: &pic.data, - // mime_type: (pic.mime_type.as_str()).try_into().ok()?, - // }) - // }) - None + let picture = self.inner.get_value("METADATA_BLOCK_PICTURE"); + + return match picture { + None => None, + Some(data) => { + let data_str = data.to_string(); + let data = data_str.as_bytes(); + + if data.is_empty() { + return None; + } + + let data = match base64::decode(data) { + Ok(o) => o, + Err(_) => data.to_vec(), + }; + + let mut i = 0; + + let picture_type = u32::from_le_bytes(match (&data[i..i + 4]).try_into() { + Ok(o) => o, + Err(_) => return None, + }); + + if picture_type != 3 { + return None; + } + + i += 4; + match data[i..i + 4].try_into() { + Ok(mime_len) => { + i += 4; + let mime_len = u32::from_le_bytes(mime_len); + + match String::from_utf8(data[i..i + mime_len as usize].to_vec()) { + Ok(mime_type) => { + let mime_type = MimeType::try_from(&*mime_type); + + match mime_type { + Ok(mime_type) => { + let content = data[(8 + mime_len) as usize..].to_vec(); + + Some(Picture { + data: content, + mime_type, + }) + }, + Err(_) => return None, + } + }, + Err(_) => return None, + } + }, + Err(_) => return None, + } + }, + }; } - fn set_album_cover(&mut self, _cover: Picture) { - // TODO - // self.remove_album_cover(); - // let mime = String::from(cover.mime_type); - // let picture_type = metaflac::block::PictureType::CoverFront; - // self.inner - // .add_picture(mime, picture_type, (cover.data).to_owned()); + fn set_album_cover(&mut self, cover: Picture) { + self.remove_album_cover(); + let mime = String::from(cover.mime_type); + let mime_len = (mime.len() as u32).to_le_bytes(); + + let picture_type = 3_u32.to_le_bytes(); + let data = cover.data; + + let mut encoded = Vec::new(); + encoded.extend(picture_type.iter()); + encoded.extend(mime_len.iter()); + encoded.extend(mime.as_bytes().iter()); + encoded.extend(data.iter()); + + let encoded = base64::encode(encoded); + + self.inner.set_value("METADATA_BLOCK_PICTURE", encoded); } fn remove_album_cover(&mut self) { - // TODO - // self.inner - // .remove_picture_type(metaflac::block::PictureType::CoverFront) + self.inner.remove_key("METADATA_BLOCK_PICTURE") } fn track_number(&self) -> Option { diff --git a/src/types/album.rs b/src/types/album.rs index 2a860d22..a8cb0537 100644 --- a/src/types/album.rs +++ b/src/types/album.rs @@ -5,7 +5,7 @@ use super::picture::Picture; pub struct Album<'a> { pub title: Option<&'a str>, pub artists: Option>, - pub cover: Option>, + pub cover: Option, } impl<'a> Default for Album<'a> { @@ -23,7 +23,7 @@ impl<'a> Album<'a> { pub fn new( title: Option<&'a str>, artists: Option>, - cover: Option>, + cover: Option, ) -> Self { Self { title, @@ -56,7 +56,7 @@ impl<'a> Album<'a> { self.artists = None } /// Set the album cover - pub fn set_cover(mut self, cover: Picture<'a>) { + pub fn set_cover(mut self, cover: Picture) { self.cover = Some(cover); } /// Clears the `cover` field diff --git a/src/types/picture.rs b/src/types/picture.rs index eb83a23a..6292c364 100644 --- a/src/types/picture.rs +++ b/src/types/picture.rs @@ -45,13 +45,13 @@ impl From for String { /// Represents a picture, with its data and mime type. #[derive(Debug, Clone, Eq, PartialEq)] -pub struct Picture<'a> { - pub data: &'a [u8], +pub struct Picture { + pub data: Vec, pub mime_type: MimeType, } -impl<'a> Picture<'a> { - pub fn new(data: &'a [u8], mime_type: MimeType) -> Self { +impl Picture { + pub fn new(data: Vec, mime_type: MimeType) -> Self { Self { data, mime_type } } } diff --git a/tests/io.rs b/tests/io.rs index 3b229dc8..39921d17 100644 --- a/tests/io.rs +++ b/tests/io.rs @@ -1,5 +1,5 @@ #![cfg(feature = "default")] -use lofty::Tag; +use lofty::{MimeType, Picture, Tag}; macro_rules! full_test { ($function:ident, $file:expr) => { @@ -35,14 +35,13 @@ macro_rules! add_tags { println!("Setting album artists"); tag.set_album_artist("foo album artist"); - // TODO - // let cover = Picture { - // mime_type: MimeType::Jpeg, - // data: &vec![0u8; 10], - // }; - // - // tags.set_album_cover(cover.clone()); - // assert_eq!(tags.album_cover(), Some(cover)); + let _cover = Picture { + mime_type: MimeType::Jpeg, + data: vec![0; 10], + }; + + // tag.set_album_cover(cover.clone()); + // assert_eq!(tag.album_cover(), Some(cover)); println!("Writing"); tag.write_to_path($file).unwrap(); @@ -109,10 +108,9 @@ macro_rules! remove_tags { assert!(tag.album_artists_vec().is_none()); tag.remove_album_artists(); - // TODO - // tags.remove_album_cover(); - // assert!(tags.album_cover().is_none()); - // tags.remove_album_cover(); + // tag.remove_album_cover(); + // assert!(tag.album_cover().is_none()); + // tag.remove_album_cover(); println!("Writing"); tag.write_to_path($file).unwrap();