mirror of
https://github.com/agersant/polaris
synced 2024-11-10 02:04:13 +00:00
Merged trivial modules
This commit is contained in:
parent
388901cf65
commit
2873f38e04
14 changed files with 527 additions and 544 deletions
|
@ -4,11 +4,17 @@ use std::path;
|
|||
|
||||
use crate::app::{ddns, settings, user, vfs};
|
||||
|
||||
mod error;
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Unspecified")]
|
||||
Unspecified,
|
||||
}
|
||||
|
||||
pub use error::*;
|
||||
impl From<anyhow::Error> for Error {
|
||||
fn from(_: anyhow::Error) -> Self {
|
||||
Error::Unspecified
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize)]
|
||||
pub struct Config {
|
||||
|
@ -108,3 +114,90 @@ impl Manager {
|
|||
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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
|
@ -9,11 +9,21 @@ use crate::app::index::Song;
|
|||
use crate::app::vfs;
|
||||
use crate::db::{playlist_songs, playlists, users, DB};
|
||||
|
||||
mod error;
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[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)]
|
||||
pub struct Manager {
|
||||
|
@ -251,3 +261,125 @@ struct NewPlaylistSong {
|
|||
struct User {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
|
@ -6,9 +6,27 @@ use std::time::Duration;
|
|||
|
||||
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)]
|
||||
pub struct AuthSecret {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -1,17 +1,29 @@
|
|||
use anyhow::*;
|
||||
use image::ImageOutputFormat;
|
||||
use image::{DynamicImage, GenericImage, GenericImageView, ImageBuffer, ImageOutputFormat};
|
||||
use std::cmp;
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::fs::{self, File};
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
mod generate;
|
||||
mod options;
|
||||
mod read;
|
||||
use crate::utils::{get_audio_format, AudioFormat};
|
||||
|
||||
pub use generate::*;
|
||||
pub use options::*;
|
||||
pub use read::*;
|
||||
#[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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Manager {
|
||||
|
@ -66,3 +78,178 @@ impl Manager {
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -7,9 +7,6 @@ use std::path::{self, Path, PathBuf};
|
|||
|
||||
use crate::db::{mount_points, DB};
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Insertable, PartialEq, Eq, Queryable, Serialize)]
|
||||
#[diesel(table_name = mount_points)]
|
||||
pub struct MountDir {
|
||||
|
@ -115,3 +112,83 @@ impl Manager {
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue