[ENG-1028] Media data for ephemeral locations (#1287)

Introducing new getEphemeralMediaData
Fixing some minor stuff
Running pnpm format

Co-authored-by: jake <77554505+brxken128@users.noreply.github.com>
This commit is contained in:
Ericson "Fogo" Soares 2023-09-04 05:28:45 -03:00 committed by GitHub
parent 881608e9a8
commit 3b8541ef54
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 129 additions and 57 deletions

View file

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en" class="vanilla-theme">
<head>
<meta charset="UTF-8" />

View file

@ -19,23 +19,29 @@ export function getDocsNavigation(docs: Doc[]): DocsNavigation {
const docsNavigation: DocsNavigation = [];
const docsBySection = coreDocs.reduce((acc, doc) => {
const section = doc.section;
acc[section] = acc[section] || [];
acc[section].push(doc);
return acc;
}, {} as Record<string, CoreContent<Doc>[]>);
const docsBySection = coreDocs.reduce(
(acc, doc) => {
const section = doc.section;
acc[section] = acc[section] || [];
acc[section].push(doc);
return acc;
},
{} as Record<string, CoreContent<Doc>[]>
);
// console.log('docsBySection', docsBySection);
for (const section in docsBySection) {
const docs = docsBySection[section];
const docsByCategory = docs.reduce((acc, doc) => {
const category = doc.category;
acc[category] = acc[category] || [];
acc[category].push(doc);
return acc;
}, {} as Record<string, CoreContent<Doc>[]>);
const docsByCategory = docs.reduce(
(acc, doc) => {
const category = doc.category;
acc[category] = acc[category] || [];
acc[category].push(doc);
return acc;
},
{} as Record<string, CoreContent<Doc>[]>
);
// console.log('docsByCategory', docsByCategory);

View file

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />

View file

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html class="vanilla-theme">
<head>
<meta charset="utf-8" />

View file

@ -14,20 +14,29 @@ use crate::{
copy::FileCopierJobInit, cut::FileCutterJobInit, delete::FileDeleterJobInit,
erase::FileEraserJobInit,
},
media::media_data_image_from_prisma_data,
media::{
media_data_extractor::{
can_extract_media_data_for_image, extract_media_data, MediaDataError,
},
media_data_image_from_prisma_data,
},
},
prisma::{file_path, location, object},
util::{db::maybe_missing, error::FileIOError},
};
use std::path::Path;
use sd_file_ext::{extensions::ImageExtension, kind::ObjectKind};
use sd_media_metadata::MediaMetadata;
use std::{
path::{Path, PathBuf},
str::FromStr,
};
use chrono::Utc;
use futures::future::join_all;
use regex::Regex;
use rspc::{alpha::AlphaRouter, ErrorCode};
use sd_file_ext::kind::ObjectKind;
use sd_media_metadata::MediaMetadata;
use serde::Deserialize;
use specta::Type;
use tokio::{fs, io};
@ -78,6 +87,35 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
})
})
})
.procedure("getEphemeralMediaData", {
R.query(|_, full_path: PathBuf| async move {
let Some(extension) = full_path.extension().and_then(|ext| ext.to_str()) else {
return Ok(None);
};
// TODO(fogodev): change this when we have media data for audio and videos
let image_extension = ImageExtension::from_str(extension).map_err(|e| {
error!("Failed to parse image extension: {e:#?}");
rspc::Error::new(ErrorCode::BadRequest, "Invalid image extension".to_string())
})?;
if !can_extract_media_data_for_image(&image_extension) {
return Ok(None);
}
match extract_media_data(full_path).await {
Ok(img_media_data) => Ok(Some(MediaMetadata::Image(Box::new(img_media_data)))),
Err(MediaDataError::MediaData(sd_media_metadata::Error::NoExifDataOnPath(
_,
))) => Ok(None),
Err(e) => Err(rspc::Error::with_cause(
ErrorCode::InternalServerError,
"Failed to extract media data".to_string(),
e,
)),
}
})
})
.procedure("getPath", {
R.with2(library())
.query(|(_, library), id: i32| async move {

View file

@ -862,7 +862,7 @@ pub(super) async fn generate_thumbnail(
}
// Otherwise we good, thumbnail doesn't exist so we can generate it
} else {
debug!(
trace!(
"Skipping thumbnail generation for {} because it already exists",
path.display()
);

View file

@ -182,6 +182,9 @@ pub async fn generate_image_thumbnail<P: AsRef<Path>>(
img = orientation.correct_thumbnail(img);
}
}
Err(sd_media_metadata::Error::NoExifDataOnSlice) => {
// No can do if we don't have exif data
}
Err(e) => warn!("Unable to extract EXIF: {:?}", e),
}

View file

@ -1,9 +1,12 @@
use std::{num::ParseFloatError, path::PathBuf};
use std::{
num::ParseFloatError,
path::{Path, PathBuf},
};
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("there was an i/o error")]
Io(#[from] std::io::Error),
#[error("there was an i/o error {0} at {}", .1.display())]
Io(std::io::Error, Box<Path>),
#[error("error from the exif crate: {0}")]
Exif(#[from] exif::Error),
#[error("there was an error while parsing time with chrono: {0}")]

View file

@ -15,7 +15,10 @@ pub struct ExifReader(Exif);
impl ExifReader {
pub fn from_path(path: impl AsRef<Path>) -> Result<Self> {
exif::Reader::new()
.read_from_container(&mut BufReader::new(File::open(&path)?))
.read_from_container(&mut BufReader::new(
File::open(&path)
.map_err(|e| Error::Io(e, path.as_ref().to_path_buf().into_boxed_path()))?,
))
.map_or_else(
|_| Err(Error::NoExifDataOnPath(path.as_ref().to_path_buf())),
|reader| Ok(Self(reader)),

View file

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />

View file

@ -110,7 +110,7 @@ export const FileThumb = memo((props: ThumbProps) => {
break;
case ThumbType.Thumbnail:
if (itemData.thumbnailKey) {
if (itemData.thumbnailKey.length > 0) {
setSrc(platform.getThumbnailUrlByThumbKey(itemData.thumbnailKey));
} else {
setThumbType(ThumbType.Icon);

View file

@ -232,6 +232,11 @@ const SingleItemMetadata = ({ item }: { item: ExplorerItem }) => {
<Divider />
{
// TODO: Call `files.getMediaData` for indexed locations when we have media data UI
// TODO: Call `files.getEphemeralMediaData` for ephemeral locations when we have media data UI
}
<MetaContainer className="flex !flex-row flex-wrap gap-1 overflow-hidden">
<InfoPill>{isDir ? 'Folder' : kind}</InfoPill>

View file

@ -353,14 +353,17 @@ export default ({ children }: { children: RenderItem }) => {
// Sets active item to selected item with least index.
// Might seem kinda weird but it's the same behaviour as Finder.
activeItem.current =
allSelected.reduce((least, current) => {
const currentItem = getElementItem(current);
if (!currentItem) return least;
allSelected.reduce(
(least, current) => {
const currentItem = getElementItem(current);
if (!currentItem) return least;
if (!least) return currentItem;
if (!least) return currentItem;
return currentItem.index < least.index ? currentItem : least;
}, null as ReturnType<typeof getElementItem>)?.data ?? null;
return currentItem.index < least.index ? currentItem : least;
},
null as ReturnType<typeof getElementItem>
)?.data ?? null;
}}
onScroll={({ direction }) => {
selecto.current?.findSelectableTargets();
@ -436,14 +439,17 @@ export default ({ children }: { children: RenderItem }) => {
const elements = [...e.added, ...e.removed];
const items = elements.reduce((items, el) => {
const item = getElementItem(el);
const items = elements.reduce(
(items, el) => {
const item = getElementItem(el);
if (!item) return items;
if (!item) return items;
columns.add(item.column);
return [...items, item];
}, [] as NonNullable<ReturnType<typeof getElementItem>>[]);
columns.add(item.column);
return [...items, item];
},
[] as NonNullable<ReturnType<typeof getElementItem>>[]
);
if (columns.size > 1) {
items.sort((a, b) => a.column - b.column);

View file

@ -89,7 +89,7 @@ export const createDefaultExplorerSettings = <TOrder extends Ordering>({
contentId: 180,
objectId: 180
}
} satisfies ExplorerSettings<TOrder>);
}) satisfies ExplorerSettings<TOrder>;
type CutCopyState =
| {

View file

@ -43,8 +43,13 @@ const JobContainer = forwardRef<HTMLLIElement, JobContainerProps>((props, ref) =
)
)}
<MetaContainer>
<Tooltip asChild tooltipClassName="bg-black max-w-[400px]" position="top" label={name}>
<p className="truncate w-fit max-w-[83%] pl-1.5 font-semibold">{name}</p>
<Tooltip
asChild
tooltipClassName="bg-black max-w-[400px]"
position="top"
label={name}
>
<p className="w-fit max-w-[83%] truncate pl-1.5 font-semibold">{name}</p>
</Tooltip>
{textItems?.map((item, index) => {
// filter out undefined text so we don't render empty TextItems

View file

@ -14,8 +14,8 @@ export const Component = () => {
const filteredLocations = useMemo(
() =>
locations.data?.filter((location) =>
location.name?.toLowerCase().includes(debouncedSearch.toLowerCase())
locations.data?.filter(
(location) => location.name?.toLowerCase().includes(debouncedSearch.toLowerCase())
),
[debouncedSearch, locations.data]
);

View file

@ -36,9 +36,7 @@ export const Component = () => {
return (
<ul className="space-y-4 p-4">
{groups?.map((group, index) => (
<OperationGroup key={index} group={group} />
))}
{groups?.map((group, index) => <OperationGroup key={index} group={group} />)}
</ul>
);
};

View file

@ -23,7 +23,7 @@ const Accordion = ({ title, className, children }: Props) => {
/>
</div>
{toggle && (
<div className="p-3 pt-2 border-t rounded-b-md border-app-line bg-app-box">
<div className="rounded-b-md border-t border-app-line bg-app-box p-3 pt-2">
{children}
</div>
)}

View file

@ -24,7 +24,12 @@ prettier.resolveConfig(join(__dirname, '..', '..', '..', '.prettierrc.js')).then
const indexFilePath = join(__dirname, '..', folder, 'index.ts');
const assetsFolderPath = join(__dirname, '..', folder);
if (await fs.access(indexFilePath).then(() => true, () => false)) {
if (
await fs.access(indexFilePath).then(
() => true,
() => false
)
) {
// Delete the index file if it already exists.
await fs.unlink(indexFilePath);
}

View file

@ -7,6 +7,7 @@ export type Procedures = {
{ key: "buildInfo", input: never, result: BuildInfo } |
{ key: "categories.list", input: LibraryArgs<null>, result: { [key in Category]: number } } |
{ key: "files.get", input: LibraryArgs<GetArgs>, result: { id: number; pub_id: number[]; kind: number | null; key_id: number | null; hidden: boolean | null; favorite: boolean | null; important: boolean | null; note: string | null; date_created: string | null; date_accessed: string | null; file_paths: FilePath[] } | null } |
{ key: "files.getEphemeralMediaData", input: string, result: MediaMetadata | null } |
{ key: "files.getMediaData", input: LibraryArgs<number>, result: MediaMetadata } |
{ key: "files.getPath", input: LibraryArgs<number>, result: string | null } |
{ key: "invalidation.test-invalidate", input: never, result: number } |

View file

@ -1,9 +1,9 @@
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';
import { comlink } from 'vite-plugin-comlink';
import { createHtmlPlugin } from 'vite-plugin-html';
import svg from 'vite-plugin-svgr';
import tsconfigPaths from 'vite-tsconfig-paths';
import { comlink } from 'vite-plugin-comlink';
import relativeAliasResolver from './relativeAliasResolver';
export default defineConfig({

View file

@ -29,14 +29,13 @@ const itemIconStyles = cva('mr-2 h-4 w-4', {
variants: {}
});
type DropdownItemProps =
| PropsWithChildren<{
to?: string;
className?: string;
icon?: any;
onClick?: () => void;
}> &
VariantProps<typeof itemStyles>;
type DropdownItemProps = PropsWithChildren<{
to?: string;
className?: string;
icon?: any;
onClick?: () => void;
}> &
VariantProps<typeof itemStyles>;
export const Item = ({ to, className, icon: Icon, children, ...props }: DropdownItemProps) => {
const content = (