mirror of
https://git.asonix.dog/asonix/pict-rs
synced 2024-12-22 19:31:35 +00:00
It compiles
Need to reinstate media limits
This commit is contained in:
parent
ad1837f9dd
commit
33b83f97f2
20 changed files with 403 additions and 1429 deletions
|
@ -12,10 +12,9 @@ use defaults::Defaults;
|
||||||
|
|
||||||
pub(crate) use commandline::Operation;
|
pub(crate) use commandline::Operation;
|
||||||
pub(crate) use file::{
|
pub(crate) use file::{
|
||||||
ConfigFile as Configuration, Media as MediaConfiguration, ObjectStorage, OpenTelemetry, Repo,
|
ConfigFile as Configuration, ObjectStorage, OpenTelemetry, Repo, Sled, Store, Tracing,
|
||||||
Sled, Store, Tracing,
|
|
||||||
};
|
};
|
||||||
pub(crate) use primitives::{AudioCodec, Filesystem, ImageFormat, LogFormat, VideoCodec};
|
pub(crate) use primitives::{Filesystem, LogFormat};
|
||||||
|
|
||||||
/// Source for pict-rs configuration when embedding as a library
|
/// Source for pict-rs configuration when embedding as a library
|
||||||
pub enum ConfigSource<P, T> {
|
pub enum ConfigSource<P, T> {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
config::primitives::{AudioCodec, ImageFormat, LogFormat, Targets, VideoCodec},
|
config::primitives::{AudioCodec, LogFormat, Targets, VideoCodec},
|
||||||
|
formats::ImageFormat,
|
||||||
serde_str::Serde,
|
serde_str::Serde,
|
||||||
};
|
};
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
config::primitives::{AudioCodec, Filesystem, ImageFormat, LogFormat, Targets, VideoCodec},
|
config::primitives::{AudioCodec, Filesystem, LogFormat, Targets, VideoCodec},
|
||||||
|
formats::ImageFormat,
|
||||||
serde_str::Serde,
|
serde_str::Serde,
|
||||||
};
|
};
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
use crate::magick::ValidInputType;
|
|
||||||
use clap::ValueEnum;
|
use clap::ValueEnum;
|
||||||
use std::{fmt::Display, path::PathBuf, str::FromStr};
|
use std::{fmt::Display, path::PathBuf, str::FromStr};
|
||||||
use tracing::Level;
|
use tracing::Level;
|
||||||
|
@ -25,28 +24,6 @@ pub(crate) enum LogFormat {
|
||||||
Pretty,
|
Pretty,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(
|
|
||||||
Clone,
|
|
||||||
Copy,
|
|
||||||
Debug,
|
|
||||||
PartialEq,
|
|
||||||
Eq,
|
|
||||||
PartialOrd,
|
|
||||||
Ord,
|
|
||||||
Hash,
|
|
||||||
serde::Deserialize,
|
|
||||||
serde::Serialize,
|
|
||||||
ValueEnum,
|
|
||||||
)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
pub(crate) enum ImageFormat {
|
|
||||||
Avif,
|
|
||||||
Jpeg,
|
|
||||||
Jxl,
|
|
||||||
Png,
|
|
||||||
Webp,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(
|
#[derive(
|
||||||
Clone,
|
Clone,
|
||||||
Copy,
|
Copy,
|
||||||
|
@ -169,32 +146,6 @@ pub(crate) enum Store {
|
||||||
ObjectStorage(ObjectStorage),
|
ObjectStorage(ObjectStorage),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ImageFormat {
|
|
||||||
pub(crate) fn as_hint(self) -> ValidInputType {
|
|
||||||
ValidInputType::from_format(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn as_magick_format(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Avif => "AVIF",
|
|
||||||
Self::Jpeg => "JPEG",
|
|
||||||
Self::Jxl => "JXL",
|
|
||||||
Self::Png => "PNG",
|
|
||||||
Self::Webp => "WEBP",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn as_ext(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Avif => ".avif",
|
|
||||||
Self::Jpeg => ".jpeg",
|
|
||||||
Self::Jxl => ".jxl",
|
|
||||||
Self::Png => ".png",
|
|
||||||
Self::Webp => ".webp",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Filesystem> for Store {
|
impl From<Filesystem> for Store {
|
||||||
fn from(f: Filesystem) -> Self {
|
fn from(f: Filesystem) -> Self {
|
||||||
Self::Filesystem(f)
|
Self::Filesystem(f)
|
||||||
|
@ -260,21 +211,6 @@ impl Display for Targets {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ImageFormat {
|
|
||||||
type Err = String;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
match s.to_lowercase().as_str() {
|
|
||||||
"avif" => Ok(Self::Avif),
|
|
||||||
"jpeg" | "jpg" => Ok(Self::Jpeg),
|
|
||||||
"jxl" => Ok(Self::Jxl),
|
|
||||||
"png" => Ok(Self::Png),
|
|
||||||
"webp" => Ok(Self::Webp),
|
|
||||||
other => Err(format!("Invalid variant: {other}")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for LogFormat {
|
impl FromStr for LogFormat {
|
||||||
type Err = String;
|
type Err = String;
|
||||||
|
|
||||||
|
@ -288,15 +224,6 @@ impl FromStr for LogFormat {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for ImageFormat {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
self.to_possible_value()
|
|
||||||
.expect("no values are skipped")
|
|
||||||
.get_name()
|
|
||||||
.fmt(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for LogFormat {
|
impl Display for LogFormat {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
self.to_possible_value()
|
self.to_possible_value()
|
||||||
|
|
106
src/details.rs
106
src/details.rs
|
@ -1,7 +1,7 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
|
discover::DiscoveryLite,
|
||||||
error::Error,
|
error::Error,
|
||||||
formats::{InternalFormat, InternalVideoFormat},
|
formats::{InternalFormat, InternalVideoFormat},
|
||||||
magick::{video_mp4, video_webm, ValidInputType},
|
|
||||||
serde_str::Serde,
|
serde_str::Serde,
|
||||||
store::Store,
|
store::Store,
|
||||||
};
|
};
|
||||||
|
@ -17,9 +17,9 @@ pub(crate) enum MaybeHumanDate {
|
||||||
|
|
||||||
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||||
pub(crate) struct Details {
|
pub(crate) struct Details {
|
||||||
width: usize,
|
width: u16,
|
||||||
height: usize,
|
height: u16,
|
||||||
frames: Option<usize>,
|
frames: Option<u32>,
|
||||||
content_type: Serde<mime::Mime>,
|
content_type: Serde<mime::Mime>,
|
||||||
created_at: MaybeHumanDate,
|
created_at: MaybeHumanDate,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
@ -27,71 +27,41 @@ pub(crate) struct Details {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Details {
|
impl Details {
|
||||||
pub(crate) fn is_motion(&self) -> bool {
|
pub(crate) fn is_video(&self) -> bool {
|
||||||
self.content_type.type_() == "video"
|
self.content_type.type_() == "video"
|
||||||
|| self.content_type.type_() == "image" && self.content_type.subtype() == "gif"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn from_bytes(input: web::Bytes, hint: ValidInputType) -> Result<Self, Error> {
|
pub(crate) async fn from_bytes(input: web::Bytes) -> Result<Self, Error> {
|
||||||
let details = if hint.is_video() {
|
let DiscoveryLite {
|
||||||
crate::ffmpeg::details_bytes(input.clone()).await?
|
format,
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let details = if let Some(details) = details {
|
|
||||||
details
|
|
||||||
} else {
|
|
||||||
crate::magick::details_bytes(input, Some(hint)).await?
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Details::now(
|
|
||||||
details.width,
|
|
||||||
details.height,
|
|
||||||
details.mime_type,
|
|
||||||
details.frames,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn from_store<S: Store + 'static>(
|
|
||||||
store: S,
|
|
||||||
identifier: S::Identifier,
|
|
||||||
expected_format: Option<ValidInputType>,
|
|
||||||
) -> Result<Self, Error> {
|
|
||||||
let details = if expected_format.map(|t| t.is_video()).unwrap_or(true) {
|
|
||||||
crate::ffmpeg::details_store(&store, &identifier).await?
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let details = if let Some(details) = details {
|
|
||||||
details
|
|
||||||
} else {
|
|
||||||
crate::magick::details_store(store, identifier, expected_format).await?
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Details::now(
|
|
||||||
details.width,
|
|
||||||
details.height,
|
|
||||||
details.mime_type,
|
|
||||||
details.frames,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn now(
|
|
||||||
width: usize,
|
|
||||||
height: usize,
|
|
||||||
content_type: mime::Mime,
|
|
||||||
frames: Option<usize>,
|
|
||||||
) -> Self {
|
|
||||||
Details {
|
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
frames,
|
frames,
|
||||||
content_type: Serde::new(content_type),
|
} = crate::discover::discover_bytes_lite(input).await?;
|
||||||
created_at: MaybeHumanDate::HumanDate(time::OffsetDateTime::now_utc()),
|
|
||||||
format: None,
|
Ok(Details::from_parts(format, width, height, frames))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn from_store<S: Store + 'static>(
|
||||||
|
store: &S,
|
||||||
|
identifier: &S::Identifier,
|
||||||
|
) -> Result<Self, Error> {
|
||||||
|
let DiscoveryLite {
|
||||||
|
format,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
frames,
|
||||||
|
} = crate::discover::discover_store_lite(store, identifier).await?;
|
||||||
|
|
||||||
|
Ok(Details::from_parts(format, width, height, frames))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn internal_format(&self) -> Option<InternalFormat> {
|
||||||
|
if let Some(format) = self.format {
|
||||||
|
return Some(format);
|
||||||
|
}
|
||||||
|
|
||||||
|
InternalFormat::maybe_from_media_type(&self.content_type, self.frames.is_some())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn content_type(&self) -> mime::Mime {
|
pub(crate) fn content_type(&self) -> mime::Mime {
|
||||||
|
@ -102,12 +72,12 @@ impl Details {
|
||||||
self.created_at.into()
|
self.created_at.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn input_format(&self) -> Option<InternalVideoFormat> {
|
pub(crate) fn video_format(&self) -> Option<InternalVideoFormat> {
|
||||||
if *self.content_type == video_mp4() {
|
if *self.content_type == crate::formats::mimes::video_mp4() {
|
||||||
return Some(InternalVideoFormat::Mp4);
|
return Some(InternalVideoFormat::Mp4);
|
||||||
}
|
}
|
||||||
|
|
||||||
if *self.content_type == video_webm() {
|
if *self.content_type == crate::formats::mimes::video_webm() {
|
||||||
return Some(InternalVideoFormat::Webm);
|
return Some(InternalVideoFormat::Webm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,9 +91,9 @@ impl Details {
|
||||||
frames: Option<u32>,
|
frames: Option<u32>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
width: width.into(),
|
width,
|
||||||
height: height.into(),
|
height,
|
||||||
frames: frames.map(|f| f.try_into().expect("Reasonable size")),
|
frames,
|
||||||
content_type: Serde::new(format.media_type()),
|
content_type: Serde::new(format.media_type()),
|
||||||
created_at: MaybeHumanDate::HumanDate(OffsetDateTime::now_utc()),
|
created_at: MaybeHumanDate::HumanDate(OffsetDateTime::now_utc()),
|
||||||
format: Some(format),
|
format: Some(format),
|
||||||
|
|
|
@ -4,7 +4,10 @@ mod magick;
|
||||||
|
|
||||||
use actix_web::web::Bytes;
|
use actix_web::web::Bytes;
|
||||||
|
|
||||||
use crate::formats::{AnimationInput, ImageInput, InputFile, InternalFormat, InternalVideoFormat};
|
use crate::{
|
||||||
|
formats::{InputFile, InternalFormat},
|
||||||
|
store::Store,
|
||||||
|
};
|
||||||
|
|
||||||
pub(crate) struct Discovery {
|
pub(crate) struct Discovery {
|
||||||
pub(crate) input: InputFile,
|
pub(crate) input: InputFile,
|
||||||
|
@ -13,20 +16,42 @@ pub(crate) struct Discovery {
|
||||||
pub(crate) frames: Option<u32>,
|
pub(crate) frames: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Discovery {
|
pub(crate) struct DiscoveryLite {
|
||||||
pub(crate) fn internal_format(&self) -> InternalFormat {
|
pub(crate) format: InternalFormat,
|
||||||
match self.input {
|
pub(crate) width: u16,
|
||||||
InputFile::Image(ImageInput { format, .. }) => InternalFormat::Image(format),
|
pub(crate) height: u16,
|
||||||
InputFile::Animation(AnimationInput { format }) => InternalFormat::Animation(format),
|
pub(crate) frames: Option<u32>,
|
||||||
// we're making up bs now lol
|
}
|
||||||
InputFile::Video(crate::formats::VideoFormat::Mp4) => {
|
|
||||||
InternalFormat::Video(InternalVideoFormat::Mp4)
|
pub(crate) async fn discover_bytes_lite(
|
||||||
}
|
bytes: Bytes,
|
||||||
InputFile::Video(crate::formats::VideoFormat::Webm { .. }) => {
|
) -> Result<DiscoveryLite, crate::error::Error> {
|
||||||
InternalFormat::Video(InternalVideoFormat::Webm)
|
if let Some(discovery) = ffmpeg::discover_bytes_lite(bytes.clone()).await? {
|
||||||
}
|
return Ok(discovery);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let discovery = magick::discover_bytes_lite(bytes).await?;
|
||||||
|
|
||||||
|
Ok(discovery)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn discover_store_lite<S>(
|
||||||
|
store: &S,
|
||||||
|
identifier: &S::Identifier,
|
||||||
|
) -> Result<DiscoveryLite, crate::error::Error>
|
||||||
|
where
|
||||||
|
S: Store + 'static,
|
||||||
|
{
|
||||||
|
if let Some(discovery) =
|
||||||
|
ffmpeg::discover_stream_lite(store.to_stream(identifier, None, None).await?).await?
|
||||||
|
{
|
||||||
|
return Ok(discovery);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let discovery =
|
||||||
|
magick::discover_stream_lite(store.to_stream(identifier, None, None).await?).await?;
|
||||||
|
|
||||||
|
Ok(discovery)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn discover_bytes(bytes: Bytes) -> Result<Discovery, crate::error::Error> {
|
pub(crate) async fn discover_bytes(bytes: Bytes) -> Result<Discovery, crate::error::Error> {
|
||||||
|
|
|
@ -2,43 +2,25 @@ use std::{collections::HashSet, sync::OnceLock};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ffmpeg::FfMpegError,
|
ffmpeg::FfMpegError,
|
||||||
formats::{AnimationFormat, AnimationInput, ImageFormat, ImageInput, InputFile, VideoFormat},
|
formats::{
|
||||||
|
AnimationFormat, AnimationInput, ImageFormat, ImageInput, InputFile, InternalFormat,
|
||||||
|
InternalVideoFormat, VideoFormat,
|
||||||
|
},
|
||||||
process::Process,
|
process::Process,
|
||||||
};
|
};
|
||||||
use actix_web::web::Bytes;
|
use actix_web::web::Bytes;
|
||||||
|
use futures_util::Stream;
|
||||||
use tokio::io::AsyncReadExt;
|
use tokio::io::AsyncReadExt;
|
||||||
|
|
||||||
use super::Discovery;
|
use super::{Discovery, DiscoveryLite};
|
||||||
|
|
||||||
const FFMPEG_FORMAT_MAPPINGS: &[(&str, InputFile)] = &[
|
const FFMPEG_FORMAT_MAPPINGS: &[(&str, InternalFormat)] = &[
|
||||||
(
|
("apng", InternalFormat::Animation(AnimationFormat::Apng)),
|
||||||
"apng",
|
("gif", InternalFormat::Animation(AnimationFormat::Gif)),
|
||||||
InputFile::Animation(AnimationInput {
|
("mp4", InternalFormat::Video(InternalVideoFormat::Mp4)),
|
||||||
format: AnimationFormat::Apng,
|
("png_pipe", InternalFormat::Image(ImageFormat::Png)),
|
||||||
}),
|
("webm", InternalFormat::Video(InternalVideoFormat::Webm)),
|
||||||
),
|
("webp_pipe", InternalFormat::Image(ImageFormat::Webp)),
|
||||||
(
|
|
||||||
"gif",
|
|
||||||
InputFile::Animation(AnimationInput {
|
|
||||||
format: AnimationFormat::Gif,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
("mp4", InputFile::Video(VideoFormat::Mp4)),
|
|
||||||
(
|
|
||||||
"png_pipe",
|
|
||||||
InputFile::Image(ImageInput {
|
|
||||||
format: ImageFormat::Png,
|
|
||||||
needs_reorient: false,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
("webm", InputFile::Video(VideoFormat::Webm { alpha: false })),
|
|
||||||
(
|
|
||||||
"webp_pipe",
|
|
||||||
InputFile::Image(ImageInput {
|
|
||||||
format: ImageFormat::Webp,
|
|
||||||
needs_reorient: false,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize)]
|
#[derive(Debug, serde::Deserialize)]
|
||||||
|
@ -76,7 +58,23 @@ struct Flags {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) async fn discover_bytes(bytes: Bytes) -> Result<Option<Discovery>, FfMpegError> {
|
pub(super) async fn discover_bytes(bytes: Bytes) -> Result<Option<Discovery>, FfMpegError> {
|
||||||
discover_file(move |mut file| async move {
|
discover_file_full(move |mut file| {
|
||||||
|
let bytes = bytes.clone();
|
||||||
|
|
||||||
|
async move {
|
||||||
|
file.write_from_bytes(bytes)
|
||||||
|
.await
|
||||||
|
.map_err(FfMpegError::Write)?;
|
||||||
|
Ok(file)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) async fn discover_bytes_lite(
|
||||||
|
bytes: Bytes,
|
||||||
|
) -> Result<Option<DiscoveryLite>, FfMpegError> {
|
||||||
|
discover_file_lite(move |mut file| async move {
|
||||||
file.write_from_bytes(bytes)
|
file.write_from_bytes(bytes)
|
||||||
.await
|
.await
|
||||||
.map_err(FfMpegError::Write)?;
|
.map_err(FfMpegError::Write)?;
|
||||||
|
@ -85,8 +83,105 @@ pub(super) async fn discover_bytes(bytes: Bytes) -> Result<Option<Discovery>, Ff
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) async fn discover_stream_lite<S>(stream: S) -> Result<Option<DiscoveryLite>, FfMpegError>
|
||||||
|
where
|
||||||
|
S: Stream<Item = std::io::Result<Bytes>> + Unpin,
|
||||||
|
{
|
||||||
|
discover_file_lite(move |mut file| async move {
|
||||||
|
file.write_from_stream(stream)
|
||||||
|
.await
|
||||||
|
.map_err(FfMpegError::Write)?;
|
||||||
|
Ok(file)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn discover_file_lite<F, Fut>(f: F) -> Result<Option<DiscoveryLite>, FfMpegError>
|
||||||
|
where
|
||||||
|
F: FnOnce(crate::file::File) -> Fut,
|
||||||
|
Fut: std::future::Future<Output = Result<crate::file::File, FfMpegError>>,
|
||||||
|
{
|
||||||
|
let Some(DiscoveryLite {
|
||||||
|
format,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
frames,
|
||||||
|
}) = discover_file(f)
|
||||||
|
.await? else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
// If we're not confident in our discovery don't return it
|
||||||
|
if width == 0 || height == 0 {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Some(DiscoveryLite {
|
||||||
|
format,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
frames,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn discover_file_full<F, Fut>(f: F) -> Result<Option<Discovery>, FfMpegError>
|
||||||
|
where
|
||||||
|
F: Fn(crate::file::File) -> Fut + Clone,
|
||||||
|
Fut: std::future::Future<Output = Result<crate::file::File, FfMpegError>>,
|
||||||
|
{
|
||||||
|
let Some(DiscoveryLite { format, width, height, frames }) = discover_file(f.clone()).await? else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
match format {
|
||||||
|
InternalFormat::Video(InternalVideoFormat::Webm) => {
|
||||||
|
static ALPHA_PIXEL_FORMATS: OnceLock<HashSet<String>> = OnceLock::new();
|
||||||
|
|
||||||
|
let format = pixel_format(f).await?;
|
||||||
|
|
||||||
|
let alpha = match ALPHA_PIXEL_FORMATS.get() {
|
||||||
|
Some(alpha_pixel_formats) => alpha_pixel_formats.contains(&format),
|
||||||
|
None => {
|
||||||
|
let pixel_formats = alpha_pixel_formats().await?;
|
||||||
|
let alpha = pixel_formats.contains(&format);
|
||||||
|
let _ = ALPHA_PIXEL_FORMATS.set(pixel_formats);
|
||||||
|
alpha
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Some(Discovery {
|
||||||
|
input: InputFile::Video(VideoFormat::Webm { alpha }),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
frames,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
InternalFormat::Video(InternalVideoFormat::Mp4) => Ok(Some(Discovery {
|
||||||
|
input: InputFile::Video(VideoFormat::Mp4),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
frames,
|
||||||
|
})),
|
||||||
|
InternalFormat::Animation(format) => Ok(Some(Discovery {
|
||||||
|
input: InputFile::Animation(AnimationInput { format }),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
frames,
|
||||||
|
})),
|
||||||
|
InternalFormat::Image(format) => Ok(Some(Discovery {
|
||||||
|
input: InputFile::Image(ImageInput {
|
||||||
|
format,
|
||||||
|
needs_reorient: false,
|
||||||
|
}),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
frames,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(f))]
|
#[tracing::instrument(skip(f))]
|
||||||
async fn discover_file<F, Fut>(f: F) -> Result<Option<Discovery>, FfMpegError>
|
async fn discover_file<F, Fut>(f: F) -> Result<Option<DiscoveryLite>, FfMpegError>
|
||||||
where
|
where
|
||||||
F: FnOnce(crate::file::File) -> Fut,
|
F: FnOnce(crate::file::File) -> Fut,
|
||||||
Fut: std::future::Future<Output = Result<crate::file::File, FfMpegError>>,
|
Fut: std::future::Future<Output = Result<crate::file::File, FfMpegError>>,
|
||||||
|
@ -134,43 +229,26 @@ where
|
||||||
|
|
||||||
let output: FfMpegDiscovery = serde_json::from_slice(&output).map_err(FfMpegError::Json)?;
|
let output: FfMpegDiscovery = serde_json::from_slice(&output).map_err(FfMpegError::Json)?;
|
||||||
|
|
||||||
let Some(discovery) = parse_discovery_ffmpeg(output)? else {
|
parse_discovery_ffmpeg(output)
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
|
|
||||||
match discovery {
|
|
||||||
Discovery {
|
|
||||||
input: InputFile::Video(VideoFormat::Webm { .. }),
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
frames,
|
|
||||||
} => {
|
|
||||||
static ALPHA_PIXEL_FORMATS: OnceLock<HashSet<String>> = OnceLock::new();
|
|
||||||
|
|
||||||
let format = pixel_format(input_file_str).await?;
|
|
||||||
|
|
||||||
let alpha = match ALPHA_PIXEL_FORMATS.get() {
|
|
||||||
Some(alpha_pixel_formats) => alpha_pixel_formats.contains(&format),
|
|
||||||
None => {
|
|
||||||
let pixel_formats = alpha_pixel_formats().await?;
|
|
||||||
let alpha = pixel_formats.contains(&format);
|
|
||||||
let _ = ALPHA_PIXEL_FORMATS.set(pixel_formats);
|
|
||||||
alpha
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Some(Discovery {
|
|
||||||
input: InputFile::Video(VideoFormat::Webm { alpha }),
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
frames,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
otherwise => Ok(Some(otherwise)),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn pixel_format(input_file: &str) -> Result<String, FfMpegError> {
|
async fn pixel_format<F, Fut>(f: F) -> Result<String, FfMpegError>
|
||||||
|
where
|
||||||
|
F: FnOnce(crate::file::File) -> Fut,
|
||||||
|
Fut: std::future::Future<Output = Result<crate::file::File, FfMpegError>>,
|
||||||
|
{
|
||||||
|
let input_file = crate::tmp_file::tmp_file(None);
|
||||||
|
let input_file_str = input_file.to_str().ok_or(FfMpegError::Path)?;
|
||||||
|
crate::store::file_store::safe_create_parent(&input_file)
|
||||||
|
.await
|
||||||
|
.map_err(FfMpegError::CreateDir)?;
|
||||||
|
|
||||||
|
let tmp_one = crate::file::File::create(&input_file)
|
||||||
|
.await
|
||||||
|
.map_err(FfMpegError::CreateFile)?;
|
||||||
|
let tmp_one = (f)(tmp_one).await?;
|
||||||
|
tmp_one.close().await.map_err(FfMpegError::CloseFile)?;
|
||||||
|
|
||||||
let process = Process::run(
|
let process = Process::run(
|
||||||
"ffprobe",
|
"ffprobe",
|
||||||
&[
|
&[
|
||||||
|
@ -182,7 +260,7 @@ async fn pixel_format(input_file: &str) -> Result<String, FfMpegError> {
|
||||||
"stream=pix_fmt",
|
"stream=pix_fmt",
|
||||||
"-of",
|
"-of",
|
||||||
"compact=p=0:nk=1",
|
"compact=p=0:nk=1",
|
||||||
input_file,
|
input_file_str,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
.map_err(FfMpegError::Process)?;
|
.map_err(FfMpegError::Process)?;
|
||||||
|
@ -193,6 +271,11 @@ async fn pixel_format(input_file: &str) -> Result<String, FfMpegError> {
|
||||||
.read_to_end(&mut output)
|
.read_to_end(&mut output)
|
||||||
.await
|
.await
|
||||||
.map_err(FfMpegError::Read)?;
|
.map_err(FfMpegError::Read)?;
|
||||||
|
|
||||||
|
tokio::fs::remove_file(input_file_str)
|
||||||
|
.await
|
||||||
|
.map_err(FfMpegError::RemoveFile)?;
|
||||||
|
|
||||||
Ok(String::from_utf8_lossy(&output).trim().to_string())
|
Ok(String::from_utf8_lossy(&output).trim().to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -238,7 +321,9 @@ fn parse_pixel_formats(formats: PixelFormatOutput) -> HashSet<String> {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_discovery_ffmpeg(discovery: FfMpegDiscovery) -> Result<Option<Discovery>, FfMpegError> {
|
fn parse_discovery_ffmpeg(
|
||||||
|
discovery: FfMpegDiscovery,
|
||||||
|
) -> Result<Option<DiscoveryLite>, FfMpegError> {
|
||||||
let FfMpegDiscovery {
|
let FfMpegDiscovery {
|
||||||
streams:
|
streams:
|
||||||
[FfMpegStream {
|
[FfMpegStream {
|
||||||
|
@ -259,10 +344,8 @@ fn parse_discovery_ffmpeg(discovery: FfMpegDiscovery) -> Result<Option<Discovery
|
||||||
// Might be AVIF, ffmpeg incorrectly detects AVIF as single-framed mp4 even when
|
// Might be AVIF, ffmpeg incorrectly detects AVIF as single-framed mp4 even when
|
||||||
// animated
|
// animated
|
||||||
|
|
||||||
return Ok(Some(Discovery {
|
return Ok(Some(DiscoveryLite {
|
||||||
input: InputFile::Animation(AnimationInput {
|
format: InternalFormat::Animation(AnimationFormat::Avif),
|
||||||
format: AnimationFormat::Avif,
|
|
||||||
}),
|
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
frames,
|
frames,
|
||||||
|
@ -273,18 +356,16 @@ fn parse_discovery_ffmpeg(discovery: FfMpegDiscovery) -> Result<Option<Discovery
|
||||||
// Might be Animated Webp, ffmpeg incorrectly detects animated webp as having no frames
|
// Might be Animated Webp, ffmpeg incorrectly detects animated webp as having no frames
|
||||||
// and 0 dimensions
|
// and 0 dimensions
|
||||||
|
|
||||||
return Ok(Some(Discovery {
|
return Ok(Some(DiscoveryLite {
|
||||||
input: InputFile::Animation(AnimationInput {
|
format: InternalFormat::Animation(AnimationFormat::Webp),
|
||||||
format: AnimationFormat::Webp,
|
|
||||||
}),
|
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
frames,
|
frames,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok(Some(Discovery {
|
return Ok(Some(DiscoveryLite {
|
||||||
input: value.clone(),
|
format: *value,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
frames,
|
frames,
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use actix_web::web::Bytes;
|
use actix_web::web::Bytes;
|
||||||
|
use futures_util::Stream;
|
||||||
use tokio::io::AsyncReadExt;
|
use tokio::io::AsyncReadExt;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -7,7 +8,7 @@ use crate::{
|
||||||
process::Process,
|
process::Process,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::Discovery;
|
use super::{Discovery, DiscoveryLite};
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize)]
|
#[derive(Debug, serde::Deserialize)]
|
||||||
struct MagickDiscovery {
|
struct MagickDiscovery {
|
||||||
|
@ -26,6 +27,29 @@ struct Geometry {
|
||||||
height: u16,
|
height: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) async fn discover_bytes_lite(bytes: Bytes) -> Result<DiscoveryLite, MagickError> {
|
||||||
|
discover_file_lite(move |mut file| async move {
|
||||||
|
file.write_from_bytes(bytes)
|
||||||
|
.await
|
||||||
|
.map_err(MagickError::Write)?;
|
||||||
|
Ok(file)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) async fn discover_stream_lite<S>(stream: S) -> Result<DiscoveryLite, MagickError>
|
||||||
|
where
|
||||||
|
S: Stream<Item = std::io::Result<Bytes>> + Unpin + 'static,
|
||||||
|
{
|
||||||
|
discover_file_lite(move |mut file| async move {
|
||||||
|
file.write_from_stream(stream)
|
||||||
|
.await
|
||||||
|
.map_err(MagickError::Write)?;
|
||||||
|
Ok(file)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) async fn confirm_bytes(
|
pub(super) async fn confirm_bytes(
|
||||||
discovery: Option<Discovery>,
|
discovery: Option<Discovery>,
|
||||||
bytes: Bytes,
|
bytes: Bytes,
|
||||||
|
@ -125,6 +149,26 @@ where
|
||||||
Ok(lines)
|
Ok(lines)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn discover_file_lite<F, Fut>(f: F) -> Result<DiscoveryLite, MagickError>
|
||||||
|
where
|
||||||
|
F: FnOnce(crate::file::File) -> Fut,
|
||||||
|
Fut: std::future::Future<Output = Result<crate::file::File, MagickError>>,
|
||||||
|
{
|
||||||
|
let Discovery {
|
||||||
|
input,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
frames,
|
||||||
|
} = discover_file(f).await?;
|
||||||
|
|
||||||
|
Ok(DiscoveryLite {
|
||||||
|
format: input.internal_format(),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
frames,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async fn discover_file<F, Fut>(f: F) -> Result<Discovery, MagickError>
|
async fn discover_file<F, Fut>(f: F) -> Result<Discovery, MagickError>
|
||||||
where
|
where
|
||||||
F: FnOnce(crate::file::File) -> Fut,
|
F: FnOnce(crate::file::File) -> Fut,
|
||||||
|
|
|
@ -99,9 +99,6 @@ pub(crate) enum UploadError {
|
||||||
#[error("Process endpoint was called with invalid extension")]
|
#[error("Process endpoint was called with invalid extension")]
|
||||||
UnsupportedProcessExtension,
|
UnsupportedProcessExtension,
|
||||||
|
|
||||||
#[error("Gif uploads are not enabled")]
|
|
||||||
SilentVideoDisabled,
|
|
||||||
|
|
||||||
#[error("Unable to download image, bad response {0}")]
|
#[error("Unable to download image, bad response {0}")]
|
||||||
Download(actix_web::http::StatusCode),
|
Download(actix_web::http::StatusCode),
|
||||||
|
|
||||||
|
@ -169,14 +166,12 @@ impl ResponseError for Error {
|
||||||
crate::repo::RepoError::AlreadyClaimed,
|
crate::repo::RepoError::AlreadyClaimed,
|
||||||
))
|
))
|
||||||
| UploadError::Repo(crate::repo::RepoError::AlreadyClaimed)
|
| UploadError::Repo(crate::repo::RepoError::AlreadyClaimed)
|
||||||
| UploadError::UnsupportedProcessExtension
|
| UploadError::UnsupportedProcessExtension,
|
||||||
| UploadError::SilentVideoDisabled,
|
|
||||||
) => StatusCode::BAD_REQUEST,
|
) => StatusCode::BAD_REQUEST,
|
||||||
Some(UploadError::Magick(e)) if e.is_client_error() => StatusCode::BAD_REQUEST,
|
Some(UploadError::Magick(e)) if e.is_client_error() => StatusCode::BAD_REQUEST,
|
||||||
Some(UploadError::Ffmpeg(e)) if e.is_client_error() => StatusCode::BAD_REQUEST,
|
Some(UploadError::Ffmpeg(e)) if e.is_client_error() => StatusCode::BAD_REQUEST,
|
||||||
Some(UploadError::Exiftool(e)) if e.is_client_error() => StatusCode::BAD_REQUEST,
|
Some(UploadError::Exiftool(e)) if e.is_client_error() => StatusCode::BAD_REQUEST,
|
||||||
Some(UploadError::MissingAlias) => StatusCode::NOT_FOUND,
|
Some(UploadError::MissingAlias) => StatusCode::NOT_FOUND,
|
||||||
Some(UploadError::Magick(e)) if e.is_not_found() => StatusCode::NOT_FOUND,
|
|
||||||
Some(UploadError::Ffmpeg(e)) if e.is_not_found() => StatusCode::NOT_FOUND,
|
Some(UploadError::Ffmpeg(e)) if e.is_not_found() => StatusCode::NOT_FOUND,
|
||||||
Some(UploadError::InvalidToken) => StatusCode::FORBIDDEN,
|
Some(UploadError::InvalidToken) => StatusCode::FORBIDDEN,
|
||||||
Some(UploadError::Range) => StatusCode::RANGE_NOT_SATISFIABLE,
|
Some(UploadError::Range) => StatusCode::RANGE_NOT_SATISFIABLE,
|
||||||
|
|
626
src/ffmpeg.rs
626
src/ffmpeg.rs
|
@ -2,44 +2,11 @@
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{AudioCodec, ImageFormat, MediaConfiguration, VideoCodec},
|
|
||||||
formats::InternalVideoFormat,
|
formats::InternalVideoFormat,
|
||||||
magick::{Details, ParseDetailsError, ValidInputType, ValidateDetailsError},
|
|
||||||
process::{Process, ProcessError},
|
process::{Process, ProcessError},
|
||||||
store::{Store, StoreError},
|
store::{Store, StoreError},
|
||||||
};
|
};
|
||||||
use actix_web::web::Bytes;
|
use tokio::io::AsyncRead;
|
||||||
use once_cell::sync::OnceCell;
|
|
||||||
use std::collections::HashSet;
|
|
||||||
use tokio::io::{AsyncRead, AsyncReadExt};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub(crate) struct TranscodeOptions {
|
|
||||||
input_format: VideoFormat,
|
|
||||||
output: TranscodeOutputOptions,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
enum TranscodeOutputOptions {
|
|
||||||
Gif,
|
|
||||||
Video {
|
|
||||||
video_codec: VideoCodec,
|
|
||||||
audio_codec: Option<AudioCodec>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
|
||||||
pub(crate) enum VideoFormat {
|
|
||||||
Gif,
|
|
||||||
Mp4,
|
|
||||||
Webm,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
|
||||||
pub(crate) enum OutputFormat {
|
|
||||||
Mp4,
|
|
||||||
Webm,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub(crate) enum ThumbnailFormat {
|
pub(crate) enum ThumbnailFormat {
|
||||||
|
@ -47,28 +14,6 @@ pub(crate) enum ThumbnailFormat {
|
||||||
// Webp,
|
// Webp,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
|
||||||
pub(crate) enum FileFormat {
|
|
||||||
Image(ImageFormat),
|
|
||||||
Video(VideoFormat),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
|
||||||
struct PixelFormatOutput {
|
|
||||||
pixel_formats: Vec<PixelFormat>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
|
||||||
struct PixelFormat {
|
|
||||||
name: String,
|
|
||||||
flags: Flags,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
|
||||||
struct Flags {
|
|
||||||
alpha: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub(crate) enum FfMpegError {
|
pub(crate) enum FfMpegError {
|
||||||
#[error("Error in ffmpeg process")]
|
#[error("Error in ffmpeg process")]
|
||||||
|
@ -101,12 +46,6 @@ pub(crate) enum FfMpegError {
|
||||||
#[error("Error removing file")]
|
#[error("Error removing file")]
|
||||||
RemoveFile(#[source] std::io::Error),
|
RemoveFile(#[source] std::io::Error),
|
||||||
|
|
||||||
#[error("Error parsing details")]
|
|
||||||
Details(#[source] ParseDetailsError),
|
|
||||||
|
|
||||||
#[error("Media details are invalid")]
|
|
||||||
ValidateDetails(#[source] ValidateDetailsError),
|
|
||||||
|
|
||||||
#[error("Error in store")]
|
#[error("Error in store")]
|
||||||
Store(#[source] StoreError),
|
Store(#[source] StoreError),
|
||||||
|
|
||||||
|
@ -117,8 +56,7 @@ pub(crate) enum FfMpegError {
|
||||||
impl FfMpegError {
|
impl FfMpegError {
|
||||||
pub(crate) fn is_client_error(&self) -> bool {
|
pub(crate) fn is_client_error(&self) -> bool {
|
||||||
// Failing validation or ffmpeg bailing probably means bad input
|
// Failing validation or ffmpeg bailing probably means bad input
|
||||||
matches!(self, Self::ValidateDetails(_))
|
matches!(self, Self::Process(ProcessError::Status(_)))
|
||||||
|| matches!(self, Self::Process(ProcessError::Status(_)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn is_not_found(&self) -> bool {
|
pub(crate) fn is_not_found(&self) -> bool {
|
||||||
|
@ -130,205 +68,6 @@ impl FfMpegError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TranscodeOptions {
|
|
||||||
pub(crate) fn new(
|
|
||||||
media: &MediaConfiguration,
|
|
||||||
details: &Details,
|
|
||||||
input_format: VideoFormat,
|
|
||||||
) -> Self {
|
|
||||||
if let VideoFormat::Gif = input_format {
|
|
||||||
if details.width <= media.gif.max_width
|
|
||||||
&& details.height <= media.gif.max_height
|
|
||||||
&& details.width * details.height <= media.gif.max_area
|
|
||||||
&& details.frames.unwrap_or(1) <= media.gif.max_frame_count
|
|
||||||
{
|
|
||||||
return Self {
|
|
||||||
input_format,
|
|
||||||
output: TranscodeOutputOptions::gif(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Self {
|
|
||||||
input_format,
|
|
||||||
output: TranscodeOutputOptions::video(media),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) const fn needs_reencode(&self) -> bool {
|
|
||||||
!matches!(
|
|
||||||
(self.input_format, &self.output),
|
|
||||||
(VideoFormat::Gif, TranscodeOutputOptions::Gif)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const fn input_file_extension(&self) -> &'static str {
|
|
||||||
self.input_format.to_file_extension()
|
|
||||||
}
|
|
||||||
|
|
||||||
const fn output_ffmpeg_format(&self) -> &'static str {
|
|
||||||
match self.output {
|
|
||||||
TranscodeOutputOptions::Gif => "gif",
|
|
||||||
TranscodeOutputOptions::Video { video_codec, .. } => {
|
|
||||||
video_codec.to_output_format().to_ffmpeg_format()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fn output_file_extension(&self) -> &'static str {
|
|
||||||
match self.output {
|
|
||||||
TranscodeOutputOptions::Gif => ".gif",
|
|
||||||
TranscodeOutputOptions::Video { video_codec, .. } => {
|
|
||||||
video_codec.to_output_format().to_file_extension()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fn supports_alpha(&self) -> bool {
|
|
||||||
matches!(
|
|
||||||
self.output,
|
|
||||||
TranscodeOutputOptions::Gif
|
|
||||||
| TranscodeOutputOptions::Video {
|
|
||||||
video_codec: VideoCodec::Vp8 | VideoCodec::Vp9,
|
|
||||||
..
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn execute(
|
|
||||||
&self,
|
|
||||||
input_path: &str,
|
|
||||||
output_path: &str,
|
|
||||||
alpha: bool,
|
|
||||||
) -> Result<Process, ProcessError> {
|
|
||||||
match self.output {
|
|
||||||
TranscodeOutputOptions::Gif => Process::run("ffmpeg", &[
|
|
||||||
"-hide_banner",
|
|
||||||
"-v",
|
|
||||||
"warning",
|
|
||||||
"-i",
|
|
||||||
input_path,
|
|
||||||
"-filter_complex",
|
|
||||||
"[0:v] split [a][b]; [a] palettegen=stats_mode=single [p]; [b][p] paletteuse=new=1",
|
|
||||||
"-an",
|
|
||||||
"-f",
|
|
||||||
self.output_ffmpeg_format(),
|
|
||||||
output_path
|
|
||||||
]),
|
|
||||||
TranscodeOutputOptions::Video {
|
|
||||||
video_codec,
|
|
||||||
audio_codec: None,
|
|
||||||
} => Process::run(
|
|
||||||
"ffmpeg",
|
|
||||||
&[
|
|
||||||
"-hide_banner",
|
|
||||||
"-v",
|
|
||||||
"warning",
|
|
||||||
"-i",
|
|
||||||
input_path,
|
|
||||||
"-pix_fmt",
|
|
||||||
video_codec.pix_fmt(alpha),
|
|
||||||
"-vf",
|
|
||||||
"scale=trunc(iw/2)*2:trunc(ih/2)*2",
|
|
||||||
"-an",
|
|
||||||
"-c:v",
|
|
||||||
video_codec.to_ffmpeg_codec(),
|
|
||||||
"-f",
|
|
||||||
self.output_ffmpeg_format(),
|
|
||||||
output_path,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
TranscodeOutputOptions::Video {
|
|
||||||
video_codec,
|
|
||||||
audio_codec: Some(audio_codec),
|
|
||||||
} => Process::run(
|
|
||||||
"ffmpeg",
|
|
||||||
&[
|
|
||||||
"-hide_banner",
|
|
||||||
"-v",
|
|
||||||
"warning",
|
|
||||||
"-i",
|
|
||||||
input_path,
|
|
||||||
"-pix_fmt",
|
|
||||||
video_codec.pix_fmt(alpha),
|
|
||||||
"-vf",
|
|
||||||
"scale=trunc(iw/2)*2:trunc(ih/2)*2",
|
|
||||||
"-c:a",
|
|
||||||
audio_codec.to_ffmpeg_codec(),
|
|
||||||
"-c:v",
|
|
||||||
video_codec.to_ffmpeg_codec(),
|
|
||||||
"-f",
|
|
||||||
self.output_ffmpeg_format(),
|
|
||||||
output_path,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) const fn output_type(&self) -> ValidInputType {
|
|
||||||
match self.output {
|
|
||||||
TranscodeOutputOptions::Gif => ValidInputType::Gif,
|
|
||||||
TranscodeOutputOptions::Video { video_codec, .. } => {
|
|
||||||
ValidInputType::from_video_codec(video_codec)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TranscodeOutputOptions {
|
|
||||||
fn video(media: &MediaConfiguration) -> Self {
|
|
||||||
Self::Video {
|
|
||||||
video_codec: media.video_codec,
|
|
||||||
audio_codec: if media.enable_full_video {
|
|
||||||
Some(
|
|
||||||
media
|
|
||||||
.audio_codec
|
|
||||||
.unwrap_or(media.video_codec.to_output_format().default_audio_codec()),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fn gif() -> Self {
|
|
||||||
Self::Gif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ValidInputType {
|
|
||||||
pub(crate) fn to_file_format(self) -> FileFormat {
|
|
||||||
match self {
|
|
||||||
Self::Gif => FileFormat::Video(VideoFormat::Gif),
|
|
||||||
Self::Mp4 => FileFormat::Video(VideoFormat::Mp4),
|
|
||||||
Self::Webm => FileFormat::Video(VideoFormat::Webm),
|
|
||||||
Self::Avif => FileFormat::Image(ImageFormat::Avif),
|
|
||||||
Self::Jpeg => FileFormat::Image(ImageFormat::Jpeg),
|
|
||||||
Self::Jxl => FileFormat::Image(ImageFormat::Jxl),
|
|
||||||
Self::Png => FileFormat::Image(ImageFormat::Png),
|
|
||||||
Self::Webp => FileFormat::Image(ImageFormat::Webp),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl VideoFormat {
|
|
||||||
const fn to_file_extension(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Gif => ".gif",
|
|
||||||
Self::Mp4 => ".mp4",
|
|
||||||
Self::Webm => ".webm",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn to_mime(self) -> mime::Mime {
|
|
||||||
match self {
|
|
||||||
Self::Gif => mime::IMAGE_GIF,
|
|
||||||
Self::Mp4 => crate::magick::video_mp4(),
|
|
||||||
Self::Webm => crate::magick::video_webm(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ThumbnailFormat {
|
impl ThumbnailFormat {
|
||||||
const fn as_ffmpeg_codec(self) -> &'static str {
|
const fn as_ffmpeg_codec(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
|
@ -352,367 +91,6 @@ impl ThumbnailFormat {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OutputFormat {
|
|
||||||
const fn to_ffmpeg_format(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Mp4 => "mp4",
|
|
||||||
Self::Webm => "webm",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fn default_audio_codec(self) -> AudioCodec {
|
|
||||||
match self {
|
|
||||||
Self::Mp4 => AudioCodec::Aac,
|
|
||||||
Self::Webm => AudioCodec::Opus,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fn to_file_extension(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Mp4 => ".mp4",
|
|
||||||
Self::Webm => ".webm",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl VideoCodec {
|
|
||||||
const fn to_output_format(self) -> OutputFormat {
|
|
||||||
match self {
|
|
||||||
Self::H264 | Self::H265 => OutputFormat::Mp4,
|
|
||||||
Self::Av1 | Self::Vp8 | Self::Vp9 => OutputFormat::Webm,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fn to_ffmpeg_codec(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Av1 => "av1",
|
|
||||||
Self::H264 => "h264",
|
|
||||||
Self::H265 => "hevc",
|
|
||||||
Self::Vp8 => "vp8",
|
|
||||||
Self::Vp9 => "vp9",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fn pix_fmt(&self, alpha: bool) -> &'static str {
|
|
||||||
match (self, alpha) {
|
|
||||||
(VideoCodec::Vp8 | VideoCodec::Vp9, true) => "yuva420p",
|
|
||||||
_ => "yuv420p",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AudioCodec {
|
|
||||||
const fn to_ffmpeg_codec(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Aac => "aac",
|
|
||||||
Self::Opus => "libopus",
|
|
||||||
Self::Vorbis => "vorbis",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const FORMAT_MAPPINGS: &[(&str, VideoFormat)] = &[
|
|
||||||
("gif", VideoFormat::Gif),
|
|
||||||
("mp4", VideoFormat::Mp4),
|
|
||||||
("webm", VideoFormat::Webm),
|
|
||||||
];
|
|
||||||
|
|
||||||
pub(crate) async fn input_type_bytes(
|
|
||||||
input: Bytes,
|
|
||||||
) -> Result<Option<(Details, ValidInputType)>, FfMpegError> {
|
|
||||||
if let Some(details) = details_bytes(input).await? {
|
|
||||||
let input_type = details
|
|
||||||
.validate_input()
|
|
||||||
.map_err(FfMpegError::ValidateDetails)?;
|
|
||||||
|
|
||||||
return Ok(Some((details, input_type)));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn details_store<S: Store>(
|
|
||||||
store: &S,
|
|
||||||
identifier: &S::Identifier,
|
|
||||||
) -> Result<Option<Details>, FfMpegError> {
|
|
||||||
details_file(move |mut tmp_one| async move {
|
|
||||||
let stream = store
|
|
||||||
.to_stream(identifier, None, None)
|
|
||||||
.await
|
|
||||||
.map_err(FfMpegError::Store)?;
|
|
||||||
tmp_one
|
|
||||||
.write_from_stream(stream)
|
|
||||||
.await
|
|
||||||
.map_err(FfMpegError::Write)?;
|
|
||||||
Ok(tmp_one)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn details_bytes(input: Bytes) -> Result<Option<Details>, FfMpegError> {
|
|
||||||
details_file(move |mut tmp_one| async move {
|
|
||||||
tmp_one
|
|
||||||
.write_from_bytes(input)
|
|
||||||
.await
|
|
||||||
.map_err(FfMpegError::Write)?;
|
|
||||||
Ok(tmp_one)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn alpha_pixel_formats() -> Result<HashSet<String>, FfMpegError> {
|
|
||||||
let process = Process::run(
|
|
||||||
"ffprobe",
|
|
||||||
&[
|
|
||||||
"-v",
|
|
||||||
"0",
|
|
||||||
"-show_entries",
|
|
||||||
"pixel_format=name:flags=alpha",
|
|
||||||
"-of",
|
|
||||||
"compact=p=0",
|
|
||||||
"-print_format",
|
|
||||||
"json",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
.map_err(FfMpegError::Process)?;
|
|
||||||
|
|
||||||
let mut output = Vec::new();
|
|
||||||
process
|
|
||||||
.read()
|
|
||||||
.read_to_end(&mut output)
|
|
||||||
.await
|
|
||||||
.map_err(FfMpegError::Read)?;
|
|
||||||
|
|
||||||
let formats: PixelFormatOutput = serde_json::from_slice(&output).map_err(FfMpegError::Json)?;
|
|
||||||
|
|
||||||
Ok(parse_pixel_formats(formats))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_pixel_formats(formats: PixelFormatOutput) -> HashSet<String> {
|
|
||||||
formats
|
|
||||||
.pixel_formats
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|PixelFormat { name, flags }| {
|
|
||||||
if flags.alpha == 0 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(name)
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize)]
|
|
||||||
struct DetailsOutput {
|
|
||||||
streams: [Stream; 1],
|
|
||||||
format: Format,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize)]
|
|
||||||
struct Stream {
|
|
||||||
width: usize,
|
|
||||||
height: usize,
|
|
||||||
nb_read_frames: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize)]
|
|
||||||
struct Format {
|
|
||||||
format_name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument(skip(f))]
|
|
||||||
async fn details_file<F, Fut>(f: F) -> Result<Option<Details>, FfMpegError>
|
|
||||||
where
|
|
||||||
F: FnOnce(crate::file::File) -> Fut,
|
|
||||||
Fut: std::future::Future<Output = Result<crate::file::File, FfMpegError>>,
|
|
||||||
{
|
|
||||||
let input_file = crate::tmp_file::tmp_file(None);
|
|
||||||
let input_file_str = input_file.to_str().ok_or(FfMpegError::Path)?;
|
|
||||||
crate::store::file_store::safe_create_parent(&input_file)
|
|
||||||
.await
|
|
||||||
.map_err(FfMpegError::CreateDir)?;
|
|
||||||
|
|
||||||
let tmp_one = crate::file::File::create(&input_file)
|
|
||||||
.await
|
|
||||||
.map_err(FfMpegError::CreateFile)?;
|
|
||||||
let tmp_one = (f)(tmp_one).await?;
|
|
||||||
tmp_one.close().await.map_err(FfMpegError::CloseFile)?;
|
|
||||||
|
|
||||||
let process = Process::run(
|
|
||||||
"ffprobe",
|
|
||||||
&[
|
|
||||||
"-v",
|
|
||||||
"quiet",
|
|
||||||
"-select_streams",
|
|
||||||
"v:0",
|
|
||||||
"-count_frames",
|
|
||||||
"-show_entries",
|
|
||||||
"stream=width,height,nb_read_frames:format=format_name",
|
|
||||||
"-of",
|
|
||||||
"default=noprint_wrappers=1:nokey=1",
|
|
||||||
"-print_format",
|
|
||||||
"json",
|
|
||||||
input_file_str,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
.map_err(FfMpegError::Process)?;
|
|
||||||
|
|
||||||
let mut output = Vec::new();
|
|
||||||
process
|
|
||||||
.read()
|
|
||||||
.read_to_end(&mut output)
|
|
||||||
.await
|
|
||||||
.map_err(FfMpegError::Read)?;
|
|
||||||
tokio::fs::remove_file(input_file_str)
|
|
||||||
.await
|
|
||||||
.map_err(FfMpegError::RemoveFile)?;
|
|
||||||
|
|
||||||
let output: DetailsOutput = serde_json::from_slice(&output).map_err(FfMpegError::Json)?;
|
|
||||||
|
|
||||||
parse_details(output)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_details(output: DetailsOutput) -> Result<Option<Details>, FfMpegError> {
|
|
||||||
tracing::debug!("OUTPUT: {:?}", output);
|
|
||||||
|
|
||||||
let [stream] = output.streams;
|
|
||||||
let Format { format_name } = output.format;
|
|
||||||
|
|
||||||
for (k, v) in FORMAT_MAPPINGS {
|
|
||||||
if format_name.contains(k) {
|
|
||||||
return parse_details_inner(
|
|
||||||
stream.width,
|
|
||||||
stream.height,
|
|
||||||
stream.nb_read_frames.as_deref(),
|
|
||||||
*v,
|
|
||||||
)
|
|
||||||
.map_err(FfMpegError::Details);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_details_inner(
|
|
||||||
width: usize,
|
|
||||||
height: usize,
|
|
||||||
frames: Option<&str>,
|
|
||||||
format: VideoFormat,
|
|
||||||
) -> Result<Option<Details>, ParseDetailsError> {
|
|
||||||
let frames = frames
|
|
||||||
.map(|frames| {
|
|
||||||
frames
|
|
||||||
.parse()
|
|
||||||
.map_err(|_| ParseDetailsError::ParseFrames(String::from(frames)))
|
|
||||||
})
|
|
||||||
.transpose()?
|
|
||||||
.unwrap_or(1);
|
|
||||||
|
|
||||||
// Probably a still image. ffmpeg thinks AVIF is an mp4
|
|
||||||
if frames == 1 {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Some(Details {
|
|
||||||
mime_type: format.to_mime(),
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
frames: Some(frames),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn pixel_format(input_file: &str) -> Result<String, FfMpegError> {
|
|
||||||
let process = Process::run(
|
|
||||||
"ffprobe",
|
|
||||||
&[
|
|
||||||
"-v",
|
|
||||||
"0",
|
|
||||||
"-select_streams",
|
|
||||||
"v:0",
|
|
||||||
"-show_entries",
|
|
||||||
"stream=pix_fmt",
|
|
||||||
"-of",
|
|
||||||
"compact=p=0:nk=1",
|
|
||||||
input_file,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
.map_err(FfMpegError::Process)?;
|
|
||||||
|
|
||||||
let mut output = Vec::new();
|
|
||||||
process
|
|
||||||
.read()
|
|
||||||
.read_to_end(&mut output)
|
|
||||||
.await
|
|
||||||
.map_err(FfMpegError::Read)?;
|
|
||||||
Ok(String::from_utf8_lossy(&output).trim().to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument(skip(input))]
|
|
||||||
pub(crate) async fn transcode_bytes(
|
|
||||||
input: Bytes,
|
|
||||||
transcode_options: TranscodeOptions,
|
|
||||||
) -> Result<impl AsyncRead + Unpin, FfMpegError> {
|
|
||||||
let input_file = crate::tmp_file::tmp_file(Some(transcode_options.input_file_extension()));
|
|
||||||
let input_file_str = input_file.to_str().ok_or(FfMpegError::Path)?;
|
|
||||||
crate::store::file_store::safe_create_parent(&input_file)
|
|
||||||
.await
|
|
||||||
.map_err(FfMpegError::CreateDir)?;
|
|
||||||
|
|
||||||
let output_file = crate::tmp_file::tmp_file(Some(transcode_options.output_file_extension()));
|
|
||||||
let output_file_str = output_file.to_str().ok_or(FfMpegError::Path)?;
|
|
||||||
crate::store::file_store::safe_create_parent(&output_file)
|
|
||||||
.await
|
|
||||||
.map_err(FfMpegError::CreateDir)?;
|
|
||||||
|
|
||||||
let mut tmp_one = crate::file::File::create(&input_file)
|
|
||||||
.await
|
|
||||||
.map_err(FfMpegError::CreateFile)?;
|
|
||||||
tmp_one
|
|
||||||
.write_from_bytes(input)
|
|
||||||
.await
|
|
||||||
.map_err(FfMpegError::Write)?;
|
|
||||||
tmp_one.close().await.map_err(FfMpegError::CloseFile)?;
|
|
||||||
|
|
||||||
let alpha = if transcode_options.supports_alpha() {
|
|
||||||
static ALPHA_PIXEL_FORMATS: OnceCell<HashSet<String>> = OnceCell::new();
|
|
||||||
|
|
||||||
let format = pixel_format(input_file_str).await?;
|
|
||||||
|
|
||||||
match ALPHA_PIXEL_FORMATS.get() {
|
|
||||||
Some(alpha_pixel_formats) => alpha_pixel_formats.contains(&format),
|
|
||||||
None => {
|
|
||||||
let pixel_formats = alpha_pixel_formats().await?;
|
|
||||||
let alpha = pixel_formats.contains(&format);
|
|
||||||
let _ = ALPHA_PIXEL_FORMATS.set(pixel_formats);
|
|
||||||
alpha
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
};
|
|
||||||
|
|
||||||
let process = transcode_options
|
|
||||||
.execute(input_file_str, output_file_str, alpha)
|
|
||||||
.map_err(FfMpegError::Process)?;
|
|
||||||
|
|
||||||
process.wait().await.map_err(FfMpegError::Process)?;
|
|
||||||
tokio::fs::remove_file(input_file)
|
|
||||||
.await
|
|
||||||
.map_err(FfMpegError::RemoveFile)?;
|
|
||||||
|
|
||||||
let tmp_two = crate::file::File::open(&output_file)
|
|
||||||
.await
|
|
||||||
.map_err(FfMpegError::OpenFile)?;
|
|
||||||
let stream = tmp_two
|
|
||||||
.read_to_stream(None, None)
|
|
||||||
.await
|
|
||||||
.map_err(FfMpegError::ReadFile)?;
|
|
||||||
let reader = tokio_util::io::StreamReader::new(stream);
|
|
||||||
let clean_reader = crate::tmp_file::cleanup_tmpfile(reader, output_file);
|
|
||||||
|
|
||||||
Ok(Box::pin(clean_reader))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument(skip(store))]
|
#[tracing::instrument(skip(store))]
|
||||||
pub(crate) async fn thumbnail<S: Store>(
|
pub(crate) async fn thumbnail<S: Store>(
|
||||||
store: S,
|
store: S,
|
||||||
|
|
|
@ -24,13 +24,6 @@ pub(crate) enum InputFile {
|
||||||
Video(VideoFormat),
|
Video(VideoFormat),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub(crate) enum OutputFile {
|
|
||||||
Image(ImageOutput),
|
|
||||||
Animation(AnimationOutput),
|
|
||||||
Video(OutputVideoFormat),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
|
#[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
|
||||||
pub(crate) enum InternalFormat {
|
pub(crate) enum InternalFormat {
|
||||||
Image(ImageFormat),
|
Image(ImageFormat),
|
||||||
|
@ -65,31 +58,6 @@ pub(crate) enum InputProcessableFormat {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InputFile {
|
impl InputFile {
|
||||||
const fn file_extension(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Image(ImageInput { format, .. }) => format.file_extension(),
|
|
||||||
Self::Animation(AnimationInput { format }) => format.file_extension(),
|
|
||||||
Self::Video(format) => format.file_extension(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fn build_output(&self, prescribed: &PrescribedFormats) -> OutputFile {
|
|
||||||
match (self, prescribed) {
|
|
||||||
(InputFile::Image(input), PrescribedFormats { image, .. }) => {
|
|
||||||
OutputFile::Image(input.build_output(*image))
|
|
||||||
}
|
|
||||||
(InputFile::Animation(input), PrescribedFormats { animation, .. }) => {
|
|
||||||
OutputFile::Animation(input.build_output(*animation))
|
|
||||||
}
|
|
||||||
(
|
|
||||||
InputFile::Video(input),
|
|
||||||
PrescribedFormats {
|
|
||||||
video, allow_audio, ..
|
|
||||||
},
|
|
||||||
) => OutputFile::Video(input.build_output(*video, *allow_audio)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) const fn internal_format(&self) -> InternalFormat {
|
pub(crate) const fn internal_format(&self) -> InternalFormat {
|
||||||
match self {
|
match self {
|
||||||
Self::Image(ImageInput { format, .. }) => InternalFormat::Image(*format),
|
Self::Image(ImageInput { format, .. }) => InternalFormat::Image(*format),
|
||||||
|
@ -99,24 +67,6 @@ impl InputFile {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OutputFile {
|
|
||||||
const fn file_extension(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Image(ImageOutput { format, .. }) => format.file_extension(),
|
|
||||||
Self::Animation(AnimationOutput { format, .. }) => format.file_extension(),
|
|
||||||
Self::Video(format) => format.file_extension(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) const fn internal_format(&self) -> InternalFormat {
|
|
||||||
match self {
|
|
||||||
Self::Image(ImageOutput { format, .. }) => InternalFormat::Image(*format),
|
|
||||||
Self::Animation(AnimationOutput { format, .. }) => InternalFormat::Animation(*format),
|
|
||||||
Self::Video(format) => InternalFormat::Video(format.internal_format()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl InternalFormat {
|
impl InternalFormat {
|
||||||
pub(crate) fn media_type(self) -> mime::Mime {
|
pub(crate) fn media_type(self) -> mime::Mime {
|
||||||
match self {
|
match self {
|
||||||
|
@ -126,6 +76,23 @@ impl InternalFormat {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn maybe_from_media_type(mime: &mime::Mime, has_frames: bool) -> Option<Self> {
|
||||||
|
match (mime.type_(), mime.subtype().as_str(), has_frames) {
|
||||||
|
(mime::IMAGE, "apng", _) => Some(Self::Animation(AnimationFormat::Apng)),
|
||||||
|
(mime::IMAGE, "avif", true) => Some(Self::Animation(AnimationFormat::Avif)),
|
||||||
|
(mime::IMAGE, "avif", false) => Some(Self::Image(ImageFormat::Avif)),
|
||||||
|
(mime::IMAGE, "gif", _) => Some(Self::Animation(AnimationFormat::Gif)),
|
||||||
|
(mime::IMAGE, "jpeg", _) => Some(Self::Image(ImageFormat::Jpeg)),
|
||||||
|
(mime::IMAGE, "jxl", _) => Some(Self::Image(ImageFormat::Jxl)),
|
||||||
|
(mime::IMAGE, "png", _) => Some(Self::Image(ImageFormat::Png)),
|
||||||
|
(mime::IMAGE, "webp", true) => Some(Self::Animation(AnimationFormat::Webp)),
|
||||||
|
(mime::IMAGE, "webp", false) => Some(Self::Image(ImageFormat::Webp)),
|
||||||
|
(mime::VIDEO, "mp4", _) => Some(Self::Video(InternalVideoFormat::Mp4)),
|
||||||
|
(mime::VIDEO, "webm", _) => Some(Self::Video(InternalVideoFormat::Webm)),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) const fn file_extension(self) -> &'static str {
|
pub(crate) const fn file_extension(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::Image(format) => format.file_extension(),
|
Self::Image(format) => format.file_extension(),
|
||||||
|
@ -161,6 +128,33 @@ impl ProcessableFormat {
|
||||||
Self::Animation(format) => format.magick_format(),
|
Self::Animation(format) => format.magick_format(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn process_to(self, output: InputProcessableFormat) -> Option<Self> {
|
||||||
|
match (self, output) {
|
||||||
|
(Self::Image(_), InputProcessableFormat::Avif) => Some(Self::Image(ImageFormat::Avif)),
|
||||||
|
(Self::Image(_), InputProcessableFormat::Jpeg) => Some(Self::Image(ImageFormat::Jpeg)),
|
||||||
|
(Self::Image(_), InputProcessableFormat::Jxl) => Some(Self::Image(ImageFormat::Jxl)),
|
||||||
|
(Self::Image(_), InputProcessableFormat::Png) => Some(Self::Image(ImageFormat::Png)),
|
||||||
|
(Self::Image(_), InputProcessableFormat::Webp) => Some(Self::Image(ImageFormat::Webp)),
|
||||||
|
(Self::Animation(_), InputProcessableFormat::Apng) => {
|
||||||
|
Some(Self::Animation(AnimationFormat::Apng))
|
||||||
|
}
|
||||||
|
(Self::Animation(_), InputProcessableFormat::Avif) => {
|
||||||
|
Some(Self::Animation(AnimationFormat::Avif))
|
||||||
|
}
|
||||||
|
(Self::Animation(_), InputProcessableFormat::Gif) => {
|
||||||
|
Some(Self::Animation(AnimationFormat::Gif))
|
||||||
|
}
|
||||||
|
(Self::Animation(_), InputProcessableFormat::Webp) => {
|
||||||
|
Some(Self::Animation(AnimationFormat::Webp))
|
||||||
|
}
|
||||||
|
(Self::Image(_), InputProcessableFormat::Apng) => None,
|
||||||
|
(Self::Image(_), InputProcessableFormat::Gif) => None,
|
||||||
|
(Self::Animation(_), InputProcessableFormat::Jpeg) => None,
|
||||||
|
(Self::Animation(_), InputProcessableFormat::Jxl) => None,
|
||||||
|
(Self::Animation(_), InputProcessableFormat::Png) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for InputProcessableFormat {
|
impl FromStr for InputProcessableFormat {
|
||||||
|
|
|
@ -1,5 +1,15 @@
|
||||||
#[derive(
|
#[derive(
|
||||||
Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize,
|
Clone,
|
||||||
|
Copy,
|
||||||
|
Debug,
|
||||||
|
PartialEq,
|
||||||
|
Eq,
|
||||||
|
PartialOrd,
|
||||||
|
Ord,
|
||||||
|
Hash,
|
||||||
|
serde::Deserialize,
|
||||||
|
serde::Serialize,
|
||||||
|
clap::ValueEnum,
|
||||||
)]
|
)]
|
||||||
pub(crate) enum ImageFormat {
|
pub(crate) enum ImageFormat {
|
||||||
#[serde(rename = "avif")]
|
#[serde(rename = "avif")]
|
||||||
|
|
|
@ -104,13 +104,6 @@ pub(crate) enum InternalVideoFormat {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VideoFormat {
|
impl VideoFormat {
|
||||||
pub(crate) const fn file_extension(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Mp4 => ".mp4",
|
|
||||||
Self::Webm { .. } => ".webm",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) const fn ffmpeg_format(self) -> &'static str {
|
pub(crate) const fn ffmpeg_format(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::Mp4 => "mp4",
|
Self::Mp4 => "mp4",
|
||||||
|
@ -247,13 +240,6 @@ impl OutputVideoFormat {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) const fn file_extension(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Mp4 { .. } => ".mp4",
|
|
||||||
Self::Webm { .. } => ".webm",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) const fn ffmpeg_format(self) -> &'static str {
|
pub(crate) const fn ffmpeg_format(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::Mp4 { .. } => "mp4",
|
Self::Mp4 { .. } => "mp4",
|
||||||
|
@ -289,14 +275,7 @@ impl OutputVideoFormat {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn media_type(self) -> mime::Mime {
|
pub(crate) const fn internal_format(self) -> InternalVideoFormat {
|
||||||
match self {
|
|
||||||
Self::Mp4 { .. } => super::mimes::video_mp4(),
|
|
||||||
Self::Webm { .. } => super::mimes::video_webm(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn internal_format(self) -> InternalVideoFormat {
|
|
||||||
match self {
|
match self {
|
||||||
Self::Mp4 { .. } => InternalVideoFormat::Mp4,
|
Self::Mp4 { .. } => InternalVideoFormat::Mp4,
|
||||||
Self::Webm { .. } => InternalVideoFormat::Webm,
|
Self::Webm { .. } => InternalVideoFormat::Webm,
|
||||||
|
|
|
@ -48,7 +48,7 @@ pub(crate) async fn generate<R: FullRepo, S: Store + 'static>(
|
||||||
async fn process<R: FullRepo, S: Store + 'static>(
|
async fn process<R: FullRepo, S: Store + 'static>(
|
||||||
repo: &R,
|
repo: &R,
|
||||||
store: &S,
|
store: &S,
|
||||||
format: InputProcessableFormat,
|
output_format: InputProcessableFormat,
|
||||||
alias: Alias,
|
alias: Alias,
|
||||||
thumbnail_path: PathBuf,
|
thumbnail_path: PathBuf,
|
||||||
thumbnail_args: Vec<String>,
|
thumbnail_args: Vec<String>,
|
||||||
|
@ -83,6 +83,24 @@ async fn process<R: FullRepo, S: Store + 'static>(
|
||||||
motion_identifier
|
motion_identifier
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let input_details = if let Some(details) = repo.details(&identifier).await? {
|
||||||
|
details
|
||||||
|
} else {
|
||||||
|
let details = Details::from_store(store, &identifier).await?;
|
||||||
|
|
||||||
|
repo.relate_details(&identifier, &details).await?;
|
||||||
|
|
||||||
|
details
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(format) = input_details.internal_format().and_then(|format| format.processable_format()) else {
|
||||||
|
todo!("Error")
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(format) = format.process_to(output_format) else {
|
||||||
|
todo!("Error")
|
||||||
|
};
|
||||||
|
|
||||||
let mut processed_reader =
|
let mut processed_reader =
|
||||||
crate::magick::process_image_store_read(store.clone(), identifier, thumbnail_args, format)?;
|
crate::magick::process_image_store_read(store.clone(), identifier, thumbnail_args, format)?;
|
||||||
|
|
||||||
|
@ -95,14 +113,7 @@ async fn process<R: FullRepo, S: Store + 'static>(
|
||||||
|
|
||||||
drop(permit);
|
drop(permit);
|
||||||
|
|
||||||
let discovery = crate::discover::discover_bytes(bytes.clone()).await?;
|
let details = Details::from_bytes(bytes.clone()).await?;
|
||||||
|
|
||||||
let details = Details::from_parts(
|
|
||||||
discovery.input.internal_format(),
|
|
||||||
discovery.width,
|
|
||||||
discovery.height,
|
|
||||||
discovery.frames,
|
|
||||||
);
|
|
||||||
|
|
||||||
let identifier = store.save_bytes(bytes.clone()).await?;
|
let identifier = store.save_bytes(bytes.clone()).await?;
|
||||||
repo.relate_details(&identifier, &details).await?;
|
repo.relate_details(&identifier, &details).await?;
|
||||||
|
|
50
src/lib.rs
50
src/lib.rs
|
@ -33,7 +33,7 @@ use actix_web::{
|
||||||
web, App, HttpRequest, HttpResponse, HttpResponseBuilder, HttpServer,
|
web, App, HttpRequest, HttpResponse, HttpResponseBuilder, HttpServer,
|
||||||
};
|
};
|
||||||
use awc::{Client, Connector};
|
use awc::{Client, Connector};
|
||||||
use formats::{InputProcessableFormat, ProcessableFormat};
|
use formats::InputProcessableFormat;
|
||||||
use futures_util::{
|
use futures_util::{
|
||||||
stream::{empty, once},
|
stream::{empty, once},
|
||||||
Stream, StreamExt, TryStreamExt,
|
Stream, StreamExt, TryStreamExt,
|
||||||
|
@ -55,13 +55,12 @@ use tracing_futures::Instrument;
|
||||||
|
|
||||||
use self::{
|
use self::{
|
||||||
backgrounded::Backgrounded,
|
backgrounded::Backgrounded,
|
||||||
config::{Configuration, ImageFormat, Operation},
|
config::{Configuration, Operation},
|
||||||
details::Details,
|
details::Details,
|
||||||
either::Either,
|
either::Either,
|
||||||
error::{Error, UploadError},
|
error::{Error, UploadError},
|
||||||
ingest::Session,
|
ingest::Session,
|
||||||
init_tracing::init_tracing,
|
init_tracing::init_tracing,
|
||||||
magick::{details_hint, ValidInputType},
|
|
||||||
middleware::{Deadline, Internal},
|
middleware::{Deadline, Internal},
|
||||||
queue::queue_generate,
|
queue::queue_generate,
|
||||||
repo::{
|
repo::{
|
||||||
|
@ -115,15 +114,20 @@ async fn ensure_details<R: FullRepo, S: Store + 'static>(
|
||||||
return Err(UploadError::MissingAlias.into());
|
return Err(UploadError::MissingAlias.into());
|
||||||
};
|
};
|
||||||
|
|
||||||
let details = repo.details(&identifier).await?;
|
let details = repo.details(&identifier).await?.and_then(|details| {
|
||||||
|
if details.internal_format().is_some() {
|
||||||
|
Some(details)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if let Some(details) = details {
|
if let Some(details) = details {
|
||||||
tracing::debug!("details exist");
|
tracing::debug!("details exist");
|
||||||
Ok(details)
|
Ok(details)
|
||||||
} else {
|
} else {
|
||||||
tracing::debug!("generating new details from {:?}", identifier);
|
tracing::debug!("generating new details from {:?}", identifier);
|
||||||
let hint = details_hint(alias);
|
let new_details = Details::from_store(store, &identifier).await?;
|
||||||
let new_details = Details::from_store(store.clone(), identifier.clone(), hint).await?;
|
|
||||||
tracing::debug!("storing details for {:?}", identifier);
|
tracing::debug!("storing details for {:?}", identifier);
|
||||||
repo.relate_details(&identifier, &new_details).await?;
|
repo.relate_details(&identifier, &new_details).await?;
|
||||||
tracing::debug!("stored");
|
tracing::debug!("stored");
|
||||||
|
@ -645,19 +649,20 @@ async fn process<R: FullRepo, S: Store + 'static>(
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if let Some(identifier) = identifier_opt {
|
if let Some(identifier) = identifier_opt {
|
||||||
let details = repo.details(&identifier).await?;
|
let details = repo.details(&identifier).await?.and_then(|details| {
|
||||||
|
if details.internal_format().is_some() {
|
||||||
|
Some(details)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let details = if let Some(details) = details {
|
let details = if let Some(details) = details {
|
||||||
tracing::debug!("details exist");
|
tracing::debug!("details exist");
|
||||||
details
|
details
|
||||||
} else {
|
} else {
|
||||||
tracing::debug!("generating new details from {:?}", identifier);
|
tracing::debug!("generating new details from {:?}", identifier);
|
||||||
let new_details = Details::from_store(
|
let new_details = Details::from_store(&store, &identifier).await?;
|
||||||
(**store).clone(),
|
|
||||||
identifier.clone(),
|
|
||||||
Some(ValidInputType::from_format(format)),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
tracing::debug!("storing details for {:?}", identifier);
|
tracing::debug!("storing details for {:?}", identifier);
|
||||||
repo.relate_details(&identifier, &new_details).await?;
|
repo.relate_details(&identifier, &new_details).await?;
|
||||||
tracing::debug!("stored");
|
tracing::debug!("stored");
|
||||||
|
@ -676,7 +681,7 @@ async fn process<R: FullRepo, S: Store + 'static>(
|
||||||
alias,
|
alias,
|
||||||
thumbnail_path,
|
thumbnail_path,
|
||||||
thumbnail_args,
|
thumbnail_args,
|
||||||
original_details.to_input_format(),
|
original_details.video_format(),
|
||||||
None,
|
None,
|
||||||
hash,
|
hash,
|
||||||
)
|
)
|
||||||
|
@ -727,7 +732,7 @@ async fn process_head<R: FullRepo, S: Store + 'static>(
|
||||||
repo: web::Data<R>,
|
repo: web::Data<R>,
|
||||||
store: web::Data<S>,
|
store: web::Data<S>,
|
||||||
) -> Result<HttpResponse, Error> {
|
) -> Result<HttpResponse, Error> {
|
||||||
let (format, alias, thumbnail_path, _) = prepare_process(query, ext.as_str())?;
|
let (_, alias, thumbnail_path, _) = prepare_process(query, ext.as_str())?;
|
||||||
|
|
||||||
let path_string = thumbnail_path.to_string_lossy().to_string();
|
let path_string = thumbnail_path.to_string_lossy().to_string();
|
||||||
let Some(hash) = repo.hash(&alias).await? else {
|
let Some(hash) = repo.hash(&alias).await? else {
|
||||||
|
@ -740,19 +745,20 @@ async fn process_head<R: FullRepo, S: Store + 'static>(
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if let Some(identifier) = identifier_opt {
|
if let Some(identifier) = identifier_opt {
|
||||||
let details = repo.details(&identifier).await?;
|
let details = repo.details(&identifier).await?.and_then(|details| {
|
||||||
|
if details.internal_format().is_some() {
|
||||||
|
Some(details)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let details = if let Some(details) = details {
|
let details = if let Some(details) = details {
|
||||||
tracing::debug!("details exist");
|
tracing::debug!("details exist");
|
||||||
details
|
details
|
||||||
} else {
|
} else {
|
||||||
tracing::debug!("generating new details from {:?}", identifier);
|
tracing::debug!("generating new details from {:?}", identifier);
|
||||||
let new_details = Details::from_store(
|
let new_details = Details::from_store(&store, &identifier).await?;
|
||||||
(**store).clone(),
|
|
||||||
identifier.clone(),
|
|
||||||
Some(ValidInputType::from_format(format)),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
tracing::debug!("storing details for {:?}", identifier);
|
tracing::debug!("storing details for {:?}", identifier);
|
||||||
repo.relate_details(&identifier, &new_details).await?;
|
repo.relate_details(&identifier, &new_details).await?;
|
||||||
tracing::debug!("stored");
|
tracing::debug!("stored");
|
||||||
|
|
424
src/magick.rs
424
src/magick.rs
|
@ -2,26 +2,17 @@
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{ImageFormat, VideoCodec},
|
|
||||||
formats::ProcessableFormat,
|
formats::ProcessableFormat,
|
||||||
process::{Process, ProcessError},
|
process::{Process, ProcessError},
|
||||||
repo::Alias,
|
|
||||||
store::Store,
|
store::Store,
|
||||||
};
|
};
|
||||||
use actix_web::web::Bytes;
|
use tokio::io::AsyncRead;
|
||||||
use tokio::io::{AsyncRead, AsyncReadExt};
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub(crate) enum MagickError {
|
pub(crate) enum MagickError {
|
||||||
#[error("Error in imagemagick process")]
|
#[error("Error in imagemagick process")]
|
||||||
Process(#[source] ProcessError),
|
Process(#[source] ProcessError),
|
||||||
|
|
||||||
#[error("Error parsing details")]
|
|
||||||
ParseDetails(#[source] ParseDetailsError),
|
|
||||||
|
|
||||||
#[error("Media details are invalid")]
|
|
||||||
ValidateDetails(#[source] ValidateDetailsError),
|
|
||||||
|
|
||||||
#[error("Invalid output format")]
|
#[error("Invalid output format")]
|
||||||
Json(#[source] serde_json::Error),
|
Json(#[source] serde_json::Error),
|
||||||
|
|
||||||
|
@ -37,9 +28,6 @@ pub(crate) enum MagickError {
|
||||||
#[error("Error creating directory")]
|
#[error("Error creating directory")]
|
||||||
CreateDir(#[source] crate::store::file_store::FileError),
|
CreateDir(#[source] crate::store::file_store::FileError),
|
||||||
|
|
||||||
#[error("Error reading file")]
|
|
||||||
Store(#[source] crate::store::StoreError),
|
|
||||||
|
|
||||||
#[error("Error closing file")]
|
#[error("Error closing file")]
|
||||||
CloseFile(#[source] std::io::Error),
|
CloseFile(#[source] std::io::Error),
|
||||||
|
|
||||||
|
@ -53,368 +41,8 @@ pub(crate) enum MagickError {
|
||||||
impl MagickError {
|
impl MagickError {
|
||||||
pub(crate) fn is_client_error(&self) -> bool {
|
pub(crate) fn is_client_error(&self) -> bool {
|
||||||
// Failing validation or imagemagick bailing probably means bad input
|
// Failing validation or imagemagick bailing probably means bad input
|
||||||
matches!(self, Self::ValidateDetails(_))
|
matches!(self, Self::Process(ProcessError::Status(_)))
|
||||||
|| matches!(self, Self::Process(ProcessError::Status(_)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn is_not_found(&self) -> bool {
|
|
||||||
if let Self::Store(e) = self {
|
|
||||||
return e.is_not_found();
|
|
||||||
}
|
|
||||||
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn details_hint(alias: &Alias) -> Option<ValidInputType> {
|
|
||||||
let ext = alias.extension()?;
|
|
||||||
if ext.ends_with(".mp4") {
|
|
||||||
Some(ValidInputType::Mp4)
|
|
||||||
} else if ext.ends_with(".webm") {
|
|
||||||
Some(ValidInputType::Webm)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn image_avif() -> mime::Mime {
|
|
||||||
"image/avif".parse().unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn image_jxl() -> mime::Mime {
|
|
||||||
"image/jxl".parse().unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn image_webp() -> mime::Mime {
|
|
||||||
"image/webp".parse().unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn video_mp4() -> mime::Mime {
|
|
||||||
"video/mp4".parse().unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn video_webm() -> mime::Mime {
|
|
||||||
"video/webm".parse().unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug)]
|
|
||||||
pub(crate) enum ValidInputType {
|
|
||||||
Mp4,
|
|
||||||
Webm,
|
|
||||||
Gif,
|
|
||||||
Avif,
|
|
||||||
Jpeg,
|
|
||||||
Jxl,
|
|
||||||
Png,
|
|
||||||
Webp,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ValidInputType {
|
|
||||||
const fn as_str(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Mp4 => "MP4",
|
|
||||||
Self::Webm => "WEBM",
|
|
||||||
Self::Gif => "GIF",
|
|
||||||
Self::Avif => "AVIF",
|
|
||||||
Self::Jpeg => "JPEG",
|
|
||||||
Self::Jxl => "JXL",
|
|
||||||
Self::Png => "PNG",
|
|
||||||
Self::Webp => "WEBP",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) const fn as_ext(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Mp4 => ".mp4",
|
|
||||||
Self::Webm => ".webm",
|
|
||||||
Self::Gif => ".gif",
|
|
||||||
Self::Avif => ".avif",
|
|
||||||
Self::Jpeg => ".jpeg",
|
|
||||||
Self::Jxl => ".jxl",
|
|
||||||
Self::Png => ".png",
|
|
||||||
Self::Webp => ".webp",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) const fn is_video(self) -> bool {
|
|
||||||
matches!(self, Self::Mp4 | Self::Webm | Self::Gif)
|
|
||||||
}
|
|
||||||
|
|
||||||
const fn video_hint(self) -> Option<&'static str> {
|
|
||||||
match self {
|
|
||||||
Self::Mp4 => Some(".mp4"),
|
|
||||||
Self::Webm => Some(".webm"),
|
|
||||||
Self::Gif => Some(".gif"),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) const fn from_video_codec(codec: VideoCodec) -> Self {
|
|
||||||
match codec {
|
|
||||||
VideoCodec::Av1 | VideoCodec::Vp8 | VideoCodec::Vp9 => Self::Webm,
|
|
||||||
VideoCodec::H264 | VideoCodec::H265 => Self::Mp4,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) const fn from_format(format: ImageFormat) -> Self {
|
|
||||||
match format {
|
|
||||||
ImageFormat::Avif => ValidInputType::Avif,
|
|
||||||
ImageFormat::Jpeg => ValidInputType::Jpeg,
|
|
||||||
ImageFormat::Jxl => ValidInputType::Jxl,
|
|
||||||
ImageFormat::Png => ValidInputType::Png,
|
|
||||||
ImageFormat::Webp => ValidInputType::Webp,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) const fn to_format(self) -> Option<ImageFormat> {
|
|
||||||
match self {
|
|
||||||
Self::Avif => Some(ImageFormat::Avif),
|
|
||||||
Self::Jpeg => Some(ImageFormat::Jpeg),
|
|
||||||
Self::Jxl => Some(ImageFormat::Jxl),
|
|
||||||
Self::Png => Some(ImageFormat::Png),
|
|
||||||
Self::Webp => Some(ImageFormat::Webp),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
|
||||||
pub(crate) struct Details {
|
|
||||||
pub(crate) mime_type: mime::Mime,
|
|
||||||
pub(crate) width: usize,
|
|
||||||
pub(crate) height: usize,
|
|
||||||
pub(crate) frames: Option<usize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument(level = "debug", skip(input))]
|
|
||||||
pub(crate) fn convert_bytes_read(
|
|
||||||
input: Bytes,
|
|
||||||
format: ImageFormat,
|
|
||||||
) -> Result<impl AsyncRead + Unpin, MagickError> {
|
|
||||||
let process = Process::run(
|
|
||||||
"magick",
|
|
||||||
&[
|
|
||||||
"convert",
|
|
||||||
"-",
|
|
||||||
"-auto-orient",
|
|
||||||
"-strip",
|
|
||||||
format!("{}:-", format.as_magick_format()).as_str(),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
.map_err(MagickError::Process)?;
|
|
||||||
|
|
||||||
Ok(process.bytes_read(input))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument(skip(input))]
|
|
||||||
pub(crate) async fn details_bytes(
|
|
||||||
input: Bytes,
|
|
||||||
hint: Option<ValidInputType>,
|
|
||||||
) -> Result<Details, MagickError> {
|
|
||||||
if let Some(hint) = hint.and_then(|hint| hint.video_hint()) {
|
|
||||||
let input_file = crate::tmp_file::tmp_file(Some(hint));
|
|
||||||
let input_file_str = input_file.to_str().ok_or(MagickError::Path)?;
|
|
||||||
crate::store::file_store::safe_create_parent(&input_file)
|
|
||||||
.await
|
|
||||||
.map_err(MagickError::CreateDir)?;
|
|
||||||
|
|
||||||
let mut tmp_one = crate::file::File::create(&input_file)
|
|
||||||
.await
|
|
||||||
.map_err(MagickError::CreateFile)?;
|
|
||||||
tmp_one
|
|
||||||
.write_from_bytes(input)
|
|
||||||
.await
|
|
||||||
.map_err(MagickError::Write)?;
|
|
||||||
tmp_one.close().await.map_err(MagickError::CloseFile)?;
|
|
||||||
|
|
||||||
return details_file(input_file_str).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
let last_arg = if let Some(expected_format) = hint {
|
|
||||||
format!("{}:-", expected_format.as_str())
|
|
||||||
} else {
|
|
||||||
"-".to_owned()
|
|
||||||
};
|
|
||||||
|
|
||||||
let process = Process::run("magick", &["convert", "-ping", &last_arg, "JSON:"])
|
|
||||||
.map_err(MagickError::Process)?;
|
|
||||||
|
|
||||||
let mut reader = process.bytes_read(input);
|
|
||||||
|
|
||||||
let mut bytes = Vec::new();
|
|
||||||
reader
|
|
||||||
.read_to_end(&mut bytes)
|
|
||||||
.await
|
|
||||||
.map_err(MagickError::Read)?;
|
|
||||||
|
|
||||||
let details_output: Vec<DetailsOutput> =
|
|
||||||
serde_json::from_slice(&bytes).map_err(MagickError::Json)?;
|
|
||||||
|
|
||||||
parse_details(details_output).map_err(MagickError::ParseDetails)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize)]
|
|
||||||
struct DetailsOutput {
|
|
||||||
image: Image,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize)]
|
|
||||||
struct Image {
|
|
||||||
format: String,
|
|
||||||
geometry: Geometry,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize)]
|
|
||||||
struct Geometry {
|
|
||||||
width: usize,
|
|
||||||
height: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument(skip(store))]
|
|
||||||
pub(crate) async fn details_store<S: Store + 'static>(
|
|
||||||
store: S,
|
|
||||||
identifier: S::Identifier,
|
|
||||||
hint: Option<ValidInputType>,
|
|
||||||
) -> Result<Details, MagickError> {
|
|
||||||
if let Some(hint) = hint.and_then(|hint| hint.video_hint()) {
|
|
||||||
let input_file = crate::tmp_file::tmp_file(Some(hint));
|
|
||||||
let input_file_str = input_file.to_str().ok_or(MagickError::Path)?;
|
|
||||||
crate::store::file_store::safe_create_parent(&input_file)
|
|
||||||
.await
|
|
||||||
.map_err(MagickError::CreateDir)?;
|
|
||||||
|
|
||||||
let mut tmp_one = crate::file::File::create(&input_file)
|
|
||||||
.await
|
|
||||||
.map_err(MagickError::CreateFile)?;
|
|
||||||
let stream = store
|
|
||||||
.to_stream(&identifier, None, None)
|
|
||||||
.await
|
|
||||||
.map_err(MagickError::Store)?;
|
|
||||||
tmp_one
|
|
||||||
.write_from_stream(stream)
|
|
||||||
.await
|
|
||||||
.map_err(MagickError::Write)?;
|
|
||||||
tmp_one.close().await.map_err(MagickError::CloseFile)?;
|
|
||||||
|
|
||||||
return details_file(input_file_str).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
let last_arg = if let Some(expected_format) = hint {
|
|
||||||
format!("{}:-", expected_format.as_str())
|
|
||||||
} else {
|
|
||||||
"-".to_owned()
|
|
||||||
};
|
|
||||||
|
|
||||||
let process = Process::run("magick", &["convert", "-ping", &last_arg, "JSON:"])
|
|
||||||
.map_err(MagickError::Process)?;
|
|
||||||
|
|
||||||
let mut reader = process.store_read(store, identifier);
|
|
||||||
|
|
||||||
let mut output = Vec::new();
|
|
||||||
reader
|
|
||||||
.read_to_end(&mut output)
|
|
||||||
.await
|
|
||||||
.map_err(MagickError::Read)?;
|
|
||||||
|
|
||||||
let details_output: Vec<DetailsOutput> =
|
|
||||||
serde_json::from_slice(&output).map_err(MagickError::Json)?;
|
|
||||||
|
|
||||||
parse_details(details_output).map_err(MagickError::ParseDetails)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument]
|
|
||||||
pub(crate) async fn details_file(path_str: &str) -> Result<Details, MagickError> {
|
|
||||||
let process = Process::run("magick", &["convert", "-ping", path_str, "JSON:"])
|
|
||||||
.map_err(MagickError::Process)?;
|
|
||||||
|
|
||||||
let mut reader = process.read();
|
|
||||||
|
|
||||||
let mut output = Vec::new();
|
|
||||||
reader
|
|
||||||
.read_to_end(&mut output)
|
|
||||||
.await
|
|
||||||
.map_err(MagickError::Read)?;
|
|
||||||
tokio::fs::remove_file(path_str)
|
|
||||||
.await
|
|
||||||
.map_err(MagickError::RemoveFile)?;
|
|
||||||
|
|
||||||
let details_output: Vec<DetailsOutput> =
|
|
||||||
serde_json::from_slice(&output).map_err(MagickError::Json)?;
|
|
||||||
|
|
||||||
parse_details(details_output).map_err(MagickError::ParseDetails)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub(crate) enum ParseDetailsError {
|
|
||||||
#[error("No frames present in image")]
|
|
||||||
NoFrames,
|
|
||||||
|
|
||||||
#[error("Multiple image formats used in same file")]
|
|
||||||
MixedFormats,
|
|
||||||
|
|
||||||
#[error("Format is unsupported: {0}")]
|
|
||||||
Unsupported(String),
|
|
||||||
|
|
||||||
#[error("Could not parse frame count from {0}")]
|
|
||||||
ParseFrames(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_details(details_output: Vec<DetailsOutput>) -> Result<Details, ParseDetailsError> {
|
|
||||||
let frames = details_output.len();
|
|
||||||
|
|
||||||
if frames == 0 {
|
|
||||||
return Err(ParseDetailsError::NoFrames);
|
|
||||||
}
|
|
||||||
|
|
||||||
let width = details_output
|
|
||||||
.iter()
|
|
||||||
.map(|details| details.image.geometry.width)
|
|
||||||
.max()
|
|
||||||
.expect("Nonempty vector");
|
|
||||||
let height = details_output
|
|
||||||
.iter()
|
|
||||||
.map(|details| details.image.geometry.height)
|
|
||||||
.max()
|
|
||||||
.expect("Nonempty vector");
|
|
||||||
|
|
||||||
let format = details_output[0].image.format.as_str();
|
|
||||||
tracing::debug!("format: {}", format);
|
|
||||||
|
|
||||||
if !details_output
|
|
||||||
.iter()
|
|
||||||
.all(|details| details.image.format == format)
|
|
||||||
{
|
|
||||||
return Err(ParseDetailsError::MixedFormats);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mime_type = match format {
|
|
||||||
"MP4" => video_mp4(),
|
|
||||||
"WEBM" => video_webm(),
|
|
||||||
"GIF" => mime::IMAGE_GIF,
|
|
||||||
"AVIF" => image_avif(),
|
|
||||||
"JPEG" => mime::IMAGE_JPEG,
|
|
||||||
"JXL" => image_jxl(),
|
|
||||||
"PNG" => mime::IMAGE_PNG,
|
|
||||||
"WEBP" => image_webp(),
|
|
||||||
e => return Err(ParseDetailsError::Unsupported(String::from(e))),
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Details {
|
|
||||||
mime_type,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
frames: if frames > 1 { Some(frames) } else { None },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn input_type_bytes(
|
|
||||||
input: Bytes,
|
|
||||||
) -> Result<(Details, ValidInputType), MagickError> {
|
|
||||||
let details = details_bytes(input, None).await?;
|
|
||||||
let input_type = details
|
|
||||||
.validate_input()
|
|
||||||
.map_err(MagickError::ValidateDetails)?;
|
|
||||||
Ok((details, input_type))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process_image(
|
fn process_image(
|
||||||
|
@ -462,51 +90,3 @@ pub(crate) fn process_image_async_read<A: AsyncRead + Unpin + 'static>(
|
||||||
.map_err(MagickError::Process)?
|
.map_err(MagickError::Process)?
|
||||||
.pipe_async_read(async_read))
|
.pipe_async_read(async_read))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub(crate) enum ValidateDetailsError {
|
|
||||||
#[error("Exceeded maximum dimensions")]
|
|
||||||
ExceededDimensions,
|
|
||||||
|
|
||||||
#[error("Exceeded maximum frame count")]
|
|
||||||
TooManyFrames,
|
|
||||||
|
|
||||||
#[error("Unsupported media type: {0}")]
|
|
||||||
UnsupportedMediaType(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Details {
|
|
||||||
#[tracing::instrument(level = "debug", name = "Validating input type")]
|
|
||||||
pub(crate) fn validate_input(&self) -> Result<ValidInputType, ValidateDetailsError> {
|
|
||||||
if self.width > crate::CONFIG.media.max_width
|
|
||||||
|| self.height > crate::CONFIG.media.max_height
|
|
||||||
|| self.width * self.height > crate::CONFIG.media.max_area
|
|
||||||
{
|
|
||||||
return Err(ValidateDetailsError::ExceededDimensions);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(frames) = self.frames {
|
|
||||||
if frames > crate::CONFIG.media.max_frame_count {
|
|
||||||
return Err(ValidateDetailsError::TooManyFrames);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let input_type = match (self.mime_type.type_(), self.mime_type.subtype()) {
|
|
||||||
(mime::VIDEO, mime::MP4 | mime::MPEG) => ValidInputType::Mp4,
|
|
||||||
(mime::VIDEO, subtype) if subtype.as_str() == "webm" => ValidInputType::Webm,
|
|
||||||
(mime::IMAGE, mime::GIF) => ValidInputType::Gif,
|
|
||||||
(mime::IMAGE, subtype) if subtype.as_str() == "avif" => ValidInputType::Avif,
|
|
||||||
(mime::IMAGE, mime::JPEG) => ValidInputType::Jpeg,
|
|
||||||
(mime::IMAGE, subtype) if subtype.as_str() == "jxl" => ValidInputType::Jxl,
|
|
||||||
(mime::IMAGE, mime::PNG) => ValidInputType::Png,
|
|
||||||
(mime::IMAGE, subtype) if subtype.as_str() == "webp" => ValidInputType::Webp,
|
|
||||||
_ => {
|
|
||||||
return Err(ValidateDetailsError::UnsupportedMediaType(
|
|
||||||
self.mime_type.to_string(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(input_type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
error::Error,
|
error::Error,
|
||||||
formats::ProcessableFormat,
|
formats::InputProcessableFormat,
|
||||||
repo::{
|
repo::{
|
||||||
Alias, AliasRepo, DeleteToken, FullRepo, HashRepo, IdentifierRepo, QueueRepo, UploadId,
|
Alias, AliasRepo, DeleteToken, FullRepo, HashRepo, IdentifierRepo, QueueRepo, UploadId,
|
||||||
},
|
},
|
||||||
|
@ -69,7 +69,7 @@ enum Process {
|
||||||
declared_alias: Option<Serde<Alias>>,
|
declared_alias: Option<Serde<Alias>>,
|
||||||
},
|
},
|
||||||
Generate {
|
Generate {
|
||||||
target_format: ProcessableFormat,
|
target_format: InputProcessableFormat,
|
||||||
source: Serde<Alias>,
|
source: Serde<Alias>,
|
||||||
process_path: PathBuf,
|
process_path: PathBuf,
|
||||||
process_args: Vec<String>,
|
process_args: Vec<String>,
|
||||||
|
@ -139,7 +139,7 @@ pub(crate) async fn queue_ingest<R: QueueRepo>(
|
||||||
|
|
||||||
pub(crate) async fn queue_generate<R: QueueRepo>(
|
pub(crate) async fn queue_generate<R: QueueRepo>(
|
||||||
repo: &R,
|
repo: &R,
|
||||||
target_format: ProcessableFormat,
|
target_format: InputProcessableFormat,
|
||||||
source: Alias,
|
source: Alias,
|
||||||
process_path: PathBuf,
|
process_path: PathBuf,
|
||||||
process_args: Vec<String>,
|
process_args: Vec<String>,
|
||||||
|
|
|
@ -144,7 +144,7 @@ async fn generate<R: FullRepo, S: Store + 'static>(
|
||||||
source,
|
source,
|
||||||
process_path,
|
process_path,
|
||||||
process_args,
|
process_args,
|
||||||
original_details.input_format(),
|
original_details.video_format(),
|
||||||
None,
|
None,
|
||||||
hash,
|
hash,
|
||||||
)
|
)
|
||||||
|
|
|
@ -114,7 +114,7 @@ pub(crate) trait FullRepo:
|
||||||
};
|
};
|
||||||
|
|
||||||
match self.details(&identifier).await? {
|
match self.details(&identifier).await? {
|
||||||
Some(details) if details.is_motion() => self.motion_identifier::<I>(hash).await,
|
Some(details) if details.is_video() => self.motion_identifier::<I>(hash).await,
|
||||||
Some(_) => Ok(Some(identifier)),
|
Some(_) => Ok(Some(identifier)),
|
||||||
None => Ok(None),
|
None => Ok(None),
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,33 +10,6 @@ use crate::{
|
||||||
use actix_web::web::Bytes;
|
use actix_web::web::Bytes;
|
||||||
use tokio::io::AsyncRead;
|
use tokio::io::AsyncRead;
|
||||||
|
|
||||||
struct UnvalidatedBytes {
|
|
||||||
bytes: Bytes,
|
|
||||||
written: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UnvalidatedBytes {
|
|
||||||
fn new(bytes: Bytes) -> Self {
|
|
||||||
UnvalidatedBytes { bytes, written: 0 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsyncRead for UnvalidatedBytes {
|
|
||||||
fn poll_read(
|
|
||||||
mut self: std::pin::Pin<&mut Self>,
|
|
||||||
_cx: &mut std::task::Context<'_>,
|
|
||||||
buf: &mut tokio::io::ReadBuf<'_>,
|
|
||||||
) -> std::task::Poll<std::io::Result<()>> {
|
|
||||||
let bytes_to_write = (self.bytes.len() - self.written).min(buf.remaining());
|
|
||||||
if bytes_to_write > 0 {
|
|
||||||
let end = self.written + bytes_to_write;
|
|
||||||
buf.put_slice(&self.bytes[self.written..end]);
|
|
||||||
self.written = end;
|
|
||||||
}
|
|
||||||
std::task::Poll::Ready(Ok(()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
pub(crate) async fn validate_bytes(
|
pub(crate) async fn validate_bytes(
|
||||||
bytes: Bytes,
|
bytes: Bytes,
|
||||||
|
|
Loading…
Reference in a new issue