Improve Thumbnail quality + fixes (#2467)

* Improve Thumbnail quality
 - Increase thumbnail size to 1024
 - Increse webp quality to 60%

* Fix thumbnails reactivity for ephemeral files

* Fix negative BigInt convertion

* Fix overflow in javascript

---------

Co-authored-by: Ericson Soares <ericson.ds999@gmail.com>
This commit is contained in:
Vítor Vasconcellos 2024-05-09 02:48:43 -03:00 committed by GitHub
parent 8e994bedaa
commit 0d451d6d90
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 50 additions and 34 deletions

View file

@ -57,7 +57,7 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
.await .await
.map(Into::into); .map(Into::into);
match kind { match kind {
Some(v) if v == ObjectKind::Image => { Some(ObjectKind::Image) => {
let Some(extension) = full_path.extension().and_then(|ext| ext.to_str()) let Some(extension) = full_path.extension().and_then(|ext| ext.to_str())
else { else {
return Ok(None); return Ok(None);

View file

@ -9,6 +9,7 @@ use crate::{
#[cfg(feature = "ai")] #[cfg(feature = "ai")]
use sd_ai::old_image_labeler::{DownloadModelError, OldImageLabeler, YoloV8}; use sd_ai::old_image_labeler::{DownloadModelError, OldImageLabeler, YoloV8};
use sd_utils::error::FileIOError;
use api::notifications::{Notification, NotificationData, NotificationId}; use api::notifications::{Notification, NotificationData, NotificationId};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
@ -23,7 +24,7 @@ use std::{
}; };
use thiserror::Error; use thiserror::Error;
use tokio::{fs, sync::broadcast}; use tokio::{fs, io, sync::broadcast};
use tracing::{error, info, warn}; use tracing::{error, info, warn};
use tracing_appender::{ use tracing_appender::{
non_blocking::{NonBlocking, WorkerGuard}, non_blocking::{NonBlocking, WorkerGuard},
@ -51,6 +52,8 @@ pub(crate) mod volume;
pub use env::Env; pub use env::Env;
use object::media::old_thumbnail::get_ephemeral_thumbnail_path;
pub(crate) use sd_core_sync as sync; pub(crate) use sd_core_sync as sync;
/// Represents a single running instance of the Spacedrive core. /// Represents a single running instance of the Spacedrive core.
@ -268,6 +271,16 @@ impl Node {
} }
} }
pub async fn ephemeral_thumbnail_exists(&self, cas_id: &str) -> Result<bool, FileIOError> {
let thumb_path = get_ephemeral_thumbnail_path(self, cas_id);
match fs::metadata(&thumb_path).await {
Ok(_) => Ok(true),
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(false),
Err(e) => Err(FileIOError::from((thumb_path, e))),
}
}
pub async fn emit_notification(&self, data: NotificationData, expires: Option<DateTime<Utc>>) { pub async fn emit_notification(&self, data: NotificationData, expires: Option<DateTime<Utc>>) {
let notification = Notification { let notification = Notification {
id: NotificationId::Node(self.notifications._internal_next_id()), id: NotificationId::Node(self.notifications._internal_next_id()),

View file

@ -223,8 +223,7 @@ pub async fn walk(
( (
Some(get_ephemeral_thumb_key(&cas_id)), Some(get_ephemeral_thumb_key(&cas_id)),
library node.ephemeral_thumbnail_exists(&cas_id)
.thumbnail_exists(&node, &cas_id)
.await .await
.map_err(NonIndexedLocationError::from)?, .map_err(NonIndexedLocationError::from)?,
) )

View file

@ -249,9 +249,9 @@ pub async fn save_ffmpeg_data(
async fn create_ffmpeg_data( async fn create_ffmpeg_data(
formats: Vec<String>, formats: Vec<String>,
bit_rate: (u32, u32), bit_rate: (i32, u32),
duration: Option<(u32, u32)>, duration: Option<(i32, u32)>,
start_time: Option<(u32, u32)>, start_time: Option<(i32, u32)>,
metadata: Metadata, metadata: Metadata,
object_id: i32, object_id: i32,
db: &PrismaClient, db: &PrismaClient,

View file

@ -90,15 +90,15 @@ pub fn ffmpeg_data_from_prisma_data(
formats: formats.split(',').map(String::from).collect::<Vec<_>>(), formats: formats.split(',').map(String::from).collect::<Vec<_>>(),
duration: duration.map(|duration| { duration: duration.map(|duration| {
let duration = ffmpeg_data_field_from_db(&duration); let duration = ffmpeg_data_field_from_db(&duration);
((duration >> 32) as u32, duration as u32) ((duration >> 32) as i32, duration as u32)
}), }),
start_time: start_time.map(|start_time| { start_time: start_time.map(|start_time| {
let start_time = ffmpeg_data_field_from_db(&start_time); let start_time = ffmpeg_data_field_from_db(&start_time);
((start_time >> 32) as u32, start_time as u32) ((start_time >> 32) as i32, start_time as u32)
}), }),
bit_rate: { bit_rate: {
let bit_rate = ffmpeg_data_field_from_db(&bit_rate); let bit_rate = ffmpeg_data_field_from_db(&bit_rate);
((bit_rate >> 32) as u32, bit_rate as u32) ((bit_rate >> 32) as i32, bit_rate as u32)
}, },
chapters: chapters chapters: chapters
.into_iter() .into_iter()
@ -115,11 +115,11 @@ pub fn ffmpeg_data_from_prisma_data(
id: chapter_id, id: chapter_id,
start: { start: {
let start = ffmpeg_data_field_from_db(&start); let start = ffmpeg_data_field_from_db(&start);
((start >> 32) as u32, start as u32) ((start >> 32) as i32, start as u32)
}, },
end: { end: {
let end = ffmpeg_data_field_from_db(&end); let end = ffmpeg_data_field_from_db(&end);
((end >> 32) as u32, end as u32) ((end >> 32) as i32, end as u32)
}, },
time_base_den, time_base_den,
time_base_num, time_base_num,

View file

@ -42,11 +42,11 @@ const EPHEMERAL_DIR: &str = "ephemeral";
/// This is the target pixel count for all thumbnails to be resized to, and it is eventually downscaled /// This is the target pixel count for all thumbnails to be resized to, and it is eventually downscaled
/// to [`TARGET_QUALITY`]. /// to [`TARGET_QUALITY`].
const TARGET_PX: f32 = 262144_f32; const TARGET_PX: f32 = 1048576.0; // 1024x1024
/// This is the target quality that we render thumbnails at, it is a float between 0-100 /// This is the target quality that we render thumbnails at, it is a float between 0-100
/// and is treated as a percentage (so 30% in this case, or it's the same as multiplying by `0.3`). /// and is treated as a percentage (so 60% in this case, or it's the same as multiplying by `0.6`).
const TARGET_QUALITY: f32 = 30_f32; const TARGET_QUALITY: f32 = 60.0;
// Some time constants // Some time constants
const ONE_SEC: Duration = Duration::from_secs(1); const ONE_SEC: Duration = Duration::from_secs(1);
@ -63,6 +63,10 @@ pub fn get_indexed_thumbnail_path(node: &Node, cas_id: &str, library_id: Library
get_thumbnail_path(node, cas_id, ThumbnailKind::Indexed(library_id)) get_thumbnail_path(node, cas_id, ThumbnailKind::Indexed(library_id))
} }
pub fn get_ephemeral_thumbnail_path(node: &Node, cas_id: &str) -> PathBuf {
get_thumbnail_path(node, cas_id, ThumbnailKind::Ephemeral)
}
/// This does not check if a thumbnail exists, it just returns the path that it would exist at /// This does not check if a thumbnail exists, it just returns the path that it would exist at
fn get_thumbnail_path(node: &Node, cas_id: &str, kind: ThumbnailKind) -> PathBuf { fn get_thumbnail_path(node: &Node, cas_id: &str, kind: ThumbnailKind) -> PathBuf {
let mut thumb_path = node.config.data_directory(); let mut thumb_path = node.config.data_directory();

View file

@ -475,7 +475,7 @@ async fn generate_video_thumbnail(
to_thumbnail( to_thumbnail(
file_path, file_path,
output_path, output_path,
ThumbnailSize::Scale(256), ThumbnailSize::Scale(1024),
TARGET_QUALITY, TARGET_QUALITY,
) )
.await .await

View file

@ -146,7 +146,7 @@ impl Default for ThumbnailerBuilder {
fn default() -> Self { fn default() -> Self {
Self { Self {
maintain_aspect_ratio: true, maintain_aspect_ratio: true,
size: ThumbnailSize::Scale(128), size: ThumbnailSize::Scale(1024),
seek_percentage: 0.1, seek_percentage: 0.1,
quality: 80.0, quality: 80.0,
prefer_embedded_metadata: true, prefer_embedded_metadata: true,

View file

@ -6,8 +6,8 @@ use super::metadata::Metadata;
#[derive(Debug, Serialize, Deserialize, Type)] #[derive(Debug, Serialize, Deserialize, Type)]
pub struct Chapter { pub struct Chapter {
pub id: i32, pub id: i32,
pub start: (u32, u32), pub start: (i32, u32),
pub end: (u32, u32), pub end: (i32, u32),
pub time_base_den: i32, pub time_base_den: i32,
pub time_base_num: i32, pub time_base_num: i32,
pub metadata: Metadata, pub metadata: Metadata,

View file

@ -21,9 +21,9 @@ use program::Program;
#[derive(Debug, Serialize, Deserialize, Type)] #[derive(Debug, Serialize, Deserialize, Type)]
pub struct FFmpegMetadata { pub struct FFmpegMetadata {
pub formats: Vec<String>, pub formats: Vec<String>,
pub duration: Option<(u32, u32)>, pub duration: Option<(i32, u32)>,
pub start_time: Option<(u32, u32)>, pub start_time: Option<(i32, u32)>,
pub bit_rate: (u32, u32), pub bit_rate: (i32, u32),
pub chapters: Vec<Chapter>, pub chapters: Vec<Chapter>,
pub programs: Vec<Program>, pub programs: Vec<Program>,
pub metadata: Metadata, pub metadata: Metadata,
@ -70,24 +70,24 @@ mod extract_data {
Self { Self {
formats, formats,
duration: duration.map(|duration| { duration: duration.map(|duration| {
#[allow(clippy::cast_possible_truncation)] #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
{ {
// SAFETY: We're splitting in (high, low) parts, so we're not going to lose data on truncation // SAFETY: We're splitting in (high, low) parts, so we're not going to lose data on truncation
((duration >> 32) as u32, duration as u32) ((duration >> 32) as i32, duration as u32)
} }
}), }),
start_time: start_time.map(|start_time| { start_time: start_time.map(|start_time| {
#[allow(clippy::cast_possible_truncation)] #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
{ {
// SAFETY: We're splitting in (high, low) parts, so we're not going to lose data on truncation // SAFETY: We're splitting in (high, low) parts, so we're not going to lose data on truncation
((start_time >> 32) as u32, start_time as u32) ((start_time >> 32) as i32, start_time as u32)
} }
}), }),
bit_rate: { bit_rate: {
#[allow(clippy::cast_possible_truncation)] #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
{ {
// SAFETY: We're splitting in (high, low) parts, so we're not going to lose data on truncation // SAFETY: We're splitting in (high, low) parts, so we're not going to lose data on truncation
((bit_rate >> 32) as u32, bit_rate as u32) ((bit_rate >> 32) as i32, bit_rate as u32)
} }
}, },
chapters: chapters.into_iter().map(Into::into).collect(), chapters: chapters.into_iter().map(Into::into).collect(),
@ -118,17 +118,17 @@ mod extract_data {
}, },
// TODO: FIX these 2 when rspc/specta supports bigint // TODO: FIX these 2 when rspc/specta supports bigint
start: { start: {
#[allow(clippy::cast_possible_truncation)] #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
{ {
// SAFETY: We're splitting in (high, low) parts, so we're not going to lose data on truncation // SAFETY: We're splitting in (high, low) parts, so we're not going to lose data on truncation
((start >> 32) as u32, start as u32) ((start >> 32) as i32, start as u32)
} }
}, },
end: { end: {
#[allow(clippy::cast_possible_truncation)] #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
{ {
// SAFETY: We're splitting in (high, low) parts, so we're not going to lose data on truncation // SAFETY: We're splitting in (high, low) parts, so we're not going to lose data on truncation
((end >> 32) as u32, end as u32) ((end >> 32) as i32, end as u32)
} }
}, },
time_base_num, time_base_num,

View file

@ -137,9 +137,9 @@ export function insertLibrary(queryClient: QueryClient, library: LibraryConfigWr
}); });
} }
// [int32, int32] => BigInt
export function int32ArrayToBigInt([high, low]: [number, number]) { export function int32ArrayToBigInt([high, low]: [number, number]) {
return (BigInt(high) << 32n) | BigInt(low); // Note: These magic shift operations internally convert the high into i32 and the low into u32
return (BigInt(high | 0) << 32n) | BigInt(low >>> 0);
} }
export function capitalize<T extends string>(string: T): Capitalize<T> { export function capitalize<T extends string>(string: T): Capitalize<T> {