Merged trivial modules

This commit is contained in:
Antoine Gersant 2022-11-09 00:33:57 -08:00
parent 388901cf65
commit 2873f38e04
14 changed files with 527 additions and 544 deletions

View file

@ -4,11 +4,17 @@ use std::path;
use crate::app::{ddns, settings, user, vfs}; use crate::app::{ddns, settings, user, vfs};
mod error; #[derive(thiserror::Error, Debug)]
#[cfg(test)] pub enum Error {
mod test; #[error("Unspecified")]
Unspecified,
}
pub use error::*; impl From<anyhow::Error> for Error {
fn from(_: anyhow::Error) -> Self {
Error::Unspecified
}
}
#[derive(Default, Deserialize)] #[derive(Default, Deserialize)]
pub struct Config { pub struct Config {
@ -108,3 +114,90 @@ impl Manager {
Ok(()) Ok(())
} }
} }
#[cfg(test)]
mod test {
use super::*;
use crate::app::test;
use crate::test_name;
#[test]
fn apply_saves_misc_settings() {
let ctx = test::ContextBuilder::new(test_name!()).build();
let new_config = Config {
settings: Some(settings::NewSettings {
album_art_pattern: Some("🖼️\\.jpg".into()),
reindex_every_n_seconds: Some(100),
}),
..Default::default()
};
ctx.config_manager.apply(&new_config).unwrap();
let settings = ctx.settings_manager.read().unwrap();
let new_settings = new_config.settings.unwrap();
assert_eq!(
settings.index_album_art_pattern,
new_settings.album_art_pattern.unwrap()
);
assert_eq!(
settings.index_sleep_duration_seconds,
new_settings.reindex_every_n_seconds.unwrap()
);
}
#[test]
fn apply_saves_mount_points() {
let ctx = test::ContextBuilder::new(test_name!()).build();
let new_config = Config {
mount_dirs: Some(vec![vfs::MountDir {
source: "/home/music".into(),
name: "🎵📁".into(),
}]),
..Default::default()
};
ctx.config_manager.apply(&new_config).unwrap();
let actual_mount_dirs: Vec<vfs::MountDir> = ctx.vfs_manager.mount_dirs().unwrap();
assert_eq!(actual_mount_dirs, new_config.mount_dirs.unwrap());
}
#[test]
fn apply_saves_ddns_settings() {
let ctx = test::ContextBuilder::new(test_name!()).build();
let new_config = Config {
ydns: Some(ddns::Config {
host: "🐸🐸🐸.ydns.eu".into(),
username: "kfr🐸g".into(),
password: "tasty🐞".into(),
}),
..Default::default()
};
ctx.config_manager.apply(&new_config).unwrap();
let actual_ddns = ctx.ddns_manager.config().unwrap();
assert_eq!(actual_ddns, new_config.ydns.unwrap());
}
#[test]
fn apply_can_toggle_admin() {
let ctx = test::ContextBuilder::new(test_name!())
.user("Walter", "Tasty🍖", true)
.build();
assert!(ctx.user_manager.list().unwrap()[0].is_admin());
let new_config = Config {
users: Some(vec![user::NewUser {
name: "Walter".into(),
password: "Tasty🍖".into(),
admin: false,
}]),
..Default::default()
};
ctx.config_manager.apply(&new_config).unwrap();
assert!(!ctx.user_manager.list().unwrap()[0].is_admin());
}
}

View file

@ -1,11 +0,0 @@
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Unspecified")]
Unspecified,
}
impl From<anyhow::Error> for Error {
fn from(_: anyhow::Error) -> Self {
Error::Unspecified
}
}

View file

@ -1,82 +0,0 @@
use super::*;
use crate::app::{ddns, settings, test, user, vfs};
use crate::test_name;
#[test]
fn apply_saves_misc_settings() {
let ctx = test::ContextBuilder::new(test_name!()).build();
let new_config = Config {
settings: Some(settings::NewSettings {
album_art_pattern: Some("🖼️\\.jpg".into()),
reindex_every_n_seconds: Some(100),
}),
..Default::default()
};
ctx.config_manager.apply(&new_config).unwrap();
let settings = ctx.settings_manager.read().unwrap();
let new_settings = new_config.settings.unwrap();
assert_eq!(
settings.index_album_art_pattern,
new_settings.album_art_pattern.unwrap()
);
assert_eq!(
settings.index_sleep_duration_seconds,
new_settings.reindex_every_n_seconds.unwrap()
);
}
#[test]
fn apply_saves_mount_points() {
let ctx = test::ContextBuilder::new(test_name!()).build();
let new_config = Config {
mount_dirs: Some(vec![vfs::MountDir {
source: "/home/music".into(),
name: "🎵📁".into(),
}]),
..Default::default()
};
ctx.config_manager.apply(&new_config).unwrap();
let actual_mount_dirs: Vec<vfs::MountDir> = ctx.vfs_manager.mount_dirs().unwrap();
assert_eq!(actual_mount_dirs, new_config.mount_dirs.unwrap());
}
#[test]
fn apply_saves_ddns_settings() {
let ctx = test::ContextBuilder::new(test_name!()).build();
let new_config = Config {
ydns: Some(ddns::Config {
host: "🐸🐸🐸.ydns.eu".into(),
username: "kfr🐸g".into(),
password: "tasty🐞".into(),
}),
..Default::default()
};
ctx.config_manager.apply(&new_config).unwrap();
let actual_ddns = ctx.ddns_manager.config().unwrap();
assert_eq!(actual_ddns, new_config.ydns.unwrap());
}
#[test]
fn apply_can_toggle_admin() {
let ctx = test::ContextBuilder::new(test_name!())
.user("Walter", "Tasty🍖", true)
.build();
assert!(ctx.user_manager.list().unwrap()[0].is_admin());
let new_config = Config {
users: Some(vec![user::NewUser {
name: "Walter".into(),
password: "Tasty🍖".into(),
admin: false,
}]),
..Default::default()
};
ctx.config_manager.apply(&new_config).unwrap();
assert!(!ctx.user_manager.list().unwrap()[0].is_admin());
}

View file

@ -9,11 +9,21 @@ use crate::app::index::Song;
use crate::app::vfs; use crate::app::vfs;
use crate::db::{playlist_songs, playlists, users, DB}; use crate::db::{playlist_songs, playlists, users, DB};
mod error; #[derive(thiserror::Error, Debug)]
#[cfg(test)] pub enum Error {
mod test; #[error("User not found")]
UserNotFound,
#[error("Playlist not found")]
PlaylistNotFound,
#[error("Unspecified")]
Unspecified,
}
pub use error::*; impl From<anyhow::Error> for Error {
fn from(_: anyhow::Error) -> Self {
Error::Unspecified
}
}
#[derive(Clone)] #[derive(Clone)]
pub struct Manager { pub struct Manager {
@ -251,3 +261,125 @@ struct NewPlaylistSong {
struct User { struct User {
id: i32, id: i32,
} }
#[cfg(test)]
mod test {
use std::path::{Path, PathBuf};
use crate::app::test;
use crate::test_name;
const TEST_USER: &str = "test_user";
const TEST_PASSWORD: &str = "password";
const TEST_PLAYLIST_NAME: &str = "Chill & Grill";
const TEST_MOUNT_NAME: &str = "root";
#[test]
fn save_playlist_golden_path() {
let ctx = test::ContextBuilder::new(test_name!())
.user(TEST_USER, TEST_PASSWORD, false)
.build();
ctx.playlist_manager
.save_playlist(TEST_PLAYLIST_NAME, TEST_USER, &Vec::new())
.unwrap();
let found_playlists = ctx.playlist_manager.list_playlists(TEST_USER).unwrap();
assert_eq!(found_playlists.len(), 1);
assert_eq!(found_playlists[0], TEST_PLAYLIST_NAME);
}
#[test]
fn save_playlist_is_idempotent() {
let ctx = test::ContextBuilder::new(test_name!())
.user(TEST_USER, TEST_PASSWORD, false)
.mount(TEST_MOUNT_NAME, "test-data/small-collection")
.build();
ctx.index.update().unwrap();
let playlist_content: Vec<String> = ctx
.index
.flatten(Path::new(TEST_MOUNT_NAME))
.unwrap()
.into_iter()
.map(|s| s.path)
.collect();
assert_eq!(playlist_content.len(), 13);
ctx.playlist_manager
.save_playlist(TEST_PLAYLIST_NAME, TEST_USER, &playlist_content)
.unwrap();
ctx.playlist_manager
.save_playlist(TEST_PLAYLIST_NAME, TEST_USER, &playlist_content)
.unwrap();
let songs = ctx
.playlist_manager
.read_playlist(TEST_PLAYLIST_NAME, TEST_USER)
.unwrap();
assert_eq!(songs.len(), 13);
}
#[test]
fn delete_playlist_golden_path() {
let ctx = test::ContextBuilder::new(test_name!())
.user(TEST_USER, TEST_PASSWORD, false)
.build();
let playlist_content = Vec::new();
ctx.playlist_manager
.save_playlist(TEST_PLAYLIST_NAME, TEST_USER, &playlist_content)
.unwrap();
ctx.playlist_manager
.delete_playlist(TEST_PLAYLIST_NAME, TEST_USER)
.unwrap();
let found_playlists = ctx.playlist_manager.list_playlists(TEST_USER).unwrap();
assert_eq!(found_playlists.len(), 0);
}
#[test]
fn read_playlist_golden_path() {
let ctx = test::ContextBuilder::new(test_name!())
.user(TEST_USER, TEST_PASSWORD, false)
.mount(TEST_MOUNT_NAME, "test-data/small-collection")
.build();
ctx.index.update().unwrap();
let playlist_content: Vec<String> = ctx
.index
.flatten(Path::new(TEST_MOUNT_NAME))
.unwrap()
.into_iter()
.map(|s| s.path)
.collect();
assert_eq!(playlist_content.len(), 13);
ctx.playlist_manager
.save_playlist(TEST_PLAYLIST_NAME, TEST_USER, &playlist_content)
.unwrap();
let songs = ctx
.playlist_manager
.read_playlist(TEST_PLAYLIST_NAME, TEST_USER)
.unwrap();
assert_eq!(songs.len(), 13);
assert_eq!(songs[0].title, Some("Above The Water".to_owned()));
let first_song_path: PathBuf = [
TEST_MOUNT_NAME,
"Khemmis",
"Hunted",
"01 - Above The Water.mp3",
]
.iter()
.collect();
assert_eq!(songs[0].path, first_song_path.to_str().unwrap());
}
}

View file

@ -1,15 +0,0 @@
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("User not found")]
UserNotFound,
#[error("Playlist not found")]
PlaylistNotFound,
#[error("Unspecified")]
Unspecified,
}
impl From<anyhow::Error> for Error {
fn from(_: anyhow::Error) -> Self {
Error::Unspecified
}
}

View file

@ -1,118 +0,0 @@
use std::path::{Path, PathBuf};
use crate::app::test;
use crate::test_name;
const TEST_USER: &str = "test_user";
const TEST_PASSWORD: &str = "password";
const TEST_PLAYLIST_NAME: &str = "Chill & Grill";
const TEST_MOUNT_NAME: &str = "root";
#[test]
fn save_playlist_golden_path() {
let ctx = test::ContextBuilder::new(test_name!())
.user(TEST_USER, TEST_PASSWORD, false)
.build();
ctx.playlist_manager
.save_playlist(TEST_PLAYLIST_NAME, TEST_USER, &Vec::new())
.unwrap();
let found_playlists = ctx.playlist_manager.list_playlists(TEST_USER).unwrap();
assert_eq!(found_playlists.len(), 1);
assert_eq!(found_playlists[0], TEST_PLAYLIST_NAME);
}
#[test]
fn save_playlist_is_idempotent() {
let ctx = test::ContextBuilder::new(test_name!())
.user(TEST_USER, TEST_PASSWORD, false)
.mount(TEST_MOUNT_NAME, "test-data/small-collection")
.build();
ctx.index.update().unwrap();
let playlist_content: Vec<String> = ctx
.index
.flatten(Path::new(TEST_MOUNT_NAME))
.unwrap()
.into_iter()
.map(|s| s.path)
.collect();
assert_eq!(playlist_content.len(), 13);
ctx.playlist_manager
.save_playlist(TEST_PLAYLIST_NAME, TEST_USER, &playlist_content)
.unwrap();
ctx.playlist_manager
.save_playlist(TEST_PLAYLIST_NAME, TEST_USER, &playlist_content)
.unwrap();
let songs = ctx
.playlist_manager
.read_playlist(TEST_PLAYLIST_NAME, TEST_USER)
.unwrap();
assert_eq!(songs.len(), 13);
}
#[test]
fn delete_playlist_golden_path() {
let ctx = test::ContextBuilder::new(test_name!())
.user(TEST_USER, TEST_PASSWORD, false)
.build();
let playlist_content = Vec::new();
ctx.playlist_manager
.save_playlist(TEST_PLAYLIST_NAME, TEST_USER, &playlist_content)
.unwrap();
ctx.playlist_manager
.delete_playlist(TEST_PLAYLIST_NAME, TEST_USER)
.unwrap();
let found_playlists = ctx.playlist_manager.list_playlists(TEST_USER).unwrap();
assert_eq!(found_playlists.len(), 0);
}
#[test]
fn read_playlist_golden_path() {
let ctx = test::ContextBuilder::new(test_name!())
.user(TEST_USER, TEST_PASSWORD, false)
.mount(TEST_MOUNT_NAME, "test-data/small-collection")
.build();
ctx.index.update().unwrap();
let playlist_content: Vec<String> = ctx
.index
.flatten(Path::new(TEST_MOUNT_NAME))
.unwrap()
.into_iter()
.map(|s| s.path)
.collect();
assert_eq!(playlist_content.len(), 13);
ctx.playlist_manager
.save_playlist(TEST_PLAYLIST_NAME, TEST_USER, &playlist_content)
.unwrap();
let songs = ctx
.playlist_manager
.read_playlist(TEST_PLAYLIST_NAME, TEST_USER)
.unwrap();
assert_eq!(songs.len(), 13);
assert_eq!(songs[0].title, Some("Above The Water".to_owned()));
let first_song_path: PathBuf = [
TEST_MOUNT_NAME,
"Khemmis",
"Hunted",
"01 - Above The Water.mp3",
]
.iter()
.collect();
assert_eq!(songs[0].path, first_song_path.to_str().unwrap());
}

View file

@ -6,9 +6,27 @@ use std::time::Duration;
use crate::db::{misc_settings, DB}; use crate::db::{misc_settings, DB};
mod error; #[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Missing auth secret")]
AuthSecretNotFound,
#[error("Auth secret does not have the expected format")]
InvalidAuthSecret,
#[error("Missing index sleep duration")]
IndexSleepDurationNotFound,
#[error("Missing index album art pattern")]
IndexAlbumArtPatternNotFound,
#[error("Index album art pattern is not a valid regex")]
IndexAlbumArtPatternInvalid,
#[error("Unspecified")]
Unspecified,
}
pub use error::*; impl From<anyhow::Error> for Error {
fn from(_: anyhow::Error) -> Self {
Error::Unspecified
}
}
#[derive(Clone, Default)] #[derive(Clone, Default)]
pub struct AuthSecret { pub struct AuthSecret {

View file

@ -1,21 +0,0 @@
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Missing auth secret")]
AuthSecretNotFound,
#[error("Auth secret does not have the expected format")]
InvalidAuthSecret,
#[error("Missing index sleep duration")]
IndexSleepDurationNotFound,
#[error("Missing index album art pattern")]
IndexAlbumArtPatternNotFound,
#[error("Index album art pattern is not a valid regex")]
IndexAlbumArtPatternInvalid,
#[error("Unspecified")]
Unspecified,
}
impl From<anyhow::Error> for Error {
fn from(_: anyhow::Error) -> Self {
Error::Unspecified
}
}

View file

@ -1,17 +1,29 @@
use anyhow::*; use anyhow::*;
use image::ImageOutputFormat; use image::{DynamicImage, GenericImage, GenericImageView, ImageBuffer, ImageOutputFormat};
use std::cmp;
use std::collections::hash_map::DefaultHasher; use std::collections::hash_map::DefaultHasher;
use std::fs::{self, File}; use std::fs::{self, File};
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
mod generate; use crate::utils::{get_audio_format, AudioFormat};
mod options;
mod read;
pub use generate::*; #[derive(Debug, Hash)]
pub use options::*; pub struct Options {
pub use read::*; pub max_dimension: Option<u32>,
pub resize_if_almost_square: bool,
pub pad_to_square: bool,
}
impl Default for Options {
fn default() -> Self {
Self {
max_dimension: Some(400),
resize_if_almost_square: true,
pad_to_square: true,
}
}
}
#[derive(Clone)] #[derive(Clone)]
pub struct Manager { pub struct Manager {
@ -66,3 +78,178 @@ impl Manager {
hasher.finish() hasher.finish()
} }
} }
fn generate_thumbnail(image_path: &Path, options: &Options) -> Result<DynamicImage> {
let source_image = DynamicImage::ImageRgb8(read(image_path)?.into_rgb8());
let (source_width, source_height) = source_image.dimensions();
let largest_dimension = cmp::max(source_width, source_height);
let out_dimension = cmp::min(
options.max_dimension.unwrap_or(largest_dimension),
largest_dimension,
);
let source_aspect_ratio: f32 = source_width as f32 / source_height as f32;
let is_almost_square = source_aspect_ratio > 0.8 && source_aspect_ratio < 1.2;
let mut final_image;
if is_almost_square && options.resize_if_almost_square {
final_image = source_image.thumbnail_exact(out_dimension, out_dimension);
} else if options.pad_to_square {
let scaled_image = source_image.thumbnail(out_dimension, out_dimension);
let (scaled_width, scaled_height) = scaled_image.dimensions();
let background = image::Rgb([255, 255_u8, 255_u8]);
final_image = DynamicImage::ImageRgb8(ImageBuffer::from_pixel(
out_dimension,
out_dimension,
background,
));
final_image.copy_from(
&scaled_image,
(out_dimension - scaled_width) / 2,
(out_dimension - scaled_height) / 2,
)?;
} else {
final_image = source_image.thumbnail(out_dimension, out_dimension);
}
Ok(final_image)
}
fn read(image_path: &Path) -> Result<DynamicImage> {
match get_audio_format(image_path) {
Some(AudioFormat::AIFF) => read_aiff(image_path),
Some(AudioFormat::APE) => read_ape(image_path),
Some(AudioFormat::FLAC) => read_flac(image_path),
Some(AudioFormat::MP3) => read_mp3(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),
Some(AudioFormat::WAVE) => read_wave(image_path),
None => Ok(image::open(image_path)?),
}
}
fn read_ape(_: &Path) -> Result<DynamicImage> {
bail!("Embedded images are not supported in APE files");
}
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)?);
}
bail!(
"Embedded flac artwork not found for file: {}",
path.display()
);
}
fn read_mp3(path: &Path) -> Result<DynamicImage> {
let tag = id3::Tag::read_from_path(path)?;
read_id3(path, &tag)
}
fn read_aiff(path: &Path) -> Result<DynamicImage> {
let tag = id3::Tag::read_from_aiff_path(path)?;
read_id3(path, &tag)
}
fn read_wave(path: &Path) -> Result<DynamicImage> {
let tag = id3::Tag::read_from_wav_path(path)?;
read_id3(path, &tag)
}
fn read_id3(path: &Path, tag: &id3::Tag) -> Result<DynamicImage> {
if let Some(p) = tag.pictures().next() {
return Ok(image::load_from_memory(&p.data)?);
}
bail!(
"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().map(|d| d.data) {
Some(v) => Ok(image::load_from_memory(v)?),
_ => bail!(
"Embedded mp4 artwork not found for file: {}",
path.display()
),
}
}
fn read_vorbis(_: &Path) -> Result<DynamicImage> {
bail!("Embedded images are not supported in Vorbis files");
}
fn read_opus(_: &Path) -> Result<DynamicImage> {
bail!("Embedded images are not supported in Opus files");
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn can_read_artwork_data() {
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 aiff_img = read(Path::new("test-data/artwork/sample.aif"))
.unwrap()
.to_rgb8();
assert_eq!(aiff_img, embedded_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);
let wave_img = read(Path::new("test-data/artwork/sample.wav"))
.unwrap()
.to_rgb8();
assert_eq!(wave_img, embedded_img);
}
}

View file

@ -1,42 +0,0 @@
use anyhow::*;
use image::{DynamicImage, GenericImage, GenericImageView, ImageBuffer};
use std::cmp;
use std::path::*;
use crate::app::thumbnail::{read, Options};
pub fn generate_thumbnail(image_path: &Path, options: &Options) -> Result<DynamicImage> {
let source_image = DynamicImage::ImageRgb8(read(image_path)?.into_rgb8());
let (source_width, source_height) = source_image.dimensions();
let largest_dimension = cmp::max(source_width, source_height);
let out_dimension = cmp::min(
options.max_dimension.unwrap_or(largest_dimension),
largest_dimension,
);
let source_aspect_ratio: f32 = source_width as f32 / source_height as f32;
let is_almost_square = source_aspect_ratio > 0.8 && source_aspect_ratio < 1.2;
let mut final_image;
if is_almost_square && options.resize_if_almost_square {
final_image = source_image.thumbnail_exact(out_dimension, out_dimension);
} else if options.pad_to_square {
let scaled_image = source_image.thumbnail(out_dimension, out_dimension);
let (scaled_width, scaled_height) = scaled_image.dimensions();
let background = image::Rgb([255, 255_u8, 255_u8]);
final_image = DynamicImage::ImageRgb8(ImageBuffer::from_pixel(
out_dimension,
out_dimension,
background,
));
final_image.copy_from(
&scaled_image,
(out_dimension - scaled_width) / 2,
(out_dimension - scaled_height) / 2,
)?;
} else {
final_image = source_image.thumbnail(out_dimension, out_dimension);
}
Ok(final_image)
}

View file

@ -1,16 +0,0 @@
#[derive(Debug, Hash)]
pub struct Options {
pub max_dimension: Option<u32>,
pub resize_if_almost_square: bool,
pub pad_to_square: bool,
}
impl Default for Options {
fn default() -> Self {
Self {
max_dimension: Some(400),
resize_if_almost_square: true,
pad_to_square: true,
}
}
}

View file

@ -1,142 +0,0 @@
use anyhow::{bail, Result};
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::AIFF) => read_aiff(image_path),
Some(AudioFormat::APE) => read_ape(image_path),
Some(AudioFormat::FLAC) => read_flac(image_path),
Some(AudioFormat::MP3) => read_mp3(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),
Some(AudioFormat::WAVE) => read_wave(image_path),
None => Ok(image::open(image_path)?),
}
}
fn read_ape(_: &Path) -> Result<DynamicImage> {
bail!("Embedded images are not supported in APE files");
}
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)?);
}
bail!(
"Embedded flac artwork not found for file: {}",
path.display()
);
}
fn read_mp3(path: &Path) -> Result<DynamicImage> {
let tag = id3::Tag::read_from_path(path)?;
read_id3(path, &tag)
}
fn read_aiff(path: &Path) -> Result<DynamicImage> {
let tag = id3::Tag::read_from_aiff_path(path)?;
read_id3(path, &tag)
}
fn read_wave(path: &Path) -> Result<DynamicImage> {
let tag = id3::Tag::read_from_wav_path(path)?;
read_id3(path, &tag)
}
fn read_id3(path: &Path, tag: &id3::Tag) -> Result<DynamicImage> {
if let Some(p) = tag.pictures().next() {
return Ok(image::load_from_memory(&p.data)?);
}
bail!(
"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().map(|d| d.data) {
Some(v) => Ok(image::load_from_memory(v)?),
_ => bail!(
"Embedded mp4 artwork not found for file: {}",
path.display()
),
}
}
fn read_vorbis(_: &Path) -> Result<DynamicImage> {
bail!("Embedded images are not supported in Vorbis files");
}
fn read_opus(_: &Path) -> Result<DynamicImage> {
bail!("Embedded images are not supported in Opus files");
}
#[test]
fn can_read_artwork_data() {
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 aiff_img = read(Path::new("test-data/artwork/sample.aif"))
.unwrap()
.to_rgb8();
assert_eq!(aiff_img, embedded_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);
let wave_img = read(Path::new("test-data/artwork/sample.wav"))
.unwrap()
.to_rgb8();
assert_eq!(wave_img, embedded_img);
}

View file

@ -7,9 +7,6 @@ use std::path::{self, Path, PathBuf};
use crate::db::{mount_points, DB}; use crate::db::{mount_points, DB};
#[cfg(test)]
mod test;
#[derive(Clone, Debug, Deserialize, Insertable, PartialEq, Eq, Queryable, Serialize)] #[derive(Clone, Debug, Deserialize, Insertable, PartialEq, Eq, Queryable, Serialize)]
#[diesel(table_name = mount_points)] #[diesel(table_name = mount_points)]
pub struct MountDir { pub struct MountDir {
@ -115,3 +112,83 @@ impl Manager {
Ok(()) Ok(())
} }
} }
#[cfg(test)]
mod test {
use super::*;
#[test]
fn converts_virtual_to_real() {
let vfs = VFS::new(vec![Mount {
name: "root".to_owned(),
source: Path::new("test_dir").to_owned(),
}]);
let real_path: PathBuf = ["test_dir", "somewhere", "something.png"].iter().collect();
let virtual_path: PathBuf = ["root", "somewhere", "something.png"].iter().collect();
let converted_path = vfs.virtual_to_real(virtual_path.as_path()).unwrap();
assert_eq!(converted_path, real_path);
}
#[test]
fn converts_virtual_to_real_top_level() {
let vfs = VFS::new(vec![Mount {
name: "root".to_owned(),
source: Path::new("test_dir").to_owned(),
}]);
let real_path = Path::new("test_dir");
let converted_path = vfs.virtual_to_real(Path::new("root")).unwrap();
assert_eq!(converted_path, real_path);
}
#[test]
fn converts_real_to_virtual() {
let vfs = VFS::new(vec![Mount {
name: "root".to_owned(),
source: Path::new("test_dir").to_owned(),
}]);
let virtual_path: PathBuf = ["root", "somewhere", "something.png"].iter().collect();
let real_path: PathBuf = ["test_dir", "somewhere", "something.png"].iter().collect();
let converted_path = vfs.real_to_virtual(real_path.as_path()).unwrap();
assert_eq!(converted_path, virtual_path);
}
#[test]
fn cleans_path_string() {
let mut correct_path = path::PathBuf::new();
if cfg!(target_os = "windows") {
correct_path.push("C:\\");
} else {
correct_path.push("/usr");
}
correct_path.push("some");
correct_path.push("path");
let tests = if cfg!(target_os = "windows") {
vec![
r#"C:/some/path"#,
r#"C:\some\path"#,
r#"C:\some\path\"#,
r#"C:\some\path\\\\"#,
r#"C:\some/path//"#,
]
} else {
vec![
r#"/usr/some/path"#,
r#"/usr\some\path"#,
r#"/usr\some\path\"#,
r#"/usr\some\path\\\\"#,
r#"/usr\some/path//"#,
]
};
for test in tests {
let mount_dir = MountDir {
source: test.to_owned(),
name: "name".to_owned(),
};
let mount: Mount = mount_dir.into();
assert_eq!(mount.source, correct_path);
}
}
}

View file

@ -1,77 +0,0 @@
use std::path::{Path, PathBuf};
use super::*;
#[test]
fn converts_virtual_to_real() {
let vfs = VFS::new(vec![Mount {
name: "root".to_owned(),
source: Path::new("test_dir").to_owned(),
}]);
let real_path: PathBuf = ["test_dir", "somewhere", "something.png"].iter().collect();
let virtual_path: PathBuf = ["root", "somewhere", "something.png"].iter().collect();
let converted_path = vfs.virtual_to_real(virtual_path.as_path()).unwrap();
assert_eq!(converted_path, real_path);
}
#[test]
fn converts_virtual_to_real_top_level() {
let vfs = VFS::new(vec![Mount {
name: "root".to_owned(),
source: Path::new("test_dir").to_owned(),
}]);
let real_path = Path::new("test_dir");
let converted_path = vfs.virtual_to_real(Path::new("root")).unwrap();
assert_eq!(converted_path, real_path);
}
#[test]
fn converts_real_to_virtual() {
let vfs = VFS::new(vec![Mount {
name: "root".to_owned(),
source: Path::new("test_dir").to_owned(),
}]);
let virtual_path: PathBuf = ["root", "somewhere", "something.png"].iter().collect();
let real_path: PathBuf = ["test_dir", "somewhere", "something.png"].iter().collect();
let converted_path = vfs.real_to_virtual(real_path.as_path()).unwrap();
assert_eq!(converted_path, virtual_path);
}
#[test]
fn cleans_path_string() {
let mut correct_path = path::PathBuf::new();
if cfg!(target_os = "windows") {
correct_path.push("C:\\");
} else {
correct_path.push("/usr");
}
correct_path.push("some");
correct_path.push("path");
let tests = if cfg!(target_os = "windows") {
vec![
r#"C:/some/path"#,
r#"C:\some\path"#,
r#"C:\some\path\"#,
r#"C:\some\path\\\\"#,
r#"C:\some/path//"#,
]
} else {
vec![
r#"/usr/some/path"#,
r#"/usr\some\path"#,
r#"/usr\some\path\"#,
r#"/usr\some\path\\\\"#,
r#"/usr\some/path//"#,
]
};
for test in tests {
let mount_dir = MountDir {
source: test.to_owned(),
name: "name".to_owned(),
};
let mount: Mount = mount_dir.into();
assert_eq!(mount.source, correct_path);
}
}