Embedded artwork support (#101)

* Embedded artwork support for mp4 and id3 tags

* Embedded artwork support for flac tags.

* small fixes

* use first embedded artwork for directory

* added artwork tests

* updated Cargo.lock

* use first embedded artwork for missing artworks
This commit is contained in:
Tobias Schmitz 2020-11-26 00:46:09 +01:00 committed by GitHub
parent 4534a84c05
commit bff49c22ec
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 1529 additions and 1229 deletions

2547
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -29,7 +29,7 @@ lewton = "0.10.1"
log = "0.4.5"
metaflac = "0.2.3"
mp3-duration = "0.1.9"
mp4ameta = "0.7.0"
mp4ameta = "0.7.1"
opus_headers = "0.1.2"
pbkdf2 = "0.4"
rand = "0.7"

118
src/artwork.rs Normal file
View file

@ -0,0 +1,118 @@
use anyhow::*;
use image::DynamicImage;
use std::path::Path;
use crate::utils;
use crate::utils::AudioFormat;
pub fn read(image_path: &Path) -> Result<DynamicImage> {
match utils::get_audio_format(image_path) {
Some(AudioFormat::APE) => read_ape(image_path),
Some(AudioFormat::FLAC) => read_flac(image_path),
Some(AudioFormat::MP3) => read_id3(image_path),
Some(AudioFormat::MP4) => read_mp4(image_path),
Some(AudioFormat::MPC) => read_ape(image_path),
Some(AudioFormat::OGG) => read_vorbis(image_path),
Some(AudioFormat::OPUS) => read_opus(image_path),
None => Ok(image::open(image_path)?),
}
}
fn read_ape(_: &Path) -> Result<DynamicImage> {
Err(crate::Error::msg("Embedded ape artworks not yet supported"))
}
fn read_flac(path: &Path) -> Result<DynamicImage> {
let tag = metaflac::Tag::read_from_path(path)?;
if let Some(p) = tag.pictures().next() {
return Ok(image::load_from_memory(&p.data)?);
}
Err(crate::Error::msg(format!(
"Embedded flac artwork not found for file: {}",
path.display()
)))
}
fn read_id3(path: &Path) -> Result<DynamicImage> {
let tag = id3::Tag::read_from_path(path)?;
if let Some(p) = tag.pictures().next() {
return Ok(image::load_from_memory(&p.data)?);
}
Err(crate::Error::msg(format!(
"Embedded id3 artwork not found for file: {}",
path.display()
)))
}
fn read_mp4(path: &Path) -> Result<DynamicImage> {
let tag = mp4ameta::Tag::read_from_path(path)?;
match tag.artwork().and_then(|d| d.image_data()) {
Some(v) => Ok(image::load_from_memory(v)?),
_ => Err(crate::Error::msg(format!(
"Embedded mp4 artwork not found for file: {}",
path.display()
))),
}
}
fn read_vorbis(_: &Path) -> Result<DynamicImage> {
Err(crate::Error::msg(
"Embedded vorbis artworks are not yet supported",
))
}
fn read_opus(_: &Path) -> Result<DynamicImage> {
Err(crate::Error::msg(
"Embedded opus artworks are not yet supported",
))
}
#[test]
fn test_read_artowork() {
let ext_img = image::open("test-data/artwork/Folder.png")
.unwrap()
.to_rgb8();
let embedded_img = image::open("test-data/artwork/Embedded.png")
.unwrap()
.to_rgb8();
let folder_img = read(Path::new("test-data/artwork/Folder.png"))
.unwrap()
.to_rgb8();
assert_eq!(folder_img, ext_img);
let ape_img = read(Path::new("test-data/artwork/sample.ape"))
.map(|d| d.to_rgb8())
.ok();
assert_eq!(ape_img, None);
let flac_img = read(Path::new("test-data/artwork/sample.flac"))
.unwrap()
.to_rgb8();
assert_eq!(flac_img, embedded_img);
let mp3_img = read(Path::new("test-data/artwork/sample.mp3"))
.unwrap()
.to_rgb8();
assert_eq!(mp3_img, embedded_img);
let m4a_img = read(Path::new("test-data/artwork/sample.m4a"))
.unwrap()
.to_rgb8();
assert_eq!(m4a_img, embedded_img);
let ogg_img = read(Path::new("test-data/artwork/sample.ogg"))
.map(|d| d.to_rgb8())
.ok();
assert_eq!(ogg_img, None);
let opus_img = read(Path::new("test-data/artwork/sample.opus"))
.map(|d| d.to_rgb8())
.ok();
assert_eq!(opus_img, None);
}

View file

@ -24,6 +24,7 @@ pub struct SongTags {
pub album_artist: Option<String>,
pub album: Option<String>,
pub year: Option<i32>,
pub has_artwork: bool,
}
#[cfg_attr(feature = "profile-index", flame)]
@ -83,6 +84,7 @@ fn read_id3(path: &Path) -> Result<SongTags> {
.map(|y| y as i32)
.or_else(|| tag.date_released().and_then(|d| Some(d.year)))
.or_else(|| tag.date_recorded().and_then(|d| Some(d.year)));
let has_artwork = tag.pictures().count() > 0;
Ok(SongTags {
artist,
@ -93,6 +95,7 @@ fn read_id3(path: &Path) -> Result<SongTags> {
disc_number,
track_number,
year,
has_artwork,
})
}
@ -143,6 +146,7 @@ fn read_ape(path: &Path) -> Result<SongTags> {
disc_number,
track_number,
year,
has_artwork: false,
})
}
@ -160,6 +164,7 @@ fn read_vorbis(path: &Path) -> Result<SongTags> {
disc_number: None,
track_number: None,
year: None,
has_artwork: false,
};
for (key, value) in source.comment_hdr.comment_list {
@ -193,6 +198,7 @@ fn read_opus(path: &Path) -> Result<SongTags> {
disc_number: None,
track_number: None,
year: None,
has_artwork: false,
};
for (key, value) in headers.comments.user_comments {
@ -230,6 +236,7 @@ fn read_flac(path: &Path) -> Result<SongTags> {
}
_ => None,
};
let has_artwork = tag.pictures().count() > 0;
Ok(SongTags {
artist: vorbis.artist().map(|v| v[0].clone()),
@ -240,6 +247,7 @@ fn read_flac(path: &Path) -> Result<SongTags> {
disc_number,
track_number: vorbis.track(),
year,
has_artwork,
})
}
@ -256,6 +264,7 @@ fn read_mp4(path: &Path) -> Result<SongTags> {
disc_number: tag.disc_number().map(|d| d as u32),
track_number: tag.track_number().map(|d| d as u32),
year: tag.year().and_then(|v| v.parse::<i32>().ok()),
has_artwork: tag.artwork().is_some(),
})
}
@ -270,6 +279,7 @@ fn test_read_metadata() {
album: Some("TEST ALBUM".into()),
duration: None,
year: Some(2016),
has_artwork: false,
};
let flac_sample_tag = SongTags {
duration: Some(0),
@ -307,4 +317,29 @@ fn test_read_metadata() {
read(Path::new("test-data/formats/sample.ape")).unwrap(),
sample_tags
);
let flac_artwork_tag = SongTags {
has_artwork: true,
..flac_sample_tag
};
let mp3_artwork_tag = SongTags {
has_artwork: true,
..mp3_sample_tag
};
let m4a_artwork_tag = SongTags {
has_artwork: true,
..m4a_sample_tag
};
assert_eq!(
read(Path::new("test-data/artwork/sample.mp3")).unwrap(),
mp3_artwork_tag
);
assert_eq!(
read(Path::new("test-data/artwork/sample.flac")).unwrap(),
flac_artwork_tag
);
assert_eq!(
read(Path::new("test-data/artwork/sample.m4a")).unwrap(),
m4a_artwork_tag
);
}

View file

@ -56,6 +56,37 @@ fn test_metadata() {
);
}
#[test]
fn test_embedded_artwork() {
let mut song_path = PathBuf::new();
song_path.push("test-data");
song_path.push("small-collection");
song_path.push("Tobokegao");
song_path.push("Picnic");
song_path.push("07 - なぜ (Why).mp3");
let db = db::get_test_db("artwork.sqlite");
update(&db).unwrap();
let connection = db.connect().unwrap();
let songs: Vec<Song> = songs::table
.filter(songs::title.eq("なぜ (Why?)"))
.load(&connection)
.unwrap();
assert_eq!(songs.len(), 1);
let song = &songs[0];
assert_eq!(song.path, song_path.to_string_lossy().as_ref());
assert_eq!(song.track_number, Some(7));
assert_eq!(song.disc_number, None);
assert_eq!(song.title, Some("なぜ (Why?)".to_owned()));
assert_eq!(song.artist, Some("Tobokegao".to_owned()));
assert_eq!(song.album_artist, None);
assert_eq!(song.album, Some("Picnic".to_owned()));
assert_eq!(song.year, Some(2016));
assert_eq!(song.artwork, Some(song_path.to_string_lossy().into_owned()));
}
#[test]
fn test_browse_top_level() {
let mut root_path = PathBuf::new();

View file

@ -114,7 +114,7 @@ impl IndexUpdater {
));
// Find artwork
let artwork = {
let mut directory_artwork = {
#[cfg(feature = "profile-index")]
let _guard = flame::start_guard("artwork");
self.get_artwork(path).unwrap_or(None)
@ -200,6 +200,13 @@ impl IndexUpdater {
.filter_map(song_metadata)
.collect::<Vec<_>>();
if directory_artwork.is_none() {
directory_artwork = song_tags
.iter()
.find(|(_, t)| t.has_artwork)
.map(|(p, _)| p.to_owned());
}
for (file_path_string, tags) in song_tags {
if tags.year.is_some() {
inconsistent_directory_year |=
@ -223,6 +230,12 @@ impl IndexUpdater {
directory_artist = tags.artist.as_ref().cloned();
}
let artwork_path = if tags.has_artwork {
Some(file_path_string.to_owned())
} else {
directory_artwork.as_ref().cloned()
};
let song = NewSong {
path: file_path_string.to_owned(),
parent: path_string.to_owned(),
@ -234,7 +247,7 @@ impl IndexUpdater {
album_artist: tags.album_artist,
album: tags.album,
year: tags.year,
artwork: artwork.as_ref().cloned(),
artwork: artwork_path,
};
self.push_song(song)?;
@ -255,7 +268,7 @@ impl IndexUpdater {
NewDirectory {
path: path_string.to_owned(),
parent: parent_string,
artwork,
artwork: directory_artwork,
album: directory_album,
artist: directory_artist,
year: directory_year,

View file

@ -34,6 +34,7 @@ mod lastfm;
mod playlist;
mod service;
mod artwork;
mod thumbnails;
mod ui;
mod user;

View file

@ -1,5 +1,4 @@
use anyhow::*;
use image;
use image::imageops::FilterType;
use image::{DynamicImage, GenericImage, GenericImageView, ImageBuffer, ImageOutputFormat};
use std::cmp;
@ -8,6 +7,8 @@ use std::fs::{DirBuilder, File};
use std::hash::{Hash, Hasher};
use std::path::*;
use crate::artwork;
pub struct ThumbnailsManager {
thumbnails_path: PathBuf,
}
@ -88,7 +89,7 @@ fn generate_thumbnail(
image_path: &Path,
thumbnailoptions: &ThumbnailOptions,
) -> Result<DynamicImage> {
let source_image = image::open(image_path)?;
let source_image = artwork::read(image_path)?;
let (source_width, source_height) = source_image.dimensions();
let largest_dimension = cmp::max(source_width, source_height);
let out_dimension = cmp::min(thumbnailoptions.max_dimension, largest_dimension);

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 B

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.