Async support for thumbnails and peaks

This commit is contained in:
Antoine Gersant 2024-09-02 13:57:25 -07:00
parent 9a30065971
commit afc5fcb4c2
3 changed files with 76 additions and 48 deletions

View file

@ -1,7 +1,5 @@
use std::{
fs::{self, File},
hash::{DefaultHasher, Hash, Hasher},
io::{self, Write},
path::{Path, PathBuf},
};
@ -14,6 +12,7 @@ use symphonia::core::{
meta::MetadataOptions,
probe::Hint,
};
use tokio::{io::AsyncWriteExt, task::spawn_blocking};
use crate::app::Error;
@ -32,10 +31,10 @@ impl Manager {
Self { peaks_dir_path }
}
pub fn get_peaks(&self, audio_path: &Path) -> Result<Peaks, Error> {
match self.read_from_cache(audio_path) {
pub async fn get_peaks(&self, audio_path: &Path) -> Result<Peaks, Error> {
match self.read_from_cache(audio_path).await {
Ok(Some(peaks)) => Ok(peaks),
_ => self.read_from_audio_file(audio_path),
_ => self.read_from_source(audio_path).await,
}
}
@ -46,10 +45,12 @@ impl Manager {
peaks_path
}
fn read_from_cache(&self, audio_path: &Path) -> Result<Option<Peaks>, Error> {
async fn read_from_cache(&self, audio_path: &Path) -> Result<Option<Peaks>, Error> {
let peaks_path = self.get_peaks_path(audio_path);
if peaks_path.exists() {
let serialized = fs::read(&peaks_path).map_err(|e| Error::Io(peaks_path.clone(), e))?;
let serialized = tokio::fs::read(&peaks_path)
.await
.map_err(|e| Error::Io(peaks_path.clone(), e))?;
let peaks =
bitcode::deserialize::<Peaks>(&serialized).map_err(Error::PeaksDeserialization)?;
Ok(Some(peaks))
@ -58,16 +59,27 @@ impl Manager {
}
}
fn read_from_audio_file(&self, audio_path: &Path) -> Result<Peaks, Error> {
let peaks = compute_peaks(audio_path)?;
async fn read_from_source(&self, audio_path: &Path) -> Result<Peaks, Error> {
let peaks = spawn_blocking({
let audio_path = audio_path.to_owned();
move || compute_peaks(&audio_path)
})
.await??;
let serialized = bitcode::serialize(&peaks).map_err(Error::PeaksSerialization)?;
fs::create_dir_all(&self.peaks_dir_path)
tokio::fs::create_dir_all(&self.peaks_dir_path)
.await
.map_err(|e| Error::Io(self.peaks_dir_path.clone(), e))?;
let path = self.get_peaks_path(audio_path);
let mut out_file = File::create(&path).map_err(|e| Error::Io(path.clone(), e))?;
let mut out_file = tokio::fs::File::create(&path)
.await
.map_err(|e| Error::Io(path.clone(), e))?;
out_file
.write_all(&serialized)
.await
.map_err(|e| Error::Io(path.clone(), e))?;
Ok(peaks)
@ -83,7 +95,8 @@ impl Manager {
fn compute_peaks(audio_path: &Path) -> Result<Peaks, Error> {
let peaks_per_minute = 4000;
let file = File::open(&audio_path).or_else(|e| Err(Error::Io(audio_path.to_owned(), e)))?;
let file =
std::fs::File::open(&audio_path).or_else(|e| Err(Error::Io(audio_path.to_owned(), e)))?;
let media_source = MediaSourceStream::new(Box::new(file), MediaSourceStreamOptions::default());
let mut peaks = Peaks::default();
@ -118,7 +131,7 @@ fn compute_peaks(audio_path: &Path) -> Result<Peaks, Error> {
let packet = match format.next_packet() {
Ok(packet) => packet,
Err(symphonia::core::errors::Error::IoError(e))
if e.kind() == io::ErrorKind::UnexpectedEof =>
if e.kind() == std::io::ErrorKind::UnexpectedEof =>
{
break;
}

View file

@ -1,15 +1,16 @@
use image::codecs::jpeg::JpegEncoder;
use image::{DynamicImage, GenericImage, GenericImageView, ImageBuffer};
use std::cmp;
use std::collections::hash_map::DefaultHasher;
use std::fs::{self, File};
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
use image::codecs::jpeg::JpegEncoder;
use image::{DynamicImage, GenericImage, GenericImageView, ImageBuffer};
use tokio::task::spawn_blocking;
use crate::app::Error;
use crate::utils::{get_audio_format, AudioFormat};
#[derive(Debug, Hash)]
#[derive(Clone, Debug, Hash)]
pub struct Options {
pub max_dimension: Option<u32>,
pub resize_if_almost_square: bool,
@ -38,56 +39,70 @@ impl Manager {
}
}
pub fn get_thumbnail(
pub async fn get_thumbnail(
&self,
image_path: &Path,
thumbnailoptions: &Options,
options: &Options,
) -> Result<PathBuf, Error> {
match self.retrieve_thumbnail(image_path, thumbnailoptions) {
match self.read_from_cache(image_path, options).await {
Some(path) => Ok(path),
None => self.create_thumbnail(image_path, thumbnailoptions),
None => self.read_from_source(image_path, options).await,
}
}
fn get_thumbnail_path(&self, image_path: &Path, thumbnailoptions: &Options) -> PathBuf {
let hash = Manager::hash(image_path, thumbnailoptions);
fn get_thumbnail_path(&self, image_path: &Path, options: &Options) -> PathBuf {
let hash = Manager::hash(image_path, options);
let mut thumbnail_path = self.thumbnails_dir_path.clone();
thumbnail_path.push(format!("{}.jpg", hash));
thumbnail_path
}
fn retrieve_thumbnail(&self, image_path: &Path, thumbnailoptions: &Options) -> Option<PathBuf> {
let path = self.get_thumbnail_path(image_path, thumbnailoptions);
if path.exists() {
Some(path)
} else {
None
async fn read_from_cache(&self, image_path: &Path, options: &Options) -> Option<PathBuf> {
let path = self.get_thumbnail_path(image_path, options);
match tokio::fs::try_exists(&path).await.ok() {
Some(true) => Some(path),
_ => None,
}
}
fn create_thumbnail(
async fn read_from_source(
&self,
image_path: &Path,
thumbnailoptions: &Options,
options: &Options,
) -> Result<PathBuf, Error> {
let thumbnail = generate_thumbnail(image_path, thumbnailoptions)?;
let quality = 80;
let thumbnail = spawn_blocking({
let image_path = image_path.to_owned();
let options = options.clone();
move || generate_thumbnail(&image_path, &options)
})
.await??;
fs::create_dir_all(&self.thumbnails_dir_path)
tokio::fs::create_dir_all(&self.thumbnails_dir_path)
.await
.map_err(|e| Error::Io(self.thumbnails_dir_path.clone(), e))?;
let path = self.get_thumbnail_path(image_path, thumbnailoptions);
let mut out_file =
File::create(&path).map_err(|e| Error::Io(self.thumbnails_dir_path.clone(), e))?;
thumbnail
.write_with_encoder(JpegEncoder::new_with_quality(&mut out_file, quality))
.map_err(|e| Error::Image(image_path.to_owned(), e))?;
let path = self.get_thumbnail_path(image_path, options);
let out_file = tokio::fs::File::create(&path)
.await
.map_err(|e| Error::Io(self.thumbnails_dir_path.clone(), e))?;
spawn_blocking({
let mut out_file = out_file.into_std().await;
move || {
let quality = 80;
thumbnail.write_with_encoder(JpegEncoder::new_with_quality(&mut out_file, quality))
}
})
.await?
.map_err(|e| Error::Image(image_path.to_owned(), e))?;
Ok(path)
}
fn hash(path: &Path, thumbnailoptions: &Options) -> u64 {
fn hash(path: &Path, options: &Options) -> u64 {
let mut hasher = DefaultHasher::new();
path.hash(&mut hasher);
thumbnailoptions.hash(&mut hasher);
options.hash(&mut hasher);
hasher.finish()
}
}

View file

@ -11,7 +11,6 @@ use axum_extra::TypedHeader;
use axum_range::{KnownSize, Ranged};
use base64::{prelude::BASE64_STANDARD_NO_PAD, Engine};
use percent_encoding::percent_decode_str;
use tokio::task::spawn_blocking;
use tower_http::{compression::CompressionLayer, CompressionLevel};
use crate::{
@ -462,10 +461,8 @@ async fn get_peaks(
) -> Result<dto::Peaks, APIError> {
let vfs = vfs_manager.get_vfs().await?;
let audio_path = vfs.virtual_to_real(&path)?;
let peaks = spawn_blocking(move || peaks_manager.get_peaks(&audio_path))
.await
.or(Err(APIError::Internal))?;
Ok(peaks?.interleaved)
let peaks = peaks_manager.get_peaks(&audio_path).await?;
Ok(peaks.interleaved)
}
async fn get_random(
@ -605,7 +602,10 @@ async fn get_thumbnail(
let options = thumbnail::Options::from(options_input);
let vfs = vfs_manager.get_vfs().await?;
let image_path = vfs.virtual_to_real(&path)?;
let thumbnail_path = thumbnails_manager.get_thumbnail(&image_path, &options)?;
let thumbnail_path = thumbnails_manager
.get_thumbnail(&image_path, &options)
.await?;
let Ok(file) = tokio::fs::File::open(thumbnail_path).await else {
return Err(APIError::ThumbnailFileIOError);