diff --git a/docs/swagger/polaris-api.json b/docs/swagger/polaris-api.json index d6e07ee..29488f0 100644 --- a/docs/swagger/polaris-api.json +++ b/docs/swagger/polaris-api.json @@ -2,7 +2,7 @@ "openapi": "3.0.0", "info": { "description": "", - "version": "4.0", + "version": "5.0", "title": "Polaris", "termsOfService": "" }, @@ -444,13 +444,13 @@ ] } }, - "/serve/{file}": { + "/audio/{file}": { "get": { "tags": [ "Collection" ], "summary": "Access a media file in the collection", - "operationId": "getServe", + "operationId": "getAudio", "parameters": [ { "name": "file", @@ -465,12 +465,53 @@ "200": { "description": "Successful operation", "content": { - "image/*": { + "audio/*": { "schema": { "format": "binary" } - }, - "audio/*": { + } + } + } + }, + "security": [ + { + "auth_http_header": [], + "auth_cookie": [] + } + ] + } + }, + "/thumbnail/{file}": { + "get": { + "tags": [ + "Collection" + ], + "summary": "Generate an image thumbnail for a media file in the collection", + "operationId": "getServe", + "parameters": [ + { + "name": "file", + "in": "path", + "description": "Path to the desired file", + "schema": { + "type": "string" + } + }, + { + "name": "pad", + "in": "query", + "description": "Indicates whether the thumbnail should be padded to a square aspect-ratio", + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Successful operation", + "content": { + "image/*": { "schema": { "format": "binary" } diff --git a/src/service/constants.rs b/src/service/constants.rs index a36931c..c9cc5d1 100644 --- a/src/service/constants.rs +++ b/src/service/constants.rs @@ -1,4 +1,4 @@ -pub const API_MAJOR_VERSION: i32 = 4; +pub const API_MAJOR_VERSION: i32 = 5; pub const API_MINOR_VERSION: i32 = 0; pub const COOKIE_SESSION: &str = "session"; pub const COOKIE_USERNAME: &str = "username"; diff --git a/src/service/rocket/api.rs b/src/service/rocket/api.rs index e5776bc..53771c6 100644 --- a/src/service/rocket/api.rs +++ b/src/service/rocket/api.rs @@ -4,6 +4,7 @@ use rocket::request::{self, FromParam, FromRequest, Request}; use rocket::response::content::Html; use rocket::{delete, get, post, put, routes, Outcome, State}; use rocket_contrib::json::Json; +use std::default::Default; use std::fs::File; use std::ops::Deref; use std::path::PathBuf; @@ -23,7 +24,6 @@ use crate::service::dto; use crate::service::error::APIError; use crate::thumbnails; use crate::user; -use crate::utils; use crate::vfs::VFSSource; pub fn get_routes() -> Vec { @@ -44,7 +44,8 @@ pub fn get_routes() -> Vec { recent, search_root, search, - serve, + audio, + thumbnail, list_playlists, save_playlist, read_playlist, @@ -305,21 +306,25 @@ fn search( Ok(Json(result)) } -#[get("/serve/")] -fn serve(db: State<'_, DB>, _auth: Auth, path: VFSPathBuf) -> Result> { +#[get("/audio/")] +fn audio(db: State<'_, DB>, _auth: Auth, path: VFSPathBuf) -> Result> { let vfs = db.get_vfs()?; let real_path = vfs.virtual_to_real(&path.into() as &PathBuf)?; - - let serve_path = if utils::is_image(&real_path) { - thumbnails::get_thumbnail(&real_path, 400)? - } else { - real_path - }; - - let file = File::open(serve_path)?; + let file = File::open(&real_path)?; Ok(serve::RangeResponder::new(file)) } +#[get("/thumbnail/?")] +fn thumbnail(db: State<'_, DB>, _auth: Auth, path: VFSPathBuf, pad: Option) -> Result { + let vfs = db.get_vfs()?; + let image_path = vfs.virtual_to_real(&path.into() as &PathBuf)?; + let mut options = thumbnails::Options::default(); + options.pad_to_square = pad.unwrap_or(options.pad_to_square); + let thumbnail_path = thumbnails::get_thumbnail(&image_path, &options)?; + let file = File::open(thumbnail_path)?; + Ok(file) +} + #[get("/playlists")] fn list_playlists(db: State<'_, DB>, auth: Auth) -> Result>> { let playlist_names = playlist::list_playlists(&auth.username, db.deref().deref())?; diff --git a/src/service/test.rs b/src/service/test.rs index 8da08f2..9b6db05 100644 --- a/src/service/test.rs +++ b/src/service/test.rs @@ -107,7 +107,7 @@ fn test_service_version() { let mut service = ServiceType::new(function_name!()); let response = service.get_json::("/api/version"); let version = response.body(); - assert_eq!(version, &dto::Version { major: 4, minor: 0 }); + assert_eq!(version, &dto::Version { major: 5, minor: 0 }); } #[named] @@ -389,7 +389,7 @@ fn test_service_serve() { path.push("Hunted"); path.push("02 - Candlelight.mp3"); let uri = format!( - "/api/serve/{}", + "/api/audio/{}", percent_encode(path.to_string_lossy().as_ref().as_bytes(), NON_ALPHANUMERIC) ); diff --git a/src/thumbnails.rs b/src/thumbnails.rs index 1ec32b1..a716ce4 100644 --- a/src/thumbnails.rs +++ b/src/thumbnails.rs @@ -16,15 +16,31 @@ use crate::utils; const THUMBNAILS_PATH: &str = "thumbnails"; -fn hash(path: &Path, dimension: u32) -> u64 { - let path_string = path.to_string_lossy(); - let hash_input = format!("{}:{}", path_string, dimension.to_string()); +#[derive(Debug, Hash)] +pub struct Options { + pub max_dimension: u32, + pub resize_if_almost_square: bool, + pub pad_to_square: bool, +} + +impl Default for Options { + fn default() -> Options { + Options { + max_dimension: 400, + resize_if_almost_square: true, + pad_to_square: true, + } + } +} + +fn hash(path: &Path, options: &Options) -> u64 { let mut hasher = DefaultHasher::new(); - hash_input.hash(&mut hasher); + path.hash(&mut hasher); + options.hash(&mut hasher); hasher.finish() } -pub fn get_thumbnail(real_path: &Path, max_dimension: u32) -> Result { +pub fn get_thumbnail(real_path: &Path, options: &Options) -> Result { let mut out_path = utils::get_data_root()?; out_path.push(THUMBNAILS_PATH); @@ -35,17 +51,21 @@ pub fn get_thumbnail(real_path: &Path, max_dimension: u32) -> Result { let source_image = image::open(real_path)?; let (source_width, source_height) = source_image.dimensions(); let largest_dimension = cmp::max(source_width, source_height); - let out_dimension = cmp::min(max_dimension, largest_dimension); + let out_dimension = cmp::min(options.max_dimension, largest_dimension); - let hash = hash(real_path, out_dimension); + let hash = hash(real_path, options); out_path.push(format!("{}.jpg", hash.to_string())); if !out_path.exists() { let quality = 80; 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 source_aspect_ratio < 0.8 || source_aspect_ratio > 1.2 { + if is_almost_square && options.resize_if_almost_square { + final_image = + source_image.resize_exact(out_dimension, out_dimension, FilterType::Lanczos3); + } else if options.pad_to_square { let scaled_image = source_image.resize(out_dimension, out_dimension, FilterType::Lanczos3); let (scaled_width, scaled_height) = scaled_image.dimensions(); @@ -61,9 +81,8 @@ pub fn get_thumbnail(real_path: &Path, max_dimension: u32) -> Result { (out_dimension - scaled_height) / 2, ); } else { - final_image = - source_image.resize_exact(out_dimension, out_dimension, FilterType::Lanczos3); - }; + final_image = source_image.resize(out_dimension, out_dimension, FilterType::Lanczos3); + } let mut out_file = File::create(&out_path)?; final_image.write_to(&mut out_file, ImageOutputFormat::JPEG(quality))?; diff --git a/src/utils.rs b/src/utils.rs index 813cd2f..7c4ceba 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -60,28 +60,3 @@ fn test_get_audio_format() { Some(AudioFormat::FLAC) ); } - -pub fn is_image(path: &Path) -> bool { - let extension = match path.extension() { - Some(e) => e, - _ => return false, - }; - let extension = match extension.to_str() { - Some(e) => e, - _ => return false, - }; - match extension.to_lowercase().as_str() { - "png" => true, - "gif" => true, - "jpg" => true, - "jpeg" => true, - "bmp" => true, - _ => false, - } -} - -#[test] -fn test_is_image() { - assert!(!is_image(Path::new("animals/🐷/my🐖file.mp3"))); - assert!(is_image(Path::new("animals/🐷/my🐖file.jpg"))); -}