Support new pictures and proc macro

This commit is contained in:
Serial 2021-05-18 22:26:27 -04:00
parent 25368a428f
commit a6d49fd9ce
11 changed files with 388 additions and 390 deletions

13
Cargo.lock generated
View file

@ -335,6 +335,7 @@ dependencies = [
"filepath",
"id3",
"lewton",
"lofty_attr",
"metaflac",
"mp3-duration",
"mp4ameta",
@ -344,6 +345,14 @@ dependencies = [
"thiserror",
]
[[package]]
name = "lofty_attr"
version = "0.1.0"
dependencies = [
"quote",
"syn",
]
[[package]]
name = "log"
version = "0.4.14"
@ -640,9 +649,9 @@ dependencies = [
[[package]]
name = "syn"
version = "1.0.68"
version = "1.0.72"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ce15dd3ed8aa2f8eeac4716d6ef5ab58b6b9256db41d7e1a0224c2788e8fd87"
checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82"
dependencies = [
"proc-macro2",
"quote",

View file

@ -1,8 +1,63 @@
use crate::vorbis_tag::VORBIS;
use crate::{Error, Result};
#[cfg(feature = "ogg")]
use ogg::PacketWriteEndInfo;
use std::collections::HashMap;
use std::fs::File;
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
pub(crate) fn vorbis_generic(
file: &mut File,
sig: &[u8],
vendor: &str,
comments: &HashMap<String, String>,
) -> Result<()> {
let mut packet = Vec::new();
packet.extend(sig.iter());
let comments: Vec<(String, String)> = comments
.iter()
.map(|(a, b)| (a.to_string(), b.to_string()))
.collect();
let vendor_len = vendor.len() as u32;
packet.extend(vendor_len.to_le_bytes().iter());
packet.extend(vendor.as_bytes().iter());
let comments_len = comments.len() as u32;
packet.extend(comments_len.to_le_bytes().iter());
let mut comment_str = Vec::new();
for (a, b) in comments {
comment_str.push(format!("{}={}", a, b));
let last = comment_str.last().unwrap();
let len = last.as_bytes().len() as u32;
packet.extend(len.to_le_bytes().iter());
packet.extend(last.as_bytes().iter());
}
if sig == VORBIS {
packet.push(1);
}
let mut file_bytes = Vec::new();
file.read_to_end(&mut file_bytes)?;
let data = if sig == VORBIS {
ogg(Cursor::new(file_bytes), &*packet)?
} else {
opus(Cursor::new(file_bytes), &*packet)?
};
file.seek(SeekFrom::Start(0))?;
file.set_len(0)?;
file.write_all(&data)?;
Ok(())
}
pub(crate) fn ogg<T>(data: T, packet: &[u8]) -> Result<Vec<u8>>
where
T: Read + Seek,

View file

@ -1,23 +1,21 @@
#![cfg(feature = "format-ape")]
use crate::{
impl_tag, Album, AnyTag, AudioTag, AudioTagEdit, AudioTagWrite, MimeType, Picture, PictureType,
Result, TagType, ToAny, ToAnyTag,
Album, AnyTag, AudioTag, AudioTagEdit, AudioTagWrite, Picture, Result, TagType, ToAny, ToAnyTag,
};
use lofty_attr::impl_tag;
pub use ape::Tag as ApeInnerTag;
use crate::types::picture::APE_PICTYPES;
use ape::Item;
use byteorder::{LittleEndian, ReadBytesExt};
use filepath::FilePath;
use std::borrow::Cow;
use std::fs::File;
use std::io::{Cursor, Seek, SeekFrom};
use std::path::Path;
#[cfg(feature = "duration")]
use std::time::Duration;
impl_tag!(ApeTag, ApeInnerTag, TagType::Ape);
#[impl_tag(ApeInnerTag, TagType::Ape)]
pub struct ApeTag;
impl ApeTag {
#[allow(missing_docs)]
@ -45,6 +43,17 @@ impl ApeTag {
None
}
#[allow(clippy::unused_self)]
fn get_picture(&self, item: &Item) -> Option<Picture> {
if let ape::ItemValue::Binary(bin) = &item.value {
if let Ok(pic) = Picture::from_ape_bytes(&item.key, bin) {
return Some(pic);
}
}
None
}
fn set_value<V>(&mut self, key: &str, val: V)
where
V: Into<String>,
@ -148,7 +157,7 @@ impl AudioTagEdit for ApeTag {
fn front_cover(&self) -> Option<Picture> {
if let Some(val) = self.inner.item("Cover Art (Front)") {
return get_picture(val);
return self.get_picture(val);
}
None
@ -157,7 +166,7 @@ impl AudioTagEdit for ApeTag {
fn set_front_cover(&mut self, cover: Picture) {
self.remove_front_cover();
if let Ok(item) = ape::Item::from_binary("Cover Art (Front)", cover.data) {
if let Ok(item) = ape::Item::from_binary("Cover Art (Front)", cover.as_ape_bytes()) {
self.inner.set_item(item)
}
}
@ -168,7 +177,7 @@ impl AudioTagEdit for ApeTag {
fn back_cover(&self) -> Option<Picture> {
if let Some(val) = self.inner.item("Cover Art (Back)") {
return get_picture(val);
return self.get_picture(val);
}
None
@ -177,7 +186,7 @@ impl AudioTagEdit for ApeTag {
fn set_back_cover(&mut self, cover: Picture) {
self.remove_back_cover();
if let Ok(item) = ape::Item::from_binary("Cover Art (Back)", cover.data) {
if let Ok(item) = ape::Item::from_binary("Cover Art (Back)", cover.as_ape_bytes()) {
self.inner.set_item(item)
}
}
@ -186,9 +195,22 @@ impl AudioTagEdit for ApeTag {
self.remove_key("Cover Art (Back)")
}
fn pictures(&self) -> Option<Vec<Picture>> {
// TODO
None
fn pictures(&self) -> Option<Cow<'static, [Picture]>> {
let mut pics = Vec::new();
for pic_type in &APE_PICTYPES {
if let Some(item) = self.inner.item(pic_type) {
if let Some(pic) = self.get_picture(item) {
pics.push(pic)
}
}
}
if pics.is_empty() {
None
} else {
Some(Cow::from(pics))
}
}
// Track number and total tracks are stored together as num/total?
@ -283,49 +305,6 @@ impl AudioTagEdit for ApeTag {
}
}
fn get_picture(item: &Item) -> Option<Picture> {
if let ape::ItemValue::Binary(bin) = &item.value {
if !bin.is_empty() {
let pic_type = match &*item.key {
"Cover Art (Front)" => PictureType::CoverFront,
"Cover Art (Back)" => PictureType::CoverBack,
_ => PictureType::Other,
};
let data_pos: Option<usize> =
if bin.starts_with(&[b'\xff']) || bin.starts_with(&[b'\x89']) {
Some(0)
} else {
bin.iter().find(|x| x == &&b'\0').map(|pos| *pos as usize)
};
if let Some(pos) = data_pos {
let mut cursor = Cursor::new(bin.clone());
if cursor.seek(SeekFrom::Start((pos + 1) as u64)).is_ok() {
if let Ok(mime) = cursor.read_u32::<LittleEndian>() {
if let Some(mime_type) = match &mime.to_le_bytes() {
b"PNG\0" => Some(MimeType::Png),
b"JPEG" => Some(MimeType::Jpeg),
_ => None,
} {
cursor.set_position(0_u64);
return Some(Picture {
pic_type,
mime_type,
data: cursor.into_inner(),
});
}
}
}
}
}
}
None
}
impl AudioTagWrite for ApeTag {
fn write_to(&self, file: &mut File) -> Result<()> {
// Write only uses paths, this is annoying

View file

@ -2,23 +2,23 @@
use crate::tag::Id3Format;
use crate::{
impl_tag, Album, AnyTag, AudioTag, AudioTagEdit, AudioTagWrite, Error, MimeType, Picture,
Result, TagType, ToAny, ToAnyTag,
Album, AnyTag, AudioTag, AudioTagEdit, AudioTagWrite, Error, MimeType, Picture, Result,
TagType, ToAny, ToAnyTag,
};
use lofty_attr::impl_tag;
pub use id3::Tag as Id3v2InnerTag;
use crate::types::picture::PictureType;
use filepath::FilePath;
use std::convert::TryInto;
use std::borrow::Cow;
use std::convert::{TryFrom, TryInto};
use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
use std::path::Path;
#[cfg(feature = "duration")]
use std::time::Duration;
impl_tag!(Id3v2Tag, Id3v2InnerTag, TagType::Id3v2(Id3Format::Default));
#[impl_tag(Id3v2InnerTag, TagType::Id3v2(Id3Format::Default))]
pub struct Id3v2Tag;
impl Id3v2Tag {
#[allow(missing_docs)]
@ -36,45 +36,54 @@ impl Id3v2Tag {
Id3Format::Riff => Ok(Self {
inner: Id3v2InnerTag::read_from_wav(&path)?,
#[cfg(feature = "duration")]
duration: None, // TODO
duration: None,
}),
Id3Format::Form => Ok(Self {
inner: Id3v2InnerTag::read_from_aiff(&path)?,
#[cfg(feature = "duration")]
duration: None, // TODO
duration: None,
}),
}
}
}
impl std::convert::TryFrom<&id3::frame::Picture> for Picture {
impl std::convert::TryFrom<id3::frame::Picture> for Picture {
type Error = Error;
fn try_from(inp: &id3::frame::Picture) -> Result<Self> {
fn try_from(inp: id3::frame::Picture) -> Result<Self> {
let id3::frame::Picture {
ref mime_type,
data,
ref picture_type,
description,
..
} = inp;
let mime_type: MimeType = mime_type.as_str().try_into()?;
let pic_type: PictureType = picture_type.into();
let pic_type = *picture_type;
let description = if description == String::new() {
None
} else {
Some(Cow::from(description))
};
Ok(Self {
pic_type,
mime_type,
data: data.clone(),
description,
data: Cow::from(data),
})
}
}
impl std::convert::TryFrom<Picture> for id3::frame::Picture {
impl TryFrom<Picture> for id3::frame::Picture {
type Error = Error;
fn try_from(inp: Picture) -> Result<Self> {
Ok(Self {
mime_type: String::from(inp.mime_type),
picture_type: inp.pic_type.into(),
description: "".to_string(),
data: inp.data,
picture_type: inp.pic_type,
description: inp
.description
.map_or_else(|| "".to_string(), |d| d.to_string()),
data: Vec::from(inp.data),
})
}
}
@ -176,8 +185,13 @@ impl AudioTagEdit for Id3v2Tag {
.and_then(|pic| {
Some(Picture {
pic_type: PictureType::CoverFront,
data: pic.data.clone(),
data: Cow::from(pic.data.clone()),
mime_type: (pic.mime_type.as_str()).try_into().ok()?,
description: if pic.description == String::new() {
None
} else {
Some(Cow::from(pic.description.clone()))
},
})
})
}
@ -202,8 +216,13 @@ impl AudioTagEdit for Id3v2Tag {
.and_then(|pic| {
Some(Picture {
pic_type: PictureType::CoverBack,
data: pic.data.clone(),
data: Cow::from(pic.data.clone()),
mime_type: (pic.mime_type.as_str()).try_into().ok()?,
description: if pic.description == String::new() {
None
} else {
Some(Cow::from(pic.description.clone()))
},
})
})
}
@ -221,20 +240,20 @@ impl AudioTagEdit for Id3v2Tag {
.remove_picture_by_type(id3::frame::PictureType::CoverBack);
}
fn pictures(&self) -> Option<Vec<Picture>> {
fn pictures(&self) -> Option<Cow<'static, [Picture]>> {
let mut pictures = self.inner.pictures().peekable();
if pictures.peek().is_some() {
let mut collection = Vec::new();
for pic in pictures {
match TryInto::<Picture>::try_into(pic) {
match TryInto::<Picture>::try_into(pic.clone()) {
Ok(p) => collection.push(p),
Err(_) => return None,
}
}
return Some(collection);
return Some(Cow::from(collection));
}
None

View file

@ -1,19 +1,20 @@
#![cfg(feature = "format-mp4")]
use crate::{
impl_tag, Album, AnyTag, AudioTag, AudioTagEdit, AudioTagWrite, Error, MimeType, Picture,
Result, TagType, ToAny, ToAnyTag,
Album, AnyTag, AudioTag, AudioTagEdit, AudioTagWrite, Error, MimeType, Picture, Result,
TagType, ToAny, ToAnyTag,
};
use lofty_attr::impl_tag;
pub use mp4ameta::{Fourcc, Tag as Mp4InnerTag};
use crate::types::picture::PictureType;
use std::borrow::Cow;
use std::fs::File;
use std::path::Path;
#[cfg(feature = "duration")]
use std::time::Duration;
impl_tag!(Mp4Tag, Mp4InnerTag, TagType::Mp4);
#[impl_tag(Mp4InnerTag, TagType::Mp4)]
pub struct Mp4Tag {}
impl Mp4Tag {
#[allow(missing_docs)]
@ -36,18 +37,21 @@ impl std::convert::TryFrom<mp4ameta::Data> for Picture {
Ok(match inp {
mp4ameta::Data::Png(data) => Self {
pic_type: PictureType::Other,
data,
mime_type: MimeType::Png,
description: None,
data: Cow::from(data),
},
mp4ameta::Data::Jpeg(data) => Self {
pic_type: PictureType::Other,
data,
mime_type: MimeType::Jpeg,
description: None,
data: Cow::from(data),
},
mp4ameta::Data::Bmp(data) => Self {
pic_type: PictureType::Other,
mime_type: MimeType::Bmp,
data,
description: None,
data: Cow::from(data),
},
_ => return Err(Error::NotAPicture),
})
@ -110,33 +114,40 @@ impl AudioTagEdit for Mp4Tag {
}
fn front_cover(&self) -> Option<Picture> {
self.inner.artwork().and_then(|data| match data {
mp4ameta::Data::Jpeg(d) => Some(Picture {
pic_type: PictureType::Other,
data: d.clone(),
mime_type: MimeType::Jpeg,
}),
mp4ameta::Data::Png(d) => Some(Picture {
pic_type: PictureType::Other,
data: d.clone(),
mime_type: MimeType::Png,
}),
mp4ameta::Data::Bmp(d) => Some(Picture {
pic_type: PictureType::Other,
data: d.clone(),
mime_type: MimeType::Bmp,
}),
_ => None,
})
if let Some(picture) = &self.inner.artwork() {
return match picture {
mp4ameta::Data::Jpeg(d) => Some(Picture {
pic_type: PictureType::Other,
mime_type: MimeType::Jpeg,
description: None,
data: Cow::from(d.clone()),
}),
mp4ameta::Data::Png(d) => Some(Picture {
pic_type: PictureType::Other,
mime_type: MimeType::Png,
description: None,
data: Cow::from(d.clone()),
}),
mp4ameta::Data::Bmp(d) => Some(Picture {
pic_type: PictureType::Other,
mime_type: MimeType::Bmp,
description: None,
data: Cow::from(d.clone()),
}),
_ => None,
};
}
None
}
fn set_front_cover(&mut self, cover: Picture) {
self.inner.remove_artwork();
self.inner.add_artwork(match cover.mime_type {
MimeType::Png => mp4ameta::Data::Png(cover.data),
MimeType::Jpeg => mp4ameta::Data::Jpeg(cover.data),
MimeType::Bmp => mp4ameta::Data::Bmp(cover.data),
MimeType::Png => mp4ameta::Data::Png(Vec::from(cover.data)),
MimeType::Jpeg => mp4ameta::Data::Jpeg(Vec::from(cover.data)),
MimeType::Bmp => mp4ameta::Data::Bmp(Vec::from(cover.data)),
_ => panic!("Attempt to add an invalid image format to MP4"),
});
}
@ -157,7 +168,7 @@ impl AudioTagEdit for Mp4Tag {
self.inner.remove_artwork();
}
fn pictures(&self) -> Option<Vec<Picture>> {
fn pictures(&self) -> Option<Cow<'static, [Picture]>> {
let mut pictures = Vec::new();
for art in self.inner.artworks() {
@ -172,7 +183,8 @@ impl AudioTagEdit for Mp4Tag {
pictures.push(Picture {
pic_type: PictureType::Other,
mime_type,
data,
description: None,
data: Cow::from(data),
})
}
}
@ -180,7 +192,7 @@ impl AudioTagEdit for Mp4Tag {
if pictures.is_empty() {
None
} else {
Some(pictures)
Some(Cow::from(pictures))
}
}

View file

@ -2,19 +2,16 @@
use crate::components::logic;
use crate::{
impl_tag, Album, AnyTag, AudioTag, AudioTagEdit, AudioTagWrite, Picture, Result, TagType,
ToAny, ToAnyTag,
Album, AnyTag, AudioTag, AudioTagEdit, AudioTagWrite, Picture, Result, TagType, ToAny, ToAnyTag,
};
use lofty_attr::impl_tag;
use std::borrow::BorrowMut;
use std::borrow::{BorrowMut, Cow};
use std::collections::HashMap;
use std::fs::File;
use std::io::{Cursor, Seek, SeekFrom, Write};
use std::path::Path;
#[cfg(feature = "duration")]
use std::time::Duration;
struct RiffInnerTag {
data: Option<HashMap<String, String>>,
}
@ -27,6 +24,9 @@ impl Default for RiffInnerTag {
}
}
#[impl_tag(RiffInnerTag, TagType::RiffInfo)]
pub struct RiffTag;
impl RiffTag {
#[allow(missing_docs)]
#[allow(clippy::missing_errors_doc)]
@ -44,8 +44,6 @@ impl RiffTag {
}
}
impl_tag!(RiffTag, RiffInnerTag, TagType::RiffInfo);
impl RiffTag {
fn get_value(&self, key: &str) -> Option<&str> {
self.inner
@ -166,7 +164,7 @@ impl AudioTagEdit for RiffTag {
fn remove_back_cover(&mut self) {}
/// This will always return `None`, as this is non-standard
fn pictures(&self) -> Option<Vec<Picture>> {
fn pictures(&self) -> Option<Cow<'static, [Picture]>> {
None
}

View file

@ -4,29 +4,30 @@
feature = "format-flac"
))]
use crate::components::logic;
use crate::tag::VorbisFormat;
use crate::components::logic::write::vorbis_generic;
use crate::{
impl_tag, Album, AnyTag, AudioTag, AudioTagEdit, AudioTagWrite, Error, MimeType, Picture,
PictureType, Result, TagType, ToAny, ToAnyTag,
Album, AnyTag, AudioTag, AudioTagEdit, AudioTagWrite, Error, Picture, PictureType, Result,
TagType, ToAny, ToAnyTag, VorbisFormat,
};
use lofty_attr::impl_tag;
use lewton::inside_ogg::OggStreamReader;
use opus_headers::OpusHeaders;
use std::borrow::Cow;
use std::collections::HashMap;
use std::convert::{TryFrom, TryInto};
use std::fs::File;
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
use std::io::Write;
use std::path::Path;
#[cfg(feature = "duration")]
use std::time::Duration;
const VORBIS: [u8; 7] = [3, 118, 111, 114, 98, 105, 115];
pub const VORBIS: [u8; 7] = [3, 118, 111, 114, 98, 105, 115];
const OPUSTAGS: [u8; 8] = [79, 112, 117, 115, 84, 97, 103, 115];
struct VorbisInnerTag {
format: Option<VorbisFormat>,
vendor: String,
comments: HashMap<String, String>,
pictures: Option<Vec<Picture>>,
pictures: Option<Cow<'static, [Picture]>>,
}
impl Default for VorbisInnerTag {
@ -66,147 +67,109 @@ impl VorbisInnerTag {
self.comments = comments;
}
fn from_path<P>(path: P, format: VorbisFormat) -> Result<Self>
fn from_path<P>(path: P, format: &VorbisFormat) -> Result<Self>
where
P: AsRef<Path>,
{
match format {
VorbisFormat::Ogg => {
let headers = lewton::inside_ogg::OggStreamReader::new(File::open(path)?).unwrap();
let tag = lewton::inside_ogg::OggStreamReader::new(File::open(path)?)?;
let vorbis_tag: VorbisTag = tag.try_into()?;
let vendor = headers.comment_hdr.vendor;
let mut comments = headers.comment_hdr.comment_list;
let mut pictures: Vec<Picture> = Vec::new();
if let Some(p) = comments
.iter()
.position(|(k, _)| *k == "METADATA_BLOCK_PICTURE")
{
let kv = comments.remove(p);
if let Some(pic) = picture_from_data(&*kv.1)? {
pictures.push(pic)
}
}
Ok(Self {
format: Some(format),
vendor,
comments: comments.into_iter().collect(),
pictures: if pictures.is_empty() {
None
} else {
Some(pictures)
},
})
Ok(vorbis_tag.inner)
},
VorbisFormat::Opus => {
let headers = opus_headers::parse_from_path(path)?;
let vendor = headers.comments.vendor;
let tag = opus_headers::parse_from_path(path)?;
let vorbis_tag: VorbisTag = tag.try_into()?;
let mut comments = headers.comments.user_comments;
// TODO: opus_headers doesn't store all keys
let pictures = if let Some(data) = comments.remove("METADATA_BLOCK_PICTURE") {
picture_from_data(&*data)?.map(|pic| vec![pic])
} else {
None
};
Ok(Self {
format: Some(format),
vendor,
comments,
pictures,
})
Ok(vorbis_tag.inner)
},
VorbisFormat::Flac => {
let headers = metaflac::Tag::read_from_path(path)?;
let as_vorbis: VorbisTag = headers.into();
let tag = metaflac::Tag::read_from_path(path)?;
let vorbis_tag: VorbisTag = tag.try_into()?;
Ok(as_vorbis.inner)
Ok(vorbis_tag.inner)
},
}
}
}
fn picture_from_data(data: &str) -> Result<Option<Picture>> {
let data = match base64::decode(data) {
Ok(o) => o,
Err(_) => data.as_bytes().to_vec(),
};
#[impl_tag(VorbisInnerTag, TagType::Vorbis(VorbisFormat::Ogg))]
pub struct VorbisTag;
let mut i = 0;
#[cfg(feature = "format-vorbis")]
impl TryFrom<lewton::inside_ogg::OggStreamReader<File>> for VorbisTag {
type Error = crate::Error;
let picture_type_b = u32::from_le_bytes(match (&data[i..i + 4]).try_into() {
Ok(o) => o,
Err(_) => return Err(Error::InvalidData),
});
let picture_type = match picture_type_b {
3 => PictureType::CoverFront,
4 => PictureType::CoverBack,
_ => PictureType::Other,
};
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();
Ok(Some(Picture {
pic_type: picture_type,
data: content,
mime_type,
}))
},
Err(_) => Ok(None),
}
},
Err(_) => Ok(None),
}
},
Err(_) => Ok(None),
}
}
impl_tag!(
VorbisTag,
VorbisInnerTag,
TagType::Vorbis(VorbisFormat::Ogg)
);
impl VorbisTag {
#[allow(missing_docs)]
#[allow(clippy::missing_errors_doc)]
pub fn read_from_path<P>(path: P, format: VorbisFormat) -> Result<Self>
where
P: AsRef<Path>,
{
Ok(Self {
inner: VorbisInnerTag::from_path(path, format)?,
#[cfg(feature = "duration")]
duration: None,
})
}
}
impl From<metaflac::Tag> for VorbisTag {
fn from(inp: metaflac::Tag) -> Self {
fn try_from(inp: OggStreamReader<File>) -> Result<Self> {
let mut tag = Self::default();
let (comments, vendor, pictures) = if let Some(comments) = inp.vorbis_comments() {
let mut comments = inp.comment_hdr.comment_list;
let mut pictures: Vec<Picture> = Vec::new();
if let Some(p) = comments
.iter()
.position(|(k, _)| *k == "METADATA_BLOCK_PICTURE")
{
let kv = comments.remove(p);
if let Ok(pic) = Picture::from_apic_bytes(&kv.1.as_bytes()) {
pictures.push(pic)
}
}
tag.inner = VorbisInnerTag {
format: Some(VorbisFormat::Ogg),
vendor: inp.comment_hdr.vendor,
comments: comments.into_iter().collect(),
pictures: if pictures.is_empty() {
None
} else {
Some(Cow::from(pictures))
},
};
Ok(tag)
}
}
#[cfg(feature = "format-opus")]
impl TryFrom<opus_headers::OpusHeaders> for VorbisTag {
type Error = crate::Error;
fn try_from(inp: OpusHeaders) -> Result<Self> {
let mut tag = Self::default();
let mut comments = inp.comments.user_comments;
// TODO: opus_headers doesn't store all keys
let mut pictures = None;
if let Some(data) = comments.remove("METADATA_BLOCK_PICTURE") {
if let Ok(pic) = Picture::from_apic_bytes(&data.as_bytes()) {
pictures = Some(Cow::from(vec![pic]))
}
}
tag.inner = VorbisInnerTag {
format: Some(VorbisFormat::Opus),
vendor: inp.comments.vendor,
comments,
pictures,
};
Ok(tag)
}
}
#[cfg(feature = "format-flac")]
impl TryFrom<metaflac::Tag> for VorbisTag {
type Error = crate::Error;
fn try_from(inp: metaflac::Tag) -> Result<Self> {
let mut tag = Self::default();
if let Some(comments) = inp.vorbis_comments() {
let comments = comments.clone();
let mut user_comments = comments.comments;
@ -214,7 +177,7 @@ impl From<metaflac::Tag> for VorbisTag {
if let Some(data) = user_comments.remove("METADATA_BLOCK_PICTURE") {
for item in data {
if let Ok(Some(pic)) = picture_from_data(&*item) {
if let Ok(pic) = Picture::from_apic_bytes(&item.as_bytes()) {
pictures.push(pic)
}
}
@ -231,48 +194,32 @@ impl From<metaflac::Tag> for VorbisTag {
let comment_collection: HashMap<String, String> =
comment_collection.into_iter().collect();
let vendor = comments.vendor_string;
tag.inner = VorbisInnerTag {
format: Some(VorbisFormat::Flac),
vendor: comments.vendor_string,
comments: comment_collection,
pictures: Some(Cow::from(pictures)),
};
(comment_collection, vendor, Some(pictures))
} else {
let comments: HashMap<String, String> = HashMap::new();
let vendor = String::new();
return Ok(tag);
}
(comments, vendor, None)
};
tag.inner = VorbisInnerTag {
format: Some(VorbisFormat::Flac),
vendor,
comments,
pictures,
};
tag
Err(Error::InvalidData)
}
}
impl From<&VorbisTag> for metaflac::Tag {
fn from(inp: &VorbisTag) -> Self {
let mut tag = Self::default();
tag.remove_blocks(metaflac::BlockType::VorbisComment);
let vendor = inp.inner.vendor.clone();
let mut comment_collection: HashMap<String, Vec<String>> = HashMap::new();
for (k, v) in inp.inner.comments.clone() {
comment_collection.insert(k, vec![v]);
}
tag.push_block(metaflac::Block::VorbisComment(
metaflac::block::VorbisComment {
vendor_string: vendor,
comments: comment_collection,
},
));
tag
impl VorbisTag {
#[allow(missing_docs)]
#[allow(clippy::missing_errors_doc)]
pub fn read_from_path<P>(path: P, format: &VorbisFormat) -> Result<Self>
where
P: AsRef<Path>,
{
Ok(Self {
inner: VorbisInnerTag::from_path(path, &format)?,
#[cfg(feature = "duration")]
duration: None,
})
}
}
@ -370,13 +317,15 @@ impl AudioTagEdit for VorbisTag {
fn set_front_cover(&mut self, cover: Picture) {
self.remove_front_cover();
let pictures = create_cover(cover, self.inner.pictures.clone());
let pictures = create_cover(&cover, &self.inner.pictures);
self.inner.pictures = pictures
}
fn remove_front_cover(&mut self) {
if let Some(mut p) = self.inner.pictures.clone() {
p.retain(|pic| Some(pic) != self.front_cover().as_ref())
if let Some(p) = self.inner.pictures.clone() {
let mut p = p.to_vec();
p.retain(|pic| Some(pic) != self.front_cover().as_ref());
self.inner.pictures = Some(Cow::from(p));
}
}
@ -387,17 +336,19 @@ impl AudioTagEdit for VorbisTag {
fn set_back_cover(&mut self, cover: Picture) {
self.remove_front_cover();
let pictures = create_cover(cover, self.inner.pictures.clone());
let pictures = create_cover(&cover, &self.inner.pictures);
self.inner.pictures = pictures
}
fn remove_back_cover(&mut self) {
if let Some(mut p) = self.inner.pictures.clone() {
p.retain(|pic| Some(pic) != self.back_cover().as_ref())
if let Some(p) = self.inner.pictures.clone() {
let mut p = p.to_vec();
p.retain(|pic| Some(pic) != self.back_cover().as_ref());
self.inner.pictures = Some(Cow::from(p));
}
}
fn pictures(&self) -> Option<Vec<Picture>> {
fn pictures(&self) -> Option<Cow<'static, [Picture]>> {
self.inner.pictures.clone()
}
@ -460,11 +411,11 @@ impl AudioTagEdit for VorbisTag {
}
}
fn get_cover(p_type: PictureType, pictures: &Option<Vec<Picture>>) -> Option<Picture> {
fn get_cover(p_type: PictureType, pictures: &Option<Cow<'static, [Picture]>>) -> Option<Picture> {
match pictures {
None => None,
Some(pictures) => {
for pic in pictures {
for pic in pictures.iter() {
if pic.pic_type == p_type {
return Some(pic.clone());
}
@ -475,37 +426,25 @@ fn get_cover(p_type: PictureType, pictures: &Option<Vec<Picture>>) -> Option<Pic
}
}
fn create_cover(cover: Picture, pictures: Option<Vec<Picture>>) -> Option<Vec<Picture>> {
let mime = String::from(cover.mime_type);
let mime_len = (mime.len() as u32).to_le_bytes();
fn create_cover(
cover: &Picture,
pictures: &Option<Cow<'static, [Picture]>>,
) -> Option<Cow<'static, [Picture]>> {
if cover.pic_type == PictureType::CoverFront || cover.pic_type == PictureType::CoverBack {
if let Ok(pic) = Picture::from_apic_bytes(&cover.as_apic_bytes()) {
if let Some(pictures) = pictures {
let mut pictures = pictures.to_vec();
pictures.retain(|p| p.pic_type != PictureType::CoverBack);
let picture_type = match cover.pic_type {
PictureType::CoverFront => 3_u32.to_le_bytes(),
PictureType::CoverBack => 4_u32.to_le_bytes(),
PictureType::Other => unreachable!(),
};
pictures.push(pic);
return Some(Cow::from(pictures));
}
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);
if let Ok(Some(pic)) = picture_from_data(&*encoded) {
if let Some(mut pictures) = pictures {
pictures.retain(|p| p.pic_type != PictureType::CoverBack);
pictures.push(pic);
Some(pictures)
} else {
Some(vec![pic])
return Some(Cow::from(vec![pic]));
}
} else {
None
}
None
}
impl AudioTagWrite for VorbisTag {
@ -513,15 +452,47 @@ impl AudioTagWrite for VorbisTag {
if let Some(format) = self.inner.format.clone() {
match format {
VorbisFormat::Ogg => {
write(file, &VORBIS, &self.inner.vendor, &self.inner.comments)?;
},
VorbisFormat::Flac => {
let mut flac_tag: metaflac::Tag = self.into();
flac_tag.write_to(file)?;
vorbis_generic(file, &VORBIS, &self.inner.vendor, &self.inner.comments)?;
},
VorbisFormat::Opus => {
write(file, &OPUSTAGS, &self.inner.vendor, &self.inner.comments)?;
vorbis_generic(file, &OPUSTAGS, &self.inner.vendor, &self.inner.comments)?;
},
VorbisFormat::Flac => {
// TODO
let tag = metaflac::Tag::read_from(file)?;
let mut blocks: Vec<metaflac::Block> =
tag.blocks().map(std::borrow::ToOwned::to_owned).collect();
let mut pictures: Vec<metaflac::Block> = Vec::new();
let mut comment_collection: HashMap<String, Vec<String>> = HashMap::new();
if let Some(pics) = self.inner.pictures.clone() {
for pic in pics.iter() {
pictures.push(metaflac::Block::Picture(
metaflac::block::Picture::from_bytes(&*pic.as_apic_bytes())?,
))
}
}
for (k, v) in self.inner.comments.clone() {
comment_collection.insert(k, vec![v]);
}
blocks[1] = metaflac::Block::VorbisComment(metaflac::block::VorbisComment {
vendor_string: self.inner.vendor.clone(),
comments: comment_collection,
});
blocks.append(&mut pictures);
file.write_all(b"fLaC")?;
let total = blocks.len();
for (i, block) in blocks.iter().enumerate() {
block.write_to(i == total - 1, file)?;
}
},
}
}
@ -529,54 +500,3 @@ impl AudioTagWrite for VorbisTag {
Ok(())
}
}
fn write(
file: &mut File,
sig: &[u8],
vendor: &str,
comments: &HashMap<String, String>,
) -> Result<()> {
let mut packet = Vec::new();
packet.extend(sig.iter());
let comments: Vec<(String, String)> = comments
.iter()
.map(|(a, b)| (a.to_string(), b.to_string()))
.collect();
let vendor_len = vendor.len() as u32;
packet.extend(vendor_len.to_le_bytes().iter());
packet.extend(vendor.as_bytes().iter());
let comments_len = comments.len() as u32;
packet.extend(comments_len.to_le_bytes().iter());
let mut comment_str = Vec::new();
for (a, b) in comments {
comment_str.push(format!("{}={}", a, b));
let last = comment_str.last().unwrap();
let len = last.as_bytes().len() as u32;
packet.extend(len.to_le_bytes().iter());
packet.extend(last.as_bytes().iter());
}
if sig == VORBIS {
packet.push(1);
}
let mut file_bytes = Vec::new();
file.read_to_end(&mut file_bytes)?;
let data = if sig == VORBIS {
logic::write::ogg(Cursor::new(file_bytes), &*packet)?
} else {
logic::write::opus(Cursor::new(file_bytes), &*packet)?
};
file.seek(SeekFrom::Start(0))?;
file.set_len(0)?;
file.write_all(&data)?;
Ok(())
}

View file

@ -107,7 +107,7 @@ impl Tag {
feature = "format-flac",
feature = "format-opus"
))]
TagType::Vorbis(format) => Ok(Box::new(VorbisTag::read_from_path(path, format)?)),
TagType::Vorbis(format) => Ok(Box::new(VorbisTag::read_from_path(path, &format)?)),
}
}
}

View file

@ -2,6 +2,7 @@
use crate::components::tags::*;
use crate::{Album, AnyTag, Picture, Result, TagType};
use std::borrow::Cow;
use std::fs::{File, OpenOptions};
/// Combination of [`AudioTagEdit`], [`AudioTagWrite`], and [`ToAnyTag`]
@ -104,7 +105,7 @@ pub trait AudioTagEdit {
fn remove_back_cover(&mut self);
/// Returns an `Iterator` over all pictures stored in the track
fn pictures(&self) -> Option<Vec<Picture>>;
fn pictures(&self) -> Option<Cow<'static, [Picture]>>;
/// Returns the track number and total tracks
fn track(&self) -> (Option<u32>, Option<u32>) {

View file

@ -1,4 +1,5 @@
use lofty::{Id3Format, Tag, TagType, ToAnyTag, VorbisTag};
use std::convert::TryInto;
#[test]
#[cfg(all(feature = "format-id3", feature = "format-flac"))]
@ -12,7 +13,7 @@ fn test_inner() {
.set_title(vec!["title from metaflac::Tag"]);
// Turn the flac tag into a VorbisTag
let tag: VorbisTag = innertag.into();
let tag: VorbisTag = innertag.try_into().unwrap();
// Turn the VorbisTag into a Box<dyn AudioTag>
let id3tag = tag.to_dyn_tag(TagType::Id3v2(Id3Format::Default));

View file

@ -1,5 +1,6 @@
#![cfg(feature = "default")]
use lofty::{MimeType, Picture, PictureType, Tag};
use std::borrow::Cow;
macro_rules! full_test {
($function:ident, $file:expr) => {
@ -39,12 +40,14 @@ macro_rules! add_tags {
Picture {
pic_type: PictureType::CoverFront,
mime_type: MimeType::Jpeg,
data: vec![0, 74, 80, 69, 71, 0, 0, 0, 0, 0, 0, 0, 0, 0],
description: Some(Cow::from("test")),
data: Cow::from(vec![0; 11]),
},
Picture {
pic_type: PictureType::CoverBack,
mime_type: MimeType::Jpeg,
data: vec![0, 74, 80, 69, 71, 0, 0, 0, 0, 0, 0, 0, 0, 0],
description: Some(Cow::from("test")),
data: Cow::from(vec![0; 11]),
},
);
@ -66,7 +69,8 @@ macro_rules! add_tags {
let cover = Picture {
pic_type: PictureType::Other,
mime_type: MimeType::Jpeg,
data: vec![0, 74, 80, 69, 71, 0, 0, 0, 0, 0, 0, 0, 0, 0],
description: None,
data: Cow::from(vec![0; 11]),
};
println!("Setting cover");