Async support for thumbnails and peaks
This commit is contained in:
parent
9a30065971
commit
afc5fcb4c2
3 changed files with 76 additions and 48 deletions
src
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Reference in a new issue