mirror of
https://github.com/agersant/polaris
synced 2024-11-10 02:04:13 +00:00
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:
parent
4534a84c05
commit
bff49c22ec
17 changed files with 1529 additions and 1229 deletions
2547
Cargo.lock
generated
2547
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -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
118
src/artwork.rs
Normal 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);
|
||||
}
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -34,6 +34,7 @@ mod lastfm;
|
|||
mod playlist;
|
||||
mod service;
|
||||
|
||||
mod artwork;
|
||||
mod thumbnails;
|
||||
mod ui;
|
||||
mod user;
|
||||
|
|
|
@ -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);
|
||||
|
|
BIN
test-data/artwork/Embedded.png
Normal file
BIN
test-data/artwork/Embedded.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 141 B |
BIN
test-data/artwork/Folder.png
Normal file
BIN
test-data/artwork/Folder.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 103 B |
BIN
test-data/artwork/sample.ape
Normal file
BIN
test-data/artwork/sample.ape
Normal file
Binary file not shown.
BIN
test-data/artwork/sample.flac
Normal file
BIN
test-data/artwork/sample.flac
Normal file
Binary file not shown.
BIN
test-data/artwork/sample.m4a
Normal file
BIN
test-data/artwork/sample.m4a
Normal file
Binary file not shown.
BIN
test-data/artwork/sample.mp3
Normal file
BIN
test-data/artwork/sample.mp3
Normal file
Binary file not shown.
BIN
test-data/artwork/sample.ogg
Normal file
BIN
test-data/artwork/sample.ogg
Normal file
Binary file not shown.
BIN
test-data/artwork/sample.opus
Normal file
BIN
test-data/artwork/sample.opus
Normal file
Binary file not shown.
Binary file not shown.
Loading…
Reference in a new issue