Start work to support back covers

Signed-off-by: Serial <69764315+Serial-ATA@users.noreply.github.com>
This commit is contained in:
Serial 2021-05-16 13:16:57 -04:00
parent 79cbbd5339
commit 29ff3d8030
11 changed files with 300 additions and 93 deletions

View file

@ -22,7 +22,7 @@ mp3-duration = {version = "0.1.10", optional = true} # Duration
lewton = {version = "0.10.2", optional = true} # Decoding
ogg = {version = "0.8.0", optional = true} # Encoding
# Mp4
mp4ameta = {version = "0.9.1", optional = true}
mp4ameta = {version = "0.10.2", optional = true}
# Flac
metaflac = {version = "0.2.4", optional = true}
# Opus

View file

@ -11,6 +11,7 @@ use ape::Item;
use filepath::FilePath;
use std::fs::File;
use std::path::Path;
#[cfg(feature = "duration")]
use std::time::Duration;
@ -143,21 +144,42 @@ impl AudioTagEdit for ApeTag {
self.remove_key("Album artist")
}
fn album_cover(&self) -> Option<Picture> {
fn front_cover(&self) -> Option<Picture> {
if let Some(val) = self.inner.item("Cover Art (Front)") {
return get_picture(val);
}
None
}
fn set_album_cover(&mut self, cover: Picture) {
fn set_front_cover(&mut self, cover: Picture) {
// TODO
self.set_value("Cover Art (Front)", "TODO")
}
fn remove_album_cover(&mut self) {
fn remove_front_cover(&mut self) {
self.remove_key("Cover Art (Front)")
}
fn back_cover(&self) -> Option<Picture> {
if let Some(val) = self.inner.item("Cover Art (Back)") {
return get_picture(val);
}
None
}
fn set_back_cover(&mut self, cover: Picture) {
// TODO
}
fn remove_back_cover(&mut self) {
self.remove_key("Cover Art (Back)")
}
fn pictures(&self) -> Option<Vec<Picture>> {
todo!()
}
// Track number and total tracks are stored together as num/total?
fn track_number(&self) -> Option<u32> {
let numbers = self.get_value("Track");

View file

@ -14,6 +14,7 @@ use std::convert::TryInto;
use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
use std::path::Path;
#[cfg(feature = "duration")]
use std::time::Duration;
@ -46,9 +47,9 @@ impl Id3v2Tag {
}
}
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,
@ -61,7 +62,19 @@ impl std::convert::TryFrom<id3::frame::Picture> for Picture {
Ok(Self {
pic_type,
mime_type,
data,
data: data.clone(),
})
}
}
impl std::convert::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,
})
}
}
@ -156,7 +169,7 @@ impl AudioTagEdit for Id3v2Tag {
self.inner.remove_album_artist()
}
fn album_cover(&self) -> Option<Picture> {
fn front_cover(&self) -> Option<Picture> {
self.inner
.pictures()
.find(|&pic| matches!(pic.picture_type, id3::frame::PictureType::CoverFront))
@ -168,20 +181,65 @@ impl AudioTagEdit for Id3v2Tag {
})
})
}
fn set_album_cover(&mut self, cover: Picture) {
self.remove_album_cover();
self.inner.add_picture(id3::frame::Picture {
mime_type: String::from(cover.mime_type),
picture_type: id3::frame::PictureType::CoverFront,
description: "".to_owned(),
data: cover.data,
});
fn set_front_cover(&mut self, cover: Picture) {
self.remove_front_cover();
if let Ok(pic) = cover.try_into() {
self.inner.add_picture(pic)
}
}
fn remove_album_cover(&mut self) {
fn remove_front_cover(&mut self) {
self.inner
.remove_picture_by_type(id3::frame::PictureType::CoverFront);
}
fn back_cover(&self) -> Option<Picture> {
self.inner
.pictures()
.find(|&pic| matches!(pic.picture_type, id3::frame::PictureType::CoverBack))
.and_then(|pic| {
Some(Picture {
pic_type: PictureType::CoverBack,
data: pic.data.clone(),
mime_type: (pic.mime_type.as_str()).try_into().ok()?,
})
})
}
fn set_back_cover(&mut self, cover: Picture) {
self.remove_back_cover();
if let Ok(pic) = cover.try_into() {
self.inner.add_picture(pic)
}
}
fn remove_back_cover(&mut self) {
self.inner
.remove_picture_by_type(id3::frame::PictureType::CoverBack);
}
fn pictures(&self) -> Option<Vec<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) {
Ok(p) => collection.push(p),
Err(_) => return None,
}
}
return Some(collection);
}
None
}
fn track_number(&self) -> Option<u32> {
self.inner.track()
}

View file

@ -5,7 +5,7 @@ use crate::{
Result, TagType, ToAny, ToAnyTag,
};
pub use mp4ameta::{FourCC, Tag as Mp4InnerTag};
pub use mp4ameta::{Fourcc, Tag as Mp4InnerTag};
use crate::types::picture::PictureType;
use std::fs::File;
@ -44,6 +44,11 @@ impl std::convert::TryFrom<mp4ameta::Data> for Picture {
data,
mime_type: MimeType::Jpeg,
},
mp4ameta::Data::Bmp(data) => Self {
pic_type: PictureType::CoverFront,
mime_type: MimeType::Bmp,
data,
},
_ => return Err(Error::NotAPicture),
})
}
@ -103,35 +108,59 @@ impl AudioTagEdit for Mp4Tag {
fn remove_album_artists(&mut self) {
self.inner.remove_album_artists();
}
fn album_cover(&self) -> Option<Picture> {
use mp4ameta::Data::{Jpeg, Png};
fn front_cover(&self) -> Option<Picture> {
self.inner.artwork().and_then(|data| match data {
Jpeg(d) => Some(Picture {
mp4ameta::Data::Jpeg(d) => Some(Picture {
pic_type: PictureType::CoverFront, // TODO
data: d.clone(),
mime_type: MimeType::Jpeg,
}),
Png(d) => Some(Picture {
mp4ameta::Data::Png(d) => Some(Picture {
pic_type: PictureType::CoverFront, // TODO
data: d.clone(),
mime_type: MimeType::Png,
}),
mp4ameta::Data::Bmp(d) => Some(Picture {
pic_type: PictureType::CoverFront, // TODO
data: d.clone(),
mime_type: MimeType::Bmp,
}),
_ => None,
})
}
fn set_album_cover(&mut self, cover: Picture) {
self.remove_album_cover();
fn set_front_cover(&mut self, cover: Picture) {
self.remove_front_cover();
self.inner.add_artwork(match cover.mime_type {
MimeType::Png => mp4ameta::Data::Png(cover.data),
MimeType::Jpeg => mp4ameta::Data::Jpeg(cover.data),
_ => panic!("Only png and jpeg are supported in m4a"),
MimeType::Bmp => mp4ameta::Data::Bmp(cover.data),
_ => panic!("Attempt to add an invalid image format to MP4"),
});
}
fn remove_album_cover(&mut self) {
fn remove_front_cover(&mut self) {
self.inner.remove_artwork();
}
fn back_cover(&self) -> Option<Picture> {
todo!()
}
fn set_back_cover(&mut self, cover: Picture) {
todo!()
}
fn remove_back_cover(&mut self) {
todo!()
}
fn pictures(&self) -> Option<Vec<Picture>> {
todo!()
}
fn remove_track(&mut self) {
self.inner.remove_track(); // faster than removing separately
}

View file

@ -11,6 +11,7 @@ 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;
@ -143,15 +144,31 @@ impl AudioTagEdit for RiffTag {
}
/// This will always return `None`, as this is non-standard
fn album_cover(&self) -> Option<Picture> {
fn front_cover(&self) -> Option<Picture> {
None
}
/// This will not do anything, as this is non-standard
fn set_album_cover(&mut self, _cover: Picture) {}
fn set_front_cover(&mut self, _cover: Picture) {}
/// This will not do anything, as this is non-standard
fn remove_album_cover(&mut self) {}
fn remove_front_cover(&mut self) {}
/// This will always return `None`, as this is non-standard
fn back_cover(&self) -> Option<Picture> {
None
}
/// This will not do anything, as this is non-standard
fn set_back_cover(&mut self, _cover: Picture) {}
/// This will not do anything, as this is non-standard
fn remove_back_cover(&mut self) {}
/// This will always return `None`, as this is non-standard
fn pictures(&self) -> Option<Vec<Picture>> {
None
}
fn track_number(&self) -> Option<u32> {
if let Some(Ok(y)) = self.get_value("TrackNumber").map(str::parse::<u32>) {

View file

@ -359,50 +359,42 @@ impl AudioTagEdit for VorbisTag {
self.inner.remove_key("ALBUMARTIST");
}
fn album_cover(&self) -> Option<Picture> {
match &self.inner.pictures {
None => None,
Some(pictures) => {
for pic in pictures {
if pic.pic_type == PictureType::CoverFront {
return Some(pic.clone());
}
}
fn front_cover(&self) -> Option<Picture> {
get_cover(PictureType::CoverFront, &self.inner.pictures)
}
None
},
fn set_front_cover(&mut self, cover: Picture) {
self.remove_front_cover();
let pictures = create_cover(cover, self.inner.pictures.clone());
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())
}
}
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();
fn back_cover(&self) -> Option<Picture> {
get_cover(PictureType::CoverBack, &self.inner.pictures)
}
let picture_type = 3_u32.to_le_bytes();
let data = cover.data;
fn set_back_cover(&mut self, cover: Picture) {
self.remove_front_cover();
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 pictures = create_cover(cover, self.inner.pictures.clone());
self.inner.pictures = pictures
}
let encoded = base64::encode(encoded);
if let Ok(Some(pic)) = picture_from_data(&*encoded) {
if let Some(mut pictures) = self.inner.pictures.clone() {
pictures.retain(|p| p.pic_type != PictureType::CoverFront);
pictures.push(pic);
self.inner.pictures = Some(pictures)
} else {
self.inner.pictures = Some(vec![pic])
}
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())
}
}
fn remove_album_cover(&mut self) {
self.inner.remove_key("METADATA_BLOCK_PICTURE")
fn pictures(&self) -> Option<Vec<Picture>> {
self.inner.pictures.clone()
}
fn track_number(&self) -> Option<u32> {
@ -464,6 +456,54 @@ impl AudioTagEdit for VorbisTag {
}
}
fn get_cover(p_type: PictureType, pictures: &Option<Vec<Picture>>) -> Option<Picture> {
match pictures {
None => None,
Some(pictures) => {
for pic in pictures {
if pic.pic_type == p_type {
return Some(pic.clone());
}
}
None
},
}
}
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();
let picture_type = match cover.pic_type {
PictureType::CoverFront => 3_u32.to_le_bytes(),
PictureType::CoverBack => 4_u32.to_le_bytes(),
PictureType::Other => unreachable!(),
};
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])
}
} else {
None
}
}
impl AudioTagWrite for VorbisTag {
fn write_to(&self, file: &mut File) -> Result<()> {
if let Some(format) = self.inner.format.clone() {

View file

@ -73,7 +73,7 @@ macro_rules! impl_tag {
album: Album::new(
inp.album_title(),
inp.album_artists_vec(),
inp.album_cover(),
inp.album_covers(),
),
track_number: inp.track_number(),
total_tracks: inp.total_tracks(),

View file

@ -57,7 +57,7 @@ pub trait AudioTagEdit {
Album {
title: self.album_title(),
artists: self.album_artists_vec(),
cover: self.album_cover(),
covers: self.album_covers(),
}
}
@ -79,12 +79,32 @@ pub trait AudioTagEdit {
/// Removes the album artist string
fn remove_album_artists(&mut self);
/// Returns the album cover
fn album_cover(&self) -> Option<Picture>;
/// Sets the album cover
fn set_album_cover(&mut self, cover: Picture);
/// Removes the album cover
fn remove_album_cover(&mut self);
/// Returns the front and back album covers
fn album_covers(&self) -> (Option<Picture>, Option<Picture>) {
(self.front_cover(), self.back_cover())
}
/// Removes both album covers
fn remove_album_covers(&mut self) {
self.remove_front_cover();
self.remove_back_cover();
}
/// Returns the front cover
fn front_cover(&self) -> Option<Picture>;
/// Sets the front cover
fn set_front_cover(&mut self, cover: Picture);
/// Removes the front cover
fn remove_front_cover(&mut self);
/// Returns the front cover
fn back_cover(&self) -> Option<Picture>;
/// Sets the front cover
fn set_back_cover(&mut self, cover: Picture);
/// Removes the front cover
fn remove_back_cover(&mut self);
/// Returns an `Iterator` over all pictures stored in the track
fn pictures(&self) -> Option<Vec<Picture>>;
/// Returns the track number and total tracks
fn track(&self) -> (Option<u32>, Option<u32>) {

View file

@ -8,8 +8,7 @@ pub struct Album<'a> {
/// A `Vec` of the album artists
pub artists: Option<Vec<&'a str>>,
/// The album's front cover
// TODO: also store the back cover
pub cover: Option<Picture>,
pub covers: (Option<Picture>, Option<Picture>),
}
impl<'a> Default for Album<'a> {
@ -17,7 +16,7 @@ impl<'a> Default for Album<'a> {
Self {
title: None,
artists: None,
cover: None,
covers: (None, None),
}
}
}
@ -27,12 +26,12 @@ impl<'a> Album<'a> {
pub fn new(
title: Option<&'a str>,
artists: Option<Vec<&'a str>>,
cover: Option<Picture>,
covers: (Option<Picture>, Option<Picture>),
) -> Self {
Self {
title,
artists,
cover,
covers,
}
}
/// Create a new album with the specified title
@ -40,7 +39,7 @@ impl<'a> Album<'a> {
Self {
title: Some(title),
artists: None,
cover: None,
covers: (None, None),
}
}
/// Set the album artists
@ -60,12 +59,12 @@ impl<'a> Album<'a> {
self.artists = None
}
/// Set the album cover
pub fn set_cover(mut self, cover: Picture) {
self.cover = Some(cover);
pub fn set_covers(mut self, covers: (Option<Picture>, Option<Picture>)) {
self.covers = covers
}
/// Clears the `cover` field
pub fn remove_cover(mut self) {
self.cover = None
/// Clears the `covers` field
pub fn remove_covers(mut self) {
self.covers = (None, None)
}
/// Turns `artists` vec into a String
pub fn artists_as_string(&self) -> Option<String> {

View file

@ -70,6 +70,16 @@ impl From<&id3PicType> for PictureType {
}
}
impl From<PictureType> for id3PicType {
fn from(inp: PictureType) -> Self {
match inp {
PictureType::CoverFront => id3PicType::CoverFront,
PictureType::CoverBack => id3PicType::CoverBack,
_ => id3PicType::Other,
}
}
}
/// Represents a picture, with its data and mime type.
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Picture {

View file

@ -35,17 +35,29 @@ macro_rules! add_tags {
println!("Setting album artists");
tag.set_album_artist("foo album artist");
let cover = Picture {
pic_type: PictureType::CoverFront,
mime_type: MimeType::Jpeg,
data: vec![0; 10],
};
let covers = (
Picture {
pic_type: PictureType::CoverFront,
mime_type: MimeType::Jpeg,
data: vec![0; 10],
},
Picture {
pic_type: PictureType::CoverBack,
mime_type: MimeType::Jpeg,
data: vec![0; 11],
},
);
let file_name = stringify!($file);
if file_name != stringify!("tests/assets/a.wav") {
tag.set_album_cover(cover.clone());
assert_eq!(tag.album_cover(), Some(cover));
println!("Setting front cover");
tag.set_front_cover(covers.0.clone());
assert_eq!(tag.front_cover(), Some(covers.0));
println!("Setting back cover");
tag.set_back_cover(covers.1.clone());
assert_eq!(tag.back_cover(), Some(covers.1));
}
println!("Writing");
@ -113,9 +125,9 @@ macro_rules! remove_tags {
assert!(tag.album_artists_vec().is_none());
tag.remove_album_artists();
tag.remove_album_cover();
assert!(tag.album_cover().is_none());
tag.remove_album_cover();
tag.remove_album_covers();
assert_eq!(tag.album_covers(), (None, None));
tag.remove_album_covers();
println!("Writing");
tag.write_to_path($file).unwrap();