mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-04 11:03:27 +00:00
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:
parent
8e994bedaa
commit
0d451d6d90
|
@ -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);
|
||||||
|
|
|
@ -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()),
|
||||||
|
|
|
@ -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)?,
|
||||||
)
|
)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
Loading…
Reference in a new issue