add audio waveform thumbnails
parent
7540b31ab8
commit
20a6fa0de7
|
@ -0,0 +1,43 @@
|
||||||
|
use anyhow::anyhow;
|
||||||
|
use std::io::Read;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
use super::Previewable;
|
||||||
|
|
||||||
|
pub struct AudioPath<'a>(pub &'a Path);
|
||||||
|
|
||||||
|
const COLOR: &str = "#dc322f"; // solarized red
|
||||||
|
|
||||||
|
impl<'a> Previewable for AudioPath<'a> {
|
||||||
|
fn get_thumbnail(&self) -> Result<Option<Vec<u8>>> {
|
||||||
|
let outfile = tempfile::Builder::new().suffix(".webp").tempfile()?;
|
||||||
|
let thumbnail_cmd = Command::new("ffmpeg")
|
||||||
|
.args(["-i", &self.0.to_string_lossy()])
|
||||||
|
.args([
|
||||||
|
"-filter_complex",
|
||||||
|
&format!(
|
||||||
|
"[0:a]aformat=channel_layouts=mono, compand=gain=-2,
|
||||||
|
showwavespic=s=860x256:colors={COLOR},
|
||||||
|
drawbox=x=(iw-w)/2:y=(ih-h)/2:w=iw:h=1:color={COLOR}"
|
||||||
|
),
|
||||||
|
])
|
||||||
|
.args(["-vframes", "1"])
|
||||||
|
.arg(&*outfile.path().to_string_lossy())
|
||||||
|
.arg("-y")
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
if !thumbnail_cmd.status.success() {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"Failed to render thumbnail: {:?}",
|
||||||
|
String::from_utf8_lossy(&thumbnail_cmd.stderr)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
outfile.as_file().read_to_end(&mut buffer)?;
|
||||||
|
Ok(Some(buffer))
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,10 +12,12 @@ use std::{
|
||||||
use self::image::ImagePath;
|
use self::image::ImagePath;
|
||||||
use self::text::TextPath;
|
use self::text::TextPath;
|
||||||
use self::video::VideoPath;
|
use self::video::VideoPath;
|
||||||
|
use self::audio::AudioPath;
|
||||||
|
|
||||||
pub mod image;
|
pub mod image;
|
||||||
pub mod text;
|
pub mod text;
|
||||||
pub mod video;
|
pub mod video;
|
||||||
|
pub mod audio;
|
||||||
|
|
||||||
pub trait Previewable {
|
pub trait Previewable {
|
||||||
fn get_thumbnail(&self) -> Result<Option<Vec<u8>>>;
|
fn get_thumbnail(&self) -> Result<Option<Vec<u8>>>;
|
||||||
|
@ -63,6 +65,9 @@ impl PreviewStore {
|
||||||
Some(tm) if tm.starts_with("video") || tm == "application/x-matroska" => {
|
Some(tm) if tm.starts_with("video") || tm == "application/x-matroska" => {
|
||||||
Ok(VideoPath(&file.path).get_thumbnail()?)
|
Ok(VideoPath(&file.path).get_thumbnail()?)
|
||||||
}
|
}
|
||||||
|
Some(tm) if tm.starts_with("audio") => {
|
||||||
|
Ok(AudioPath(&file.path).get_thumbnail()?)
|
||||||
|
}
|
||||||
Some(tm) if tm.starts_with("image") => {
|
Some(tm) if tm.starts_with("image") => {
|
||||||
Ok(ImagePath(&file.path).get_thumbnail()?)
|
Ok(ImagePath(&file.path).get_thumbnail()?)
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,10 +31,19 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if audio}
|
{#if audio}
|
||||||
|
{#if imageLoaded != address}
|
||||||
|
<Spinner />
|
||||||
|
{/if}
|
||||||
|
<img
|
||||||
|
src="/api/thumb/{address}"
|
||||||
|
alt={address}
|
||||||
|
on:load={() => (imageLoaded = address)}
|
||||||
|
on:error={() => (imageLoaded = address)}
|
||||||
|
/>
|
||||||
<audio controls preload="auto" src="/api/raw/{address}" />
|
<audio controls preload="auto" src="/api/raw/{address}" />
|
||||||
{/if}
|
{/if}
|
||||||
{#if video}
|
{#if video}
|
||||||
{#if imageLoaded != address }
|
{#if imageLoaded != address}
|
||||||
<Spinner />
|
<Spinner />
|
||||||
|
|
||||||
<img
|
<img
|
||||||
|
@ -83,6 +92,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
audio,
|
audio,
|
||||||
|
|
Loading…
Reference in New Issue